Skip to content

test+fix(setup): complete __DREAM_RESULT__ sentinel contract (exception path + tests)#1019

Open
yasinBursali wants to merge 5 commits intoLight-Heart-Labs:mainfrom
yasinBursali:test/setup-wizard-template-picker-coverage
Open

test+fix(setup): complete __DREAM_RESULT__ sentinel contract (exception path + tests)#1019
yasinBursali wants to merge 5 commits intoLight-Heart-Labs:mainfrom
yasinBursali:test/setup-wizard-template-picker-coverage

Conversation

@yasinBursali
Copy link
Copy Markdown
Contributor

@yasinBursali yasinBursali commented Apr 23, 2026

✅ Rebased on current main 2026-04-28 + audit feedback addressed

#1003 has merged. The PR diff reduced to: routers/setup.py exception-path sentinel + 3 new test files (484 lines total). Two test fixes added on top per the audit:

  • SetupWizard.test.jsx — mock by URL: replaced fetch.mockImplementationOnce with a URL-dispatched fetch helper (makeFetchMock + stubFetchWithSetupStream). Step 2 of the wizard fetches /api/templates and /api/extensions/catalog before the diagnostic POST, so the one-shot mock was being consumed by the wrong call. Routing by URL is the only stable pattern.
  • TemplatePicker.a11y.test.jsx — narrow selector: narrowed the decorative-icon selector from container.querySelectorAll('svg') (which caught the unhidden HardDrive disk-size glyph) to div.rounded-lg svg — the status icon wrapper specifically. The test's stated intent ("Loader2, AlertTriangle, Check, default Icon") now matches what it actually asserts.

What

Backend — routers/setup.py exception-path sentinel

Wraps the run_setup_diagnostics generator body in try/except. Re-raises CancelledError and OSError (transport-level concerns — client disconnect, broken pipe — should propagate normally). Catches generic Exception, logs with logger.exception(), yields a FAIL:1 sentinel so the frontend parser never gets stuck in partial-state UI.

The except Exception is tagged # noqa: BLE001 — sentinel contract requires *some* terminal signal. CG reviewed and accepted this as a CLAUDE.md §2 I/O-boundary exception — narrow, logged, and the wire contract with the React parser is explicit.

Tests (3 new files, 18 assertions)

  • pytest — test_setup_sentinel.py (5 cases): asserts PASS:0 on success, FAIL:<rc> on non-zero exit, fallback sentinel when script missing, machine-parseable regex format, auth required. 5/5 pass locally.
  • Vitest — SetupWizard.test.jsx (6 cases): sentinel parser happy path (PASS and FAIL with rc), fallback-scan on "All tests passed!" without sentinel, default-failure when neither signal is present, AbortController unmount cleanup, mocked ReadableStream chunk split across sentinel boundary.
  • Vitest — TemplatePicker.a11y.test.jsx (7 cases): isApplied render branch + aria-hidden="true" on decorative Lucide icons (scoped to status icon wrapper) + defensive undefined-template handling.

Testing

  • pytest: 5/5 pass (python3 -m pytest tests/test_setup_sentinel.py -v)
  • Vitest: 13/13 pass on the rebased branch (npx vitest run SetupWizard.test.jsx TemplatePicker.a11y.test.jsx)
  • Pre-commit hooks clean
  • Sentinel format matches frontend parser regex at SetupWizard.jsx:104: ^__DREAM_RESULT__:(PASS|FAIL):(-?\d+)$

Platform Impact

  • All three: dashboard runs in browser; dashboard-api in Docker. Same behavior across macOS / Linux / Windows (WSL2).

Known Considerations (will file as fork follow-up issues if not addressed in review)

  1. The exception-path yield is not directly exercised by the 5 pytest cases — forcing it requires monkeypatching asyncio.create_subprocess_exec to raise. Small follow-up test worth adding.
  2. The HardDrive disk-size glyph in TemplatePicker does not currently carry aria-hidden="true". Production a11y could be tightened by adding it; this PR's test scope is decorative status icons, so the test selector was narrowed instead. A separate small follow-up could add the marker.

@Lightheartdevs
Copy link
Copy Markdown
Collaborator

Audit follow-up: needs test fixes before merge.

The sentinel contract work is directionally good, but the new SetupWizard tests mock the wrong fetch call: Step 2 consumes /api/templates and /api/extensions/catalog before the diagnostic POST, so one-shot stream mocks get used too early. The a11y assertion also currently catches an unhidden HardDrive icon in TemplatePicker. Please mock by URL or queue Step 2 responses, and mark/narrow the decorative icon test.

yasinBursali and others added 4 commits April 28, 2026 22:55
The PASS/FAIL paths already terminate the /api/setup/test stream with
the machine-readable sentinel that the SetupWizard parser consumes, but
an unexpected exception inside run_tests() (e.g. a process.stdout read
error) would exit the generator without yielding anything terminal. The
frontend defaults absence-of-sentinel to "failure" and falls back to
scraping log lines for "All tests passed!", which is fragile.

Wrap the existing try/finally body in a nested try/except that catches
generic Exception, logs the trace, and yields a final FAIL sentinel
before unwinding. CancelledError and OSError are re-raised so client
disconnects and process kills propagate normally instead of trying to
write into a dead stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…icker a11y

SetupWizard.test.jsx mocks fetch with a chunk-yielding ReadableStream
shim and pins six diagnostic-stream behaviors:
- PASS sentinel transitions the run to "All systems operational" and
  hides the sentinel line from user-visible output.
- FAIL sentinel surfaces the failure UI and keeps Complete Setup
  disabled (tested=false).
- Absent sentinel falls back to scanning collected output for
  "All tests passed!" trailer (older backend compatibility).
- Stream with neither sentinel nor success trailer defaults to
  failure — wizard refuses to greenlight unknown outcomes.
- Sentinel split across two chunks (e.g. "...PA" + "SS:0\n") still
  parses, exercising the buffer-and-split decoder loop.
- Component unmount during an in-flight diagnostic fires the cleanup
  effect which aborts the AbortController; the captured signal goes
  from aborted=false to aborted=true.

TemplatePicker.a11y.test.jsx pins:
- Available card is enabled with the template name in its accessible
  name; isApplied / inProgress / hasErrors all disable the button.
- The corresponding "Applied" / "Installing…" / "Has errors" status
  caption is rendered as text inside the card.
- Every Lucide icon emitted by the picker carries aria-hidden="true"
  so the adjacent text label is the screen-reader-visible carrier.
- Empty and undefined template lists render nothing (defensive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ission

Locks in the streaming contract that the SetupWizard frontend parser
consumes. Five tests against /api/setup/test:
- Script exits 0 → final stream line is "__DREAM_RESULT__:PASS:0".
- Script exits 3 → final stream line is "__DREAM_RESULT__:FAIL:3"
  (the literal subprocess returncode, not a coerced 1).
- Script missing on both INSTALL_DIR/scripts and cwd → error_stream()
  fallback still emits a structurally valid sentinel.
- Sentinel matches the exact regex the frontend pins
  (^__DREAM_RESULT__:(PASS|FAIL):(-?\d+)$) — guards against stray
  whitespace, prefix drift, or trailing-character regressions on either
  side of the contract.
- Unauthenticated POST returns 401 before any stream is opened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two test-only fixes addressing maintainer audit feedback:

1. SetupWizard.test.jsx: replace fetch.mockImplementationOnce with a
   URL-dispatched fetch mock helper (makeFetchMock + stubFetchWithSetupStream).
   Step 2 of the wizard fetches /api/templates and /api/extensions/catalog
   before the diagnostic POST — the previous mockImplementationOnce was
   consumed by the wrong call, leaving /api/setup/test unmocked. Routing
   by URL is the only stable pattern.

2. TemplatePicker.a11y.test.jsx: narrow the decorative-icon selector
   from `container.querySelectorAll('svg')` (which caught the unhidden
   HardDrive disk-size glyph) to `div.rounded-lg svg` — the status icon
   wrapper specifically. Out-of-scope SVGs (HardDrive) are no longer
   asserted by this test, matching the test's stated intent ("decorative
   status icons (Loader2, AlertTriangle, Check, default Icon)").

All 13 tests in the two files pass: 6 SetupWizard sentinel-parser cases
+ 7 TemplatePicker a11y/defensive cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yasinBursali yasinBursali force-pushed the test/setup-wizard-template-picker-coverage branch from f8016f1 to a69f2af Compare April 28, 2026 19:58
@yasinBursali
Copy link
Copy Markdown
Contributor Author

Rebased on current main (#1003 has merged) and addressed both audit asks in a follow-up commit:

  1. Mock by URL — replaced fetch.mockImplementationOnce with a URL-dispatched fetch helper. Step 2's /api/templates + /api/extensions/catalog calls no longer consume the diagnostic-stream mock prematurely.
  2. Narrow a11y selector — scoped the decorative-icon assertion from container.querySelectorAll('svg') to div.rounded-lg svg (the status icon wrapper). HardDrive disk-size glyph is no longer caught by this test.

Re-ran on the rebased branch: pytest 5/5 + Vitest 13/13 pass. Description updated; left a note about adding aria-hidden to HardDrive as a separate small follow-up.

CI Lint-Python-with-Ruff job was failing on this PR with:

  F401 [*] `os` imported but unused
   --> dream-server/extensions/services/dashboard-api/tests/test_setup_sentinel.py:12:8

The `os` module is referenced only inside the docstring at line 55
(`Path(os.getcwd()) / "dream-test-functional.sh"`), not in actual code.
The `stat` module IS used (`stat.S_IXUSR`/`S_IXGRP`/`S_IXOTH` at line 45)
so it stays.

Verified: `ruff check --select E,F,W` passes; 5/5 pytest cases still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants