From e2078bf061fb3b5862cee5ee9a57975758d75844 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Sat, 7 Nov 2026 14:19:20 +0800 Subject: [PATCH] dev: refuse a bench start without pnpm or the harness native addon dev/bench.sh started a bench without checking the two things it cannot start without, and both failures surfaced 17-42 smoke steps later as if the plugin were broken. bench-preflight.mjs decides pass / missing-pnpm / missing-native-addon and start calls it before writing the profile; ensure_profile keeps the pnpm install output and stops on a non-zero exit. Fixes #2576 --- core/dev/bench-preflight.mjs | 86 ++++++++++++ core/dev/bench.sh | 10 +- core/test/bench-preflight.test.js | 130 ++++++++++++++++++ docs/subsystems/ds-bot-core.md | 3 +- patches/ds-bot/0007-version-and-tooling.patch | 27 ++++ patches/ds-bot/files/dev/bench-preflight.mjs | 86 ++++++++++++ .../ds-bot/files/test/bench-preflight.test.js | 130 ++++++++++++++++++ patches/ds-bot/topics.json | 1 + 8 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 core/dev/bench-preflight.mjs create mode 100644 core/test/bench-preflight.test.js create mode 100644 patches/ds-bot/files/dev/bench-preflight.mjs create mode 100644 patches/ds-bot/files/test/bench-preflight.test.js diff --git a/core/dev/bench-preflight.mjs b/core/dev/bench-preflight.mjs new file mode 100644 index 000000000..db5db7cef --- /dev/null +++ b/core/dev/bench-preflight.mjs @@ -0,0 +1,86 @@ +// Bench preflight (dev/bench.sh): refuse before the profile is written when the bench cannot +// start, and say what to do about it. Both failures otherwise surface much later as smoke +// failures that look like plugin bugs — no pnpm leaves the profile without node_modules, so +// `dsh` never resolves the plugin bundle; no prebuilt native addon boots the web server but +// fails every Bot turn. A deepseek-harness source checkout carries no native `bin/`, so it +// needs the prebuilt copy from the installed `@deepseek-ai/node-addon-system-` package. +// +// node dev/bench-preflight.mjs +// +// Exit 0 when the bench can start, 1 with the remedy on stderr when it cannot, 2 on bad usage. +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { pathToFileURL } from 'node:url' + +// The addon loader picks the directory from Node's report, not from the ELF interpreter, so the +// preflight must read the same signal to name the file the loader would try. +export function libcName(platform = process.platform, report = process.report?.getReport()) { + if (platform !== 'linux') return undefined + return report?.header?.glibcVersionRuntime ? 'glibc' : 'musl' +} + +export function addonSubpath({ platform = process.platform, libc = libcName(platform) } = {}) { + return join('bin', ...(libc ? [libc] : []), 'system.node') +} + +export function nativeAddonPath(harnessDir, { platform = process.platform, arch = process.arch, libc = libcName(platform) } = {}) { + return join(harnessDir, 'native', 'system', 'packages', `${platform}-${arch}`, addonSubpath({ platform, libc })) +} + +export function commandExists(command) { + const result = spawnSync(command, ['--version'], { stdio: 'ignore' }) + return !result.error && result.status === 0 +} + +export function checkBench({ + harnessDir, + platform = process.platform, + arch = process.arch, + libc = libcName(platform), + hasPnpm = commandExists('pnpm'), + exists = existsSync, +} = {}) { + const addon = harnessDir ? nativeAddonPath(harnessDir, { platform, arch, libc }) : undefined + const result = { + platform, + arch, + libc, + harnessDir, + addon, + package: `@deepseek-ai/node-addon-system-${platform}-${arch}`, + subpath: addonSubpath({ platform, libc }), + } + if (!hasPnpm) return { ...result, ok: false, state: 'missing-pnpm' } + // An installed harness resolves the addon from its own node_modules; only a source checkout + // has native/system/packages, and only that tree needs the prebuilt bin/ copied into it. + const sourceTree = harnessDir && exists(join(harnessDir, 'native', 'system', 'packages')) + if (sourceTree && !exists(addon)) return { ...result, ok: false, state: 'missing-native-addon' } + return { ...result, ok: true, state: 'pass' } +} + +export function problemLine(result) { + if (result.state === 'missing-pnpm') { + return 'bench preflight: pnpm is not on PATH — the profile install needs it; `corepack pnpm` supplies pnpm (put a `pnpm` shim that execs it on PATH, e.g. tools/bin/pnpm).' + } + if (result.state === 'missing-native-addon') { + return `bench preflight: no native addon at ${result.addon} — copy ${result.package}/${result.subpath} (installed under core/dev/harness by \`npm run harness\`) into ${dirname(result.addon)}.` + } + return undefined +} + +export function main(argv = process.argv.slice(2)) { + const harnessDir = argv[0] ?? process.env.DSH_HARNESS + if (!harnessDir) { + console.error('usage: node dev/bench-preflight.mjs ') + return 2 + } + const result = checkBench({ harnessDir }) + if (result.ok) return 0 + console.error(problemLine(result)) + return 1 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main()) +} diff --git a/core/dev/bench.sh b/core/dev/bench.sh index 258a76426..65ba1a524 100755 --- a/core/dev/bench.sh +++ b/core/dev/bench.sh @@ -59,7 +59,12 @@ ensure_profile() { } EOF printf 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n' > "$profile/pnpm-workspace.yaml" - (cd "$profile" && DSH_HOME="$HOME_DIR" pnpm install >/dev/null) + if ! install_output=$(cd "$profile" && DSH_HOME="$HOME_DIR" pnpm install 2>&1); then + printf '%s\n' "$install_output" >&2 + rm -rf "$profile" + echo "pnpm install failed; the bench profile was not created" >&2 + exit 1 + fi } # Bench-only plugin config, layered over the profile with --patch. @@ -80,6 +85,9 @@ write_overlay() { } start() { + # Refuse before the profile is written: a missing pnpm or native addon otherwise surfaces + # as smoke failures that look like plugin bugs (#2576). + node "$PLUGIN/dev/bench-preflight.mjs" "$DSH_HARNESS" || exit 1 mkdir -p "$HOME_DIR" ensure_profile write_overlay diff --git a/core/test/bench-preflight.test.js b/core/test/bench-preflight.test.js new file mode 100644 index 000000000..2a04fc986 --- /dev/null +++ b/core/test/bench-preflight.test.js @@ -0,0 +1,130 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { after, test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { addonSubpath, checkBench, libcName, nativeAddonPath, problemLine } from '../dev/bench-preflight.mjs' + +const here = dirname(fileURLToPath(import.meta.url)) + +const roots = [] + +after(() => { + for (const root of roots) rmSync(root, { recursive: true, force: true }) +}) + +function tree(base = tmpdir()) { + mkdirSync(base, { recursive: true }) + const root = mkdtempSync(join(base, 'flybot-bench-preflight-')) + roots.push(root) + return { root, harness: join(root, 'harness') } +} + +// A shell fixture has to execute `pnpm`, and a tmpdir can sit on a noexec mount (it does on +// the shared box). node_modules is inside the checkout, ignored by git, and always executable. +function execTree() { + return tree(join(here, '..', 'node_modules', '.cache')) +} + +function writeAddon(harness, { platform = 'linux', arch = 'arm64', libc = 'glibc' } = {}) { + const file = nativeAddonPath(harness, { platform, arch, libc }) + mkdirSync(dirname(file), { recursive: true }) + writeFileSync(file, '') + return file +} + +test('passes when pnpm and the addon the loader would require are there', () => { + const { harness } = tree() + const file = writeAddon(harness) + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.ok, true) + assert.equal(result.state, 'pass') + assert.equal(result.addon, file) +}) + +test('refuses with missing-pnpm before it looks at the addon', () => { + const { harness } = tree() + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: false }) + assert.equal(result.state, 'missing-pnpm') + assert.match(problemLine(result), /corepack pnpm/) +}) + +test('refuses with missing-native-addon and names the exact path', () => { + const { harness } = tree() + mkdirSync(join(harness, 'native', 'system', 'packages'), { recursive: true }) + const file = nativeAddonPath(harness, { platform: 'linux', arch: 'arm64', libc: 'glibc' }) + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.state, 'missing-native-addon') + assert.equal(result.addon, file) + const line = problemLine(result) + assert.ok(line.includes(file), line) + assert.match(line, /node-addon-system-linux-arm64/) +}) + +test('does not demand a source-tree addon from an installed harness', () => { + const { harness } = tree() + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.ok, true) + assert.equal(result.state, 'pass') +}) + +test('follows the loader on libc and platform', () => { + assert.equal(addonSubpath({ platform: 'linux', libc: 'glibc' }), join('bin', 'glibc', 'system.node')) + assert.equal(addonSubpath({ platform: 'linux', libc: 'musl' }), join('bin', 'musl', 'system.node')) + assert.equal(addonSubpath({ platform: 'darwin' }), join('bin', 'system.node')) + assert.equal(libcName('darwin'), undefined) +}) + +test('bench.sh refuses before writing the profile when pnpm is missing', { skip: process.platform === 'win32' }, () => { + const { root, harness, path, home } = benchTree() + const result = runBench({ root, path, harness }, 'start') + assert.notEqual(result.status, 0) + assert.match(result.stderr, /pnpm is not on PATH/) + assert.equal(existsSync(join(home, 'profiles')), false) +}) + +test('bench.sh shows a failed pnpm install and leaves no half-built profile', { skip: process.platform === 'win32' }, () => { + const script = '#!/bin/sh\n[ "$1" = "--version" ] && exit 0\necho "install failed: boom" >&2\nexit 1\n' + const { root, harness, path, home } = benchTree({ pnpm: script }) + const result = runBench({ root, path, harness }, 'start') + assert.notEqual(result.status, 0) + assert.match(result.stderr, /install failed: boom/) + assert.equal(existsSync(join(home, 'profiles', 'web')), false) +}) + +// A checkout-shaped tree: core/dev/{bench.sh,bench-preflight.mjs}, a harness with the host's +// addon, and a bin directory that holds only what bench.sh needs before the preflight. +function benchTree({ pnpm } = {}) { + const { root, harness } = execTree() + const dev = join(root, 'core', 'dev') + mkdirSync(dev, { recursive: true }) + copyFileSync(join(here, '../dev/bench.sh'), join(dev, 'bench.sh')) + copyFileSync(join(here, '../dev/bench-preflight.mjs'), join(dev, 'bench-preflight.mjs')) + writeAddon(harness, { platform: process.platform, arch: process.arch, libc: libcName() }) + const bin = join(root, 'bin') + mkdirSync(bin) + symlinkSync(process.execPath, join(bin, 'node')) + // bench.sh's only external tool before the preflight is `dirname "$0"`, and $0 here is an + // absolute path; a shell-only stand-in keeps the fixture independent of the host's bin dirs. + writeFileSync(join(bin, 'dirname'), '#!/bin/sh\nprintf "%s\\n" "${1%/*}"\n') + chmodSync(join(bin, 'dirname'), 0o755) + if (pnpm) { + writeFileSync(join(bin, 'pnpm'), pnpm) + chmodSync(join(bin, 'pnpm'), 0o755) + } + // The rest of the box, minus any directory that holds a real pnpm: these tests decide that + // answer themselves, and a runner with pnpm installed must not leak one in. + const rest = (process.env.PATH ?? '').split(':').filter((dir) => dir && !existsSync(join(dir, 'pnpm'))) + return { root, harness, bin, home: join(root, '.dsh-test-smoke'), path: [bin, ...rest].join(':') } +} + +function runBench({ root, path, harness }, action) { + const result = spawnSync('/bin/sh', [join(root, 'core', 'dev', 'bench.sh'), 'smoke', '3097', '8797', action], { + encoding: 'utf8', + env: { PATH: path, DSH_HARNESS: harness, HOME: root }, + }) + if (result.error) assert.fail(`could not run bench.sh: ${result.error.message}`) + return result +} diff --git a/docs/subsystems/ds-bot-core.md b/docs/subsystems/ds-bot-core.md index 7bc657c48..3c696551b 100644 --- a/docs/subsystems/ds-bot-core.md +++ b/docs/subsystems/ds-bot-core.md @@ -44,6 +44,7 @@ holds. Task bookkeeping does not imply runtime execution or tool permission. | `core/dev/token-report.mjs` | Reads the usage ledger into per-Bot, per-Session and per-day token numbers and each Session's prompt growth, keeping fresh input, cache read and cache write apart (#799). Read-only; `--db`, `--from`, `--to`, `--tz`, `--bot`, `--top`, `--growth`, `--json`. | | `core/dev/turn-split.mjs` | Reads Session logs into the split of one turn's prompt: the fixed block (system prompt plus tool schemas) measured as each Session's fit intercept, then history, tool results and task by character share (#796). Read-only; `--sessions`, `--session`, `--turn`, `--tz`, `--days`, `--top`, `--stats`, `--room`, `--json`. | | `core/dev/perf-baseline.mjs` | Measures the four baseline numbers (#356) on the mock bench: turn end-to-end p50/p95, tool-call latency p50/p95, resident RSS peak, plugin load time — read from the Session events the activity feed folds, never from the page. Prints its markdown table and writes the `--out` JSON; no thresholds (see [performance-baseline](../performance-baseline.md)). Tooling only: reads the bench and starts nothing. | +| `core/dev/bench-preflight.mjs` | Bench preflight (#2576): `dev/bench.sh start` calls it before the profile is written and refuses with the remedy when `pnpm` is not on PATH or the harness checkout lacks the host's prebuilt native addon (`native/system/packages//bin//system.node`). Exit 0 pass, 1 refusal on stderr, 2 usage. Tests: `core/test/bench-preflight.test.js`. | | `core/dev/experience/` | Offline experience library (#874 Step 2): JSON lessons under `repos///` and `stacks//`. The read API is `core/src/host/experience.js` (see [core-host](core-host.md)). `check.mjs` validates the tree and can add one entry file. Not part of the npm `files` list, and not injected into a harness tier. | | `core/src/host/hooks.js` (approvals) | Held sends bound to their owner — provenance, owner-bound decisions, stale revalidation. Chief replacement invalidates sender-side holds eagerly on `setMain` (not via `staleReason`). The authority contract lives in [team](team.md) (#52). | @@ -51,7 +52,7 @@ holds. Task bookkeeping does not imply runtime execution or tool permission. - `core/index.js` — plugin server. - `core/client.js` — built browser bundle (generated; never hand-edit). -- `dev/bench.sh`, `dev/smoke.mjs` — test bench. The mock reads who leads a group from the system prompt (`Members: … and you (admin).`), which is where the standing section puts that line (#894). A turn's own text no longer carries it, so "admin calls everyone" in the smoke is decided from the section (#917). +- `dev/bench.sh`, `dev/bench-preflight.mjs`, `dev/smoke.mjs` — test bench; `start` runs the preflight first, so a missing `pnpm` or native addon stops it instead of surfacing as smoke failures. The mock reads who leads a group from the system prompt (`Members: … and you (admin).`), which is where the standing section puts that line (#894). A turn's own text no longer carries it, so "admin calls everyone" in the smoke is decided from the section (#917). ## Reviewing a tip without checking it out diff --git a/patches/ds-bot/0007-version-and-tooling.patch b/patches/ds-bot/0007-version-and-tooling.patch index f3aadd465..c2d16133e 100644 --- a/patches/ds-bot/0007-version-and-tooling.patch +++ b/patches/ds-bot/0007-version-and-tooling.patch @@ -1,5 +1,32 @@ FlyBot patch 0007-version-and-tooling on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/). +diff --git a/dev/bench.sh b/dev/bench.sh +--- a/dev/bench.sh ++++ b/dev/bench.sh +@@ -59,7 +59,12 @@ ensure_profile() { + } + EOF + printf 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n' > "$profile/pnpm-workspace.yaml" +- (cd "$profile" && DSH_HOME="$HOME_DIR" pnpm install >/dev/null) ++ if ! install_output=$(cd "$profile" && DSH_HOME="$HOME_DIR" pnpm install 2>&1); then ++ printf '%s\n' "$install_output" >&2 ++ rm -rf "$profile" ++ echo "pnpm install failed; the bench profile was not created" >&2 ++ exit 1 ++ fi + } + + # Bench-only plugin config, layered over the profile with --patch. +@@ -80,6 +80,9 @@ write_overlay() { + } + + start() { ++ # Refuse before the profile is written: a missing pnpm or native addon otherwise surfaces ++ # as smoke failures that look like plugin bugs (#2576). ++ node "$PLUGIN/dev/bench-preflight.mjs" "$DSH_HARNESS" || exit 1 + mkdir -p "$HOME_DIR" + ensure_profile + write_overlay diff --git a/dev/harness/package.json b/dev/harness/package.json --- a/dev/harness/package.json +++ b/dev/harness/package.json diff --git a/patches/ds-bot/files/dev/bench-preflight.mjs b/patches/ds-bot/files/dev/bench-preflight.mjs new file mode 100644 index 000000000..db5db7cef --- /dev/null +++ b/patches/ds-bot/files/dev/bench-preflight.mjs @@ -0,0 +1,86 @@ +// Bench preflight (dev/bench.sh): refuse before the profile is written when the bench cannot +// start, and say what to do about it. Both failures otherwise surface much later as smoke +// failures that look like plugin bugs — no pnpm leaves the profile without node_modules, so +// `dsh` never resolves the plugin bundle; no prebuilt native addon boots the web server but +// fails every Bot turn. A deepseek-harness source checkout carries no native `bin/`, so it +// needs the prebuilt copy from the installed `@deepseek-ai/node-addon-system-` package. +// +// node dev/bench-preflight.mjs +// +// Exit 0 when the bench can start, 1 with the remedy on stderr when it cannot, 2 on bad usage. +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { pathToFileURL } from 'node:url' + +// The addon loader picks the directory from Node's report, not from the ELF interpreter, so the +// preflight must read the same signal to name the file the loader would try. +export function libcName(platform = process.platform, report = process.report?.getReport()) { + if (platform !== 'linux') return undefined + return report?.header?.glibcVersionRuntime ? 'glibc' : 'musl' +} + +export function addonSubpath({ platform = process.platform, libc = libcName(platform) } = {}) { + return join('bin', ...(libc ? [libc] : []), 'system.node') +} + +export function nativeAddonPath(harnessDir, { platform = process.platform, arch = process.arch, libc = libcName(platform) } = {}) { + return join(harnessDir, 'native', 'system', 'packages', `${platform}-${arch}`, addonSubpath({ platform, libc })) +} + +export function commandExists(command) { + const result = spawnSync(command, ['--version'], { stdio: 'ignore' }) + return !result.error && result.status === 0 +} + +export function checkBench({ + harnessDir, + platform = process.platform, + arch = process.arch, + libc = libcName(platform), + hasPnpm = commandExists('pnpm'), + exists = existsSync, +} = {}) { + const addon = harnessDir ? nativeAddonPath(harnessDir, { platform, arch, libc }) : undefined + const result = { + platform, + arch, + libc, + harnessDir, + addon, + package: `@deepseek-ai/node-addon-system-${platform}-${arch}`, + subpath: addonSubpath({ platform, libc }), + } + if (!hasPnpm) return { ...result, ok: false, state: 'missing-pnpm' } + // An installed harness resolves the addon from its own node_modules; only a source checkout + // has native/system/packages, and only that tree needs the prebuilt bin/ copied into it. + const sourceTree = harnessDir && exists(join(harnessDir, 'native', 'system', 'packages')) + if (sourceTree && !exists(addon)) return { ...result, ok: false, state: 'missing-native-addon' } + return { ...result, ok: true, state: 'pass' } +} + +export function problemLine(result) { + if (result.state === 'missing-pnpm') { + return 'bench preflight: pnpm is not on PATH — the profile install needs it; `corepack pnpm` supplies pnpm (put a `pnpm` shim that execs it on PATH, e.g. tools/bin/pnpm).' + } + if (result.state === 'missing-native-addon') { + return `bench preflight: no native addon at ${result.addon} — copy ${result.package}/${result.subpath} (installed under core/dev/harness by \`npm run harness\`) into ${dirname(result.addon)}.` + } + return undefined +} + +export function main(argv = process.argv.slice(2)) { + const harnessDir = argv[0] ?? process.env.DSH_HARNESS + if (!harnessDir) { + console.error('usage: node dev/bench-preflight.mjs ') + return 2 + } + const result = checkBench({ harnessDir }) + if (result.ok) return 0 + console.error(problemLine(result)) + return 1 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main()) +} diff --git a/patches/ds-bot/files/test/bench-preflight.test.js b/patches/ds-bot/files/test/bench-preflight.test.js new file mode 100644 index 000000000..2a04fc986 --- /dev/null +++ b/patches/ds-bot/files/test/bench-preflight.test.js @@ -0,0 +1,130 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { after, test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { addonSubpath, checkBench, libcName, nativeAddonPath, problemLine } from '../dev/bench-preflight.mjs' + +const here = dirname(fileURLToPath(import.meta.url)) + +const roots = [] + +after(() => { + for (const root of roots) rmSync(root, { recursive: true, force: true }) +}) + +function tree(base = tmpdir()) { + mkdirSync(base, { recursive: true }) + const root = mkdtempSync(join(base, 'flybot-bench-preflight-')) + roots.push(root) + return { root, harness: join(root, 'harness') } +} + +// A shell fixture has to execute `pnpm`, and a tmpdir can sit on a noexec mount (it does on +// the shared box). node_modules is inside the checkout, ignored by git, and always executable. +function execTree() { + return tree(join(here, '..', 'node_modules', '.cache')) +} + +function writeAddon(harness, { platform = 'linux', arch = 'arm64', libc = 'glibc' } = {}) { + const file = nativeAddonPath(harness, { platform, arch, libc }) + mkdirSync(dirname(file), { recursive: true }) + writeFileSync(file, '') + return file +} + +test('passes when pnpm and the addon the loader would require are there', () => { + const { harness } = tree() + const file = writeAddon(harness) + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.ok, true) + assert.equal(result.state, 'pass') + assert.equal(result.addon, file) +}) + +test('refuses with missing-pnpm before it looks at the addon', () => { + const { harness } = tree() + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: false }) + assert.equal(result.state, 'missing-pnpm') + assert.match(problemLine(result), /corepack pnpm/) +}) + +test('refuses with missing-native-addon and names the exact path', () => { + const { harness } = tree() + mkdirSync(join(harness, 'native', 'system', 'packages'), { recursive: true }) + const file = nativeAddonPath(harness, { platform: 'linux', arch: 'arm64', libc: 'glibc' }) + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.state, 'missing-native-addon') + assert.equal(result.addon, file) + const line = problemLine(result) + assert.ok(line.includes(file), line) + assert.match(line, /node-addon-system-linux-arm64/) +}) + +test('does not demand a source-tree addon from an installed harness', () => { + const { harness } = tree() + const result = checkBench({ harnessDir: harness, platform: 'linux', arch: 'arm64', libc: 'glibc', hasPnpm: true }) + assert.equal(result.ok, true) + assert.equal(result.state, 'pass') +}) + +test('follows the loader on libc and platform', () => { + assert.equal(addonSubpath({ platform: 'linux', libc: 'glibc' }), join('bin', 'glibc', 'system.node')) + assert.equal(addonSubpath({ platform: 'linux', libc: 'musl' }), join('bin', 'musl', 'system.node')) + assert.equal(addonSubpath({ platform: 'darwin' }), join('bin', 'system.node')) + assert.equal(libcName('darwin'), undefined) +}) + +test('bench.sh refuses before writing the profile when pnpm is missing', { skip: process.platform === 'win32' }, () => { + const { root, harness, path, home } = benchTree() + const result = runBench({ root, path, harness }, 'start') + assert.notEqual(result.status, 0) + assert.match(result.stderr, /pnpm is not on PATH/) + assert.equal(existsSync(join(home, 'profiles')), false) +}) + +test('bench.sh shows a failed pnpm install and leaves no half-built profile', { skip: process.platform === 'win32' }, () => { + const script = '#!/bin/sh\n[ "$1" = "--version" ] && exit 0\necho "install failed: boom" >&2\nexit 1\n' + const { root, harness, path, home } = benchTree({ pnpm: script }) + const result = runBench({ root, path, harness }, 'start') + assert.notEqual(result.status, 0) + assert.match(result.stderr, /install failed: boom/) + assert.equal(existsSync(join(home, 'profiles', 'web')), false) +}) + +// A checkout-shaped tree: core/dev/{bench.sh,bench-preflight.mjs}, a harness with the host's +// addon, and a bin directory that holds only what bench.sh needs before the preflight. +function benchTree({ pnpm } = {}) { + const { root, harness } = execTree() + const dev = join(root, 'core', 'dev') + mkdirSync(dev, { recursive: true }) + copyFileSync(join(here, '../dev/bench.sh'), join(dev, 'bench.sh')) + copyFileSync(join(here, '../dev/bench-preflight.mjs'), join(dev, 'bench-preflight.mjs')) + writeAddon(harness, { platform: process.platform, arch: process.arch, libc: libcName() }) + const bin = join(root, 'bin') + mkdirSync(bin) + symlinkSync(process.execPath, join(bin, 'node')) + // bench.sh's only external tool before the preflight is `dirname "$0"`, and $0 here is an + // absolute path; a shell-only stand-in keeps the fixture independent of the host's bin dirs. + writeFileSync(join(bin, 'dirname'), '#!/bin/sh\nprintf "%s\\n" "${1%/*}"\n') + chmodSync(join(bin, 'dirname'), 0o755) + if (pnpm) { + writeFileSync(join(bin, 'pnpm'), pnpm) + chmodSync(join(bin, 'pnpm'), 0o755) + } + // The rest of the box, minus any directory that holds a real pnpm: these tests decide that + // answer themselves, and a runner with pnpm installed must not leak one in. + const rest = (process.env.PATH ?? '').split(':').filter((dir) => dir && !existsSync(join(dir, 'pnpm'))) + return { root, harness, bin, home: join(root, '.dsh-test-smoke'), path: [bin, ...rest].join(':') } +} + +function runBench({ root, path, harness }, action) { + const result = spawnSync('/bin/sh', [join(root, 'core', 'dev', 'bench.sh'), 'smoke', '3097', '8797', action], { + encoding: 'utf8', + env: { PATH: path, DSH_HARNESS: harness, HOME: root }, + }) + if (result.error) assert.fail(`could not run bench.sh: ${result.error.message}`) + return result +} diff --git a/patches/ds-bot/topics.json b/patches/ds-bot/topics.json index 3dc892c27..f4eda696f 100644 --- a/patches/ds-bot/topics.json +++ b/patches/ds-bot/topics.json @@ -114,6 +114,7 @@ "scripts/lint.mjs", "package-lock.json", "test/diagnostics.test.js", + "dev/bench.sh", "dev/smoke.mjs", "dev/harness/package.json" ],