Skip to content

test: cover state/run/graph handlers + state internals (#29)#35

Merged
lex00 merged 5 commits into
mainfrom
feat/handler-test-coverage-issue-29
May 9, 2026
Merged

test: cover state/run/graph handlers + state internals (#29)#35
lex00 merged 5 commits into
mainfrom
feat/handler-test-coverage-issue-29

Conversation

@lex00
Copy link
Copy Markdown
Contributor

@lex00 lex00 commented May 9, 2026

Summary

Closes #29. Goes from 0 tests on cli/handlers/{state,run,graph}.ts and the state internals to a layered, mock-backed test surface that makes the area refactorable. Six new test files + two mock factories + a small pre-existing-bug fix that surfaced during the work.

Coverage delta

Source file LOC Before After
packages/core/src/state/digest.ts 88 0 tests 11 tests
packages/core/src/state/snapshot.ts 179 0 tests 6 tests
packages/core/src/state/git.ts 317 0 tests 7 tests
packages/core/src/cli/handlers/state.ts 374 3 tests (from #32) 14 tests
packages/core/src/cli/handlers/graph.ts 23 0 tests 5 tests
packages/core/src/cli/handlers/run.ts 453 0 tests 13 tests

Workspace test totals: 1434 → 1493 (+59 new tests).

Commits

  1. chore(test-utils): add mockTemporalClient factory — configurable describe/history/list responses + mock.calls recorder for signal/cancel/start. 6 self-tests.
  2. test(state): cover digest + snapshot internals — pure-function and orchestration tests using the existing mockPlugin factory.
  3. test(state/git): orphan-branch operations against a real temp git repo — uses withTestDir + git init. Found and fixed a pre-existing dead-call hang in writeSnapshot: a git hash-object -w --stdin invocation with nothing piped to stdin that hung tests forever. The actual blob write happens via the shell-pipeline call directly below it; the dead call was harmless in production where the chant CLI inherits a closed-stdin terminal but blocked test contexts.
  4. test(cli): graph handler + extended state handler coverage — covers runStateSnapshot/Show/Log/Unknown (existing --live diff tests preserved) and the small runGraph handler.
  5. test(cli): cover run handlers with mockTemporalClient — covers runOpList/Status/Log/Signal/Cancel happy paths and error paths.

Intentionally out of scope

runOp itself — the main chant run <name> command — is not covered. It spawns a worker subprocess (spawn(['npx', 'tsx', ...])) and connects to a live Temporal server. Mocking those side effects is a much bigger harness than the rest of the run handlers needed, and would partly duplicate examples/temporal-self-hosted smoke testing. Worth a follow-up issue if priorities shift.

Verification

  • just build clean
  • npx vitest run → 1493/1493 pass (workspace-wide)
  • All new test files follow the structure of packages/core/src/cli/commands/build.test.ts

Mock factories now available from @intentius/chant-test-utils

Test plan

  • CI green
  • Coverage report shows nonzero coverage on each of the six target files (per npx vitest run --coverage)

lex00 added 5 commits May 9, 2026 12:40
Adds createMockTemporalClient() and supporting types for testing chant
run handlers (and any other code that depends on @temporalio/client)
without spinning up a real cluster.

Configurable surface:
  - describeByWorkflowId: per-workflow describe() responses
  - historyByWorkflowId:  per-workflow fetchHistory() events
  - list:                 workflows yielded by workflow.list()
  - describeError:        if set, every describe() throws (simulates
                          UNAVAILABLE / no cluster)

Recorded call surface (mock.calls):
  - startCalls:  args to workflow.start()
  - signalCalls: workflow id + signal name
  - cancelCalls: workflow id

Six self-tests cover the recorder + the fixture surface.
Two new test files for the pure-function and orchestration parts of
the state pipeline:

  state/digest.test.ts (11 tests)
    - hashProps: deterministic, order-independent, distinct on different inputs
    - computeBuildDigest: shape + missing-props default + manifest mirror
    - diffDigests: no-prior, identical, changed, removed, mixed scenarios

  state/snapshot.test.ts (6 tests)
    - happy path: writeSnapshot called once per plugin with describeResources
    - plugin without describeResources is skipped
    - plugin throws -> per-plugin error, other plugins still proceed
    - empty result -> error and no snapshot
    - validation drops resources missing type/status with warning
    - sensitive-data warnings for suspect attribute names

snapshot.test.ts uses vi.mock to stub git operations so tests run in
isolation. Both files use createMockPlugin from @intentius/chant-test-utils.
#29)

Adds 7 tests using withTestDir() + a freshly-initialized git repo to
exercise the plumbing in state/git.ts:

  - writeSnapshot creates the orphan branch and JSON is addressable
  - readSnapshot returns null for missing env/lexicon
  - subsequent writes preserve other env+lexicon entries
  - re-writing the same env+lexicon updates the entry (no duplicates)
  - readEnvironmentSnapshots returns all lexicons for an env
  - listSnapshots returns commit history of the orphan branch
  - getHeadCommit returns the working-branch HEAD sha

Discovered a pre-existing dead call in writeSnapshot: a
'git hash-object -w --stdin' invocation with nothing piped into stdin
that hung forever in the test context. Removed; the real blob write
happens via the shell-pipeline call directly below it. The dead call
was harmless in production where the chant CLI inherits a closed-stdin
terminal but blocked test contexts where stdin stays open.
cli/handlers/graph.test.ts (5 tests):
  - empty discovery -> 'No Ops found'
  - ops with no depends -> 'No Op dependencies'
  - prints 'dep -> name' edge per dependency
  - multi-edge graph
  - discovery errors forwarded to stderr

cli/handlers/state.test.ts grows from 3 -> 14 tests:
  + runStateSnapshot: missing env, unknown env, no plugins implementing
    describeResources, happy path with mocked takeSnapshot
  + runStateShow: missing env, specific lexicon found / not found,
    list-all when no lexicon specified
  + runStateLog: empty case, history with multiple entries
  + runStateUnknown: subcommand-list output

State tests use vi.mock to stub build, state/git, state/snapshot, and
config; no real I/O. Graph tests stub op/discover.
cli/handlers/run.test.ts (13 tests) covers the read-only and side-effect
run subcommands without spawning a real Temporal cluster:

  runOpList:
    - empty discovery -> warning + exit 0
    - Temporal unavailable -> degrades gracefully, table still printed
    - Temporal available -> annotates rows with workflow status

  runOpStatus:
    - missing op name -> exit 1
    - connection error -> exit 1 with message
    - happy path: prints workflow id/run id/status + activity counts

  runOpLog:
    - missing op name -> exit 1
    - prints one row per workflow execution from client.workflow.list()

  runOpSignal:
    - missing op or signal name -> exit 1
    - happy path: signal recorded against deterministic workflow id

  runOpCancel:
    - missing op name -> exit 1
    - --force required to cancel
    - --force present -> cancel recorded

runOp itself (the main 'chant run <name>' command) is intentionally not
covered: it spawns a worker subprocess and connects to a live Temporal
server, which would require either a real cluster or a much more
involved process-mock harness. Out of scope for this PR.

Also fixes a small TS spread-order warning in mock-temporal-client.ts
that surfaced when first added.
@lex00 lex00 merged commit 341b2db into main May 9, 2026
9 checks passed
@lex00 lex00 deleted the feat/handler-test-coverage-issue-29 branch May 9, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zero tests on cli/handlers/{state,run,graph}.ts — refactoring is unsafe

1 participant