import { afterEach, describe, expect, test } from 'bun:test'; import { spawnSync } from 'child_process'; import { builtinModules } from 'module'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { runBashScript } from './helpers/bash-script'; // ./setup installs production dependencies only (GSTA-97). devDependencies are // the test and eval toolchain, chiefly the Claude Agent SDK's per-platform // native binaries (~440 MB on Linux). This file pins the two halves of that: // a user install (no dev toolchain in node_modules) is production-only, while a // checkout that already has the dev toolchain gets the full frozen install; and // nothing setup, the build or an installed skill loads is a devDependency. const ROOT = path.resolve(import.meta.dir, '..'); const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf-8'); const PKG = JSON.parse(read('package.json')); const bases: string[] = []; afterEach(() => { for (const b of bases.splice(0)) fs.rmSync(b, { recursive: true, force: true }); }); /** process.env without the developer's global Bun config: HOME and XDG_CONFIG_HOME move into the temp base, BUN_CONFIG_* is dropped. */ function isolatedBunEnv(base: string): NodeJS.ProcessEnv { const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('BUN_CONFIG_'))); for (const dir of ['home', 'xdg']) fs.mkdirSync(path.join(base, dir), { recursive: true }); return { ...env, HOME: path.join(base, 'home'), XDG_CONFIG_HOME: path.join(base, 'xdg') }; } function tempBase(): string { const base = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-prod-deps-')); bases.push(base); return base; } /** A bash function sliced out of a script, by its `name() {` line through the closing `}`. */ function sliceFunction(source: string, name: string): string { const start = source.indexOf(`\n${name}() {\n`); if (start === -1) throw new Error(`${name}() not found`); const end = source.indexOf('\n}\n', start); return source.slice(start + 1, end + 3); } /** * A bun that records each argv line. Like Bun, a --frozen-lockfile install fails * ($FROZEN_RC) until `install --lockfile-only` has refreshed the lockfile; any other * call exits $FALLBACK_RC. */ function stubBun(base: string): { bun: string; calls: () => string[] } { const log = path.join(base, 'calls'); const bun = path.join(base, 'bun'); const fresh = path.join(base, 'lockfile-refreshed'); fs.writeFileSync(bun, `#!/usr/bin/env bash\necho "$*" >> ${JSON.stringify(log)}\ncase "$*" in\n *--frozen-lockfile*) [ -f ${JSON.stringify(fresh)} ] && exit 0; exit "\${FROZEN_RC:-0}" ;;\n *--lockfile-only*) rc="\${FALLBACK_RC:-0}"; [ "$rc" -eq 0 ] && : > ${JSON.stringify(fresh)}; exit "$rc" ;;\nesac\nexit "\${FALLBACK_RC:-0}"\n`, { mode: 0o755 }); return { bun, calls: () => (fs.existsSync(log) ? fs.readFileSync(log, 'utf-8').trim().split('\n') : []) }; } describe.skipIf(process.platform === 'win32')('setup installs production dependencies only', () => { const setup = read('setup'); const script = (bun: string, call: string) => `set -e\nBUN_CMD=${JSON.stringify(bun)}\n${sliceFunction(setup, 'bun_cmd')}\n${sliceFunction(setup, 'install_runtime_deps')}\n${call}\n`; test('install_runtime_deps passes --production with the frozen lockfile, and refreshes a stale lockfile before installing production-only again', () => { const base = tempBase(); const { bun, calls } = stubBun(base); expect(runBashScript(script(bun, 'install_runtime_deps'), { cwd: base, env: { ...process.env, FROZEN_RC: '0' } }).status).toBe(0); expect(calls()).toEqual(['install --production --frozen-lockfile']); const base2 = tempBase(); const stub2 = stubBun(base2); expect(runBashScript(script(stub2.bun, 'install_runtime_deps'), { cwd: base2, env: { ...process.env, FROZEN_RC: '1' } }).status).toBe(0); expect(stub2.calls()).toEqual(['install --production --frozen-lockfile', 'install --lockfile-only', 'install --production --frozen-lockfile']); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=a failed fallback install stops setup's install step instead of building on a missing node_modules; // fails_when=install_runtime_deps swallows the fallback's failure (an added `|| true` or a trailing command); // why_new=both rows above end in a successful install; seam=none; no_claim=setup's own subshell wiring around the call const base3 = tempBase(); const stub3 = stubBun(base3); const failed = runBashScript(script(stub3.bun, 'install_runtime_deps\necho AFTER'), { cwd: base3, env: { ...process.env, FROZEN_RC: '1', FALLBACK_RC: '3' } }); expect(failed.status).toBe(3); expect(failed.stdout).not.toContain('AFTER'); expect(stub3.calls()).toEqual(['install --production --frozen-lockfile', 'install --lockfile-only']); }); // Value: protects=a contributor checkout (dev toolchain present) keeps its dev packages at bun.lock's versions after ./setup; // fails_when=setup runs the production install there, which Bun reports as "no changes" and leaves old dev versions; // why_new=every other row starts without node_modules; seam=none; no_claim=Bun's own update behavior (stub bun) test('a checkout with the dev toolchain gets the full frozen install, falling back to a plain install', () => { const base = tempBase(); fs.mkdirSync(path.join(base, 'node_modules', 'typescript'), { recursive: true }); const { bun, calls } = stubBun(base); expect(runBashScript(script(bun, 'install_runtime_deps'), { cwd: base, env: { ...process.env, FROZEN_RC: '0' } }).status).toBe(0); expect(calls()).toEqual(['install --frozen-lockfile']); const base2 = tempBase(); fs.mkdirSync(path.join(base2, 'node_modules', 'typescript'), { recursive: true }); const stub2 = stubBun(base2); expect(runBashScript(script(stub2.bun, 'install_runtime_deps'), { cwd: base2, env: { ...process.env, FROZEN_RC: '1' } }).status).toBe(0); expect(stub2.calls()).toEqual(['install --frozen-lockfile', 'install']); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=a contributor checkout whose full install fails stops setup's install step instead of building on stale packages; // fails_when=the dev branch swallows the plain install's failure (an added `|| true`, or a warn-and-continue fallback); // why_new=both rows above succeed, and the failure row in the test above runs the production branch; seam=none; // no_claim=setup's own subshell wiring around the call (stub bun) const base3 = tempBase(); fs.mkdirSync(path.join(base3, 'node_modules', 'typescript'), { recursive: true }); const stub3 = stubBun(base3); const failed = runBashScript(script(stub3.bun, 'install_runtime_deps\necho AFTER'), { cwd: base3, env: { ...process.env, FROZEN_RC: '1', FALLBACK_RC: '4' } }); expect(failed.status).toBe(4); expect(failed.stdout).not.toContain('AFTER'); expect(stub3.calls()).toEqual(['install --frozen-lockfile', 'install']); }); test('every bun install in setup goes through install_runtime_deps', () => { const body = setup.replace(sliceFunction(setup, 'install_runtime_deps'), ''); const directInstall = /(?:^|[\s;&|(])(?:bun_cmd|"?\$\{?BUN_CMD\}?"?|bun)\s+install\b/; const direct = body.split('\n').filter((l) => directInstall.test(l) && !l.trim().startsWith('#')); expect(direct).toEqual([]); // The build, the Codex render and each host render install first. expect(setup.match(/^\s+install_runtime_deps$/gm)?.length).toBeGreaterThanOrEqual(7); // Negative controls: the pre-GSTA-97 line and every other spelling of a direct install are caught. for (const line of [ ' bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install', ' "$BUN_CMD" install --frozen-lockfile', ' $BUN_CMD install', ' "${BUN_CMD}" install', ' (cd x && bun install)', ]) expect(directInstall.test(line), line).toBe(true); expect(directInstall.test(' bun_cmd x playwright install chromium')).toBe(false); }); test('gstack-browser-ensure never runs a full bun install', () => { const fullInstalls = (text: string) => text.split('\n') .filter((l) => !l.trim().startsWith('#') && !/^\s*log /.test(l)) .flatMap((l) => [...l.matchAll(/\bbun install\b(?! --production| --lockfile-only| failed)/g)].map(() => l.trim())); const ensure = read('bin/gstack-browser-ensure'); expect(ensure).toContain('bun install --production --frozen-lockfile'); expect(fullInstalls(ensure)).toEqual([]); // Negative control: the pre-GSTA-97 install line is caught. expect(fullInstalls(' (cd "$ROOT" && { bun install --frozen-lockfile >/dev/null 2>&1 || bun install >/dev/null; })')).toHaveLength(2); }); test('bin/dev-setup installs the full set even over a production-only node_modules', () => { const devSetup = read('bin/dev-setup'); const start = devSetup.indexOf('# 2. Install dependencies'); const block = devSetup.slice(start, devSetup.indexOf('# 3.', start)); const base = tempBase(); const { bun, calls } = stubBun(base); fs.mkdirSync(path.join(base, 'node_modules')); const r = runBashScript(`set -e\nREPO_ROOT=${JSON.stringify(base)}\nbun() { ${JSON.stringify(bun)} "$@"; }\n${block}`); expect(r.status).toBe(0); expect(calls()).toEqual(['install']); }); }); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=with the real Bun, install_runtime_deps omits devDependencies on a fresh install and keeps the ones a full install already put there; // fails_when=Bun starts pruning devDependencies on --production (a later ./setup strips a contributor's toolchain) or --production stops excluding them; // why_new=every other test here stubs bun, so the CHANGELOG and CONTRIBUTING claim that ./setup never removes packages was untested; // seam=none; no_claim=registry packages or the repo's own bun.lock (two local file: packages stand in; offline) describe.skipIf(process.platform === 'win32')('install_runtime_deps with the real Bun', () => { test('a fresh install leaves out devDependencies; a later production install keeps the ones a full install added', () => { const base = tempBase(); const project = path.join(base, 'project'); for (const name of ['runtime-pkg', 'dev-pkg']) { fs.mkdirSync(path.join(project, 'pkgs', name), { recursive: true }); fs.writeFileSync(path.join(project, 'pkgs', name, 'package.json'), JSON.stringify({ name, version: '1.0.0' })); } fs.writeFileSync(path.join(project, 'package.json'), JSON.stringify({ name: 'prod-deps-fixture', version: '0.0.0', dependencies: { 'runtime-pkg': 'file:./pkgs/runtime-pkg' }, devDependencies: { 'dev-pkg': 'file:./pkgs/dev-pkg' }, })); const env = { ...isolatedBunEnv(base), BUN_INSTALL_CACHE_DIR: path.join(base, 'cache') }; const setup = read('setup'); const run = (call: string) => runBashScript( `set -e\nBUN_CMD=${JSON.stringify(process.execPath)}\n${sliceFunction(setup, 'bun_cmd')}\n${sliceFunction(setup, 'install_runtime_deps')}\n${call}\n`, { cwd: project, env, timeout: 90_000 }); const has = (name: string) => fs.existsSync(path.join(project, 'node_modules', name, 'package.json')); const lockfile = run('bun_cmd install'); expect(lockfile.status, lockfile.stderr).toBe(0); const lock = fs.readFileSync(path.join(project, 'bun.lock'), 'utf-8'); fs.rmSync(path.join(project, 'node_modules'), { recursive: true, force: true }); const fresh = run('install_runtime_deps'); expect(fresh.status, fresh.stderr).toBe(0); expect([has('runtime-pkg'), has('dev-pkg')]).toEqual([true, false]); const full = run('bun_cmd install'); expect(full.status, full.stderr).toBe(0); expect(has('dev-pkg')).toBe(true); const later = run('install_runtime_deps'); expect(later.status, later.stderr).toBe(0); expect([has('runtime-pkg'), has('dev-pkg')]).toEqual([true, true]); expect(fs.readFileSync(path.join(project, 'bun.lock'), 'utf-8')).toBe(lock); }); // Value: protects=a checkout whose bun.lock is behind package.json (a fork, a local edit) still installs, production-only; // fails_when=the fallback reruns a --production install, which Bun treats as frozen, so setup aborts on the stale lockfile; // why_new=the stub-bun rows cannot model Bun's rule that --production implies a frozen lockfile; // seam=none; no_claim=registry resolution (local file: packages; offline) test('a stale lockfile is refreshed, then the install stays production-only', () => { const base = tempBase(); const project = path.join(base, 'project'); for (const name of ['runtime-pkg', 'late-pkg', 'dev-pkg']) { fs.mkdirSync(path.join(project, 'pkgs', name), { recursive: true }); fs.writeFileSync(path.join(project, 'pkgs', name, 'package.json'), JSON.stringify({ name, version: '1.0.0' })); } const manifest = (deps: Record) => fs.writeFileSync(path.join(project, 'package.json'), JSON.stringify({ name: 'prod-deps-fixture', version: '0.0.0', dependencies: deps, devDependencies: { 'dev-pkg': 'file:./pkgs/dev-pkg' }, })); const env = { ...isolatedBunEnv(base), BUN_INSTALL_CACHE_DIR: path.join(base, 'cache') }; const setup = read('setup'); const run = (call: string) => runBashScript( `set -e\nBUN_CMD=${JSON.stringify(process.execPath)}\n${sliceFunction(setup, 'bun_cmd')}\n${sliceFunction(setup, 'install_runtime_deps')}\n${call}\n`, { cwd: project, env, timeout: 90_000 }); const has = (name: string) => fs.existsSync(path.join(project, 'node_modules', name, 'package.json')); manifest({ 'runtime-pkg': 'file:./pkgs/runtime-pkg' }); expect(run('bun_cmd install --lockfile-only').status).toBe(0); manifest({ 'runtime-pkg': 'file:./pkgs/runtime-pkg', 'late-pkg': 'file:./pkgs/late-pkg' }); const stale = run('install_runtime_deps'); expect(stale.status, stale.stderr).toBe(0); expect([has('runtime-pkg'), has('late-pkg'), has('dev-pkg')]).toEqual([true, true, false]); expect(fs.readFileSync(path.join(project, 'bun.lock'), 'utf-8')).toContain('late-pkg'); }); }); /** Bare package names a source file loads (import/require/dynamic import), type-only imports excluded. */ function importedPackages(text: string): string[] { const out = new Set(); const patterns = [ /^\s*(?:import|export)\s+(?!type\b)[^'";]*?from\s+['"]([^'"]+)['"]/gm, /^\s*import\s+['"]([^'"]+)['"]/gm, /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g, /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g, ]; for (const re of patterns) { for (const m of text.matchAll(re)) { const spec = m[1]; if (spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('node:') || spec === 'bun' || spec.startsWith('bun:')) continue; const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]; if (builtinModules.includes(name)) continue; out.add(name); } } return [...out]; } /** Every shipped-runtime file that loads a package which is not a production dependency. */ function nonProductionImports(pkg: { dependencies?: Record }, files: Record): string[] { const deps = pkg.dependencies ?? {}; const bad: string[] = []; for (const [file, text] of Object.entries(files)) { for (const name of importedPackages(text)) if (!(name in deps)) bad.push(`${file}: ${name}`); } return bad.sort(); } /** The code setup, the build and installed skills run: shipped dirs plus the scripts setup and the build call by path. */ function runtimeFiles(): Record { const roots = ['bin/', 'lib/', 'browse/src/', 'design/src/', 'make-pdf/src/', 'hosts/', 'extension/', 'scripts/resolvers/']; const tracked = spawnSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }).stdout .split('\n').filter((rel) => roots.some((r) => rel.startsWith(r))); const ownPackage = (rel: string) => rel.startsWith('lib/diagram-render/'); const invoked = new Set(['scripts/gen-skill-docs.ts']); for (const src of ['setup', 'scripts/build.sh']) for (const m of read(src).matchAll(/scripts\/[A-Za-z0-9_./-]+\.ts/g)) invoked.add(m[0]); const files: Record = {}; for (const rel of [...tracked, ...invoked]) { if (ownPackage(rel) || /(^|\/)(test|node_modules|dist)\//.test(rel) || /\.(test\.ts|md|json|png|svg|css|html|c|h)$/.test(rel)) continue; const abs = path.join(ROOT, rel); if (!fs.existsSync(abs) || !fs.statSync(abs).isFile() || fs.statSync(abs).size > 2_000_000) continue; const text = fs.readFileSync(abs, 'utf-8'); if (text.includes('\u0000')) continue; files[rel] = text; } return files; } describe('setup and runtime need only production dependencies', () => { test('no shipped code loads a devDependency or an undeclared package', () => { const files = runtimeFiles(); expect(Object.keys(files).length).toBeGreaterThan(300); expect(nonProductionImports(PKG, files)).toEqual([]); }); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=modules the shipped entry points load by relative import (gen-skill-docs' scripts/ helpers) use production dependencies only; // fails_when=a scripts/ module gen-skill-docs imports adds a devDependency import, breaking every user render; // why_new=runtimeFiles() scans fixed roots, and scripts/discover-skills.ts, gen-agents-digest.ts and the rest are outside them; // seam=none; no_claim=dynamic import paths built at runtime, or packages loaded by a spawned subprocess test('every module the shipped code reaches by relative import loads only production dependencies', () => { const files = runtimeFiles(); const reached: Record = {}; const queue = Object.keys(files); const relSpec = /^\s*(?:import|export)\s+(?!type\b)[^'";]*?from\s+['"](\.\.?\/[^'"]+)['"]|\b(?:import|require)\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)|^\s*import\s+['"](\.\.?\/[^'"]+)['"]/gm; while (queue.length) { const rel = queue.pop()!; const text = files[rel] ?? reached[rel]; for (const m of text.matchAll(relSpec)) { const spec = path.posix.normalize(path.posix.join(path.posix.dirname(rel), m[1] ?? m[2] ?? m[3])); const target = [spec, `${spec}.ts`, `${spec}.js`, `${spec}/index.ts`].find((c) => fs.existsSync(path.join(ROOT, c)) && fs.statSync(path.join(ROOT, c)).isFile()); if (!target || target.startsWith('..') || target in files || target in reached || /(^|\/)node_modules\//.test(target) || (/(^|\/)test\//.test(target) && !target.startsWith('test/helpers/')) || !/\.(ts|js|mjs)$/.test(target)) continue; reached[target] = fs.readFileSync(path.join(ROOT, target), 'utf-8'); queue.push(target); } } expect(Object.keys(reached)).toContain('scripts/discover-skills.ts'); expect(Object.keys(reached)).toContain('test/helpers/providers/claude.ts'); expect(nonProductionImports(PKG, reached)).toEqual([]); }); test('negative controls: a devDependency import and a moved-back runtime package are caught', () => { expect(nonProductionImports(PKG, { 'bin/x': "import { query } from '@anthropic-ai/claude-agent-sdk';" })).toEqual(['bin/x: @anthropic-ai/claude-agent-sdk']); expect(nonProductionImports(PKG, { 'bin/x': "import type { Q } from '@anthropic-ai/claude-agent-sdk';\nimport fs from 'node:fs';" })).toEqual([]); expect(nonProductionImports(PKG, { 'lib/x.ts': "export { query } from '@anthropic-ai/claude-agent-sdk';" })).toEqual(['lib/x.ts: @anthropic-ai/claude-agent-sdk']); expect(nonProductionImports(PKG, { 'lib/x.ts': "export * from 'typescript';" })).toEqual(['lib/x.ts: typescript']); expect(nonProductionImports(PKG, { 'lib/x.ts': "export type { Q } from '@anthropic-ai/claude-agent-sdk';" })).toEqual([]); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=the scan sees an import statement split over several lines; // fails_when=importedPackages only matches one-line imports, so a multi-line devDependency import ships unflagged; // why_new=both existing controls are one-line imports; seam=none; no_claim=none beyond protects expect(nonProductionImports(PKG, { 'bin/x': "import {\n query,\n type SDKMessage,\n} from '@anthropic-ai/claude-agent-sdk';" })).toEqual(['bin/x: @anthropic-ai/claude-agent-sdk']); const { ['@anthropic-ai/sdk']: _sdk, ...rest } = PKG.dependencies; const files = runtimeFiles(); expect(nonProductionImports({ dependencies: rest }, files)).toContain('bin/gstack-distill-free-text: @anthropic-ai/sdk'); }); test('packages the build copies out of node_modules are production dependencies', () => { const scripts: Record = PKG.scripts; const run = [...read('scripts/build.sh').matchAll(/"\$BUN_CMD" run ([A-Za-z0-9:_-]+)/g)].map((m) => m[1]); expect(run).toContain('vendor:xterm'); const copied = run.flatMap((name) => [...(scripts[name] ?? '').matchAll(/node_modules\/((?:@[^/\s]+\/)?[^/\s]+)\//g)].map((m) => m[1])); expect(copied.sort()).toEqual(['xterm', 'xterm', 'xterm-addon-fit']); for (const name of copied) expect(PKG.dependencies).toHaveProperty([name]); }); // Generated by /ship Step 7 coverage audit (GSTA-97). // Value: protects=a production-only install never creates the dev-toolchain marker install_runtime_deps reads, so a user's next ./setup stays production-only; // fails_when=a production dependency, or a package it pulls in (required peers included), starts depending on the marker package, so every repeat ./setup goes full; // why_new=the real-Bun rows use local fixture packages, and nothing walks the repo's own bun.lock production closure; // seam=none; no_claim=Bun's actual hoisting or platform filtering (a static, name-based walk of bun.lock that counts every candidate entry) test('the production closure in bun.lock never installs the dev-toolchain marker setup checks', () => { const marker = sliceFunction(read('setup'), 'install_runtime_deps').match(/\[ -d node_modules\/([^\s\]]+) \]/)?.[1]; expect(marker).toBeDefined(); expect(PKG.devDependencies).toHaveProperty([marker!]); type Lock = { workspaces: Record }>; packages: Record }; const closure = (lock: Lock): Set => { const seen = new Set(); const queue = Object.keys(lock.workspaces[''].dependencies ?? {}); while (queue.length) { const name = queue.pop()!; if (seen.has(name)) continue; seen.add(name); for (const [key, entry] of Object.entries(lock.packages)) { if (key !== name && !(key.endsWith(`/${name}`) && key.slice(0, -name.length - 1) in lock.packages)) continue; const meta = (entry.find((e) => e && typeof e === 'object' && !Array.isArray(e)) ?? {}) as Record; const optionalPeers = new Set(meta.optionalPeers ?? []); for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) { for (const dep of Object.keys(meta[field] ?? {})) if (field !== 'peerDependencies' || !optionalPeers.has(dep)) queue.push(dep); } } } return seen; }; const lock: Lock = JSON.parse(read('bun.lock').replace(/,(\s*[}\]])/g, '$1')); const production = closure(lock); expect(production.has('playwright-core')).toBe(true); expect(production.has(marker!)).toBe(false); // Negative controls: a required peer on the marker is caught, through a nested entry too; an optional peer is not. const synthetic = (meta: Record): Lock => ({ workspaces: { '': { dependencies: { a: '1' } } }, packages: { a: ['a@1', '', { dependencies: { b: '1' } }, 'sha'], b: ['b@1', '', {}, 'sha'], 'a/b': ['b@2', '', meta, 'sha'] }, }); expect(closure(synthetic({ peerDependencies: { [marker!]: '*' } })).has(marker!)).toBe(true); expect(closure(synthetic({ peerDependencies: { [marker!]: '*' }, optionalPeers: [marker!] })).has(marker!)).toBe(false); }); test('the dev toolchain stays out of the user install', () => { for (const name of ['@anthropic-ai/claude-agent-sdk', 'typescript', 'prettier', '@types/bun']) { expect(PKG.devDependencies).toHaveProperty([name]); expect(PKG.dependencies).not.toHaveProperty([name]); } }); });