@@ -357,3 +357,106 @@ struct DoctorRunnerIntegrationTests {
357357 try runner. run ( )
358358 }
359359}
360+
361+ // MARK: - Summary Warning-Count Tests
362+
363+ /// Regression coverage for the doctor summary warning tally. The count must
364+ /// include warnings emitted *outside* the check loop (collision renames,
365+ /// unregistered packs), which previously printed but were never counted.
366+ ///
367+ /// Assertions are deltas: ambient checks (e.g. ProjectIndexCheck) may add
368+ /// warnings of their own, so each test compares two otherwise-identical runs
369+ /// that differ only by the single side-channel warning under test.
370+ struct DoctorSummaryWarningCountTests {
371+ /// The injected counter is shared across CLIOutput copies, so a warning
372+ /// emitted through any copy (e.g. the one handed to the collision resolver)
373+ /// is tallied. This is the mechanism the doctor summary relies on.
374+ @Test ( " WarningCounter is shared across CLIOutput value copies " )
375+ func warningCounterSharedAcrossCopies( ) {
376+ let counter = WarningCounter ( )
377+ let output = CLIOutput ( colorsEnabled: false , warningCounter: counter)
378+ let copy = output // value-type copy, same counter instance
379+
380+ #expect( counter. count == 0 )
381+ output. warn ( " first " )
382+ copy. warn ( " second " )
383+ #expect( counter. count == 2 )
384+ }
385+
386+ @Test ( " Skill colliding with a pre-existing unmanaged file is counted in the summary " )
387+ func collisionWarningCounted( ) throws {
388+ let ( home, project) = try makeSandboxProject ( label: " warncount-collision " )
389+ defer { try ? FileManager . default. removeItem ( at: home) }
390+
391+ // A pack whose skill targets destination "my-skill".
392+ let skillSource = home. appendingPathComponent ( " pack-my-skill " )
393+ try FileManager . default. createDirectory ( at: skillSource, withIntermediateDirectories: true )
394+ try " managed " . write (
395+ to: skillSource. appendingPathComponent ( " SKILL.md " ) , atomically: true , encoding: . utf8
396+ )
397+ let component = ComponentDefinition (
398+ id: " test-pack.my-skill " ,
399+ displayName: " my-skill " ,
400+ description: " Skill " ,
401+ type: . skill,
402+ packIdentifier: " test-pack " ,
403+ dependencies: [ ] ,
404+ isRequired: true ,
405+ installAction: . copyPackFile( source: skillSource, destination: " my-skill " , fileType: . skill)
406+ )
407+ let pack = MockTechPack (
408+ identifier: " test-pack " , displayName: " Test Pack " , components: [ component]
409+ )
410+ let registry = TechPackRegistry ( packs: [ pack] )
411+
412+ // Configure the pack (no artifacts → nothing tracked at the destination).
413+ var state = try ProjectState ( projectRoot: project)
414+ state. recordPack ( " test-pack " )
415+ try state. save ( )
416+
417+ // Baseline: no pre-existing file at the destination → no collision.
418+ var baselineRunner = makeRunner ( home: home, projectRoot: project, registry: registry)
419+ let baseline = try baselineRunner. run ( )
420+
421+ // Now plant a pre-existing UNMANAGED skill at the same destination.
422+ let existingSkill = project. appendingPathComponent ( " .claude/skills/my-skill " )
423+ try FileManager . default. createDirectory ( at: existingSkill, withIntermediateDirectories: true )
424+ try " user content " . write (
425+ to: existingSkill. appendingPathComponent ( " SKILL.md " ) , atomically: true , encoding: . utf8
426+ )
427+
428+ var collisionRunner = makeRunner ( home: home, projectRoot: project, registry: registry)
429+ let withCollision = try collisionRunner. run ( )
430+
431+ // The only difference between the two runs is the collision warning.
432+ #expect( withCollision. warnings == baseline. warnings + 1 )
433+ }
434+
435+ @Test ( " Unregistered --pack filter warning is counted in the summary " )
436+ func unregisteredPackWarningCounted( ) throws {
437+ let ( home, project) = try makeSandboxProject ( label: " warncount-unregistered " )
438+ defer { try ? FileManager . default. removeItem ( at: home) }
439+
440+ let pack = MockTechPack ( identifier: " test-pack " , displayName: " Test Pack " )
441+ let registry = TechPackRegistry ( packs: [ pack] )
442+
443+ var state = try ProjectState ( projectRoot: project)
444+ state. recordPack ( " test-pack " )
445+ try state. save ( )
446+
447+ // Baseline: filter to the registered pack only.
448+ var baselineRunner = makeRunner (
449+ home: home, projectRoot: project, registry: registry, packFilter: " test-pack "
450+ )
451+ let baseline = try baselineRunner. run ( )
452+
453+ // Add an unregistered pack id to the filter — same checks, plus one
454+ // "not registered" advisory warning.
455+ var ghostRunner = makeRunner (
456+ home: home, projectRoot: project, registry: registry, packFilter: " test-pack,ghost-pack "
457+ )
458+ let withGhost = try ghostRunner. run ( )
459+
460+ #expect( withGhost. warnings == baseline. warnings + 1 )
461+ }
462+ }
0 commit comments