Skip to content

Add AI-generated E2E testing feature - #3681

Merged
wwwillchen merged 29 commits into
dyad-sh:mainfrom
azizmejri1:feature/ai-e2e-testing
Jul 9, 2026
Merged

Add AI-generated E2E testing feature#3681
wwwillchen merged 29 commits into
dyad-sh:mainfrom
azizmejri1:feature/ai-e2e-testing

Conversation

@azizmejri1

@azizmejri1 azizmejri1 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

HIghlight of the changes made after the last time you saw this :
Interface of the testing panel before enabling testing (In this case we're using a Neon database) :
image

When fixing a test we attach the screenshot now + giving the agent a nudge to read error-context.md file :
image

Review in cubic

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an experimental end-to-end testing feature, allowing users to record user flows in the preview iframe, generate Playwright tests via chat, and run them within a new 'Tests' panel. For database-connected Neon apps, test runs are isolated using ephemeral copy-on-write branches. Feedback focuses on improving the robustness of this system: propagating and checking the abort signal during database setup and server polling, only clearing the test branch ID in the database upon successful deletion to avoid orphaned branches, re-activating the test recorder upon iframe navigation/reload, and safely handling empty paths in test path normalization to prevent directory-write crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/ipc/handlers/tests_handlers.ts Outdated
Comment thread src/ipc/services/isolated_test_db.ts
Comment thread src/ipc/services/isolated_test_db.ts Outdated
Comment thread src/ipc/utils/neon_test_branch.ts
Comment thread src/ipc/utils/neon_test_branch.ts
Comment thread src/components/preview_panel/PreviewIframe.tsx Outdated
Comment thread src/ipc/utils/normalize_test_path.ts

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 4 inline finding(s).

Comment thread src/ipc/handlers/tests_handlers.ts Outdated
Comment thread src/components/preview_panel/TestsPanel.tsx
Comment thread src/components/chat/DyadMarkdownParser.tsx Outdated
Comment thread src/components/preview_panel/TestsPanel.tsx
@dyad-assistant

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge
Recommendation: ready

This is a well-structured, large feature PR (~5900 lines across 47 files) that adds AI-generated E2E testing with a Tests panel, Playwright integration, record-a-test flow, and Neon branch isolation for database-connected apps.

What's done well:

  • Security defense-in-depth: regex-validated test file paths, normalizeTestPath forcing files under tests/, realpathSync + containment checks for screenshots, shell: false on Unix in spawnStreaming, DyadError with proper error kinds for expected failures.
  • The Neon test branch lifecycle is thoughtfully designed: crash recovery via reconcileOrphanTestBranches on startup, env snapshot/restore teardown, and the "never run against real data" dead-end when isolation fails.
  • Good test coverage for new utilities: playwright report parsing, test case extraction, tag parsing, neon branch operations, and isolated test DB orchestration.
  • The infra-vs-assertion error taxonomy (amber vs red) is a smart UX choice that prevents "your app is broken" false alarms from flaky test infrastructure.
  • Accessibility: aria-label, aria-pressed, aria-live="polite" for the counter, keyboard-focusable run buttons with focus-visible:opacity-100.

Issues Summary

Severity File Issue
🟑 MEDIUM src/ipc/handlers/tests_handlers.ts:300 getTestScreenshot containment check fails when app path involves symlinks
🟑 MEDIUM src/components/preview_panel/TestsPanel.tsx:226 FailureDetails shows 'Loading screenshot...' forever when screenshot is unavailable
🟑 MEDIUM src/components/chat/DyadMarkdownParser.tsx:472 dyad-write to DyadGenerateTest redirect catches all .spec.ts files, not just tests
🟑 MEDIUM src/components/preview_panel/TestsPanel.tsx:530 IPC data fetching uses manual state management instead of TanStack Query
🟒 Low Priority Notes (6 items)
  • aggregateFileResult duplicates aggregateTestResults - The file-level aggregation function in TestsPanel.tsx is essentially the same logic as aggregateTestResults in playwright_report.ts. Consider reusing the existing function. (src/components/preview_panel/TestsPanel.tsx)

  • getDyadGenerateTestTags is a near-identical clone of getDyadWriteTags - The new tag parser duplicates the regex-based XML parsing, attribute extraction, and code fence stripping. A generic parseDyadTags(response, tagName) helper would reduce duplication. (src/ipc/utils/dyad_tag_parser.ts)

  • Experimental badge text is not i18n-ized - The "Experimental" badge in the toolbar and Tests panel header is a hardcoded English string rather than using t(...). The locale file gets a preview.tests entry but not the badge. (src/components/preview_panel/PreviewToolbar.tsx)

  • spawn_streaming.ts accumulates unbounded stdout/stderr - Both output strings grow without limit during long Playwright runs. Unlikely to be a practical issue for typical test suites, but a cap or ring buffer would be safer for edge cases. (src/ipc/utils/spawn_streaming.ts)

  • Record button relies on color alone for recording state - The button toggles between Circle (idle) and Square (recording) icons, which is good, but the red/white color is the primary differentiator for sighted users. A pulsing animation or "REC" label would improve accessibility. (src/components/preview_panel/PreviewIframe.tsx)

  • normalizeTestPath passes through input of exactly "tests" - If rawPath is "tests" (no file), it returns "tests" verbatim. This isn't a security issue (downstream validation catches it) but could be tightened. (src/ipc/utils/normalize_test_path.ts)


Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 4/5

All review comments were addressed with code changes or a reasoned engineering decision, lint/typecheck pass, and changes are pushed β€” held at 4 only because the cancellation and isolation paths warrant a manual smoke-test before merge.

Unresolved Threads

Thread Rationale Link

No unresolved threads

Resolved Threads

Issue Rationale Link
Thread cancellation didn't reach the isolated-DB setup phase (signal not passed; waitForServerReady could hang ~120s after Stop) Threaded signal from the handler into prepareIsolatedTestDatabase and into waitForServerReady, which now throws on abort View Β· View Β· View
Failed Neon branch deletion still cleared neonTestBranchId, orphaning branches permanently deleteBranchBestEffort now returns a boolean; the column is cleared only on confirmed deletion, so the startup sweep can retry View Β· View
Recorder silently stopped capturing after in-iframe navigation/reload (active=false re-injected) Re-send recorder state on the iframe load event, with cleanup View
Empty/all-dots normalized test path collapsed to tests/, causing EISDIR on write Fall back to tests/generated.spec.ts when the sanitized path is empty View
getTestScreenshot containment check failed under ancestor symlinks (e.g. /varβ†’/private/var), perpetual "Loading…" Resolve appPath through realpathSync before path.relative View
FailureDetails showed "Loading screenshot…" forever when a screenshot was unavailable Added a loaded flag; renders "Screenshot unavailable" once the fetch settles with no image (Principle #4: Transparent Over Magical, #6: Delightful) View
dyad-write β†’ test-card redirect caught any .spec.ts, including unit specs like src/components/Button.spec.ts Tightened condition to require a tests/ prefix View
Suggestion to migrate loadSpecs from manual state to TanStack Query Resolved with explanation: test state lives in per-app jotai atoms mirroring previewRunStateByAppIdAtom; a partial migration would split state across two systems. Engineering-consistency call (product principles cover product/UX, not renderer data-flow) View
Product Principle Suggestions
  • General: "Add a principle (or sub-note) covering internal code-architecture consistency β€” e.g. 'prefer the established state-management pattern of the surrounding module over introducing a parallel system for marginal gains' β€” so code-health/refactor suggestions can be adjudicated against an explicit rule rather than ad-hoc engineering judgment."

πŸ€– Generated by Claude Code

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 5 inline finding(s).

Comment thread src/ipc/utils/normalize_test_path.ts Outdated
Comment thread src/components/chat/DyadMarkdownParser.tsx Outdated
Comment thread src/components/preview_panel/TestsPanel.tsx Outdated
Comment thread src/ipc/utils/normalize_test_path.ts
Comment thread src/components/preview_panel/PreviewIframe.tsx
@dyad-assistant

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge
Recommendation: auto-fix

Large, well-structured feature PR that adds AI-generated E2E testing with a thoughtful UX (result taxonomy, database isolation, lazy Playwright bootstrap, record-a-test). The security posture for screenshot reading (symlink resolution + containment check), command injection prevention (array args, no shell on Unix), and path normalization is solid. Two mechanical bugs need fixing before merge.

Issues Summary

Severity File Issue
πŸ”΄ HIGH src/ipc/utils/normalize_test_path.ts:22 normalizeTestPath("tests") returns bare directory, causing EISDIR crash on write
πŸ”΄ HIGH src/components/chat/DyadMarkdownParser.tsx:472 Extension mismatch: only .spec.ts gets the test card, not .spec.tsx/.js/.jsx
🟑 MEDIUM src/components/preview_panel/TestsPanel.tsx:569 TestsPanel uses manual IPC + useState instead of TanStack Query
🟑 MEDIUM src/ipc/utils/normalize_test_path.ts:10 normalizeTestPath does not validate spec file extension on write path
🟑 MEDIUM src/components/preview_panel/PreviewIframe.tsx:940 Recorder action handler does not check event.origin

HIGH #1 β€” normalizeTestPath("tests") crashes. If the LLM emits <dyad-generate-test path="tests">, the function returns the bare string "tests" (via sanitized === "tests" early return). response_processor.ts then calls fs.writeFileSync on a directory, throwing an unhandled EISDIR error. Fix: fold the bare "tests" case into the empty-path fallback: if (!sanitized || sanitized === "tests") return "tests/generated.spec.ts";.

HIGH #2 β€” Extension mismatch loses the test card for non-.ts specs. The <dyad-write> β†’ test card routing in DyadMarkdownParser.tsx only checks writePath.endsWith(".spec.ts"), but listAppTests globs *.spec.{ts,tsx,js,jsx} and TEST_FILE_PATTERN matches all four. Agent-mode specs with .spec.tsx/.spec.js/.spec.jsx extensions render as a plain file card with no "View in Tests" link. Fix: const isTestSpec = /\.spec\.(ts|tsx|js|jsx)$/.test(writePath) && ....

🟒 Low Priority Notes (2 items)
  • Unused get param in write atom - setTestRunStateForAppAtom declares get but never uses it; convention is _get. (src/atoms/testRuntimeAtoms.ts)
  • Password masking sentinel is ambiguous - The recorder replaces password values with "********". The prompt explains the convention, but a more explicit sentinel like "[MASKED_PASSWORD]" would be less likely to confuse the LLM. (worker/dyad-recorder-client.js)

What's done well:

  • The infra-vs-assertion error taxonomy (amber/red) is a genuinely useful UX distinction that avoids the "Dyad is broken" false alarm.
  • Screenshot path validation resolves symlinks before the containment check β€” correct defense against symlink escapes.
  • spawnStreaming uses array args (no shell on Unix) and treeKill for clean abort β€” solid process management.
  • The Neon branch isolation flow (create β†’ swap env β†’ restart β†’ run β†’ teardown β†’ restore) with crash-recovery reconciliation at startup is thorough.
  • Comprehensive test coverage for the new utilities (parse_test_cases, playwright_report, neon_test_branch, isolated_test_db, normalize paths, tag parser).
  • The basicAgentMode / PRO_AGENT_ONLY_TOOLS removal is clean dead-code deletion (the set was empty, the filter was a no-op).

Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 5 inline finding(s).

Comment thread src/components/chat/DyadMarkdownParser.tsx Outdated
Comment thread src/ipc/utils/supabase_test_user.ts Outdated
Comment thread src/components/preview_panel/TestsPanel.tsx Outdated
Comment thread src/ipc/services/isolated_test_db.ts
Comment thread src/ipc/utils/spawn_streaming.ts
@dyad-assistant

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge
Recommendation: ready

This is a well-architected, security-conscious feature PR that adds AI-generated E2E testing with appropriate defense-in-depth across all layers. The core security decisions are sound:

  • Path traversal: Multi-layer protection via TEST_FILE_PATTERN regex, normalizeTestPath, safeJoin, and symlink resolution in the screenshot handler.
  • SQL injection: UUID_RE validation on userId, SAFE_IDENT_RE on identifiers, hardcoded OWNER_COLUMNS list.
  • Command injection: Arguments passed as arrays to spawn with shell: false on Unix. Windows shell: true is necessary for .cmd shims and current callers only pass validated/static args.
  • IPC boundary: All handlers use createTypedHandler with Zod-validated contracts. Screenshot reading validates containment after resolving symlinks. Test file paths are validated against a strict regex before reaching the Playwright CLI.
  • Error handling: Expected errors use DyadError with appropriate DyadErrorKind. The isolated DB service never runs tests against real data on setup failure.

The DB isolation design (Neon branches + Supabase test users with crash-safe reconciliation) is thoughtful, and the test coverage across 8 test files is solid.

Issues Summary

Severity File Issue
🟑 MEDIUM src/components/chat/DyadMarkdownParser.tsx:470 isTestSpec only matches .spec.ts, missing .tsx/.js/.jsx extensions
🟑 MEDIUM src/ipc/utils/supabase_test_user.ts:376 userId not validated as UUID before URL interpolation in deleteUserBestEffort
🟑 MEDIUM src/components/preview_panel/TestsPanel.tsx:568 TestsPanel uses direct IPC calls instead of TanStack Query for data fetching
🟑 MEDIUM src/ipc/services/isolated_test_db.ts:131 Neon teardown restarts the app even if setup failed before any env modification
🟑 MEDIUM src/ipc/utils/spawn_streaming.ts:62 Exported utility uses shell: true on Windows without documenting injection constraints
🟒 Low Priority Notes (4 items)
  • Optional chain on os.cpus() - parallelWorkerCount() in tests_handlers.ts uses os.cpus()?.length ?? 2 β€” the optional chain is unnecessary since os.cpus() always returns an array, though it's harmless defensive coding. (src/ipc/handlers/tests_handlers.ts)
  • postMessage origin - The recorder's postMessage(message, "*") in PreviewIframe.tsx is consistent with every other postMessage call in that file β€” no change needed, but worth noting the existing pattern uses "*" throughout. (src/components/preview_panel/PreviewIframe.tsx)
  • Dead code removal - Removing the unused basicAgentMode option and empty PRO_AGENT_ONLY_TOOLS set in tool_definitions.ts is good cleanup, though it's unrelated to the E2E testing feature. (src/pro/main/ipc/handlers/local_agent/tool_definitions.ts)
  • Prompt duplication in snapshots - The E2E test writing guidance block appears 8 times in the snapshot file (once per agent mode variant) β€” this is the expected behavior of the snapshot structure, not a bug. (src/prompts/__snapshots__/local_agent_prompt.test.ts.snap)

Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 2 inline finding(s).

Comment thread src/components/chat/DyadMarkdownParser.tsx Outdated
Comment thread src/ipc/services/isolated_test_db.ts
@dyad-assistant

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge
Recommendation: ready

This is a well-designed, large feature PR that adds AI-generated E2E testing with good security practices (path traversal protection, CLI flag injection prevention, symlink resolution, SQL injection guards), thorough test coverage (unit tests for all critical utilities and the isolation service), and careful UX design (infra-vs-assertion taxonomy, isolated test data, crash reconciliation). The architecture follows established Dyad patterns for IPC contracts, jotai atoms, and panel components.

Issues Summary

Severity File Issue
🟑 MEDIUM src/components/chat/DyadMarkdownParser.tsx:472 isTestSpec check only matches .spec.ts, not .spec.tsx/.js/.jsx
🟑 MEDIUM src/ipc/services/isolated_test_db.ts:87 AbortSignal not forwarded to Supabase isolation path
🟒 Low Priority Notes (6 items)
  • parallelWorkerCount floor of 2 on 1-2 core machines - The Math.max(2, ...) floor means parallel mode always spawns at least 2 workers even on 1-core machines, defeating the "leave one core free" comment. Consider Math.max(1, ...). (src/ipc/handlers/tests_handlers.ts)

  • waitForServerReady effective timeout exceeds 120s - Each loop iteration can block 3.5s (500ms sleep + 3s fetch timeout), so the Date.now() < deadline check doesn't guarantee a 120s cap. Unlikely to matter in practice since most probes fail fast. (src/ipc/services/isolated_test_db.ts)

  • EMPTY_TEST_RUN_STATE double-cast to escape Object.freeze types - The as unknown as TestRunState cast hides a genuine type mismatch from frozen arrays. A factory function returning fresh plain objects would be safer. (src/atoms/testRuntimeAtoms.ts)

  • askAiToFix sends current run output, not snapshot from the failing run - If the user triggers "Ask AI to fix" from run N's failure while run N+1 is streaming, the output context sent to chat mixes both runs. Minor incoherence since the error text itself is stable. (src/components/preview_panel/TestsPanel.tsx)

  • Headed mode on Linux without DISPLAY - CI=true is set in the env, but --headed overrides it. On headless Linux (cloud desktops, WSL), the browser launch will fail with an opaque amber error. Consider detecting and warning. (src/ipc/handlers/tests_handlers.ts)

  • basicAgentMode removal is unrelated cleanup - The removed PRO_AGENT_ONLY_TOOLS set was already empty (new Set<string>()), so the removal has zero behavioral effect. Worth noting in the PR description since it's unrelated to the testing feature. (src/pro/main/ipc/handlers/local_agent/tool_definitions.ts)


Generated by Dyadbot persona-based code review

azizmejri1 and others added 6 commits July 8, 2026 17:56
- tighten test isolation warnings and setup safety

- fix Playwright bootstrap/run/report edge cases

- prevent stale test output and clean up runtime state
`playwright test` has no `--base-url` option, so the run aborted with
"unknown option '--base-url'". The base URL is already supplied via the
DYAD_TEST_BASE_URL env var read by the generated playwright.config.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the three highest-value flows of the Tests panel + <dyad-generate-test>
feature, driven with the fake LLM (no real Playwright run):

- Generate a test: renders a chat card, writes the spec under tests/, and
  auto-discovers it in the Tests panel when the turn ends.
- Per-app opt-in gate: off by default, persists across a panel remount, and
  can be disabled.
- Data-safety warning scales to the backend: strong warning with no isolation,
  calm warning when tests run against an isolated Neon branch copy.

Also teach the PreviewPanel page object about the overflow-hosted `tests` mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dyad-assistant

dyad-assistant Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Recommendation: human-review

PR Review: #3681 β€” Add AI-generated E2E testing feature

Author: azizmejri1
Branch: feature/ai-e2e-testing β†’ main
Changed files: 64 | +9215 / βˆ’20


Verdict

βœ… YES β€” Ready to merge

Recommendation: ready

This is a well-engineered feature PR with impressive attention to security, data safety, and edge cases. The isolation strategy (Neon branch copy-on-write + Supabase ephemeral test users with RLS) ensures production data is never at risk. Security boundaries β€” path traversal defense, SQL injection prevention, command injection hardening, and IPC contract validation via Zod β€” are thorough and layered. No issues warrant blocking merge.


Review Personas Applied

1. Correctness & Debugging Expert

Run serialization (tests_handlers.ts): The synchronous controller registration pattern β€” registering the new AbortController before await-ing the prior run's completion β€” correctly prevents races where two concurrent runAppTests calls could both proceed. The lock-based serialization in isolated_test_db.ts (withLock) properly gates reconciliation against concurrent test runs.

Crash reconciliation (main.ts): Fire-and-forget void reconcileOrphanTestBranches() and void reconcileOrphanTestUsers() at startup correctly cleans up resources orphaned by prior crashes, using persisted IDs (neon_test_branch_id, supabase_test_user_id) on the app record.

Process tree-killing (spawn_streaming.ts): Abort signal integration with tree-kill and timeout enforcement handles hung test processes. The stdin: 'ignore' setting prevents interactive prompts from blocking the process.

2. Code Health Expert

IPC contracts: All test-related IPC channels use Zod schemas (tests.ts) for input validation and output typing. The TestOutputPayload discriminated union cleanly separates stdout/stderr/lifecycle events.

State management: Jotai atoms for per-app test state and TanStack Query for IPC-backed fetching follow established Dyad patterns. The testRunStateAtom family correctly scopes runtime state per app ID.

Migration: drizzle/0034_motionless_ego.sql adds three columns with appropriate defaults (testing_enabled defaults to 0). Schema doc comments in schema.ts clearly explain each field's lifecycle.

3. UX Wizard

Opt-in gate: EnableTestingScreen in TestsPanel.tsx provides a clear entry point with explanation before enabling the feature. The isolation badges communicate which safety mechanism is active.

Output streaming: The OutputDrawer streams test output in real-time with proper ANSI handling, giving users immediate feedback during test runs.

Deep-linking: DyadGenerateTest.tsx provides a "View in Tests" action from chat-generated test cards, connecting the AI generation flow to the test panel.


Issues

No HIGH-severity issues found.

LOW notes (non-blocking)
# Severity File Note
L1 LOW src/prompts/local_agent_prompt.ts AGENT_TEST_WRITING_GUIDANCE is injected unconditionally into agent mode prompts regardless of whether testingEnabled is true for the app. Build-mode guidance is correctly gated. Consider gating agent-mode guidance similarly to avoid prompt bloat for non-testing users.
L2 LOW src/ipc/utils/supabase_test_user.ts SQL cleanup uses DO $dyad_cleanup$ BEGIN EXECUTE format(...) β€” if a user-controlled identifier ever contained $dyad_cleanup$, it could break the dollar-quote boundary. Current defense (SAFE_IDENT_RE: /^[a-z_][a-z0-9_]{0,62}$/i) makes this unexploitable since $ is not in the allowed character set. Defense-in-depth is solid.
L3 LOW src/ipc/utils/normalize_test_path.ts Located in src/ipc/utils/ but uses pure string manipulation (no node:path) specifically so it can be shared with the renderer. A brief comment or co-location note would help future maintainers understand this design choice.
L4 LOW src/ipc/utils/playwright_bootstrap.ts System browser detection (Chrome/Edge) falls back to bundled Chromium. The Yarn PnP rejection is a pragmatic choice given Playwright's incompatibility, with a clear error message.
L5 LOW src/ipc/handlers/tests_handlers.ts getTestScreenshot performs symlink-aware containment checks (fs.realpath + safeJoin + starts-with validation). The defense is correct and thorough.

Security Assessment

  • Path traversal: normalizeTestPath() strips .. segments and forces tests/ prefix; getTestScreenshot resolves symlinks before containment check
  • SQL injection: SAFE_IDENT_RE + UUID_RE regex validation before format(%I, %L) parameterization; dollar-quote tags use safe identifiers
  • Command injection: TEST_FILE_PATTERN regex prevents CLI flag injection in test file paths; assertWindowsShellSafe blocks shell metacharacters on Windows
  • Data isolation: Neon creates copy-on-write branches (throwaway); Supabase creates ephemeral test users scoped by RLS. Both tear down after runs and reconcile orphans on startup
  • IPC boundaries: All channels validated with Zod schemas; no remote module usage; mutations locked by appId

Reviewed with three personas: Correctness & Debugging Expert, Code Health Expert, UX Wizard.
PR: dyad-sh/dyad#3681

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@dyad-assistant

dyad-assistant Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

PR Review: #3681 β€” Add AI-generated E2E testing feature

Verdict: βœ… YES β€” Ready to merge
Recommendation: human-review (default)

Recommendation: ready β€” no blocking issues; two medium-severity findings should be addressed before or shortly after merge.


Summary

This PR adds a full-stack AI-generated E2E testing feature (+9232/βˆ’20 across 65 files): Playwright-based test execution with a Tests panel UI, copy-on-write Neon branch isolation, throwaway Supabase test-user isolation with RLS checks, crash-recovery reconciliation, prompt guidance for test generation in both build and local-agent modes, and comprehensive unit/E2E test coverage.

The code quality is high overall. Security-sensitive surfaces (screenshot handler, SQL construction, spawn_streaming, path validation) are defended in depth with layered guards and detailed inline comments. The crash-recovery model (persist-before-run, reconcile-on-startup) is well-designed. Test coverage is thorough.


Issues

# Severity File Title
M1 MEDIUM src/prompts/local_agent_prompt.ts:348 AGENT_TEST_WRITING_GUIDANCE included unconditionally in local agent prompt
M2 MEDIUM src/ipc/utils/supabase_test_user.ts:~420 SQL construction in cleanUpRowsOwnedBy is fragile despite current safety

M1 β€” AGENT_TEST_WRITING_GUIDANCE unconditionally included in local agent prompt

File: src/prompts/local_agent_prompt.ts, lines 348 and 376

The build-mode prompt in system_prompt.ts correctly gates test-writing guidance behind testingEnabled:

(testingEnabled ? `\n\n${TEST_WRITING_GUIDANCE}` : "")

But in local_agent_prompt.ts, ${AGENT_TEST_WRITING_GUIDANCE} is interpolated into both the pro and basic agent prompts unconditionally β€” no testingEnabled check. This means every local-agent session gets ~98 lines of test-writing guidance regardless of whether the app has opted into E2E testing.

Impact: Token waste on every local-agent request; the agent may suggest writing tests to users who haven't enabled the feature, creating confusion. Inconsistent with build-mode gating.

Fix: The testingEnabled parameter is already threaded through chat_stream_handlers.ts β€” add a conditional in the prompt builder functions:

${testingEnabled ? AGENT_TEST_WRITING_GUIDANCE : ""}

M2 β€” SQL construction in cleanUpRowsOwnedBy is fragile despite current safety

File: src/ipc/utils/supabase_test_user.ts, cleanUpRowsOwnedBy function

The DO $dyad_cleanup$ block interpolates table, column, and userId into a JS template string that is then passed to format(%I, %L). The inline SECURITY comment correctly notes that format() is not the primary defense β€” it only escapes values that reach it intact. The real guards are SAFE_IDENT_RE, UUID_RE, and the $-check.

This is currently safe: the regexes are tight, the $-check prevents dollar-quote breakout, and the OWNER_COLUMNS validation loop at module init enforces the invariant statically. However, the design is inherently fragile:

  • A future developer who relaxes SAFE_IDENT_RE (e.g., to support quoted identifiers or schemas) could inadvertently break the injection defense.
  • The format() call creates a false sense of security β€” it looks like parameterized SQL but isn't.

Suggestion: Add an integration-test assertion that SAFE_IDENT_RE rejects quotes, dollars, and backslashes, so a relaxation would be caught by CI. Consider extracting the validate-then-interpolate pattern into a helper to centralize the invariant.


Low-priority notes (not blocking)
  • L1: The hardcoded OWNER_COLUMNS array (user_id, owner_id, created_by, author_id, uid) is best-effort and documented as such, but custom schemas with non-standard owner columns won't get cleanup. Consider surfacing a brief note in the Tests panel when non-cascading tables are found.

  • L2: classifyErrorText in playwright_report.ts uses heuristic string matching ("timed out", "no element", etc.) to distinguish infra from assertion failures. Framework-specific or localized error messages may be misclassified. The default-to-"assertion" bias is reasonable but could paint an app bug red when the test itself is broken.

  • L3: The assertWindowsShellSafe guard in spawn_streaming.ts is a no-op on non-Windows. If a test path containing metacharacters is validated on macOS/Linux CI but the app later runs on Windows, the guard fires at runtime. Consider validating on all platforms for consistency (fail-fast on bad paths regardless of OS).

  • L4: The prompt snapshot (local_agent_prompt.test.ts.snap) shows AGENT_TEST_WRITING_GUIDANCE appearing in all 8 prompt variants unconditionally, confirming M1. Once M1 is fixed, the snapshot should show it only in test-enabled variants.

  • L5: The escapeRegexLiteral + new RegExp() pattern in getDyadGenerateTestTags (dyad_tag_parser.ts) constructs a regex from a string at runtime. The escapeRegexLiteral function must escape all regex metacharacters correctly β€” a missed one could cause a parse failure or over-match. Low risk since the input is a controlled tag name, but worth a unit test covering edge-case characters.


Signature: Multi-persona review (Correctness & Debugging, Code Health, UX) Β· Claude Β· 2026-07-08

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 229 22 0 6

Summary: 229 passed, 22 failed, 6 skipped

Failed Tests

🍎 macOS

Show all 22 failures
  • capacitor.spec.ts > capacitor upgrade and sync works
    • Error: expect(string).toMatchSnapshot(expected) failed
  • cloud_sandbox.spec.ts > cloud sandbox runtime mode runs previews
    • Error: expect(locator).toBeVisible() failed
  • context_window.spec.ts > context window
    • Error: expect(string).toMatchSnapshot(expected) failed
  • dump_messages.spec.ts > dump messages
    • Error: expect(string).toMatchSnapshot(expected) failed
  • engine.spec.ts > send message to engine
    • Error: expect(string).toMatchSnapshot(expected) failed
  • engine.spec.ts > send message to engine - openai gpt-5
    • Error: expect(string).toMatchSnapshot(expected) failed
  • engine.spec.ts > send message to engine - anthropic claude sonnet 4
    • Error: expect(string).toMatchSnapshot(expected) failed
  • engine.spec.ts > smart auto should send message to engine
    • Error: expect(string).toMatchSnapshot(expected) failed
  • engine.spec.ts > regular auto should send message to engine
    • Error: expect(string).toMatchSnapshot(expected) failed
  • local_agent_auto.spec.ts > local-agent - auto model
    • Error: expect(string).toMatchSnapshot(expected) failed
  • mention_app.spec.ts > mention app (without pro)
    • Error: expect(string).toMatchSnapshot(expected) failed
  • mention_app.spec.ts > mention app (with pro)
    • Error: expect(string).toMatchSnapshot(expected) failed
  • mention_files.spec.ts > mention file
    • Error: expect(string).toMatchSnapshot(expected) failed
  • prompt_library.spec.ts > slash command is expanded to prompt content in message
    • Error: expect(string).toMatchSnapshot(expected) failed
  • security_review.spec.ts > security review
    • Error: expect(string).toMatchSnapshot(expected) failed
  • security_review.spec.ts > security review - edit and use knowledge
    • Error: expect(string).toMatchSnapshot(expected) failed
  • smart_context_balanced.spec.ts > smart context balanced - simple
    • Error: expect(string).toMatchSnapshot(expected) failed
  • smart_context_deep.spec.ts > smart context deep - read write read
    • Error: expect(string).toMatchSnapshot(expected) failed
  • smart_context_deep.spec.ts > smart context deep - mention app should fallback to balanced
    • Error: expect(string).toMatchSnapshot(expected) failed
  • thinking_budget.spec.ts > thinking budget
    • Error: expect(string).toMatchSnapshot(expected) failed
  • turbo_edits_v2.spec.ts > turbo edits v2 - search-replace dump
    • Error: expect(string).toMatchSnapshot(expected) failed
  • turbo_edits_v2.spec.ts > turbo edits v2 - search-replace fallback
    • Error: expect(string).toMatchSnapshot(expected) failed

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/capacitor.spec.ts \
  e2e-tests/cloud_sandbox.spec.ts \
  e2e-tests/context_window.spec.ts \
  e2e-tests/dump_messages.spec.ts \
  e2e-tests/engine.spec.ts \
  e2e-tests/local_agent_auto.spec.ts \
  e2e-tests/mention_app.spec.ts \
  e2e-tests/mention_files.spec.ts \
  e2e-tests/prompt_library.spec.ts \
  e2e-tests/security_review.spec.ts \
  e2e-tests/smart_context_balanced.spec.ts \
  e2e-tests/smart_context_deep.spec.ts \
  e2e-tests/thinking_budget.spec.ts \
  e2e-tests/turbo_edits_v2.spec.ts

πŸ“Š View full report

azizmejri1 and others added 2 commits July 8, 2026 18:54
- Gate AGENT_TEST_WRITING_GUIDANCE in local-agent prompts on
  testingEnabled, mirroring the build-mode gating so non-testing apps
  don't carry the guidance in every prompt.
- Enforce app.testingEnabled in the tests:run IPC handler before
  preparing isolation or bootstrapping Playwright, so a stale/buggy
  renderer caller can't run tests against real data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scaffold .gitignore now includes Playwright test artifacts
(/test-results/, /playwright-report/, /playwright/.cache/, .last-run.json)
for the AI-generated E2E testing feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 31 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid β€” if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="e2e-tests/snapshots/context_window.spec.ts_context-window-2.txt">

<violation number="1" location="e2e-tests/snapshots/context_window.spec.ts_context-window-2.txt:36">
P3: The `.gitignore` snapshot includes `/playwright/.cache/`, but this is not a standard Playwright artifact path. Playwright stores browser binaries at the OS level (`~/.cache/ms-playwright`), not in a project-level `playwright/.cache/` directory. The official Playwright .gitignore only recommends `/test-results/` and `playwright-report`. This entry in the AI context window snapshot could lead the AI to generate non-standard `.gitignore` patterns. Consider removing `/playwright/.cache/` to align with Playwright conventions.</violation>
</file>

<file name="e2e-tests/snapshots/context_window.spec.ts_context-window-1.txt">

<violation number="1" location="e2e-tests/snapshots/context_window.spec.ts_context-window-1.txt:37">
P3: The snapshot includes `.last-run.json` as a Playwright test artifact to ignore, but `.last-run.json` is not a standard Playwright output file β€” Playwright uses `.last-run.json` internally via the `@playwright/test` runner configuration, but it's stored under the `test-results/` directory by default, not at the repo root. Including it as a standalone entry in the AI prompt's gitignore could mislead the LLM into thinking it's a root-level Playwright artifact.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/ipc/handlers/tests_handlers.ts
# Playwright test artifacts
/test-results/
/playwright-report/
/playwright/.cache/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The .gitignore snapshot includes /playwright/.cache/, but this is not a standard Playwright artifact path. Playwright stores browser binaries at the OS level (~/.cache/ms-playwright), not in a project-level playwright/.cache/ directory. The official Playwright .gitignore only recommends /test-results/ and playwright-report. This entry in the AI context window snapshot could lead the AI to generate non-standard .gitignore patterns. Consider removing /playwright/.cache/ to align with Playwright conventions.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At e2e-tests/snapshots/context_window.spec.ts_context-window-2.txt, line 36:

<comment>The `.gitignore` snapshot includes `/playwright/.cache/`, but this is not a standard Playwright artifact path. Playwright stores browser binaries at the OS level (`~/.cache/ms-playwright`), not in a project-level `playwright/.cache/` directory. The official Playwright .gitignore only recommends `/test-results/` and `playwright-report`. This entry in the AI context window snapshot could lead the AI to generate non-standard `.gitignore` patterns. Consider removing `/playwright/.cache/` to align with Playwright conventions.</comment>

<file context>
@@ -29,6 +29,12 @@ dist-ssr
+# Playwright test artifacts
+/test-results/
+/playwright-report/
+/playwright/.cache/
+.last-run.json
 .dyad/
</file context>

/test-results/
/playwright-report/
/playwright/.cache/
.last-run.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The snapshot includes .last-run.json as a Playwright test artifact to ignore, but .last-run.json is not a standard Playwright output file β€” Playwright uses .last-run.json internally via the @playwright/test runner configuration, but it's stored under the test-results/ directory by default, not at the repo root. Including it as a standalone entry in the AI prompt's gitignore could mislead the LLM into thinking it's a root-level Playwright artifact.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At e2e-tests/snapshots/context_window.spec.ts_context-window-1.txt, line 37:

<comment>The snapshot includes `.last-run.json` as a Playwright test artifact to ignore, but `.last-run.json` is not a standard Playwright output file β€” Playwright uses `.last-run.json` internally via the `@playwright/test` runner configuration, but it's stored under the `test-results/` directory by default, not at the repo root. Including it as a standalone entry in the AI prompt's gitignore could mislead the LLM into thinking it's a root-level Playwright artifact.</comment>

<file context>
@@ -29,6 +29,12 @@ dist-ssr
+/test-results/
+/playwright-report/
+/playwright/.cache/
+.last-run.json
 .dyad/
 
</file context>

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 3 inline finding(s).

let run;
try {
run = await spawnStreaming({
command: "npx",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

npx resolves Playwright via remote fallback instead of local binary

The test runner invokes npx playwright test which, if the local node_modules/.bin resolution fails for any reason, falls back to downloading the playwright package from the npm registry. While ensurePlaywrightBootstrap installs @playwright/test beforehand, and the user's app is already trusted (it runs a dev server executing arbitrary code), resolving the binary directly from node_modules/.bin would eliminate the remote-fetch fallback entirely as a defense-in-depth measure.

πŸ’‘ Suggestion: Consider resolving the Playwright CLI path directly via require.resolve('@playwright/test/cli', { paths: [appPath] }) or path.join(appPath, 'node_modules', '.bin', 'playwright') instead of going through npx.

}

function projectUrlFor(ref: string): string {
return `https://${ref}.supabase.co`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

projectUrlFor constructs URL from DB value without format validation

The function projectUrlFor(ref) constructs https://${ref}.supabase.co using the supabaseProjectId column value directly. While this column is populated from the Supabase integration flow, there is no validation that the value is a safe URL hostname component. A corrupted or unexpected value (containing slashes, colons, or special characters) could produce a malformed URL pointing to an unintended host. The deleteUserBestEffort function validates userId with UUID_RE, but the host portion has no equivalent guard.

πŸ’‘ Suggestion: Add a simple alphanumeric regex check (e.g. /^[a-z]{20}$/ or a more permissive Supabase ref pattern) on the ref parameter before constructing the URL.

// later try to re-delete a branch that's already gone. If the delete
// failed, leave the column set so startup reconciliation retries it.
if (deleted) {
await db

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

db.update failure after branch deletion leaves stale neonTestBranchId column

When Neon Auth provisioning fails (line 222), the code deletes the branch (line 223) then tries to clear neonTestBranchId (lines 228-230). If db.update fails (e.g. transient SQLite lock), the branch is deleted on Neon but the column still points to it. On next startup, reconciliation self-heals by trying to delete the non-existent branch (404 is treated as success) then clearing the column, but the error thrown at line 233 does not indicate that the branch was already cleaned up, which could confuse debugging.

πŸ’‘ Suggestion: Wrap the db.update in a try/catch that logs a warning rather than letting it throw, since the branch is already deleted and the stale column self-heals at startup.

@dyad-assistant

dyad-assistant Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge
Recommendation: ready

This is a large, well-architected PR (+8992/-38 across 92 files) that adds a complete AI-generated E2E testing feature: a Tests panel with opt-in gate, <dyad-generate-test> tag, Playwright bootstrapping and runner orchestration, isolated test databases (Neon branch copies + Supabase test users), and comprehensive unit/E2E test coverage.

Security highlights: The screenshot IPC handler (getTestScreenshot) is exemplary -- it validates the extension, resolves symlinks before the containment check, re-checks the extension on the resolved path, and restricts reads to the test-results/ subtree. Test file paths are validated against a strict regex (TEST_FILE_PATTERN) before reaching the Playwright CLI. The Supabase SQL cleanup uses regex guards (SAFE_IDENT_RE, UUID_RE) as the load-bearing injection defense, with format(%I, %L) as a second layer, and dollar-quoting to isolate the DO block.

Architecture highlights: Test run lifecycle management correctly chains runs via done promises and synchronously registers controllers to prevent race conditions. The isolation prepare/teardown pattern ensures env vars are always restored even on abort or error. IPC channels use createTypedHandler with Zod contracts. The UI uses TanStack Query for data fetching, Jotai atoms for runtime state, and avoids Radix UI.

Issues Summary

Severity File Issue
🟑 MEDIUM src/ipc/handlers/tests_handlers.ts:251 npx resolves Playwright via remote fallback instead of local binary
🟑 MEDIUM src/ipc/utils/supabase_test_user.ts:58 projectUrlFor constructs URL from DB value without format validation
🟑 MEDIUM src/ipc/utils/neon_test_branch.ts:228 db.update failure after branch deletion leaves stale neonTestBranchId column
🟒 Low Priority Notes (4 items)
  • Test failure label puts blame on the app - The statusLabel for "failed" says "your app may not match the test", but assertion failures can also mean the test itself is wrong (stale selectors, wrong expected values). A more neutral label like "Test failed" would be less misleading. (src/components/preview_panel/testResultUtils.ts)

  • Header toolbar buttons have no overflow handling - The Tests panel header packs many buttons (Disable, Parallel/Serial, Headed/Headless, Run all/Stop) horizontally with no overflow handling for narrow widths, unlike the PreviewToolbar which uses a dropdown. (src/components/preview_panel/TestsPanel.tsx)

  • ensureTestScript normalizes package.json formatting - JSON.stringify(pkg, null, 2) re-serializes package.json, which will normalize formatting in imported or hand-edited apps. The function already gates on !pkg.scripts.test so it only writes when needed, but the formatting disruption is a minor DX annoyance. (src/ipc/utils/playwright_bootstrap.ts)

  • JSDoc continuation line detection in parse_test_cases is fragile - The line.trimStart().startsWith("*") check could skip a real test(...) call on a line whose first non-whitespace character is *. Acceptable for a best-effort parser since the fallback (file is still runnable as a whole) mitigates the impact. (src/ipc/utils/parse_test_cases.ts)


Generated by Dyadbot persona-based code review

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 250 1 1 6

Summary: 250 passed, 1 failed, 1 flaky, 6 skipped

Failed Tests

🍎 macOS

  • edit_code.spec.ts > edit code
    • Error: expect(locator).toBeVisible() failed

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/edit_code.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • engine.spec.ts > regular auto should send message to engine (passed after 1 retry)

πŸ“Š View full report

The "Fix" button handed a failing test to the AI as text only. The
failure screenshot was captured and shown in the panel but never sent to
the model, so the agent couldn't see the real UI state at the point of
failure. (Pointing the agent at the on-disk PNG doesn't help either: its
read_file tool decodes files as UTF-8.)

askAiToFix now fetches the screenshot via getTestScreenshot and passes it
as a chat-context image attachment, which is converted to a model image
part server-side. Falls back to text-only if the screenshot is missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review: 4 inline finding(s).

*/
async function cleanUpRowsOwnedBy({
projectId,
organizationSlug,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

SQL cleanup uses string interpolation inside DO block despite format()

In cleanUpRowsOwnedBy, the SQL DO block passes table, column, and userId as JS template-literal interpolations inside format() arguments: format('DELETE FROM public.%I WHERE %I = %L', '${table}', '${column}', '${userId}'). While SAFE_IDENT_RE and UUID_RE regex guards are correct and load-bearing, this pattern is fragile to future regex relaxations. A future maintainer relaxing SAFE_IDENT_RE to allow quotes or dollar signs would silently open a SQL injection through the dollar-quote boundary or the format() arguments. The inline comments document this, but a parameterized approach would be more robust.

πŸ’‘ Suggestion: Consider a two-step approach: discover columns with the current query, then delete via a parameterized SQL function or at minimum add a runtime assertion that rejects single-quotes and dollar signs in the values before interpolation, separate from the regex.

try {
// checkRls and createTempTestUser each make network requests that can take
// several seconds; honor a Stop pressed in between them so cancellation
// takes effect promptly instead of only after they complete.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

restartAppInPlace does not verify app restarted with correct branch

During teardown, after restoreEnvFile + restartAppInPlace, the code does not call waitForServerReady or otherwise verify the restarted dev server is reading the restored real env. In setup, waitForServerReady is called after the restart. If the teardown restart partially fails (process starts but reads cached env), the user could unknowingly interact with the test branch database after the run. This is low-probability since env changes are picked up on restart, but the asymmetry between setup (which waits) and teardown (which does not) is worth noting.

πŸ’‘ Suggestion: Consider calling waitForServerReady after the teardown restart, or at minimum logging a warning if the proxy URL doesn't come back within a short timeout.

// under `tests/`, ending in a spec extension, with no traversal or leading
// dash. This stops a compromised renderer from passing a flag-like value
// (e.g. `--config=…`) that Playwright would interpret as a CLI option. The
// allowed characters must cover everything the listing glob can surface

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

TEST_FILE_PATTERN allows NUL bytes in path via regex gap

The TEST_FILE_PATTERN regex character class [^\\:\x00-\x1f] correctly blocks ASCII control characters including NUL, but does not restrict non-ASCII characters that could be problematic in certain filesystem encodings or Unicode normalization scenarios. While path.posix.normalize handles most cases and Playwright ultimately opens the file (not a shell), the pattern is more permissive than the glob that discovers test files. This is defense-in-depth rather than an active vulnerability.

πŸ’‘ Suggestion: Consider whether restricting to printable ASCII and a curated set of Unicode categories would be appropriate, or add a comment documenting the intentional permissiveness for non-ASCII filenames.

// All test activity lives behind the opt-in gate; don't register the IPC
// listener (or accumulate output) for apps that haven't enabled testing.
if (!testingEnabled) return;
const unsubscribe = ipc.events.tests.onOutput((payload) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 MEDIUM

IPC output listener registered without appId filter

The tests:output IPC event listener accumulates output chunks from ALL apps into the pendingOutputRef buffer, not just the currently selected app. While per-appId atom writes and MAX_OUTPUT_LENGTH bound the impact, this means output from background test runs on other apps accumulates in renderer memory even when that app is not selected and will never be viewed. For single-app usage this is a non-issue, but in multi-app workflows it represents unnecessary memory churn.

πŸ’‘ Suggestion: Consider filtering the listener callback to only buffer chunks for the currently selected appId, or documenting that cross-app output accumulation is intentional for preserving output when switching apps.

@dyad-assistant

dyad-assistant Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

πŸ” Dyadbot Code Review Summary

Verdict: βœ… YES - Ready to merge
Recommendation: ready

This is a well-engineered, large feature PR that adds a complete AI-generated E2E testing pipeline to Dyad. The architecture is sound: database isolation for Neon (copy-on-write branches) and Supabase (throwaway RLS-scoped users), crash recovery via startup reconciliation, thorough input validation, and a carefully gated opt-in UX. The code demonstrates strong security awareness throughout -- path traversal protections, symlink resolution, command injection guards, SQL injection defenses, and proper IPC boundary enforcement.

Issues Summary

Severity File Issue
🟑 MEDIUM src/ipc/utils/supabase_test_user.ts:357 SQL cleanup uses string interpolation inside DO block despite format()
🟑 MEDIUM src/ipc/services/isolated_test_db.ts:296 restartAppInPlace does not verify app restarted with correct branch
🟑 MEDIUM src/ipc/handlers/tests_handlers.ts:47 TEST_FILE_PATTERN allows NUL bytes in path via regex gap
🟑 MEDIUM src/components/preview_panel/TestsPanel.tsx:595 IPC output listener registered without appId filter

Details

SQL cleanup uses string interpolation inside DO block despite format() (MEDIUM)

In cleanUpRowsOwnedBy, the SQL in the DO block passes table, column, and userId values as JS template-literal interpolations inside the format() call arguments: format('DELETE FROM public.%I WHERE %I = %L', '${table}', '${column}', '${userId}'). While the SAFE_IDENT_RE and UUID_RE regex guards are the load-bearing defense (and are correct), this pattern is fragile: any future relaxation of those regexes could silently open a SQL injection via the $dyad_cleanup$ dollar-quote or the format arguments. The code comments acknowledge this, but parameterized queries or a two-step approach (discover columns, then delete via parameterized SQL) would be more robust. Not a current vulnerability given the regex guards, but worth noting for maintainability.

restartAppInPlace does not verify app restarted with correct branch (MEDIUM)

In isolated_test_db.ts, after restoreEnvFile + restartAppInPlace during teardown, the code does not verify the restarted dev server is actually reading the restored (real) env. If the file restore succeeds but the restart partially fails (e.g., the new process starts but reads a cached/stale env), subsequent user interactions could unknowingly operate against the test branch's database. The waitForServerReady call in setup verifies the server is responding, but teardown only calls restartAppInPlace without waiting or verifying. This is a low-probability scenario since env file changes are picked up on restart, but worth noting.

TEST_FILE_PATTERN allows NUL bytes in path via regex gap (MEDIUM)

The TEST_FILE_PATTERN regex in tests_handlers.ts blocks control characters \x00-\x1f in the character class, but the character class syntax [^\\\\:\\x00-\\x1f] is applied inside the regex constructor via a string. This correctly blocks NUL bytes (\x00) and other control chars. However, the regex does not block characters above \x7f that could be problematic in some filesystem encodings (e.g., Unicode normalization attacks). Given that the test file path is resolved via path.posix.normalize and the file is ultimately opened by Playwright (not the shell), this is defense-in-depth rather than an active vulnerability.

IPC output listener registered without appId filter (MEDIUM)

In TestsPanel.tsx, the tests:output IPC event listener accumulates output from ALL apps into the pending buffer (line 595-601), not just the currently selected app. While the atom writes are per-appId and the UI only shows the selected app's output, this means output from a test run on app A continues accumulating in memory even when app B is selected. For users who switch between apps during a test run, this is correct behavior (preserving output), but for users who never switch back, the output for app A's run would accumulate indefinitely until the component remounts. This is bounded by MAX_OUTPUT_LENGTH in the atom, so it is a minor memory concern at most.

🟒 Low Priority Notes (5 items)
  • Comprehensive unit and integration tests - The PR includes thorough unit tests for every new utility (normalize_test_path, playwright_bootstrap, playwright_report, neon_test_branch, supabase_test_user, isolated_test_db, parse_test_cases, testResultUtils). Coverage is strong. (src/ipc/utils/*.test.ts)

  • TanStack Query patterns followed correctly - The useSetTestingEnabled hook correctly uses useMutation with query invalidation, and TestsPanel uses useQuery for spec listing with proper queryKey integration. (src/hooks/useSetTestingEnabled.ts, src/components/preview_panel/TestsPanel.tsx)

  • Drizzle migration present and correct - The schema changes (3 new columns) have a corresponding migration file drizzle/0035_slim_mariko_yashida.sql with the correct journal entry. (drizzle/0035_slim_mariko_yashida.sql)

  • TestsPanel is 1328 lines - While large, it is well-structured with extracted sub-components (StatusIcon, FailureDetails, RunButton, FixButton, TestCaseRow, FileRow, EnableTestingScreen, OutputDrawer) and the OutputDrawer is properly memoized to avoid re-rendering the full panel on output flushes. Could be split into separate files in a future refactor, but is not a problem now. (src/components/preview_panel/TestsPanel.tsx)

  • Output buffering strategy is well-considered - The OUTPUT_FLUSH_INTERVAL_MS approach (100ms batching) prevents render storms from chatty npm/playwright output while keeping the UI responsive. The separate output atom avoids re-rendering every test row on each chunk. (src/components/preview_panel/TestsPanel.tsx)

Security Review Summary

The security posture of this PR is strong:

  1. Path traversal (getTestScreenshot): Properly uses fs.realpathSync on both the app path and the screenshot path to resolve symlinks before the containment check. Re-checks the .png extension on the resolved path. Restricts to the test-results/ subdirectory. Correctly avoids TOCTOU by skipping the existsSync pre-check.

  2. Command injection (spawnStreaming): Uses array-based args (never shell strings). Windows shell metacharacter validation. stdin: 'ignore' prevents interactive prompts from hanging. The test file path is validated by normalizeRunTestFile before reaching the CLI.

  3. IPC input validation: All handlers use typed contracts with Zod schemas. The testLine parameter is validated as a positive integer at the schema level.

  4. SQL injection (Supabase cleanup): UUID_RE and SAFE_IDENT_RE are the load-bearing guards. The dollar-quote tag $dyad_cleanup$ is safe given the regex constraints. The explicit $ check on table/column names is a thoughtful belt-and-suspenders measure.

  5. normalizeTestPath: Defense-in-depth path sanitization that drops . segments, resolves .. within the sanitized path, and forces the result under tests/. Combined with safeJoin in the response processor, this provides double protection against directory traversal.

  6. Opt-in gate: Testing is disabled by default per-app. The testingEnabled check in the run handler provides a server-side gate, not just a UI-only one.


Generated by Dyadbot persona-based code review

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

βœ… All tests passed!

OS Passed Flaky Skipped
🍎 macOS 251 3 6

Total: 251 tests passed (3 flaky) (6 skipped)

⚠️ Flaky Tests

🍎 macOS

  • edit_code.spec.ts > edit code (passed after 1 retry)
  • mention_files.spec.ts > mention file (passed after 1 retry)
  • package_manager.spec.ts > build mode - safe npm package installs through the real socket firewall path (passed after 1 retry)

πŸ“Š View full report

- Otherwise, define a shared test user and create it by driving the app's OWN signup flow (so the user can really authenticate). If the flow needs a login and the app has no signup flow yet, build one (or an equivalent way to create a user) first. Say so clearly if you add it.
- Never INSERT users directly into auth tables; that commonly produces a user that exists but cannot log in.`;

/** Build-mode test-writing guidance: emit the spec via a `<dyad-generate-test>` tag. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, I think we can just have build mode not support generating tests, but I'm OK with deferring this in a future PR.

I think the core loop of automatically writing + running tests + fixing tests is really the main benefit of tests and you lose that with build mode.

If the cost of keeping test mode in build mode is cheap, then I think it's OK

@wwwillchen wwwillchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome job @azizmejri1 - I think there's some UX improvements we can make (e.g. making it clear why Generate test button is disabled, or by sending it to the prompt queue), but this is a great starting point. merging

  • left a comment around build mode

@wwwillchen

Copy link
Copy Markdown
Collaborator

and thanks for the screenshots!

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

@wwwillchen Awesome. Thanks for reviewing this so fast!

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 388 1 4 130
πŸͺŸ Windows 386 3 2 130

Summary: 774 passed, 4 failed, 6 flaky, 260 skipped

Failed Tests

🍎 macOS

  • package_manager.spec.ts > pnpm version migration upgrade updates the pin and lockfile together
    • Error: expect(locator).toBeHidden() failed

πŸͺŸ Windows

  • concurrent_chat.spec.ts > concurrent chat
    • Error: expect(received).toBe(expected) // Object.is equality
  • edit_code.spec.ts > edit code edits the right file during rapid switches
    • Error: expect(received).toEqual(expected) // deep equality
  • template-create-nextjs.spec.ts > create next.js app
    • Error: expect(locator).toBeEnabled() failed

πŸ“‹ Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/package_manager.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • engine.spec.ts > send message to engine - anthropic claude sonnet 4 (passed after 1 retry)
  • engine.spec.ts > smart auto should send message to engine (passed after 1 retry)
  • smart_context_balanced.spec.ts > smart context balanced - simple (passed after 1 retry)
  • visual_editing.spec.ts > swap image via URL (passed after 1 retry)

πŸͺŸ Windows

  • app_screenshot.spec.ts > captures an app screenshot after the first generated commit (passed after 1 retry)
  • app_search.spec.ts > app search - search by app name (passed after 2 retries)

πŸ“Š View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human:review-issue ai agent flagged an issue that requires human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants