From de12b9377eef6c48ace961631c443c99d4c07ca9 Mon Sep 17 00:00:00 2001 From: Vivek Date: Wed, 5 Aug 2026 19:12:57 +0530 Subject: [PATCH 1/2] fix(docs): the fresh-worktree node_modules remedy produces a broken worktree AGENTS.md told you to symlink the primary checkout's root node_modules. Doing exactly that leaves a worktree that looks set up and then fails hundreds of assertions for reasons that point nowhere near the cause. The root tree carries ws@7, hoisted for another dependent, while packages/server declares ^8.20.0 and keeps ws@8 nested. Link only the root and WebSocketServer, a ws@8-only named export, resolves up to ws@7 and throws at module load, reddening the server, integration, and smoke suites without ever naming ws. packages/core/dist is the second gap: it is built rather than committed, so every test importing the built bundle fails to resolve it. npm run worktree:link discovers both from the primary rather than hardcoding a list, since the set changes whenever a package gains a nested tree. Measured on a virgin worktree: 936 failures before, 29 after, and that residual set is the same one the primary checkout fails locally. The safety rules are not hypothetical. The naive version of this script created a dangling node_modules/node_modules and deleted a git-tracked directory that happened to be named node_modules, so it now refuses to overwrite any existing path, refuses to create a dangling link, and never descends into node_modules. --- AGENTS.md | 11 +- package.json | 1 + scripts/link-worktree-deps.mjs | 137 +++++++++++++++++ test/repo-health/link-worktree-deps.test.mjs | 148 +++++++++++++++++++ 4 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 scripts/link-worktree-deps.mjs create mode 100644 test/repo-health/link-worktree-deps.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 996ad4a6a..2f28cabd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,16 @@ cd ../- # do ALL work for the task here # ... commit + push from this worktree; after merge: git worktree remove ../- ``` -**A fresh worktree has NO `node_modules`** (git worktrees do not copy it), so running an app from one (`webjs dev` / `webjs start`, the test runner, a scaffolded app) fails to resolve `@webjsdev/*` until you install or link it. `webjs doctor` warns for this exact case (#954) and `webjs dev` / `webjs start` print the cause + remedy instead of a raw `ERR_MODULE_NOT_FOUND`. Fix by installing in the worktree (`npm install`) or symlinking the primary checkout's modules (`ln -s ..//node_modules node_modules`). When only a subset of `@webjsdev/*` packages was edited, link those from the worktree and the rest from the primary checkout so a built `dist/` (e.g. `@webjsdev/core`) still resolves. +**A fresh worktree has NO `node_modules`** (git worktrees do not copy it), so running an app from one (`webjs dev` / `webjs start`, the test runner, a scaffolded app) fails to resolve `@webjsdev/*` until you install or link it. `webjs doctor` warns for this exact case (#954) and `webjs dev` / `webjs start` print the cause + remedy instead of a raw `ERR_MODULE_NOT_FOUND`. + +Fix it with **`npm run worktree:link`** from inside the worktree (or a full `npm install` there, which is correct but slow and duplicates a large tree per worktree). **Do NOT hand-symlink only the root `node_modules`.** That is the obvious move and it produces a worktree that looks set up and then fails dozens of tests for reasons that point nowhere near the real cause. Two things beyond the root tree are needed, and the script handles both: + +- **Every NESTED `node_modules`, not just the root.** npm hoists what it can, but a workspace whose range conflicts with the hoisted copy keeps its own nested tree. `packages/server` is the live example: the root carries `ws@7` (hoisted for another dependent) while `packages/server` declares `^8.20.0` and keeps `ws@8` nested. Link only the root and `WebSocketServer`, a ws@8-only named export, resolves up to ws@7 and throws at module load. That single miss failed hundreds of assertions across the server, integration, and smoke suites, none of them naming ws. +- **`packages/core/dist`**, which is built rather than committed, so a fresh worktree has none and every test importing the built bundle fails to resolve it. + +The script discovers both from the primary checkout rather than hardcoding a list (the set changes whenever a package gains a nested tree), never overwrites an existing path, and never creates a dangling link, so it is safe to re-run and safe in a worktree where you already ran a real `npm install`. + +When only a subset of `@webjsdev/*` packages was edited, link those from the worktree and the rest from the primary checkout so a built `dist/` (e.g. `@webjsdev/core`) still resolves. Git enforces one-branch-per-worktree, so separate worktrees make the collision impossible. There is NO lone-agent exception: every task cuts a worktree, and the primary checkout stays an untouched mirror of main (tracked-file edits there are hook-blocked). The repo's `.hooks/pre-commit` additionally BLOCKS a published-library (`core`/`server`/`cli`/`mcp`/`ui`/`intellisense`) version bump on any non-`chore/release-*` branch, the canonical wrong-branch-release symptom. diff --git a/package.json b/package.json index 5b25dcf5b..b4afd17c5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prepare": "node scripts/git-worktree-safe.mjs 2>/dev/null || true", "fix:git": "node scripts/git-worktree-safe.mjs", "check:git": "node scripts/git-worktree-safe.mjs --check", + "worktree:link": "node scripts/link-worktree-deps.mjs", "dev": "node scripts/dev-all.js", "pretest": "node website/scripts/copy-registry.mjs", "test": "node scripts/run-node-tests.js", diff --git a/scripts/link-worktree-deps.mjs b/scripts/link-worktree-deps.mjs new file mode 100644 index 000000000..6001ed2f2 --- /dev/null +++ b/scripts/link-worktree-deps.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node +/** + * Link a fresh git worktree's dependencies to the primary checkout's, so the + * worktree can run the test suite without a full `npm install`. + * + * A git worktree does not copy `node_modules`, and this repo needs MORE than + * the root one: + * + * - **Every nested `node_modules`**, not just the root. npm hoists what it can, + * but a workspace whose range conflicts with the hoisted copy keeps its own + * nested tree. `packages/server` is the live example: the root has `ws@7` + * (hoisted for another dependent) while `packages/server` declares `^8.20.0` + * and carries `ws@8` nested. Link only the root and `WebSocketServer`, a + * ws@8-only named export, resolves up to ws@7 and throws at module load, + * which surfaces as dozens of unrelated-looking failures across the server, + * integration, and smoke suites. + * - **`packages/core/dist`**, which is gitignored and built rather than + * committed. Tests that import the built bundle cannot resolve it in a fresh + * worktree. + * + * Both sets are discovered from the primary checkout rather than hardcoded, + * because the list changes whenever a package gains a nested tree. + * + * Safety rules, all of which exist because the naive version of this script + * broke a worktree while it was being written: + * + * - Never delete or overwrite anything. A path that already exists is left + * alone, so a worktree with a real `npm install` is untouched and re-running + * is a no-op. + * - Never create a dangling link. A source that does not exist is skipped, + * because a dangling `node_modules` resolves more confusingly than a missing + * one. + * - Never treat `/node_modules` itself as a search root. Descending + * into it yields thousands of nested `node_modules` belonging to third-party + * packages, none of which should be linked. + * + * Usage, from inside the worktree: + * + * node scripts/link-worktree-deps.mjs # link from the default primary + * node scripts/link-worktree-deps.mjs # or name it explicitly + * npm run worktree:link + */ +import { existsSync, lstatSync, mkdirSync, readdirSync, symlinkSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { execFileSync } from 'node:child_process'; + +/** Directory names never worth descending into when hunting for nested trees. */ +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.webjs', 'coverage']); + +/** + * Collect every directory under `root` that has its own `node_modules`, + * returned as paths relative to `root`. Never descends INTO a `node_modules`. + * + * @param {string} root + * @param {number} [maxDepth] how many directory levels below root to search + * @returns {string[]} relative paths of the `node_modules` directories + */ +function findNodeModules(root, maxDepth = 4) { + /** @type {string[]} */ + const out = []; + /** @param {string} dir @param {number} depth */ + const walk = (dir, depth) => { + if (depth > maxDepth) return; + let entries; + try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const e of entries) { + if (!e.isDirectory() && !e.isSymbolicLink()) continue; + if (e.name === 'node_modules') { out.push(relative(root, join(dir, e.name))); continue; } + if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue; + walk(join(dir, e.name), depth + 1); + } + }; + walk(root, 0); + return out; +} + +/** + * Create one symlink, refusing every unsafe case. + * + * @param {string} src absolute path in the primary checkout + * @param {string} dst absolute path in this worktree + * @param {string} label what to print on success + * @returns {'linked' | 'exists' | 'missing-source'} + */ +function link(src, dst, label) { + if (!existsSync(src)) return 'missing-source'; + // lstat, not existsSync: a dangling symlink left by an earlier run must count + // as present so we neither clobber it nor silently stack a second link. + try { lstatSync(dst); return 'exists'; } catch { /* not there, good */ } + mkdirSync(dirname(dst), { recursive: true }); + symlinkSync(src, dst, 'junction'); + console.log(` linked ${label}`); + return 'linked'; +} + +/** @returns {string} absolute path of the primary checkout */ +function defaultPrimary() { + // The common dir of a linked worktree is `/.git/worktrees/`, + // so the primary checkout is three levels up from it. In the primary itself + // the common dir is just `/.git`. + const common = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { + encoding: 'utf8', + }).trim(); + return resolve(dirname(common)); +} + +const here = process.cwd(); +const primary = resolve(process.argv[2] || defaultPrimary()); + +if (primary === here) { + console.log('[link-worktree-deps] this IS the primary checkout, nothing to link.'); + process.exit(0); +} +if (!existsSync(join(primary, 'package.json'))) { + console.error(`[link-worktree-deps] not a checkout: ${primary}`); + process.exit(1); +} + +console.log(`[link-worktree-deps] linking from ${primary}`); + +let linked = 0; +let skipped = 0; +for (const rel of findNodeModules(primary)) { + const r = link(join(primary, rel), join(here, rel), rel); + if (r === 'linked') linked += 1; else skipped += 1; +} + +// `packages/core/dist` is built, not committed, so a fresh worktree has none +// and any test importing the built bundle fails to resolve it. +for (const rel of ['packages/core/dist']) { + const r = link(join(primary, rel), join(here, rel), rel); + if (r === 'linked') linked += 1; + else if (r === 'missing-source') console.log(` skipped ${rel} (not built in the primary; run npm run build there)`); + else skipped += 1; +} + +console.log(`[link-worktree-deps] ${linked} linked, ${skipped} already present.`); diff --git a/test/repo-health/link-worktree-deps.test.mjs b/test/repo-health/link-worktree-deps.test.mjs new file mode 100644 index 000000000..431d76885 --- /dev/null +++ b/test/repo-health/link-worktree-deps.test.mjs @@ -0,0 +1,148 @@ +/** + * `scripts/link-worktree-deps.mjs` links a fresh worktree's dependencies to the + * primary checkout's (#1287). + * + * The behaviours asserted here are the ones whose absence produced real + * breakage while the script was written: linking only the ROOT `node_modules` + * leaves a ws version skew that fails hundreds of assertions elsewhere, and a + * naive implementation clobbered a git-tracked directory that happened to be + * named `node_modules`. + * + * These drive the script as a subprocess against a synthetic "primary" and + * "worktree" pair in a temp dir, so nothing touches the real checkout. + */ +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync, lstatSync, readlinkSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT = fileURLToPath(new URL('../../scripts/link-worktree-deps.mjs', import.meta.url)); + +/** Build a fake primary checkout with the nested-tree shape this repo has. */ +function makePrimary() { + const root = mkdtempSync(join(tmpdir(), 'wjprimary-')); + writeFileSync(join(root, 'package.json'), '{"name":"fake-primary"}'); + for (const d of [ + 'node_modules/ws', + 'packages/server/node_modules/ws', + 'packages/ui/node_modules', + 'website/node_modules', + 'packages/core/dist', + ]) mkdirSync(join(root, d), { recursive: true }); + // a third-party package with its OWN nested node_modules, which must never + // be treated as a link target + mkdirSync(join(root, 'node_modules/some-dep/node_modules/inner'), { recursive: true }); + return root; +} + +function makeWorktree() { + const root = mkdtempSync(join(tmpdir(), 'wjworktree-')); + writeFileSync(join(root, 'package.json'), '{"name":"fake-worktree"}'); + mkdirSync(join(root, 'packages/server'), { recursive: true }); + return root; +} + +/** @returns {string} combined stdout of the script run inside `cwd` */ +function run(cwd, primary) { + return execFileSync(process.execPath, [SCRIPT, primary], { cwd, encoding: 'utf8' }); +} + +describe('link-worktree-deps (#1287)', () => { + test('links the nested node_modules, not just the root', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + run(wt, primary); + // The root alone is the trap this whole script exists to close. + assert.ok(lstatSync(join(wt, 'node_modules')).isSymbolicLink(), 'root linked'); + assert.ok( + lstatSync(join(wt, 'packages/server/node_modules')).isSymbolicLink(), + 'packages/server nested node_modules linked (the ws@7 vs ws@8 skew)', + ); + assert.ok(lstatSync(join(wt, 'packages/ui/node_modules')).isSymbolicLink(), 'packages/ui linked'); + assert.ok(lstatSync(join(wt, 'website/node_modules')).isSymbolicLink(), 'website linked'); + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); + + test('links packages/core/dist, which is built and not committed', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + run(wt, primary); + assert.ok(lstatSync(join(wt, 'packages/core/dist')).isSymbolicLink(), 'core dist linked'); + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); + + test('never descends into node_modules looking for more node_modules', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + const out = run(wt, primary); + // Assert on what the script CLAIMS to have linked, not on the filesystem: + // the worktree's `node_modules` is a symlink to the primary's, so a + // third-party nested tree is visible THROUGH it either way, and an + // existsSync check here passes for the wrong reason. + const linkedPaths = out.split('\n').filter((l) => l.includes('linked ')).map((l) => l.trim()); + for (const l of linkedPaths) { + assert.equal( + (l.match(/node_modules/g) || []).length <= 1, + true, + `must not link a tree nested inside node_modules, got: ${l}`, + ); + } + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); + + test('is idempotent and never clobbers a real install', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + // a REAL directory where a link would otherwise go, as if npm install ran + mkdirSync(join(wt, 'node_modules/already-here'), { recursive: true }); + const out = run(wt, primary); + assert.ok(!lstatSync(join(wt, 'node_modules')).isSymbolicLink(), 'real node_modules left as a directory'); + assert.ok(existsSync(join(wt, 'node_modules/already-here')), 'existing contents untouched'); + assert.match(out, /already present/, 'reports it skipped something'); + // second run changes nothing + const out2 = run(wt, primary); + assert.match(out2, /0 linked/, 're-running links nothing new'); + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); + + test('skips a source that does not exist rather than creating a dangling link', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + rmSync(join(primary, 'packages/core/dist'), { recursive: true, force: true }); + const out = run(wt, primary); + assert.ok(!existsSync(join(wt, 'packages/core/dist')), 'no dist link created'); + let dangling = false; + try { dangling = lstatSync(join(wt, 'packages/core/dist')).isSymbolicLink(); } catch { /* absent, good */ } + assert.equal(dangling, false, 'a dangling link resolves more confusingly than a missing one'); + assert.match(out, /not built in the primary/, 'says why it skipped'); + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); + + test('refuses to link a checkout to itself', () => { + const primary = makePrimary(); + try { + const out = run(primary, primary); + assert.match(out, /this IS the primary checkout/); + assert.ok(!lstatSync(join(primary, 'node_modules')).isSymbolicLink(), 'root untouched'); + } finally { rmSync(primary, { recursive: true, force: true }); } + }); + + test('leaves an existing symlink alone instead of stacking a second one', () => { + const primary = makePrimary(); + const wt = makeWorktree(); + try { + symlinkSync(join(primary, 'node_modules'), join(wt, 'node_modules')); + run(wt, primary); + assert.equal(readlinkSync(join(wt, 'node_modules')), join(primary, 'node_modules')); + assert.ok(!existsSync(join(wt, 'node_modules/node_modules')), 'no link nested inside the existing one'); + } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } + }); +}); From 14015da9127d0744bd58516de5cad9a086e486fc Mon Sep 17 00:00:00 2001 From: Vivek Date: Wed, 5 Aug 2026 19:51:29 +0530 Subject: [PATCH 2/2] docs: state that linking does not make a worktree test its own source Through the linked root, every bare @webjsdev/* specifier resolves into the PRIMARY checkout, because node_modules/@webjsdev/core is a relative symlink into the primary's packages/core. So a worktree editing framework source runs the suite against the primary's copy through those paths, while relative imports and the browser suite do use its own. Linking makes the suite runnable, not self-testing, and presenting it as a fix without saying so was misleading. That also retires the retained subset-linking sentence, which after linking would have written into the primary's own node_modules. The doctor and dev remedy message still suggests the root-only symlink. It is correct for a scaffolded app worktree, which has no nested trees and no built dist, so it is noted rather than changed. Also corrects the defaultPrimary comment, which described --git-dir arithmetic rather than the --git-common-dir the code actually uses, and covers that branch with a test; it was the only one nothing exercised. --- AGENTS.md | 6 +++-- scripts/link-worktree-deps.mjs | 27 ++++++++++++++++---- test/repo-health/link-worktree-deps.test.mjs | 11 ++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2f28cabd8..795c4a198 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,9 +60,11 @@ Fix it with **`npm run worktree:link`** from inside the worktree (or a full `npm - **Every NESTED `node_modules`, not just the root.** npm hoists what it can, but a workspace whose range conflicts with the hoisted copy keeps its own nested tree. `packages/server` is the live example: the root carries `ws@7` (hoisted for another dependent) while `packages/server` declares `^8.20.0` and keeps `ws@8` nested. Link only the root and `WebSocketServer`, a ws@8-only named export, resolves up to ws@7 and throws at module load. That single miss failed hundreds of assertions across the server, integration, and smoke suites, none of them naming ws. - **`packages/core/dist`**, which is built rather than committed, so a fresh worktree has none and every test importing the built bundle fails to resolve it. -The script discovers both from the primary checkout rather than hardcoding a list (the set changes whenever a package gains a nested tree), never overwrites an existing path, and never creates a dangling link, so it is safe to re-run and safe in a worktree where you already ran a real `npm install`. +The script discovers the `node_modules` set from the primary checkout rather than hardcoding a list (it changes whenever a package gains a nested tree), never overwrites an existing path, and never creates a dangling link, so it is safe to re-run and safe in a worktree where you already ran a real `npm install`. -When only a subset of `@webjsdev/*` packages was edited, link those from the worktree and the rest from the primary checkout so a built `dist/` (e.g. `@webjsdev/core`) still resolves. +**Know what this does NOT give you.** The worktree then runs the PRIMARY checkout's framework source through every bare `@webjsdev/*` specifier, because `/node_modules/@webjsdev/core` is a relative symlink into `/packages/core` and resolving through the linked root lands there. Relative imports (`../../../src/x.js`) and the browser suite, which web-test-runner serves from the worktree, do use the worktree's own files. So linking makes the suite RUNNABLE, not self-testing: if you are editing `packages/core/src` or `packages/server/src` and need a bare-specifier consumer to exercise YOUR copy, run a real `npm install` in the worktree, or repoint the individual `@webjsdev/` entries at it. CI always builds from the branch, so it is unaffected either way. + +Note the `webjs doctor` / `webjs dev` remedy message still suggests the root-only symlink. That advice is correct for a scaffolded APP worktree, which has no nested trees and no built `dist/`, and wrong only for this monorepo. Git enforces one-branch-per-worktree, so separate worktrees make the collision impossible. There is NO lone-agent exception: every task cuts a worktree, and the primary checkout stays an untouched mirror of main (tracked-file edits there are hook-blocked). The repo's `.hooks/pre-commit` additionally BLOCKS a published-library (`core`/`server`/`cli`/`mcp`/`ui`/`intellisense`) version bump on any non-`chore/release-*` branch, the canonical wrong-branch-release symptom. diff --git a/scripts/link-worktree-deps.mjs b/scripts/link-worktree-deps.mjs index 6001ed2f2..a95cc2fa5 100644 --- a/scripts/link-worktree-deps.mjs +++ b/scripts/link-worktree-deps.mjs @@ -18,8 +18,25 @@ * committed. Tests that import the built bundle cannot resolve it in a fresh * worktree. * - * Both sets are discovered from the primary checkout rather than hardcoded, - * because the list changes whenever a package gains a nested tree. + * The `node_modules` set is discovered from the primary checkout rather than + * hardcoded, because it changes whenever a package gains a nested tree. The + * `packages/core/dist` entry is an explicit one-off, since it is the only built + * output the suite imports. + * + * ## What this does NOT give you + * + * The worktree runs the PRIMARY checkout's framework source through every bare + * `@webjsdev/*` specifier. `/node_modules/@webjsdev/core` is a relative + * symlink into `/packages/core`, so resolving through the linked root + * lands in the primary, not here. Relative imports (`../../../src/x.js`) and the + * browser suite, which web-test-runner serves from this worktree, do use the + * worktree's own files. + * + * So this makes the suite RUNNABLE, not self-testing. If you are editing + * `packages/core/src` or `packages/server/src` and need a bare-specifier + * consumer to exercise YOUR copy, run a real `npm install` in the worktree, or + * point the individual `@webjsdev/` entries at this worktree instead. CI + * always builds from the branch, so it is unaffected either way. * * Safety rules, all of which exist because the naive version of this script * broke a worktree while it was being written: @@ -95,9 +112,9 @@ function link(src, dst, label) { /** @returns {string} absolute path of the primary checkout */ function defaultPrimary() { - // The common dir of a linked worktree is `/.git/worktrees/`, - // so the primary checkout is three levels up from it. In the primary itself - // the common dir is just `/.git`. + // `--git-common-dir` is `/.git` from ANY worktree, linked or not + // (the per-worktree `.git/worktrees/` path is what `--git-dir` gives), + // so the primary checkout is its parent. const common = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', }).trim(); diff --git a/test/repo-health/link-worktree-deps.test.mjs b/test/repo-health/link-worktree-deps.test.mjs index 431d76885..ec114a35e 100644 --- a/test/repo-health/link-worktree-deps.test.mjs +++ b/test/repo-health/link-worktree-deps.test.mjs @@ -126,6 +126,17 @@ describe('link-worktree-deps (#1287)', () => { } finally { rmSync(primary, { recursive: true, force: true }); rmSync(wt, { recursive: true, force: true }); } }); + test('defaultPrimary() resolves the real primary from this checkout', () => { + // Every other test passes `primary` as argv[2], so without this the + // git-derived default branch never executes at all. + const out = execFileSync(process.execPath, [SCRIPT], { + cwd: process.cwd(), encoding: 'utf8', + }); + // Run from the repo itself, so it must recognise the primary and no-op + // rather than linking anything. + assert.match(out, /this IS the primary checkout|linking from \//); + }); + test('refuses to link a checkout to itself', () => { const primary = makePrimary(); try {