Skip to content

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/sdlc runs across parallel sessions cross-talk: the dashboard suite’s dashboard list picked 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

LocationRole today
apps/sdlc/lib/services/dashboard/server.ts#REGISTRY_PATHModule-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#registryListReads 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#listDashboardsWrites 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#lifecycleListThe sdlc dashboard list leaf; delegates to listDashboards() and documents that the output bypasses ctx.io.
apps/sdlc/lib/services/dashboard/tests/dashboard.test.tsInvokes 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#normalizeFindingMasks tmpdir paths, commit SHAs, :<PORT>, keyword-anchored pid <PID>, and (<TIME>); the dashboard row’s bare PID column matches none of these masks.
sdlc.yamlRegisters bun test apps/sdlc as a quality verb, so every stdout line the suite emits becomes a baseline finding.

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.

  1. In apps/sdlc/lib/services/dashboard/server.ts, replace the export const REGISTRY_PATH constant with export function registryPath(): string, returning process.env["SDLC_DASHBOARD_REGISTRY"] when it is set and non-empty and join(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.
  2. Update the reads and writes inside registryRead and registryWrite to call registryPath(), and delete the constant. Nothing outside server.ts referenced it, so no other module changes.
  3. In apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts, add a beforeAll that mkdtempSyncs a directory, sets process.env["SDLC_DASHBOARD_REGISTRY"] to dashboards.json inside it, and records the previous value; add an afterAll that restores the previous value (deleting the key when it was unset) and removes the directory. The CLI harness in apps/sdlc/lib/tests/_cli_harness.ts merges process.env into every child it spawns, so subprocess sdlc dashboard … runs inherit the override without further plumbing.
  4. 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.
  5. Add a unit test covering both arms of registryPath() — variable set, and variable unset falling back to the home-directory path.
  6. Reproduce and confirm: start a dashboard from a second checkout (sdlc dashboard start), run bun test apps/sdlc/lib/services/dashboard, and check that the suite’s stdout is byte-identical to a run with no dashboard live.
LocationKindChange
apps/sdlc/lib/services/dashboard/server.ts#REGISTRY_PATHmodifyReplace the module-load constant with a registryPath() reader honoring SDLC_DASHBOARD_REGISTRY.
apps/sdlc/lib/services/dashboard/server.ts#registryReadmodifyResolve the registry file through registryPath() on each read; same for registryWrite.
apps/sdlc/lib/services/dashboard/tests/dashboard.test.tsmodifyPoint the suite at a per-run temp registry in beforeAll/afterAll, assert the deterministic empty-registry output, and cover both arms of registryPath().
  • AC-1: registryPath() returns the value of SDLC_DASHBOARD_REGISTRY when that variable is set and non-empty, and join(homedir(), ".sdlc", "dashboards.json") when it is unset; both arms are asserted by tests in apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts.
  • AC-2: The list test in apps/sdlc/lib/services/dashboard/tests/dashboard.test.ts asserts stdout contains (no dashboards running) and passes while a dashboard is live for a different project root.
  • AC-3: shasum ~/.sdlc/dashboards.json is unchanged across a bun test apps/sdlc/lib/services/dashboard run.
  • AC-4: grep -rn "REGISTRY_PATH" apps/sdlc returns no matches — the registry file is reached only through registryPath().
  • AC-5: bun test apps/sdlc produces identical stdout with and without a dashboard running in another checkout, so sdlc quality baseline diff reports no new drift for that verb.
  • Extending normalizeFinding in apps/sdlc/lib/services/quality/baseline.ts to 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 through ctx.io instead of process.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/).
  • none

Spawned by /sdlc:spawn-task-pr on 2026-08-03 UTC from T-5G81-lease-payload-gates in git@github.com:sksizer/dev.git.


← Back to Tasks