Test coverage: mailbox owner-resolution wiring (escalateHeldToOwner) and cleanupBuilder() held-row dismissal (#1477) - #1625
Test coverage: mailbox owner-resolution wiring (escalateHeldToOwner) and cleanupBuilder() held-row dismissal (#1477)#1625mohidmakhdoomi wants to merge 11 commits into
Conversation
… held-row dismissal Two Spec 1313 round-3 glue points were verified by inspection only. Cover both directly, with no production change. escalateHeldToOwner (servers/mailbox-wiring.ts:405) was reachable in tests only as a stub — send-delivery.test.ts replaces the port, so it tests the drainer's decision to escalate, never the binding that resolves the owner. Drive it through the exported makeDeliveryPorts seam instead: architect-skip, spawning-architect affinity, the main/id-order fallback chain, supersede-keyed coalescing, the drain scheduled for the resolved owner, cross-workspace resolution, and the keyed clearHeldOwnerNotice. cleanupBuilder's dismissal (commands/cleanup.ts:389) was covered by a test that says in its own header it re-implements the seam. Drive the real exported cleanup() end to end instead, so the normalized-workspace round-trip and the #1118 cross-workspace scoping are actually asserted. Both substitute only the DB singleton (one in-memory GLOBAL_SCHEMA database from getDb/getGlobalDb, the Issue #1118 production shape), leaving state.ts, tower-messages.ts, db/mailbox.ts and workspace-path.ts real. Every assertion mutation-checked against the real source: removing the dismissal call, its workspace scope, the sender affinity argument, the architect-skip guard, the scheduleDrain target, or the clear-notice key each fails at least one test. Refs #1313 (PR #1330), maintainer optional-2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architect integration review — 3-way CMAPVerdicts: gemini APPROVE (HIGH) · codex COMMENT (MEDIUM) · claude REQUEST_CHANGES (HIGH). Reviewed at high depth: the diff is +615 lines, which crosses the >500 line threshold in All three agree the PR is well-integrated and that both #1477 premises were real. I verified the substantive findings against the source myself rather than relaying them. Blocking — one vacuous test (claude, verified)
Two consequences:
Fix: make the two paths genuinely differ, e.g. a symlinked root: const realRoot = realpathSync(mkdtempSync(join(tmpdir(), 'air-1477-cleanup-')));
const linkRoot = join(realpathSync(tmpdir()), `air-1477-link-${process.pid}`);
symlinkSync(realRoot, linkRoot); // rmSync in afterAll
// config({ workspaceRoot: linkRoot }) → a raw key ≠ WS → dismisses nothingThe Also fix (codex, both confirmed by reading)
Non-blocking
The production observation is real — architect verifiedConfirmed against source, independently of the builder's report:
So an offline hold can name a different architect than a live send to the same workspace, and the doc comments at What was verified as accurateTest-only is real (no |
… tied startedAt Review round 2 on PR #1625. The workspace-normalization test pinned nothing. Two mistakes stacked: the fixture realpathSync'd its root at creation, so normalizeWorkspacePath on it was the identity; and the "non-canonical" override join(root,'nested','..') collapses lexically inside path.join, before normalization is ever reached. So config.workspaceRoot === WS in every test and the suite stayed green with normalizeWorkspacePath stripped from cleanup.ts:389 — reproduced before fixing. The fixture now creates a real directory and a SYMLINK to it, and hands the symlink to getConfig(), so every test in the file traverses the normalization rather than just one. A symlink cannot be resolved lexically; only realpathSync gets from one name to the other. A guard assertion fails loudly if the two paths ever become equal again. Stripping normalizeWorkspacePath now fails 3 tests. Round 1 had "verified this by mutation" — but only mutated the call, never its argument. A mutation sweep is only as good as the mutations you think to try. Also: scheduleDrain's mockRestore was the last statement of its it() body, so a failed assertion above it leaked the spy into later tests — moved to afterEach. And both architects in the fallback fixture shared new Date(), which can tie; startedAt is now explicit, with zeta registered EARLIER so id order and registration order disagree. Switching getArchitects to ORDER BY started_at now fails that test, which it would not have before. Cross-referenced spec-1313-cleanup-dismiss.test.ts's header to this invocation test so the seam-vs-wiring split is discoverable from either file. Mutation sweep now 7 points, all caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Architect follow-up — blocker resolved, verified independently
The vacuous test is now discriminating. The fixture makes Better than what was asked for: Both codex items fixed and checked. The false prose is corrected in both the inline comment and the PR description, the latter carrying an explicit correction paragraph rather than a quiet rewrite. That matters for the git record. Three non-blocking items were declined with reasons I accept: Mutation sweep is now 7 points, all caught. The builder's own retrospective is the most useful line in this round and worth recording for the cohort: "I mutated the CALL and never its ARGUMENT, so the one thing that test existed to pin was the one thing I never tried to break. A mutation sweep is only as good as the set of mutations you think to try." No further changes requested from me. The |
…ded link
PR-stage CMAP (gemini APPROVE, codex COMMENT, claude APPROVE — no blockers).
The module-scope symlinkSync uses a PID-derived name, so a hard-killed run
(SIGKILL/OOM) skips afterAll and strands the link; a later run drawing the same
PID then fails with EEXIST at COLLECTION time, losing the whole file rather than
one test, and with an opaque error. rmSync(..., { force: true }) first makes the
fixture idempotent. Verified both ways: EEXIST without it, clean with it.
Thread log records the PR-stage verdicts, the stale size disclosure codex caught
(511 was the round-1 count, left unrevised after round 2 grew the files; actual
is 544), and why the ORDER BY id follow-up issue is the architect's to file
rather than mine despite a reviewer flagging that it currently has no durable
record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The human's call was that I file the resolveRegistryArchitect divergence rather than leave it as a comment in a test file, so it is now Issue #1633 (area/tower, deliberately not under #1483 — that tracks the post-1313 mailbox program, this is a Spec 755 defect). Cross-linked both directions so neither record dies alone: the test names the issue and says that fixing #1633 SHOULD break this assertion (flip it to zeta, do not delete it), and the issue names the test and explains why it pins current behaviour rather than the documented contract. Worth recording from writing that issue: the same bug was already fixed once, in the singular. getArchitect (state.ts:487) uses ORDER BY started_at and carries the comment "not the lexicographically-first name", with spec-755-migration.test.ts:279 pinning it as a Codex-flagged regression. getArchitects (plural) — what the offline resolver actually reads — never got the same treatment. Half-fixed, not merely inconsistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #1477.
Test-only. No production file is modified —
git diffagainstmaintouches nothing undersrc/, and all three reviewers verified that independently.Issue #1477 named two Spec 1313 round-3 glue points that were verified by inspection only. Both
are now covered directly.
What was actually uncovered
escalateHeldToOwner(servers/mailbox-wiring.ts:405)send-delivery.test.tsonly ever stubsports.escalateHeldToOwner(:150,:1189,:1196), so it tests the drainer's decision to escalate, never the binding that resolves the owner.cleanupBuilder's held-row dismissal (commands/cleanup.ts:389)spec-1313-cleanup-dismiss.test.tssays in its own header that it re-implements the seam (mailbox.dismissHeldForAgentcalled directly) and that "the fullcleanupBuilder… is out of scope here".The resolvers underneath (
resolveAgentInRegistry) andformatOwnerNoticeBodyare already testedin isolation. These two files are the missing third leg: the binding between them.
Approach — substitute the DB singleton, nothing else
vi.mock('../db/index.js')returns one in-memoryGLOBAL_SCHEMAdatabase from bothgetDb()andgetGlobalDb()— which is the Issue #1118 production shape. That leavesstate.ts,servers/tower-messages.ts,db/mailbox.tsandutils/workspace-path.tsall running for real, sothe tests observe rows the production code path actually transitioned.
Precedent:
spec-755-lookup-builder.test.ts, improved on by importing the realGLOBAL_SCHEMArather than hand-copying a
CREATE TABLE.The cleanup test drives the real exported
cleanup()end to end, stubbing only side-effectingcollaborators (git, Tower, forge,
ps, state writes).Coverage
air-1477-owner-escalation-wiring.test.ts(9 tests) — architect-skip; spawning-architectaffinity;
mainfallback when the spawner is deregistered; the no-maintie-break; the no-op thatreturns
falseso the drainer's once-per-episode guard stays unarmed and retries; supersede-keyedcoalescing; drain scheduled for the resolved owner; cross-workspace owner resolution;
clearHeldOwnerNoticekey specificity.air-1477-cleanup-dismiss-invocation.test.ts(4 tests) — dismissal reached fromcleanup(),siblings untouched; the normalized-workspace round-trip; cross-workspace isolation of the same
builder id (#1118); ephemeral builders; non-fatal on a DB failure.
The fixture hands
getConfig()a symlink to the workspace directory while the mailbox keysrows under the canonical path, so every test in that file traverses
normalizeWorkspacePathratherthan just one, and a guard assertion fails loudly if the two paths ever collapse to the same
string.
Verification
Every assertion was mutation-checked against the real source, then the source restored:
dismissHeldForAgent(...)fromcleanup.ts:389normalizeWorkspacePathfrom that callsenderarg fromresolveAgentInRegistry('architect', ws, info.toAgent)getArchitectByNamearchitect-skip guardscheduleDrainat the starving agent instead of the ownerclearHeldOwnerNoticeignore the agent keygetArchitectstoORDER BY started_at13 tests pass;
tsc --noEmitclean; the fullagent-farmsuite is green (163 files, 3442 tests).On size, against AIR's "<300 LOC" framing:
This number has been wrong three times now — 511 went stale when the round-2 fixes grew the files,
544 when the
rmSynclines landed, 554 when the #1633 cross-link did. Each time I retyped asnapshot instead of re-running the command. Pasting the output so the next reader can see what it
counts, and re-checked against the final commit. Comment density is high, but flagging the number rather than letting you
discover it. Happy to trim; the cross-workspace cases are the ones I'd cut last.
Note for anyone running the suite in a fresh worktree: 12 unrelated suites fail until
pnpm bundle-assetsandtschave run (they needpackages/codev/skeleton/anddist/). All 12pass afterwards.
Correction to an earlier revision of this description
The first push of this PR claimed the normalization test proved "a raw
config.workspaceRootwouldkey a different string and dismiss nothing." That was false, and the review caught it. The
fixture pre-
realpathSync'd its root (makingnormalizeWorkspacePaththe identity) and its"non-canonical" override
join(root, 'nested', '..')collapsed lexically insidepath.joinbefore normalization was ever reached — so the two strings were already equal and the suite stayed
green with
normalizeWorkspacePathdeleted fromcleanup.ts. I reproduced that before fixing it.Round 1 said the tests were "mutation-checked", and they were — but I mutated the call and never
its argument, so the one thing that test existed to pin was the one thing I never tried to break.
The symlink fixture fixes it, and stripping
normalizeWorkspacePathnow fails 3 tests.Reviewed 3 ways
Gemini APPROVE. Codex and Claude each found real defects in the first draft, all fixed here — most
importantly that
scheduleDrainwas uncovered: the drainer is never started in a unit test, sothe call no-ops and deleting it left every test green. It now has a spy asserting the drain targets
the resolved owner.
Two other corrections worth naming, because both were cases of the tests claiming more than they
did:
owner.agent === info.toAgentself-notify guard. It does not —that branch is unreachable under the real resolver, since resolving to an architect requires that
architect to be registered, which the architect-skip already caught. Comment corrected rather than
mocking a path to dead code.
TowerClientstub was backwards: it implementedkillTerminal(never called) and omittedrefreshOverview(called atcleanup.ts:422), so every test silently ran the Tower-unreachablebranch.
Round 2 (on this PR) fixed the vacuous test above, plus a
scheduleDrainspy whosemockRestoresat at the end of the
itbody (a failed assertion above it would leak the spy into later tests),and an architect fixture where both records shared
new Date()and could tie —startedAtis nowexplicit, with
zetaregistered earlier, so id order and registration order genuinely disagree.Two review items are deliberately not addressed, rather than silently skipped:
scheduleDrain'sworkspace argument cannot be pinned from outside (
resolveRegistryArchitectreturns the workspaceit was asked about, so owner and starving workspace are equal by construction — noted in a comment
instead of faked), and the
loadConfig(homedir())hermeticity smell is inherited from productionand would need a production change.
One production observation — now filed as #1633
resolveRegistryArchitecttakesarchitects[0]fromgetArchitects, which isORDER BY id—alphabetical by architect name. The doc comments there and in
mailbox-wiring.tssay"first-registered", and both
loadState(ORDER BY (id != 'main'), started_at) and the liveresolver (registry insertion order) implement it that way. So an offline hold can name a different
architect than a live send to the same workspace would.
Deterministic and low-impact, and changing it is a production decision outside a test-only AIR — so
the test pins current behaviour and says plainly in a comment that it diverges from the documented
contract, rather than either blessing the wording or quietly asserting the aspiration.
Filed as #1633. Worth noting there:
getArchitect(singular) was already fixed for exactly thisbug —
spec-755-migration.test.ts:279pins it as "started_at order, not lexicographic" — butgetArchitects(plural), which the offline resolver reads, never got the same treatment. The fixlanded on one of the two functions.
PR-stage CMAP (AIR
prphase)Distinct from the architect's integration review above — different stage, different prompt.
gemini APPROVE (HIGH) · codex COMMENT (HIGH) · claude APPROVE (HIGH). No blockers.
Claude independently re-ran three mutations against real source (restoring afterwards) and
reproduced the claimed counts exactly: stripping
normalizeWorkspacePath→ 3 failures, retargetingscheduleDrain→ 1, dropping thesenderarg → 2. It also confirmed CI runs ubuntu + macos only,so the
symlinkSyncfixture has no Windows exposure.Two review items acted on:
round-1 number left unrevised after round 2 grew the files.
symlinkSyncused a PID-derived name, so a hard-killed run(SIGKILL/OOM) could strand the link and make a later run drawing the same PID fail with
EEXISTat collection, losing the whole file rather than one test. Now
rmSync(…, { force: true })first, which I verified reproduces
EEXISTwithout it and succeeds with it.Claude also noted the
ORDER BY iddivergence had no filed issue and could die with this thread.The human's call was that I file it: #1633. The test that pins the current behaviour now names
that issue in its comment, and the issue names the test — including the fact that fixing #1633
should break that assertion, and that flipping it to
zetais the intended response rather thandeleting it.
Refs #1313 (PR #1330), maintainer optional-2 in the Follow-up section of
codev/reviews/1313-afx-send-mailbox-first-delivery.md.🤖 Generated with Claude Code