Skip to content

fix(e2e): replace nextjs deploy-in-webServer with pnpm injected deps - #452

Merged
omridevk merged 2 commits into
mainfrom
fix-e2e-nextjs-injected-deps-451
Aug 12, 2026
Merged

fix(e2e): replace nextjs deploy-in-webServer with pnpm injected deps#452
omridevk merged 2 commits into
mainfrom
fix-e2e-nextjs-injected-deps-451

Conversation

@omridevk

@omridevk omridevk commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • e2e/nextjs and e2e/nextjs-component used to pnpm deploy --legacy the whole workspace graph into a mktemp dir inside playwright's webServer.command on every run, purely to give Next/Turbopack a real non-symlinked node_modules (serverExternalPackages requires resolution under node_modules/; workspace symlinks resolve outside it). That deploy chain was slow and flaked under one 180s webServer timeout (flaky: e2e consumer webServer boot timeout (180s) on nextjs shard #435).
  • Replaced it with pnpm's own injected-deps mechanism: dependenciesMeta.<pkg>.injected: true on each app's runtime @conciv/* deps (verified per app: nextjs only imports @conciv/it; nextjs-component imports @conciv/it, @conciv/react, @conciv/extension-terminal), plus syncInjectedDepsAfterScripts: [build] in pnpm-workspace.yaml so the injected hard-copies stay synced to each package's latest dist after every build (confirmed by inode: rebuilding @conciv/it/@conciv/react re-links the injected copy to the fresh dist file, both under turbo run build and a direct pnpm --filter run build).
  • Both playwright configs now run a plain in-place pnpm exec next dev --port <port> with an honest 60s webServer timeout (was 180s for the deploy chain); no deploy helpers, no root-walkers.
  • Added webServerTimeout as an explicit, optional per-app override on e2eConfig in @conciv/e2e-utils/config (other e2e apps keep the existing 180s default).

The silent-failure mode this closes

Under the old deploy, extension packages resolved fine. Under a naive plain-symlink alternative (no injection at all), the terminal builtin server extension's dynamic require throws MODULE_NOT_FOUND and the code swallows it (packages/it/src/plugin/nextjs.ts), logging conciv: failed to load builtin server extensions ... while the suite stays green — a real coverage gap. Added a positive assertion in e2e/nextjs/tests/widget.spec.ts that opens the Terminal extension's connect menu and asserts the real --mcp-config connect command comes back (a genuine RPC round trip through the server-registered terminal extension), so this failure mode goes red instead of silent.

I verified the new test both ways: reverted just the dependenciesMeta flag on e2e/nextjs (back to a plain symlink), reinstalled, and the new test failed red on the real regression; restored the fix, reinstalled, and it passed green again.

Closes #435
Closes #451

Test plan

  • pnpm turbo run typecheck --filter=conciv-e2e-nextjs --filter=conciv-e2e-nextjs-component --filter=@conciv/e2e-utils — pass
  • pnpm lint — pass (0 errors; pre-existing unrelated warnings in apps/site)
  • pnpm format:check — pass on all touched files (one pre-existing unrelated failure in docs/assets/page-session-card-spike/fake-form.html, untouched by this PR)
  • pnpm exec fallow audit --changed-since main --format json — verdict pass, no introduced findings
  • e2e/nextjs: pnpm exec playwright test — 11 passed (in-place next dev booted in 752ms, vs. minutes for the old deploy chain)
  • e2e/nextjs-component: pnpm exec playwright test — 1 passed
  • Revert-check: reverting the dependenciesMeta.injected flag for @conciv/it in e2e/nextjs and reinstalling reproduces a plain symlink; the new extension-load test fails red under that condition

Notes for reviewer / orchestrator

  • PR fix(e2e): deploy packed apps before webServer, avoid fresh resolution (#435) #443 (prior diagnosis attempt) should be closed as superseded by this PR's mechanism — leaving that to the orchestrator per the tracking issue.
  • No CONCIV_DEPLOY_FRESH references existed anywhere in the repo to clean up.
  • e2e/nextjs/packed/* (the tarball-shape verifier) is untouched, as intended — it's orthogonal to this dev-server mechanism.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved end-to-end test setup for Next.js applications by starting development servers directly.
    • Ensured required workspace packages are built and synchronized before tests run.
  • Tests

    • Added coverage confirming the terminal extension loads correctly.
    • Verified the “Copy command” action displays an MCP configuration argument.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e18c043-e8d4-4e68-b0fa-d6ed5d022627

📥 Commits

Reviewing files that changed from the base of the PR and between 88ebb97 and 77b3c09.

📒 Files selected for processing (5)
  • e2e/nextjs-component/package.json
  • e2e/nextjs-component/playwright.config.ts
  • e2e/nextjs/package.json
  • e2e/nextjs/playwright.config.ts
  • pnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • e2e/nextjs/playwright.config.ts
  • e2e/nextjs-component/playwright.config.ts
  • pnpm-workspace.yaml

📝 Walkthrough

Walkthrough

The PR updates Next.js E2E projects to build injected workspace dependencies, synchronize them after builds, and start development servers directly. The widget test verifies that the terminal extension generates a command containing --mcp-config.

Changes

Next.js E2E setup

Layer / File(s) Summary
Injected dependency synchronization
e2e/nextjs-component/package.json, e2e/nextjs/package.json, pnpm-workspace.yaml
The E2E scripts build required workspace packages before tests. pnpm marks the dependencies as injected and synchronizes their copies after builds.
Direct Next.js servers and widget validation
e2e/nextjs*/playwright.config.ts, e2e/nextjs/tests/widget.spec.ts
The Playwright servers run pnpm exec next dev directly. The widget test checks the terminal extension’s “Copy command” output for --mcp-config.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: ⚪ Minimal · up to 77b3c

This PR replaces the slow deployment-based test-server setup with injected dependencies and adds coverage for terminal extension loading; no actionable merge-blocking risk remains after normal checks and review.

Possibly related issues

  • Issue 451 — The PR implements the issue’s pnpm injected-dependency setup, direct Next.js servers, and builtin-extension test coverage.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes replacing the Next.js E2E deployment workflow with pnpm injected dependencies.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-e2e-nextjs-injected-deps-451

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpm-workspace.yaml`:
- Around line 11-14: Add an uncached synchronization task in the workspace
configuration for the e2e test flow, using the existing
syncInjectedDepsAfterScripts behavior to refresh injected `@conciv/`* packages
before test:e2e. Ensure it runs even when Turbo restores cached build outputs,
and avoid changing the existing build synchronization setting.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c57eb26-0d1b-4e73-b2c5-d76ae6264fe9

📥 Commits

Reviewing files that changed from the base of the PR and between 8a093d3 and 88ebb97.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • e2e/e2e-utils/src/config.ts
  • e2e/nextjs-component/package.json
  • e2e/nextjs-component/playwright.config.ts
  • e2e/nextjs/package.json
  • e2e/nextjs/playwright.config.ts
  • e2e/nextjs/tests/widget.spec.ts
  • pnpm-workspace.yaml

Comment thread pnpm-workspace.yaml Outdated
Comment on lines +11 to +14
# on disk. This keeps the injected copies synced to a package's latest dist after every build:
# fires under turbo (which shells to `pnpm run`) and under a direct `pnpm --filter run build`.
syncInjectedDepsAfterScripts:
- build

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

required='10.5.0'
actual="$(pnpm --version)"

if [[ "$(printf '%s\n' "$required" "$actual" | sort -V | head -n1)" != "$required" ]]; then
  printf 'pnpm %s is older than required pnpm %s\n' "$actual" "$required" >&2
  exit 1
fi

rg -n '"packageManager"|"pnpm"' package.json
# Also run a clean-install build with the normal CI cache settings and inspect the injected copies after a cache hit.

Repository: conciv-dev/conciv

Length of output: 519


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- local guidance and skills ---'
find .. -name AGENTS.md -o -path '*/skills/*' -o -iname '*tanstack*intent*' 2>/dev/null | sort | head -200

printf '%s\n' '--- workspace and package metadata ---'
cat -n pnpm-workspace.yaml
cat -n package.json | sed -n '1,180p'

printf '%s\n' '--- turbo and CI references ---'
find . -maxdepth 4 -type f \( -iname '*turbo*' -o -path './.github/workflows/*' -o -iname '*ci*' \) -print | sort
rg -n -i 'turbo|cache|pnpm|inject|syncInjectedDepsAfterScripts|workspace:|dist' .github package.json pnpm-workspace.yaml turbo.json turbo.jsonc 2>/dev/null | head -300

Repository: conciv-dev/conciv

Length of output: 50373


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- repository guidance ---'
cat -n AGENTS.md 2>/dev/null || true
printf '%s\n' '--- review skill ---'
cat -n .claude/skills/conciv-review/SKILL.md 2>/dev/null || true
printf '%s\n' '--- intent configuration and local checker references ---'
rg -n -C 3 'intent|TanStack Intent|skill check|skills' package.json AGENTS.md .claude .github 2>/dev/null | head -240

printf '%s\n' '--- CI cache and build steps ---'
for f in .github/workflows/ci.yml .github/workflows/fallow.yml .github/workflows/cache-cleanup.yml; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    rg -n -C 5 'setup-node|pnpm/action-setup|cache:|actions/cache|turbo|pnpm (install|i|run|test|build)|frozen|restore|save' "$f"
  fi
done

printf '%s\n' '--- Turbo configuration ---'
cat -n turbo.json
for f in e2e/nextjs/turbo.json e2e/nextjs-component/turbo.json apps/examples/nextjs-app/turbo.json; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '--- injected dependency declarations and relevant scripts ---'
rg -l -n 'injected|dependenciesMeta|`@conciv/`' --glob 'package.json' . | sort | while read -r f; do
  printf '\n--- %s ---\n' "$f"
  rg -n -C 5 'injected|dependenciesMeta|scripts|build|`@conciv/`' "$f"
done

Repository: conciv-dev/conciv

Length of output: 50376


🌐 Web query:

pnpm 11 syncInjectedDepsAfterScripts Turbo cache hit build script does not execute documentation

💡 Result:

The issue where pnpm's syncInjectedDepsAfterScripts does not trigger after a Turbo cache hit is a known limitation caused by the design of how Turbo and pnpm interact [1][2][3]. When Turbo restores a build output from its cache, it bypasses the execution of the build script [1][2]. Because pnpm's syncInjectedDepsAfterScripts hook is designed to run only after the successful completion of a script directly invoked by pnpm, the sync process never fires when Turbo simply replays cached files [1][2][4]. As a result, the injected copies of dependencies in the pnpm store remain stale [2][3]. To resolve this, you can implement the following strategies: 1. Disable caching for the synchronization task: Create a dedicated synchronization task in Turbo (e.g., _syncPnpm) that runs after your build tasks [2][3]. Ensure this task has "cache": false in your turbo.json configuration so it always executes, even if the upstream build scripts were cache hits [2][3]. 2. Use a custom sync command: Instead of relying solely on the automated pnpm hook, create a script that performs the synchronization and include it in your task pipeline [1][3]. For example, you can run pnpm install --frozen-lockfile or a dedicated sync tool as a post-build step [2][3]. 3. Orchestration: Ensure your main build tasks depend on the synchronization task, or use Turbo's pipeline to explicitly define the execution order so that synchronization is forced to run after dependencies are restored [1][2]. Note that pnpm documentation defines syncInjectedDepsAfterScripts as a list of script names that, when executed, trigger the synchronization of injected dependencies [5][6]. It is explicitly documented that this mechanism relies on the script being executed by pnpm, which is why external task runners like Turbo can cause the hook to be skipped [1][2].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- Turbo root task graph ---'
cat -n turbo.json

printf '%s\n' '--- affected package manifests ---'
for f in e2e/nextjs/package.json e2e/nextjs-component/package.json apps/examples/nextjs-app/package.json; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '--- all injected metadata ---'
rg -n -C 8 '"injected"|dependenciesMeta' --glob 'package.json' e2e apps packages

printf '%s\n' '--- all relevant task declarations ---'
rg -n -C 8 'build|test:e2e|cache: false|dependsOn|outputs|\.turbo' \
  turbo.json e2e/nextjs/turbo.json e2e/nextjs-component/turbo.json \
  .github/workflows/ci.yml .github/workflows/fallow.yml

printf '%s\n' '--- install and synchronization commands ---'
rg -n -C 4 'pnpm (i|install|run|exec)|syncInjectedDepsAfterScripts|node_modules|dist' \
  .github/workflows e2e/nextjs e2e/nextjs-component package.json pnpm-workspace.yaml

Repository: conciv-dev/conciv

Length of output: 50373


🌐 Web query:

site:pnpm.io/10.x/workspaces syncInjectedDepsAfterScripts injected dependencies scripts executed

💡 Result:

The syncInjectedDepsAfterScripts setting is a configuration option available in pnpm (introduced in version 10.5.0) for managing workspaces [1]. In pnpm workspaces, injected dependencies are implemented as collections of hardlinks [1]. Because these do not automatically update when the source files change, this can cause issues in environments where packages need to be built, such as TypeScript projects [1]. By specifying a list of script names in the syncInjectedDepsAfterScripts configuration, you instruct pnpm to synchronize the injected dependencies within node_modules whenever any of the listed scripts are executed in a workspace package [1]. This ensures that the hardlinked files remain up-to-date with their source counterparts following the execution of those scripts [1].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
import json
import re
from pathlib import Path

root = Path(".")
workspace = (root / "pnpm-workspace.yaml").read_text()
turbo = json.loads((root / "turbo.json").read_text())
ci = (root / ".github/workflows/ci.yml").read_text()

affected = [
    Path("e2e/nextjs/package.json"),
    Path("e2e/nextjs-component/package.json"),
]

for path in affected:
    manifest = json.loads(path.read_text())
    injected = [
        name for name, meta in manifest.get("dependenciesMeta", {}).items()
        if meta.get("injected") is True
    ]
    assert injected, f"{path}: no injected dependencies found"
    print(f"{path}: injected={','.join(injected)}")

assert "syncInjectedDepsAfterScripts:\n  - build" in workspace
assert turbo["tasks"]["build"]["outputs"] == ["dist/**"]
assert turbo["tasks"]["build"]["dependsOn"] == ["^build"]
assert turbo["tasks"]["test:e2e"]["cache"] is False
assert turbo["tasks"]["test:e2e"]["dependsOn"] == ["^build"]

restore = ci.index("path: .turbo", ci.index("name: e2e consumer apps"))
install = ci.index("sfw pnpm i --frozen-lockfile", restore)
run_e2e = ci.index("conciv-ci-shards run --task e2e", install)
assert restore < install < run_e2e
e2e_block = ci[restore:run_e2e]
assert "turbo-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-" in e2e_block
assert "turbo-e2e-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-" in e2e_block

after_e2e_install = ci[install:run_e2e]
assert "syncInjectedDepsAfterScripts" not in after_e2e_install
assert not re.search(r"pnpm\s+(run\s+)?build|turbo\s+run\s+build", after_e2e_install)

print("CI ordering: restore .turbo < pnpm install < uncached test:e2e")
print("No post-install build or explicit injected-dependency synchronization occurs in the e2e job")
print("Result: a build cache hit can bypass the pnpm build hook for injected dependencies")
PY

Repository: conciv-dev/conciv

Length of output: 534


Add an uncached synchronization step after dependency builds.

When CI restores a .turbo build result, Turbo restores dist/** without running build, so pnpm does not invoke syncInjectedDepsAfterScripts. The e2e job can then use stale or empty injected @conciv/* packages. Add an uncached synchronization task before test:e2e.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpm-workspace.yaml` around lines 11 - 14, Add an uncached synchronization
task in the workspace configuration for the e2e test flow, using the existing
syncInjectedDepsAfterScripts behavior to refresh injected `@conciv/`* packages
before test:e2e. Ensure it runs even when Turbo restores cached build outputs,
and avoid changing the existing build synchronization setting.

Source: MCP tools

@omridevk
omridevk force-pushed the fix-e2e-nextjs-injected-deps-451 branch from a2122a7 to 77b3c09 Compare August 12, 2026 21:49
The nextjs e2e apps deployed the whole workspace graph into a mktemp
dir on every playwright run just to give Next/Turbopack a real
non-symlinked node_modules (serverExternalPackages requires resolution
under node_modules/). That deploy was slow and flaked under a single
180s webServer timeout (#435).

dependenciesMeta.injected gives pnpm's own hard-copy node_modules
without a deploy step, and syncInjectedDepsAfterScripts: [build] keeps
those copies synced — but only when a package's own "build" script
actually RUNS. CI's `turbo run test:e2e` gets a cache HIT on every
package build, which restores dist straight into the source package
dir without executing the script, so the sync never fires: a cold
`pnpm install` (before any dist exists) leaves the injected copies
permanently distless, and next dev fails with MODULE_NOT_FOUND. Ruled
out two lighter fixes with evidence before landing on this one: pnpm
11.7.0 has no manual resync CLI command, and a plain `pnpm install
--frozen-lockfile` no-ops on an already-satisfied lockfile even with a
drifted injected copy (directory deps carry no integrity hash for pnpm
to notice the drift). So each nextjs e2e app's playwright.config.ts
webServer command prepends a direct `pnpm --filter <its injected deps>
run build` before next dev, bypassing turbo's cache so the script
always actually runs and the sync always fires. Both playwright
configs keep the existing 180s webServer default.

The build-and-sync step lives in playwright.config.ts, not the app's
own package.json test:e2e script: e2e/init clones these apps as
consumer templates and strips @conciv/* wiring from package.json
before running `conciv init`, and a test:e2e script naming @conciv/it
would leak conciv wiring into that stripped clone. dependenciesMeta
has to stay in package.json (that's the injection mechanism itself),
so e2e/init's own stripper (withoutConcivDependencies) now also drops
@conciv/* entries from dependenciesMeta, deleting the field if that
empties it — the stripper doing its declared job of removing all
conciv wiring from the template.

The previous symlink-only setup silently dropped builtin server
extensions (MODULE_NOT_FOUND from the terminal extension's dynamic
require) while the suite stayed green. Added a positive assertion in
e2e/nextjs's widget spec that exercises the terminal extension's real
connect-command RPC round trip, so that failure mode goes red instead
of silent; verified it fails under the pre-fix symlinked state and
passes under the injected-deps fix.

Closes #435
Refs #451

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk force-pushed the fix-e2e-nextjs-injected-deps-451 branch from 77b3c09 to 1e0a837 Compare August 12, 2026 22:24
The mechanism story lives in the history of this branch and PR #452:
injected deps exist because Turbopack's serverExternalPackages needs
real dirs under node_modules; syncInjectedDepsAfterScripts only fires
when a dep's build script runs, so each nextjs e2e app's playwright
webServer command runs those builds directly (turbo cache hits restore
dist without executing scripts and would leave injected copies stale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@omridevk
omridevk merged commit caf416c into main Aug 12, 2026
24 checks passed
@omridevk
omridevk deleted the fix-e2e-nextjs-injected-deps-451 branch August 12, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant