// Tool-body self-test — the layer that neither `node --check` nor a // plugin-tree boot smoke can see. // // Why it exists: booting the plugin tree only proves the tree *loads*; it never // executes the tool function body. A running harness keeps serving the code it // booted with, so a source edit that breaks the tool body stays green until the // next restart — at which point the first `plugin_check` call throws. This file // closes that gap: it copies `lib/` and `assets/` into a throwaway harness under // the OS temp directory, replaces the two `@deepseek-ai/*` imports with stubs, // drives `apply(ctx)` and the tool's `execute()` in a **fresh Node process**, // and fails loudly on an undefined identifier or a bad provider shape. // // Usage: // node test/tool-body-selftest.mjs # positive control: this repo // node test/tool-body-selftest.mjs --plugin # negative control: any other plugin dir // // Exit code 0 = the tool body really ran. Non-zero = it threw. // // Boundary: the plugin source is only read, never modified. The stub harness // lives in the OS temp directory and can be deleted at any time. import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { tmpdir } from 'node:os'; /** The plugin root: this file lives in `/test/`. */ const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); /** Files of `lib/` that make up the plugin. Keep in sync when lib/ changes. */ const LIB_FILES = ['index.js', 'smoke.js', 'page-smoke.js', 'instances.js']; const argv = process.argv.slice(2); const flagIndex = argv.indexOf('--plugin'); const PLUGIN = flagIndex >= 0 ? argv[flagIndex + 1] : REPO_ROOT; if (!PLUGIN) { console.error('usage: node test/tool-body-selftest.mjs [--plugin ]'); process.exit(2); } const HARNESS = join(tmpdir(), 'dsh-startup-check-selftest'); // ---- Stub packages: make the plugin's `@deepseek-ai/*` imports resolve in bare Node ---- const stub = (pkg, body) => { const dir = join(HARNESS, 'node_modules/@deepseek-ai', pkg); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: `@deepseek-ai/${pkg}`, version: '0.0.0-stub', type: 'module', main: 'index.js' })); writeFileSync(join(dir, 'index.js'), body); }; stub('dsh-tools', 'export function defineTool(spec) { return spec; }\n'); stub('dsh-skill', 'export const BUNDLED_SKILL_RANK = 600;\n'); // ---- Copy the plugin under test (read-only source) ---- writeFileSync(join(HARNESS, 'package.json'), JSON.stringify({ name: 'tool-body-selftest-harness', private: true, type: 'module' })); mkdirSync(join(HARNESS, 'lib'), { recursive: true }); mkdirSync(join(HARNESS, 'assets'), { recursive: true }); for (const f of LIB_FILES) copyFileSync(join(PLUGIN, 'lib', f), join(HARNESS, 'lib', f)); copyFileSync(join(PLUGIN, 'assets/plugin-fault-diagnosis.md'), join(HARNESS, 'assets/plugin-fault-diagnosis.md')); // ---- Stub ctx: canned fs data, no real subprocess ---- // The stub process table holds two canned DSH processes: 4242 is a leftover // isolated smoke instance (900s old), 4243 is a live host. After a stub // `taskkill`, 4242 disappears — so the stray-instance audit and `killStray` // cleanup really execute here instead of merely "looking right". const spawned = []; const killedPids = new Set(); const fakeHandle = (out) => ({ done: Promise.resolve({ exitCode: 0 }), // `collected[side].readFrom(offset)` is the real contract (byte-offset, // non-consuming). The stub has to honour it, or `collectedText()` would always // return '' and the audit would report "not measured". collected: { stdout: { readFrom: (from) => ({ text: out.slice(from), nextOffset: out.length, lossy: false }) }, stderr: { readFrom: () => ({ text: '', nextOffset: 0, lossy: false }) }, }, terminate: () => {}, }); const STUB_PROCESS_ROWS = [ { ProcessId: 4242, ParentProcessId: 1, AgeSeconds: 900, CommandLine: '"node" "C:\\x\\node_modules\\@deepseek-ai\\dsh\\lib\\bin.js" --profile web --port 0 --no-open' }, { ProcessId: 4243, ParentProcessId: 1, AgeSeconds: 30, CommandLine: '"node" "C:\\x\\node_modules\\@deepseek-ai\\dsh\\lib\\bin.js" web --no-open' }, ]; const ctx = { fs: { async resolve(p) { return { localPath: p, displayPath: p }; }, async readText(target) { const p = target.localPath; if (p.endsWith('cordis.patch.yml')) return "plugins:\n - name: '@cningan/dsh-visualize'\n - name: '@cningan/dsh-oauth'\n"; if (p.endsWith('package.json')) return JSON.stringify({ name: '@cningan/dsh-x', main: 'lib/index.js' }); return ''; }, async listDir(target) { const p = target.localPath; if (p.endsWith('plugins')) return ['dsh-visualize', 'dsh-oauth', 'dsh-startup-check']; if (p.endsWith('lib')) return ['index.js']; return []; }, processPath(target) { return target.localPath; }, }, subprocess: { spawn(spec) { spawned.push(spec.argv.join(' ')); if (spec.argv[0] === 'taskkill') { killedPids.add(String(spec.argv[2])); return fakeHandle(''); } if (spec.argv[0] === 'powershell') { const alive = STUB_PROCESS_ROWS.filter((row) => !killedPids.has(String(row.ProcessId))); return fakeHandle(JSON.stringify(alive)); } return fakeHandle(''); }, }, skills: { registered: [], registerProvider(create) { this.registered.push(create()); return () => {}; } }, tools: { registered: [], register(tool) { this.registered.push(tool); return () => {}; } }, }; const mod = await import(pathToFileURL(join(HARNESS, 'lib/index.js')).href + '?t=' + Date.now()); console.log('plugin:', PLUGIN); console.log('inject:', JSON.stringify(mod.inject)); mod.apply(ctx); const tool = ctx.tools.registered[0]; console.log('tool:', tool && tool.name); if (!tool) { console.error('FAIL: apply() registered no tool'); process.exit(1); } const provider = ctx.skills.registered[0]; if (!provider) { console.error('FAIL: apply() registered no skill provider'); process.exit(1); } const listed = await provider.list(); const got = await provider.get(listed[0]); console.log('skill:', got.name, '| rank:', listed[0].rank, '| bytes:', Buffer.byteLength(got.content), '| whenToUse:', got.whenToUse ? 'yes' : 'MISSING'); if (!got.content || !got.whenToUse) { console.error('FAIL: bundled skill body or whenToUse is missing'); process.exit(1); } const result = await tool.execute({ target: 'dsh-visualize' }, {}); console.log('execute ->', JSON.stringify(result)); // Stray-instance audit + killStray: the stub process table must be recognised, // and cleanup must make the stray disappear. const sweep = await tool.execute({ sweep: true }, {}); const sweepRow = (sweep.checks || []).find((c) => c.name === '残留实例审计'); console.log('sweep ->', JSON.stringify(sweepRow)); const kill = await tool.execute({ sweep: true, killStray: true }, {}); const killRow = (kill.checks || []).find((c) => c.name === '残留实例审计'); console.log('killStray ->', JSON.stringify(killRow)); const auditOk = !!sweepRow && /残留隔离实例 1 个/.test(sweepRow.detail) && /4242/.test(sweepRow.detail) // recognises the stray, and only it && /宿主 1 个/.test(sweepRow.detail) && /4243/.test(sweepRow.detail) // reports the host, never touches it && /未清理|killStray:true/.test(sweepRow.detail) // default: report only && !!killRow && /已清理/.test(killRow.detail); // explicit killStray cleans up console.log('spawned:', JSON.stringify(spawned)); console.log('VERDICT:', result && Array.isArray(result.checks) && result.checks.length >= 4 && auditOk ? 'OK' : 'SUSPECT'); process.exit(result && Array.isArray(result.checks) && result.checks.length >= 4 && auditOk ? 0 : 1);