T-B75W-dashboard-test-isolates-live-dashboards
Status: open/ready · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-08-03 because the task was spawned non-interactively from a post-mortem. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
The dashboard test suite is not isolated from dashboards running in other
sessions: sdlc dashboard list reads one machine-global registry at
~/.sdlc/dashboards.json, so a concurrent bun test apps/sdlc prints a row
for whatever dashboard another checkout happens to have live. That row carries
a bare PID column that the quality baseline’s masking rules do not touch, so
the line surfaces as new drift on every baseline diff. Give the registry an
environment-controlled path so the suite reads its own empty registry instead
of the machine’s.
Concurrent
bun test apps/sdlcruns across parallel sessions cross-talk: the dashboard suite’sdashboard listpicked up another session’s live dashboard and reported it as new drift, because that table row carries an un-normalized PID. The dashboard test needs isolation from concurrently-running sibling suites, or its row needs the same<PID>normalization the surrounding lines already get.
— from T-5G81-lease-payload-gates
| Location | Role today |
|---|---|
apps/sdlc/lib/services/dashboard/server.ts#REGISTRY_PATH | Module-load constant pinning the registry to ~/.sdlc/dashboards.json; no override seam, so every process on the machine shares one file. |
apps/sdlc/lib/services/dashboard/server.ts#registryList | Reads that shared file and returns one row per live dashboard, regardless of which session or checkout started it. |
apps/sdlc/lib/services/dashboard/server.ts#listDashboards | Writes a PROJECT / PID / URL table straight to process.stdout; the PID column is a bare integer with no pid keyword in front of it. |
apps/sdlc/lib/services/dashboard/dashboard-service.ts#lifecycleList | The sdlc dashboard list leaf; delegates to listDashboards() and documents that the output bypasses ctx.io. |
apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts | Invokes the list leaf and calls registryRemove() against the real machine-global registry, so sibling sessions leak into the suite’s stdout and the suite mutates the user’s registry. |
apps/sdlc/lib/services/quality/baseline.ts#normalizeFinding | Masks tmpdir paths, commit SHAs, :<PORT>, keyword-anchored pid <PID>, and (<TIME>); the dashboard row’s bare PID column matches none of these masks. |
sdlc.yaml | Registers bun test apps/sdlc as a quality verb, so every stdout line the suite emits becomes a baseline finding. |
Proposed
Section titled “Proposed”The registry path resolves through a function that reads
SDLC_DASHBOARD_REGISTRY at call time and falls back to
~/.sdlc/dashboards.json. The dashboard suite points that variable at a
per-run temp file, so sdlc dashboard list inside the suite sees an empty
registry and deterministically prints (no dashboards running) no matter what
other sessions are doing. As a side benefit the suite stops writing to the
user’s real registry. The quality baseline’s masking rules are left alone —
the drift disappears at its source rather than being masked after the fact.
Approach
Section titled “Approach”- In
apps/sdlc/lib/services/dashboard/server.ts, replace theexport const REGISTRY_PATHconstant withexport function registryPath(): string, returningprocess.env["SDLC_DASHBOARD_REGISTRY"]when it is set and non-empty andjoin(homedir(), ".sdlc", "dashboards.json")otherwise. Resolving per call rather than at module load is what lets a test set the variable after the module is imported. - Update the reads and writes inside
registryReadandregistryWriteto callregistryPath(), and delete the constant. Nothing outsideserver.tsreferenced it, so no other module changes. - In
apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts, add abeforeAllthatmkdtempSyncs a directory, setsprocess.env["SDLC_DASHBOARD_REGISTRY"]todashboards.jsoninside it, and records the previous value; add anafterAllthat restores the previous value (deleting the key when it was unset) and removes the directory. The CLI harness inapps/sdlc/lib/tests/_cli_harness.tsmergesprocess.envinto every child it spawns, so subprocesssdlc dashboard …runs inherit the override without further plumbing. - Tighten the
list returns 0 …test: with the registry isolated the output is deterministic, so assert stdout contains(no dashboards running)instead of only checking the exit code, and drop the comment that excuses the non-determinism. - Add a unit test covering both arms of
registryPath()— variable set, and variable unset falling back to the home-directory path. - Reproduce and confirm: start a dashboard from a second checkout
(
sdlc dashboard start), runbun test apps/sdlc/lib/services/dashboard, and check that the suite’s stdout is byte-identical to a run with no dashboard live.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/services/dashboard/server.ts#REGISTRY_PATH | modify | Replace the module-load constant with a registryPath() reader honoring SDLC_DASHBOARD_REGISTRY. |
apps/sdlc/lib/services/dashboard/server.ts#registryRead | modify | Resolve the registry file through registryPath() on each read; same for registryWrite. |
apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts | modify | Point the suite at a per-run temp registry in beforeAll/afterAll, assert the deterministic empty-registry output, and cover both arms of registryPath(). |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
registryPath()returns the value ofSDLC_DASHBOARD_REGISTRYwhen that variable is set and non-empty, andjoin(homedir(), ".sdlc", "dashboards.json")when it is unset; both arms are asserted by tests inapps/sdlc/lib/services/dashboard/tests/dashboard.test.ts. - AC-2: The
listtest inapps/sdlc/lib/services/dashboard/tests/dashboard.test.tsasserts stdout contains(no dashboards running)and passes while a dashboard is live for a different project root. - AC-3:
shasum ~/.sdlc/dashboards.jsonis unchanged across abun test apps/sdlc/lib/services/dashboardrun. - AC-4:
grep -rn "REGISTRY_PATH" apps/sdlcreturns no matches — the registry file is reached only throughregistryPath(). - AC-5:
bun test apps/sdlcproduces identical stdout with and without a dashboard running in another checkout, sosdlc quality baseline diffreports no new drift for that verb.
Out of scope
Section titled “Out of scope”- Extending
normalizeFindinginapps/sdlc/lib/services/quality/baseline.tsto mask a bare PID column. Isolation removes the drift at its source, and a column-position mask would be far broader than the keyword-anchored rule that module documents. - Routing
listDashboards()output throughctx.ioinstead ofprocess.stdout. The column-width calculation is written against the real stream and isolation does not need the change. - Isolating the other machine-global state sibling suites read
(
~/.config/sdlc/host.yaml,~/.claude/projects/).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-08-03 UTC from
T-5G81-lease-payload-gates in git@github.com:sksizer/dev.git.