Skip to content

refactor(cli): no type is widened through unknown, in tests as well as source #688

Description

@blafourcade

The state

44 occurrences of as unknown as across 22 files — 42 in tests, 2 in production. Each one is a place where the compiler was told to stop checking.

They accumulated because the guardrail only watches half the repository: scripts/check-cli-layering.mjs scans cli/src and nothing else. Tests have never been held to the rule.

Four causes, four different fixes

Cause Count What the fix actually is
A — a partial double of a port or use-case 31 Not a cast to replace. Either use the real in-memory doubles that already exist in cli/tests/helpers/ports/, or narrow the port.
C — a branded type or a typed Node API return 9 A test-only constructor for FileHash, and properly typed execSync mocks. Mechanical.
D — a deliberately invalid value, to test rejection 2 @ts-expect-error, which turns the cast into an assertion that fails the day the type stops forbidding it.
E — a real hole in production code 2 framework-build-use-case.ts:88 and marketplace-build-strategy.ts:132, the two entries check-cli-layering.mjs currently grandfathers.

Group A is a design signal, not laziness

{ two methods } as unknown as FileReader usually means the use-case genuinely needs two methods of a port that exposes twelve. The cast is hiding an interface-segregation problem, and "fixing" it has two honest answers — implement ten unused methods in a fake, or split the port. The second changes production interfaces.

A generic stub<T>(partial: Partial<T>): T is the same hole with a nicer name. It should not be the answer here.

Group D is already solved once

cli/tests/application/use-cases/telemetry/read-local-cost-use-case.unit.test.ts shows the pattern: the cast became a @ts-expect-error on a typed literal. Removing tool from the omitted set then failed the build in four places — the assertion itself plus all three readers, which are forced to name their tool. That is a stronger guarantee than the runtime check it replaced, and it cost two lines.

Done when

  • No as unknown as remains in cli/, or each survivor is listed with the reason it cannot be removed — the way the layering script already lists its two.
  • check-cli-layering.mjs scans cli/tests as well, so this cannot silently grow back.
  • Every port whose partial double motivated a cast is either fully faked in a shared helper or narrowed, and which of the two was chosen is recorded per port.
  • FileHash and any other branded type has a test-only constructor, so no test invents one through a cast.
  • The two production casts are removed or, if one genuinely cannot be, its reason is written where the next reader will look.

Sequencing

After #687. The telemetry work is the v1 path; 22 files of unrelated churn on top of it would make both diffs unreadable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions