dsh-vibe-math 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +161 -389
- package/README.md +161 -390
- package/cordis.patch.yml +62 -14
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +1 -0
- package/docs/formal-verification.md +184 -29
- package/docs/math-computation.md +163 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.en.md +151 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.0.md +115 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.en.md +86 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.1.md +86 -0
- package/docs/test-timing.md +24 -12
- package/installer.js +7 -4
- package/package.json +21 -3
- package/prompt-corpus-persona/persona-corpus.json +12 -12
- package/prompt-corpus-persona/persona-corpus.md +128 -32
- package/prompt-corpus-v2/formal-verify-v2.json +182 -92
- package/prompt-corpus-v2/formal-verify-v2.md +2379 -55
- package/prompt-corpus-v3/formal-verify-v3.json +215 -53
- package/prompt-corpus-v3/formal-verify-v3.md +2147 -22
- package/prompt-corpus-v4/formal-verify-v4.json +14 -14
- package/prompt-corpus-v4/formal-verify-v4.md +267 -14
- package/prompt-corpus-v5/prompt-corpus-v5.json +62 -62
- package/prompt-corpus-v5/prompt-corpus-v5.md +631 -3
- package/tests/audit-math-computation-contract.mjs +204 -0
- package/tests/audit-math-computation-parity.mjs +198 -0
- package/tests/audit-math-computation-sensitivity.mjs +231 -0
- package/tests/audit-persona-sensitivity.mjs +4 -4
- package/tests/audit-persona-surface.test.mjs +38 -10
- package/tests/audit-v5-integrity.mjs +14 -0
- package/tests/e2e-v5-round2.test.mjs +459 -2
- package/tests/formal-verify-v2.test.mjs +362 -8
- package/tests/formal-verify-v3.test.mjs +355 -6
- package/tests/formal-verify-v4.test.mjs +295 -6
- package/tests/formal-verify-v5.test.mjs +9 -1
- package/tests/helpers/math-computation-fake-seam.mjs +163 -0
- package/tests/math-computation-shared.test.mjs +806 -0
- package/tests/prompt-v5-integrity.test.mjs +5 -5
- package/vibe-math-v2/agent.cordis.yml +22 -8
- package/vibe-math-v2/math-computation.js +1279 -0
- package/vibe-math-v2/math-engines.js +197 -0
- package/vibe-math-v2/vibe-math-v2.js +853 -40
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v3/agent.cordis.yml +22 -8
- package/vibe-math-v3/math-computation.js +1279 -0
- package/vibe-math-v3/math-engines.js +197 -0
- package/vibe-math-v3/vibe-math-v3.js +844 -47
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v4/agent.cordis.yml +38 -6
- package/vibe-math-v4/math-computation.js +1279 -0
- package/vibe-math-v4/math-engines.js +197 -0
- package/vibe-math-v4/vibe-math-v4.js +816 -31
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +6 -0
- package/vibe-math-v5/agent.cordis.yml +42 -6
- package/vibe-math-v5/math-computation.js +1279 -0
- package/vibe-math-v5/math-engines.js +197 -0
- package/vibe-math-v5/vibe-math-v5.js +1132 -37
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +83 -3
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +251 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
// ============================================================================================
|
|
2
|
+
// math_computation — shared module contract suite (integration owner).
|
|
3
|
+
//
|
|
4
|
+
// Drives the canonical `math-computation.js` through the fake host seam (tests/helpers/
|
|
5
|
+
// math-computation-fake-seam.mjs): no real engine, no real subprocess, runs on any machine.
|
|
6
|
+
// Covers the freeze (INTERFACE-FREEZE.md): closed schema, 11 failure codes, argv echo, receipt
|
|
7
|
+
// determinism, cli default-on + both disable paths, licence probe, install plan->confirm, timeout,
|
|
8
|
+
// truncation, bad-argv-not-collapsed, and the availability/rule prompt text.
|
|
9
|
+
//
|
|
10
|
+
// Falsifiability: every assertion below fails for a specific mutation; the companion probe
|
|
11
|
+
// `tests/audit-math-computation-sensitivity.mjs` mutates a copy of the module and requires THIS
|
|
12
|
+
// suite to go red (cli default off / policy ignored / argv echo dropped / bad-argv collapsed /
|
|
13
|
+
// timeout timer removed / param renamed).
|
|
14
|
+
//
|
|
15
|
+
// Usage: node tests/math-computation-shared.test.mjs
|
|
16
|
+
// ============================================================================================
|
|
17
|
+
import { makeFakeHost } from './helpers/math-computation-fake-seam.mjs'
|
|
18
|
+
import { pathToFileURL } from 'node:url'
|
|
19
|
+
|
|
20
|
+
// The sensitivity probe (tests/audit-math-computation-sensitivity.mjs) points this at a MUTATED copy
|
|
21
|
+
// of the module pair; the default is the shipped copy in vibe-math-v2/.
|
|
22
|
+
const MODULE = process.env.MATH_COMPUTATION_MODULE
|
|
23
|
+
? pathToFileURL(process.env.MATH_COMPUTATION_MODULE).href
|
|
24
|
+
: new URL('../vibe-math-v2/math-computation.js', import.meta.url).href
|
|
25
|
+
const M = await import(MODULE)
|
|
26
|
+
|
|
27
|
+
let passed = 0, failed = 0
|
|
28
|
+
const failures = []
|
|
29
|
+
function ok(cond, label, detail) {
|
|
30
|
+
if (cond) { passed++; return true }
|
|
31
|
+
failed++; failures.push(label + (detail ? ' — ' + detail : ''))
|
|
32
|
+
console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
const eq = (a, b, label) => ok(JSON.stringify(a) === JSON.stringify(b), label, 'got ' + JSON.stringify(a) + ' want ' + JSON.stringify(b))
|
|
36
|
+
|
|
37
|
+
async function withHost(opts, fn) {
|
|
38
|
+
const state = makeFakeHost(opts)
|
|
39
|
+
const reg = M.registerMathComputation(state.host)
|
|
40
|
+
await fn(state, reg)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('-- math_computation shared contract --')
|
|
44
|
+
|
|
45
|
+
// ── 1. freeze: names, codes, defaults, prompt text ─────────────────────────────────────────────
|
|
46
|
+
{
|
|
47
|
+
ok(M.MATH_TOOL_NAME === 'math_computation', 'tool name is math_computation')
|
|
48
|
+
eq(M.MATH_PARAM_NAMES, ['mathComputation', 'mathMode', 'mathEngines', 'mathTimeoutMs', 'mathPackages', 'mathInstallScope'], 'six frozen param names in order')
|
|
49
|
+
eq(M.MATH_FAILURE_CODES.length, 11, 'eleven failure codes')
|
|
50
|
+
ok(M.MATH_FAILURE_CODES.indexOf('MATH_ENGINE_BAD_ARGV') !== -1 && M.MATH_FAILURE_CODES.indexOf('MATH_ENGINE_LICENSE_REQUIRED') !== -1, 'canonical codes present')
|
|
51
|
+
ok(M.MATH_PARAM_DEFAULTS.mathMode === 'typed+shell' && M.MATH_PARAM_DEFAULTS.mathComputation === 'auto', 'defaults: auto + typed+shell')
|
|
52
|
+
ok(M.MATH_PARAM_DEFAULTS.mathEngines.indexOf('cli') !== -1, 'cli is in the default engine set (default-on)')
|
|
53
|
+
ok(M.MATH_TOOL_DESCRIPTION.indexOf('computation/<id>/') === -1 && M.MATH_TOOL_DESCRIPTION.indexOf('Computation/<id>/') !== -1, 'description points at the receipt dir')
|
|
54
|
+
ok(M.MATH_TOOL_DESCRIPTION.indexOf('默认开启') !== -1, 'description says cli is on by default')
|
|
55
|
+
ok(M.MATH_TOOL_DESCRIPTION.indexOf('实际 argv') !== -1, 'description mentions the argv echo')
|
|
56
|
+
ok(M.MATH_TOOL_DESCRIPTION.indexOf('mathEngineOverride') !== -1, 'description mentions mathEngineOverride')
|
|
57
|
+
ok(M.MATH_RULE_LINES.some((l) => l.indexOf('①') !== -1 && l.indexOf('③') !== -1), 'rule lines carry the three selection criteria')
|
|
58
|
+
ok(M.MATH_SHELL_RULE_LINE.indexOf(M.MATH_SHELL_FALLBACK_MARK) !== -1, 'shell rule line carries the marking')
|
|
59
|
+
ok(M.MATH_RULE_LINES.every((l) => l.indexOf(M.MATH_SHELL_FALLBACK_MARK) === -1), 'mode-independent rule lines contain NO shell sentence')
|
|
60
|
+
ok(M.MATH_SHELL_FALLBACK_MARK_EN === 'not tool-archived', 'EN marking constant')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 2. normalizeMathParams: explicit coercion ──────────────────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
eq(M.normalizeMathParams({ mathComputation: 'off' }).mathComputation, 'off', 'enum accepted')
|
|
66
|
+
eq(M.normalizeMathParams({ mathComputation: 'nope' }).mathComputation, 'auto', 'bad enum falls back to default')
|
|
67
|
+
eq(M.normalizeMathParams({ mathMode: 'typed' }).mathMode, 'typed', 'mathMode typed accepted')
|
|
68
|
+
eq(M.normalizeMathParams({ mathMode: 'typed+shell' }).mathMode, 'typed+shell', 'mathMode typed+shell accepted')
|
|
69
|
+
eq(M.normalizeMathParams({ mathEngines: ['cli', 'python', 'cli', 'bogus'] }).mathEngines, ['cli', 'python'], 'engine list filtered + deduped, order kept')
|
|
70
|
+
eq(M.normalizeMathParams({ mathEngines: [] }).mathEngines, M.MATH_PARAM_DEFAULTS.mathEngines, 'empty engine list falls back to defaults')
|
|
71
|
+
eq(M.normalizeMathParams({ mathTimeoutMs: '0' }).mathTimeoutMs, 60000, 'non-positive timeout falls back (no silent instant timeout)')
|
|
72
|
+
eq(M.normalizeMathParams({ mathTimeoutMs: 10 }).mathTimeoutMs, 1000, 'timeout clamped to >= 1000')
|
|
73
|
+
eq(M.normalizeMathParams({ mathPackages: ['a', 'a', ''] }).mathPackages, ['a'], 'packages deduped and blanks dropped')
|
|
74
|
+
eq(M.normalizeMathParams({ mathInstallScope: 'system' }).mathInstallScope, 'system', 'system scope accepted when explicit')
|
|
75
|
+
eq(M.normalizeMathParams({ mathInstallScope: 'yes' }).mathInstallScope, 'user', 'bad scope falls back to user')
|
|
76
|
+
eq(M.normalizeMathParams({ nope: 1 }), {}, 'unknown keys are not echoed into params')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── 3. validateMathArgs: closed schema ─────────────────────────────────────────────────────────
|
|
80
|
+
{
|
|
81
|
+
ok(M.validateMathArgs({ op: 'probe', extra: 1 }).ok === false, 'unknown argument rejected')
|
|
82
|
+
ok(M.validateMathArgs({ op: 'nope' }).ok === false, 'unknown op rejected')
|
|
83
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code' }).ok === false, 'mode=code without code rejected')
|
|
84
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'file', file: '../../etc/passwd' }).code === 'MATH_REFUSED', 'path escape refused at validation')
|
|
85
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'file', file: '/abs/x.py' }).code === 'MATH_REFUSED', 'absolute path refused at validation')
|
|
86
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'expr', expr: '1', engine: 'cli', cli: { command: 'x' } }).code === 'MATH_REFUSED', 'cli + expr refused')
|
|
87
|
+
ok(M.validateMathArgs({ op: 'run', engine: 'cli', mode: 'code', code: 'x' }).code === 'MATH_REFUSED', 'cli without cli{} refused')
|
|
88
|
+
ok(M.validateMathArgs({ op: 'run', engine: 'cli', cli: { command: '/x/y' }, mode: 'code', code: 'z' }, { mathMode: 'typed' }).code === 'MATH_REFUSED', 'cli refused while mathMode=typed')
|
|
89
|
+
ok(M.validateMathArgs({ op: 'run', engine: 'cli', cli: { command: '/x/y' }, mode: 'code', code: 'z' }, { mathEngines: ['python'] }).code === 'MATH_REFUSED', 'cli refused when absent from mathEngines')
|
|
90
|
+
ok(M.validateMathArgs({ op: 'install', packages: [] }).ok === false, 'install without packages rejected')
|
|
91
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', timeoutMs: 10 }).ok === false, 'timeoutMs < 1000 rejected')
|
|
92
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x' }).ok === true, 'minimal run accepted')
|
|
93
|
+
ok(M.validateMathArgs({ op: 'receipt', file: 'Computation/abc/receipt.json' }).ok === true, 'receipt accepted')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 4. registration shape ──────────────────────────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
await withHost({}, async (state) => {
|
|
99
|
+
const r = state.registrations[0]
|
|
100
|
+
ok(!!r, 'register() called exactly once')
|
|
101
|
+
ok(r.name === 'math_computation', 'registered under the frozen tool name')
|
|
102
|
+
ok(r.description === M.MATH_TOOL_DESCRIPTION, 'registered description is the frozen text')
|
|
103
|
+
ok(r.parameters && r.parameters.additionalProperties === false, 'registered schema is closed')
|
|
104
|
+
eq(r.parameters.required, ['op'], 'schema requires op')
|
|
105
|
+
ok(typeof r.handler === 'function', 'registered handler is callable')
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 5. probe ───────────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
{
|
|
111
|
+
await withHost({}, async (state) => {
|
|
112
|
+
const r = await state.call({ op: 'probe' })
|
|
113
|
+
ok(r.ok === true && r.available === true, 'probe finds an installed engine')
|
|
114
|
+
ok(r.engines.length >= 1 && r.engines[0].name === 'python', 'first detected engine is python (order honoured)')
|
|
115
|
+
})
|
|
116
|
+
await withHost({ installed: [] }, async (state) => {
|
|
117
|
+
const r = await state.call({ op: 'probe' })
|
|
118
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no engine -> ENGINE_NOT_FOUND')
|
|
119
|
+
ok(r.next && r.next.kind === 'user-install' && r.next.command, 'ENGINE_NOT_FOUND carries the per-OS user-install guide')
|
|
120
|
+
})
|
|
121
|
+
await withHost({ packages: { numpy: 'present', sympy: 'missing' } }, async (state) => {
|
|
122
|
+
const r = await state.call({ op: 'probe', packages: ['numpy', 'sympy'] })
|
|
123
|
+
ok(r.ok === true && r.packages.found.numpy === 'present' && r.packages.found.sympy === null, 'package probe reports present/missing')
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 6. run: code mode, receipt, argv echo, determinism ─────────────────────────────────────────
|
|
128
|
+
{
|
|
129
|
+
await withHost({}, async (state) => {
|
|
130
|
+
const r1 = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)' })
|
|
131
|
+
ok(r1.ok === true && r1.exit === 0, 'run code succeeds')
|
|
132
|
+
ok(!!r1.receipt && r1.receipt.dir.indexOf('Computation/vibe-math-v2-') === 0, 'receipt dir is Computation/<runId>')
|
|
133
|
+
ok(!!state.file(r1.receipt.dir + '/script.py'), 'script original is on disk')
|
|
134
|
+
ok(state.file(r1.receipt.dir + '/stdout.txt') !== undefined && state.file(r1.receipt.dir + '/stderr.txt') !== undefined, 'full stdout/stderr are on disk')
|
|
135
|
+
const rj = JSON.parse(state.file(r1.receipt.json))
|
|
136
|
+
ok(!!rj.receipt === false && rj.runId === r1.receipt.dir.split('/')[1], 'receipt.json runId matches the directory')
|
|
137
|
+
eq(rj.argv, r1.argv, 'receipt argv === returned argv')
|
|
138
|
+
ok(r1.argv[r1.argv.length - 1].indexOf('script.py') !== -1, 'argv ends with the script path (copy-to-rerun)')
|
|
139
|
+
eq(rj.cwd, 'X:/fake/project', 'cwd is the project root')
|
|
140
|
+
ok(rj.determinism && rj.determinism.noWallClockInId === true, 'receipt declares no wall clock in the id')
|
|
141
|
+
// P2a item 1: the script is discoverable from BOTH the return value and the receipt.
|
|
142
|
+
ok(r1.scriptPath === rj.scriptPath && r1.scriptHash === rj.scriptHash, 'scriptPath + scriptHash are in the return value AND the receipt')
|
|
143
|
+
ok(r1.scriptHash === rj.script.sha256 && r1.scriptPath.indexOf('/script.py') !== -1, 'scriptHash matches the receipt script object')
|
|
144
|
+
const r2 = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)' })
|
|
145
|
+
ok(r2.baseRunDir === r1.baseRunDir, 'same input -> same archive id (baseRunDir)')
|
|
146
|
+
ok(r2.attempt === 2 && r2.attemptDir !== r1.attemptDir, 'a repeat is appended as attempt 2 (never overwriting attempt 1)')
|
|
147
|
+
ok(state.file(r2.attemptDir + '/receipt.json') !== undefined && state.file(r1.attemptDir + '/stdout.txt') === 'ran-ok\n', 'attempt 1 files are left untouched by attempt 2')
|
|
148
|
+
const r3 = await state.call({ op: 'run', engine: 'octave', mode: 'code', code: 'print(1)' })
|
|
149
|
+
ok(r3.baseRunDir !== r1.baseRunDir, 'different engine -> different archive id')
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── 7. run: file + expr + captureFiles ─────────────────────────────────────────────────────────
|
|
154
|
+
{
|
|
155
|
+
await withHost({ files: { 'Problems/calc.py': 'print(2)' } }, async (state) => {
|
|
156
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/calc.py', captureFiles: ['Problems/calc.py'] })
|
|
157
|
+
ok(r.ok === true, 'run file succeeds')
|
|
158
|
+
eq(JSON.parse(state.file(r.receipt.json)).captured, ['Problems/calc.py'], 'captureFiles recorded and copied')
|
|
159
|
+
ok(!!state.file(r.receipt.dir + '/captured/Problems/calc.py'), 'captured file copied into the receipt')
|
|
160
|
+
})
|
|
161
|
+
await withHost({}, async (state) => {
|
|
162
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'file', file: '../../../etc/passwd' })
|
|
163
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED', 'file escape refused at run time')
|
|
164
|
+
const r2 = await state.call({ op: 'run', engine: 'python', mode: 'file', file: 'nope.py' })
|
|
165
|
+
ok(r2.ok === false && r2.code === 'MATH_REFUSED', 'missing file refused')
|
|
166
|
+
const r3 = await state.call({ op: 'run', engine: 'python', mode: 'expr', expr: '1+1' })
|
|
167
|
+
ok(r3.ok === true, 'expr mode runs through the eval template')
|
|
168
|
+
const argv = state.spawns[state.spawns.length - 1].argv
|
|
169
|
+
ok(argv[argv.length - 1] === '1+1' && argv.indexOf('-c') !== -1, 'expr occupies exactly one argv element after -c')
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 8. cli: default-on + both disable paths + receipt ──────────────────────────────────────────
|
|
174
|
+
{
|
|
175
|
+
await withHost({}, async (state) => {
|
|
176
|
+
const r = await state.call({ op: 'run', engine: 'cli', mode: 'code', code: 'echo hi', cli: { command: '/fake/bin/mytool', argv: ['--flag', 'x'] } })
|
|
177
|
+
ok(r.ok === true, 'cli runs with DEFAULT params (default-on)')
|
|
178
|
+
// round-7 (finding 4): mode:'code' append the archived script so the code actually RUNS (the
|
|
179
|
+
// caller cannot know the runId up front). The user argv stays verbatim in receipt.cli.argv.
|
|
180
|
+
eq(r.argv.slice(0, 3), ['/fake/bin/mytool', '--flag', 'x'], 'cli argv starts with command + user argv')
|
|
181
|
+
ok(/Computation\/.*script\.txt$/.test(String(r.argv[3] || '')), 'mode:code appends the archived script path to the effective argv')
|
|
182
|
+
ok(r.cliScriptAppended === true, 'the return shell flags cliScriptAppended')
|
|
183
|
+
ok(r.engine === 'cli:/fake/bin/mytool' && r.engineInfo.name === 'cli:/fake/bin/mytool', 'cli is labelled cli:<command> in the shell and engineInfo')
|
|
184
|
+
const rj = JSON.parse(state.file(r.receipt.json))
|
|
185
|
+
ok(rj.engine.name === 'cli:/fake/bin/mytool' && rj.engine.source === 'cli' && rj.cli && rj.cli.argv.length === 2, 'cli receipt records cli:<command>, source=cli and the USER argv')
|
|
186
|
+
ok(rj.cli.scriptAppended === true && rj.argv[rj.argv.length - 1] === r.argv[3], 'the receipt records scriptAppended and the executed argv')
|
|
187
|
+
})
|
|
188
|
+
await withHost({ params: { mathMode: 'typed' } }, async (state) => {
|
|
189
|
+
const r = await state.call({ op: 'run', engine: 'cli', mode: 'code', code: 'x', cli: { command: '/fake/bin/mytool' } })
|
|
190
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED', 'cli disabled while mathMode=typed')
|
|
191
|
+
ok(!!r.next && r.next.kind === 'reason' && r.next.reason === 'policy', 'policy refusal carries next:{kind:reason, reason:policy}')
|
|
192
|
+
eq(state.spawns.length, 0, 'disabled cli never spawns anything')
|
|
193
|
+
})
|
|
194
|
+
await withHost({ params: { mathEngines: ['python'] } }, async (state) => {
|
|
195
|
+
const r = await state.call({ op: 'run', engine: 'cli', mode: 'code', code: 'x', cli: { command: '/fake/bin/mytool' } })
|
|
196
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED', 'cli disabled when removed from mathEngines')
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── 9. missing packages / licence / commercial install ─────────────────────────────────────────
|
|
201
|
+
{
|
|
202
|
+
await withHost({ packages: { sympy: 'missing' } }, async (state) => {
|
|
203
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'import sympy', packages: ['sympy'] })
|
|
204
|
+
ok(r.ok === false && r.code === 'MATH_MISSING_PACKAGES', 'missing package -> MISSING_PACKAGES')
|
|
205
|
+
eq(r.missing, ['sympy'], 'missing list reported')
|
|
206
|
+
ok(r.next && r.next.kind === 'agent-install' && r.next.dryRun === true, 'missing package leads to an install PLAN (not an install)')
|
|
207
|
+
ok(state.spawns.every((s) => s.argv.join(' ').indexOf('script.py') === -1), 'the user script was NOT executed when a package is missing')
|
|
208
|
+
})
|
|
209
|
+
await withHost({ licence: false }, async (state) => {
|
|
210
|
+
const r = await state.call({ op: 'run', engine: 'matlab', mode: 'expr', expr: 'disp(1)' })
|
|
211
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_LICENSE_REQUIRED', 'unlicensed commercial engine -> LICENSE_REQUIRED (not NOT_FOUND)')
|
|
212
|
+
ok(r.next && r.next.kind === 'vendor' && /mathworks/.test(r.next.url || ''), 'vendor link attached, no install plan')
|
|
213
|
+
const i = await state.call({ op: 'install', engine: 'matlab', packages: ['Statistics_Toolbox'] })
|
|
214
|
+
ok(i.ok === false && i.code === 'MATH_REFUSED', 'commercial install always refused')
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 10. timeout / non-zero / truncation / bad-argv ─────────────────────────────────────────────
|
|
219
|
+
{
|
|
220
|
+
await withHost({ hang: true }, async (state) => {
|
|
221
|
+
const r = await state.call({ op: 'run', engine: 'julia', mode: 'code', code: 'sleep(1)', timeoutMs: 1200 })
|
|
222
|
+
ok(r.ok === false && r.code === 'MATH_TIMEOUT', 'hang -> MATH_TIMEOUT')
|
|
223
|
+
ok(r.timedOut === true && !!r.receipt, 'timeout is flagged and still leaves a receipt')
|
|
224
|
+
const rj = JSON.parse(state.file(r.receipt.json))
|
|
225
|
+
ok(rj.timedOut === true && rj.exit === null, 'receipt records timedOut + null exit')
|
|
226
|
+
})
|
|
227
|
+
await withHost({ exit: 3 }, async (state) => {
|
|
228
|
+
const r = await state.call({ op: 'run', engine: 'julia', mode: 'code', code: 'boom' })
|
|
229
|
+
ok(r.ok === false && r.code === 'MATH_NONZERO_EXIT' && r.exit === 3, 'non-zero exit -> NONZERO_EXIT with the code')
|
|
230
|
+
})
|
|
231
|
+
await withHost({ stdoutBytes: 200000 }, async (state) => {
|
|
232
|
+
const r = await state.call({ op: 'run', engine: 'octave', mode: 'code', code: 'x=1' })
|
|
233
|
+
ok(r.ok === true && r.truncated.stdout === true, 'oversized stdout is a WARNING, not a failure')
|
|
234
|
+
ok(r.warnings.length === 1 && r.warnings[0].code === 'OUTPUT_TRUNCATED', 'OUTPUT_TRUNCATED warning present')
|
|
235
|
+
ok(r.stdout.length === M.MATH_CAPS.stdout, 'returned stdout is capped at 64KB')
|
|
236
|
+
ok(state.file(r.receipt.dir + '/stdout.txt').length === 200000, 'full output is on disk')
|
|
237
|
+
})
|
|
238
|
+
await withHost({ argError: "Error, unknown option '-q'" }, async (state) => {
|
|
239
|
+
const r = await state.call({ op: 'run', engine: 'maple', mode: 'code', code: 'x' })
|
|
240
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_BAD_ARGV', 'usage/option error -> MATH_ENGINE_BAD_ARGV (not collapsed into NONZERO_EXIT)')
|
|
241
|
+
ok(r.next && r.next.kind === 'engine-override' && r.next.hint === 'mathEngineOverride', 'next points at mathEngineOverride')
|
|
242
|
+
eq(r.next.argv, r.argv, 'the failing argv is echoed in next.argv')
|
|
243
|
+
ok(r.message.indexOf('mathEngineOverride') !== -1, 'message mentions the override escape hatch')
|
|
244
|
+
})
|
|
245
|
+
await withHost({ resolveThrows: true }, async (state) => {
|
|
246
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'x' })
|
|
247
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'unresolvable executable -> ENGINE_NOT_FOUND (no crash)')
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── 11. receipt op (idempotent re-render) ──────────────────────────────────────────────────────
|
|
252
|
+
{
|
|
253
|
+
await withHost({}, async (state) => {
|
|
254
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)' })
|
|
255
|
+
const before = state.file(r.receipt.md)
|
|
256
|
+
const again = await state.call({ op: 'receipt', file: r.receipt.dir })
|
|
257
|
+
ok(again.ok === true, 'op=receipt re-renders an existing receipt')
|
|
258
|
+
ok(state.file(r.receipt.md) === before, 're-render is idempotent (same bytes)')
|
|
259
|
+
const miss = await state.call({ op: 'receipt', file: 'Computation/nope' })
|
|
260
|
+
// Round-2 lens-1: a DELETED/missing archive is an explicit refusal, not a malformed call.
|
|
261
|
+
ok(miss.ok === false && miss.code === 'MATH_REFUSED', 'op=receipt on a missing archive -> MATH_REFUSED')
|
|
262
|
+
ok(miss.next && miss.next.kind === 'reason' && miss.next.reason === 'archive-missing', 'the refusal carries next.reason=archive-missing')
|
|
263
|
+
// A corrupt receipt.json is still a malformed artifact (different code, different reason).
|
|
264
|
+
await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)' })
|
|
265
|
+
const dirs = [...state.files.keys()].filter((k) => /^Computation\/[^/]+\/receipt\.json$/.test(k))
|
|
266
|
+
if (dirs.length) {
|
|
267
|
+
state.files.set(dirs[0], '{not json')
|
|
268
|
+
const corrupt = await state.call({ op: 'receipt', file: dirs[0].replace('/receipt.json', '') })
|
|
269
|
+
ok(corrupt.ok === false && corrupt.code === 'MATH_INVALID_ARGUMENT' && corrupt.next.reason === 'receipt-unparsable', 'a corrupt receipt.json stays MATH_INVALID_ARGUMENT (receipt-unparsable)')
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── 12. install: plan -> confirm, scope, audit ─────────────────────────────────────────────────
|
|
275
|
+
{
|
|
276
|
+
await withHost({}, async (state) => {
|
|
277
|
+
const plan = await state.call({ op: 'install', engine: 'python', packages: ['sympy'] })
|
|
278
|
+
ok(plan.ok === true && !!plan.planToken, 'install returns a plan + planToken')
|
|
279
|
+
ok(plan.plan.scope === 'user', 'plan defaults to user scope')
|
|
280
|
+
ok(plan.plan.commands[0].argv.indexOf('--user') !== -1, 'user scope uses the --user template')
|
|
281
|
+
eq(state.spawns.length, 0, 'planning spawns nothing (never auto-install)')
|
|
282
|
+
const wrong = await state.call({ op: 'install', engine: 'python', packages: ['sympy'], confirm: 'deadbeef' })
|
|
283
|
+
ok(wrong.ok === false && wrong.code === 'MATH_REFUSED', 'confirm with a mismatched token is refused')
|
|
284
|
+
const done = await state.call({ op: 'install', engine: 'python', packages: ['sympy'], confirm: plan.planToken })
|
|
285
|
+
ok(done.ok === true && done.executed === true, 'confirm with the plan token executes')
|
|
286
|
+
const audit = JSON.parse(state.file(done.audit))
|
|
287
|
+
ok(audit.scope === 'user' && audit.installed[0] === 'sympy' && audit.rollback.commands.length === 1, 'audit records scope, installed list and an uninstall command')
|
|
288
|
+
ok(audit.network === 'not-enforced-by-plugin', 'audit is honest about network enforcement')
|
|
289
|
+
const plan2 = await state.call({ op: 'install', engine: 'python', packages: ['numpy'] })
|
|
290
|
+
ok(plan2.plan.scope === 'user', 'scope is NOT remembered: a later call is user again')
|
|
291
|
+
const sys = await state.call({ op: 'install', engine: 'python', packages: ['numpy'], scope: 'system' })
|
|
292
|
+
ok(sys.plan.scope === 'system', 'system scope only when explicitly requested per call')
|
|
293
|
+
const rSys = await state.call({ op: 'install', engine: 'r', packages: ['MASS'], scope: 'system' })
|
|
294
|
+
ok(rSys.ok === false && rSys.code === 'MATH_REFUSED', 'system scope refused when the manager has no system template')
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── 13. tier off + availability line ───────────────────────────────────────────────────────────
|
|
299
|
+
{
|
|
300
|
+
await withHost({ params: { mathComputation: 'off' } }, async (state) => {
|
|
301
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'x' })
|
|
302
|
+
ok(r.ok === false && r.code === 'MATH_NOT_AVAILABLE', 'mathComputation=off is a true no-op')
|
|
303
|
+
ok(r.next && r.next.kind === 'enable', 'off tier tells the caller how to enable it')
|
|
304
|
+
eq(state.spawns.length, 0, 'off tier spawns nothing')
|
|
305
|
+
})
|
|
306
|
+
const probe = { ok: true, engines: [{ name: 'python', version: '1.2.3' }], available: true }
|
|
307
|
+
const zh = M.mathAvailabilityLine(probe, 'zh', 'typed+shell')
|
|
308
|
+
const typed = M.mathAvailabilityLine(probe, 'zh', 'typed')
|
|
309
|
+
const en = M.mathAvailabilityLine(probe, 'en', 'typed+shell')
|
|
310
|
+
ok(zh.indexOf('先 probe 再 run') !== -1 && zh.indexOf(M.MATH_SHELL_FALLBACK_MARK) !== -1, 'zh availability line: probe-first + shell marking')
|
|
311
|
+
ok(typed.indexOf(M.MATH_SHELL_FALLBACK_MARK) === -1, 'typed tier drops the shell-fallback sentence')
|
|
312
|
+
ok(en.indexOf(M.MATH_SHELL_FALLBACK_MARK_EN) !== -1 && en.indexOf('not tool-archived') !== -1, 'en availability line carries the marking')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── 14. no-subprocess matrix (adjudication c) ───────────────────────────────────────────────────
|
|
316
|
+
{
|
|
317
|
+
await withHost({ hasSubprocess: false }, async (state) => {
|
|
318
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'x' })
|
|
319
|
+
ok(r.ok === false && r.code === 'MATH_NO_SUBPROCESS', 'host that declares no subprocess -> MATH_NO_SUBPROCESS (not ENGINE_NOT_FOUND)')
|
|
320
|
+
ok(r.next && r.next.kind === 'note', 'no-subprocess carries the note next')
|
|
321
|
+
const p = await state.call({ op: 'probe' })
|
|
322
|
+
ok(p.ok === false && p.code === 'MATH_NO_SUBPROCESS', 'op=probe also reports NO_SUBPROCESS on such a host')
|
|
323
|
+
})
|
|
324
|
+
await withHost({ resolveThrows: true }, async (state) => {
|
|
325
|
+
const r = await state.call({ op: 'run', engine: 'python', mode: 'code', code: 'x' })
|
|
326
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'without the flag, an unresolvable host still reports ENGINE_NOT_FOUND + install guide')
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── 15. P2a: archive -> edit -> re-run integrity ───────────────────────────────────────────────
|
|
331
|
+
{
|
|
332
|
+
// 15a. mode:'file' is KEYED BY PATH, so an edit + re-run lands in the same archive id and the
|
|
333
|
+
// hash reconciliation reports the change (never a silent pass).
|
|
334
|
+
const st = makeFakeHost({ files: { 'Problems/calc.py': 'print(2)\n' } })
|
|
335
|
+
M.registerMathComputation(st.host)
|
|
336
|
+
const first = await st.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/calc.py' })
|
|
337
|
+
ok(first.ok === true && first.scriptChanged === false && first.scriptChangedDuringRun === false, 'first file run: no change flags')
|
|
338
|
+
st.files.set('Problems/calc.py', 'print(3)\n')
|
|
339
|
+
const second = await st.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/calc.py' })
|
|
340
|
+
ok(second.baseRunDir === first.baseRunDir, 'file mode is path-keyed: the edited file reuses the archive id')
|
|
341
|
+
ok(second.scriptChanged === true, 'edited file -> scriptChanged:true (no silent pass)')
|
|
342
|
+
ok(!!second.previousReceipt && second.previousReceipt.scriptHash === first.scriptHash, 'the RETURN SHELL also exposes previousReceipt (the stale hash)')
|
|
343
|
+
ok(second.warnings.some((w) => w.code === M.MATH_SCRIPT_CHANGED_WARNING), 'scriptChanged is warned visibly')
|
|
344
|
+
ok(second.attempt === 2 && second.scriptHash !== first.scriptHash, 'the new attempt carries the new scriptHash')
|
|
345
|
+
const secondReceipt = JSON.parse(st.file(second.receipt.json))
|
|
346
|
+
ok(secondReceipt.scriptChanged === true && !!secondReceipt.previousReceipt && secondReceipt.previousReceipt.scriptHash === first.scriptHash, 'receipt records scriptChanged + the previous receipt hash')
|
|
347
|
+
|
|
348
|
+
// 15b. a mid-run edit (one member edits while another runs) is flagged after the run.
|
|
349
|
+
const st2 = makeFakeHost({ files: { 'Problems/race.py': 'print(1)\n' }, mutateOnRun: { rel: 'Problems/race.py', text: 'print(9)\n' } })
|
|
350
|
+
M.registerMathComputation(st2.host)
|
|
351
|
+
const raced = await st2.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/race.py' })
|
|
352
|
+
ok(raced.ok === true && raced.scriptChangedDuringRun === true, 'mid-run edit -> scriptChangedDuringRun:true')
|
|
353
|
+
ok(raced.warnings.some((w) => w.code === M.MATH_SCRIPT_CHANGED_DURING_RUN_WARNING), 'mid-run edit is warned visibly')
|
|
354
|
+
ok(JSON.parse(st2.file(raced.receipt.json)).scriptChangedDuringRun === true, 'receipt records scriptChangedDuringRun')
|
|
355
|
+
|
|
356
|
+
// 15c. op:'receipt' reconciles the archived script hash, and never rewrites existing files.
|
|
357
|
+
const st3 = makeFakeHost({})
|
|
358
|
+
M.registerMathComputation(st3.host)
|
|
359
|
+
const run = await st3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
360
|
+
const jsonBefore = st3.file(run.receipt.json)
|
|
361
|
+
const mdBefore = st3.file(run.receipt.md)
|
|
362
|
+
const clean = await st3.call({ op: 'receipt', file: run.baseRunDir })
|
|
363
|
+
ok(clean.ok === true && clean.scriptChanged === false, 'op=receipt on an untouched archive: no change')
|
|
364
|
+
ok(st3.file(run.receipt.json) === jsonBefore && st3.file(run.receipt.md) === mdBefore, 'op=receipt never rewrites existing receipt files')
|
|
365
|
+
st3.files.set(run.scriptPath, 'print(42)\n')
|
|
366
|
+
const dirty = await st3.call({ op: 'receipt', file: run.baseRunDir })
|
|
367
|
+
ok(dirty.ok === true && dirty.scriptChanged === true && dirty.currentScriptHash !== dirty.scriptHash, 'op=receipt reports scriptChanged for an edited archive script')
|
|
368
|
+
ok(dirty.warnings.some((w) => w.code === M.MATH_SCRIPT_CHANGED_WARNING), 'op=receipt warns visibly')
|
|
369
|
+
ok(st3.file(run.receipt.json) === jsonBefore, 'reconciliation still does not rewrite receipt.json')
|
|
370
|
+
|
|
371
|
+
// 15d. retention: over the documented cap the tool WARNS and never deletes.
|
|
372
|
+
const st4 = makeFakeHost({})
|
|
373
|
+
M.registerMathComputation(st4.host)
|
|
374
|
+
const cap = M.MATH_ARCHIVE_MAX_ATTEMPTS_PER_RUN
|
|
375
|
+
let last = null
|
|
376
|
+
for (let i = 0; i < cap + 1; i++) last = await st4.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
377
|
+
ok(last.attempt === cap + 1, 'attempts keep being appended past the cap (no deletion, no refusal)')
|
|
378
|
+
ok(last.warnings.some((w) => w.code === M.MATH_ARCHIVE_WARNING), 'over the retention cap the tool warns (ARCHIVE_RETENTION_EXCEEDED)')
|
|
379
|
+
ok(st4.file(last.baseRunDir + '/receipt.json') !== undefined, 'the FIRST attempt is still present after exceeding the cap')
|
|
380
|
+
|
|
381
|
+
// 15e. the failure return also carries the archive coordinates.
|
|
382
|
+
const st5 = makeFakeHost({ exit: 3 })
|
|
383
|
+
M.registerMathComputation(st5.host)
|
|
384
|
+
const bad = await st5.call({ op: 'run', engine: 'python', mode: 'code', code: 'boom\n' })
|
|
385
|
+
ok(bad.ok === false && bad.code === 'MATH_NONZERO_EXIT' && !!bad.scriptPath && !!bad.scriptHash, 'failure return carries scriptPath + scriptHash')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── 16. AUDIT-C fixes: concurrency, listDir retention, string-level path guard ──────────────────
|
|
389
|
+
{
|
|
390
|
+
// 16a (HIGH). Two same-id runs fired concurrently must never share an attempt dir: allocation +
|
|
391
|
+
// the claiming receipt write are serialised per archive id. Before the fix both took attempt 1 and
|
|
392
|
+
// the second silently overwrote the first receipt.
|
|
393
|
+
{
|
|
394
|
+
const stc = makeFakeHost({})
|
|
395
|
+
M.registerMathComputation(stc.host)
|
|
396
|
+
const [a, b] = await Promise.all([
|
|
397
|
+
stc.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' }),
|
|
398
|
+
stc.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' }),
|
|
399
|
+
])
|
|
400
|
+
ok(a.receipt.dir !== b.receipt.dir, '★ 同一 id 的并发运行绝不共用一个 attempt 目录(并发下的 append-only)')
|
|
401
|
+
ok([a.attempt, b.attempt].sort().join(',') === '1,2', 'concurrent same-id runs take attempts 1 and 2 (neither archive is lost)')
|
|
402
|
+
ok(stc.file(a.attemptDir + '/receipt.json') !== undefined && stc.file(b.attemptDir + '/receipt.json') !== undefined, 'both concurrent attempts have their own receipt on disk')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 16b (MEDIUM). Project-level retention via the optional listDir, and the no-listDir path.
|
|
406
|
+
{
|
|
407
|
+
const many = []
|
|
408
|
+
for (let i = 0; i < M.MATH_ARCHIVE_MAX_RUNS + 1; i++) many.push({ name: 'run-' + i, type: 'directory' })
|
|
409
|
+
const stl = makeFakeHost({ listDir: many })
|
|
410
|
+
M.registerMathComputation(stl.host)
|
|
411
|
+
const over = await stl.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
412
|
+
ok(over.warnings.some((w) => w.code === M.MATH_ARCHIVE_WARNING), 'listDir shows more than MATH_ARCHIVE_MAX_RUNS -> ARCHIVE_RETENTION_EXCEEDED (warn only)')
|
|
413
|
+
ok(stl.listDirCalls.length > 0 && stl.listDirCalls[0].indexOf('Computation') !== -1, 'the retention check asks the host to list Computation/')
|
|
414
|
+
ok(stl.file(over.attemptDir + '/receipt.json') !== undefined, 'nothing is deleted when the cap is exceeded')
|
|
415
|
+
const stn = makeFakeHost({})
|
|
416
|
+
M.registerMathComputation(stn.host)
|
|
417
|
+
const under = await stn.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
418
|
+
ok(stn.listDirCalls.length === 0, 'a host without listDir is never asked for a listing')
|
|
419
|
+
ok(!under.warnings.some((w) => w.code === M.MATH_ARCHIVE_WARNING), 'no listDir -> no project-level retention check and no false alarm')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 16c (MEDIUM). The path guard is STRING-LEVEL: pin what it really does, including the limitation.
|
|
423
|
+
{
|
|
424
|
+
const sp = makeFakeHost({ files: { 'Problems/calc.py': 'print(1)\n', 'Problems/link.py': 'print(2)\n' } })
|
|
425
|
+
M.registerMathComputation(sp.host)
|
|
426
|
+
const up = await sp.call({ op: 'run', engine: 'python', mode: 'file', file: '../outside.py' })
|
|
427
|
+
ok(up.ok === false && up.code === 'MATH_REFUSED' && up.next.reason === 'path-outside-project', 'lexical escape ../ is refused')
|
|
428
|
+
const deep = await sp.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/../../outside.py' })
|
|
429
|
+
ok(deep.ok === false && deep.code === 'MATH_REFUSED', 'a lexical escape buried mid-path is refused')
|
|
430
|
+
const abs = await sp.call({ op: 'run', engine: 'python', mode: 'file', file: 'C:/Windows/system32/calc.py' })
|
|
431
|
+
ok(abs.ok === false && abs.code === 'MATH_REFUSED', 'an absolute path is refused')
|
|
432
|
+
// A symlink/junction is invisible to a string guard. This pins the LIMITATION (docs §6): a path
|
|
433
|
+
// that LOOKS in-project is accepted, because the guard never resolves it. The host fs owns real
|
|
434
|
+
// containment, so this assertion exists to stop the docs from claiming otherwise.
|
|
435
|
+
const linked = await sp.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/link.py' })
|
|
436
|
+
ok(linked.ok === true, 'an in-project path is accepted without resolving it (string-level guard: links/junctions are NOT followed)')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 16d (AUDIT-A). Two more reconciliation false negatives, both fail-closed now:
|
|
440
|
+
// - the source file disappearing WHILE the run is in flight must count as a change;
|
|
441
|
+
// - a deleted archive script must make op:'receipt' report scriptChanged (the receipt cannot be
|
|
442
|
+
// evidence for any code once its script is gone).
|
|
443
|
+
{
|
|
444
|
+
const sd = makeFakeHost({ files: { 'Problems/gone.py': 'print(1)\n' }, deleteOnRun: { rel: 'Problems/gone.py' } })
|
|
445
|
+
M.registerMathComputation(sd.host)
|
|
446
|
+
const g = await sd.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/gone.py' })
|
|
447
|
+
ok(g.ok === true && g.scriptChangedDuringRun === true, 'the source file disappearing mid-run is a change: scriptChangedDuringRun:true')
|
|
448
|
+
ok(g.warnings.some((w) => w.code === M.MATH_SCRIPT_CHANGED_DURING_RUN_WARNING), 'a disappeared source is warned visibly')
|
|
449
|
+
ok(JSON.parse(sd.file(g.attemptDir + '/receipt.json')).scriptChangedDuringRun === true, 'the receipt records it too')
|
|
450
|
+
|
|
451
|
+
const sm = makeFakeHost({})
|
|
452
|
+
M.registerMathComputation(sm.host)
|
|
453
|
+
const run2 = await sm.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
454
|
+
sm.files.delete(run2.scriptPath)
|
|
455
|
+
const rec2 = await sm.call({ op: 'receipt', file: run2.baseRunDir })
|
|
456
|
+
ok(rec2.ok === true && rec2.scriptChanged === true && rec2.currentScriptHash === null, 'a deleted archive script makes op=receipt report scriptChanged (fail-closed)')
|
|
457
|
+
ok(rec2.warnings.some((w) => w.code === M.MATH_SCRIPT_CHANGED_WARNING), 'the deletion is warned visibly')
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── 17. AUDIT-B #1: mathEngineOverride is a REAL, accepted argument ─────────────────────────────
|
|
462
|
+
{
|
|
463
|
+
// 17a. It is accepted (not "unknown argument") and it really changes the assembled argv.
|
|
464
|
+
{
|
|
465
|
+
const so = makeFakeHost({})
|
|
466
|
+
M.registerMathComputation(so.host)
|
|
467
|
+
const r = await so.call({
|
|
468
|
+
op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n',
|
|
469
|
+
mathEngineOverride: { python: { scriptArgv: ['-u', '<script>'], versionArgv: ['--version', '--override-proof'] } },
|
|
470
|
+
})
|
|
471
|
+
ok(r.ok === true, 'mathEngineOverride is ACCEPTED (previously rejected as an unknown argument)')
|
|
472
|
+
eq(r.argv[1], '-u', 'the overridden scriptArgv really changes the assembled argv')
|
|
473
|
+
ok(r.argv[r.argv.length - 1].indexOf('script.py') !== -1, 'the <script> placeholder is still substituted with the archived script path')
|
|
474
|
+
const versionSpawn = so.spawns.filter((s) => s.argv.indexOf('--override-proof') !== -1)
|
|
475
|
+
ok(versionSpawn.length > 0, 'the overridden versionArgv reaches the VERSION probe (not just the run argv)')
|
|
476
|
+
ok(!so.spawns.some((s) => s.argv.length === 2 && s.argv[1] === '--version'), 'the default versionArgv is not used once overridden')
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 17b. mode:'expr' override.
|
|
480
|
+
{
|
|
481
|
+
const se = makeFakeHost({})
|
|
482
|
+
M.registerMathComputation(se.host)
|
|
483
|
+
const r = await se.call({ op: 'run', engine: 'python', mode: 'expr', expr: '1+1', mathEngineOverride: { python: { evalArgv: ['-X', 'utf8', '-c', '<expr>'] } } })
|
|
484
|
+
eq(r.argv.slice(1, 4), ['-X', 'utf8', '-c'], 'the overridden evalArgv is used for mode=expr')
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 17c. The override stays CLOSED: bad shapes are refused with MATH_INVALID_ARGUMENT.
|
|
488
|
+
{
|
|
489
|
+
const sv = makeFakeHost({})
|
|
490
|
+
M.registerMathComputation(sv.host)
|
|
491
|
+
const unknownEngine = await sv.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', mathEngineOverride: { fortran: { scriptArgv: ['<script>'] } } })
|
|
492
|
+
ok(unknownEngine.ok === false && unknownEngine.code === 'MATH_INVALID_ARGUMENT' && /unknown engine/.test(unknownEngine.message), 'an override for an unknown engine is refused')
|
|
493
|
+
const badShape = await sv.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', mathEngineOverride: { python: { scriptArgv: 'not-an-array' } } })
|
|
494
|
+
ok(badShape.ok === false && badShape.code === 'MATH_INVALID_ARGUMENT' && /string\[\]/.test(badShape.message), 'a non-array template is refused')
|
|
495
|
+
const badKey = await sv.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', mathEngineOverride: { python: { shellArgv: ['x'] } } })
|
|
496
|
+
ok(badKey.ok === false && badKey.code === 'MATH_INVALID_ARGUMENT' && /unknown key/.test(badKey.message), 'an unknown key inside the override is refused')
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 17d. The advertised hint and the schema agree with the implementation.
|
|
500
|
+
{
|
|
501
|
+
ok(!!M.MATH_TOOL_SCHEMA.properties.mathEngineOverride, 'MATH_TOOL_SCHEMA advertises mathEngineOverride')
|
|
502
|
+
ok(M.MATH_BAD_ARGV_HINT === 'mathEngineOverride', 'MATH_BAD_ARGV_HINT still names the escape hatch')
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── 18. round-6: substitution honesty (A), manager dispatch (B), scope policy (C), version syntax (D)
|
|
507
|
+
{
|
|
508
|
+
// 18a (A): the rule is part of the frozen rule block and reaches both language lines.
|
|
509
|
+
ok(M.MATH_RULE_LINES.join('\n').indexOf(M.MATH_SUBSTITUTION_RULE_LINE) !== -1, 'MATH_RULE_LINES carries the substitution-honesty rule (zh)')
|
|
510
|
+
ok(M.MATH_RULE_LINES_EN.join('\n').indexOf(M.MATH_SUBSTITUTION_RULE_LINE_EN) !== -1, 'MATH_RULE_LINES_EN carries the substitution-honesty rule (en)')
|
|
511
|
+
ok(/替代方案若改变精确性/.test(String(M.MATH_TOOL_DESCRIPTION)), 'the tool description states the substitution duty (paraphrase)')
|
|
512
|
+
{
|
|
513
|
+
const p = await M.probeMathEngines(makeFakeHost({ installed: ['python3'] }).host)
|
|
514
|
+
const zh = M.mathAvailabilityLine(p, 'zh', 'typed+shell')
|
|
515
|
+
const en = M.mathAvailabilityLine(p, 'en', 'typed+shell')
|
|
516
|
+
ok(zh.indexOf('替代必须声明') !== -1, 'the injected (zh) availability line carries the substitution rule')
|
|
517
|
+
ok(en.indexOf('Declare substitutions') !== -1, 'the injected (en) availability line carries the substitution rule')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 18b (B): python dispatch follows the DETECTED environment, never a guess.
|
|
521
|
+
{
|
|
522
|
+
const condaHost = makeFakeHost({ resolveMap: { python3: 'C:/miniconda3/envs/math/python.exe', conda: 'C:/miniconda3/Scripts/conda.exe' } })
|
|
523
|
+
M.registerMathComputation(condaHost.host)
|
|
524
|
+
const c = await condaHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
|
|
525
|
+
ok(c.ok === true && c.plan.manager === 'conda' && c.plan.managerAssumed === false, 'a conda-style interpreter plans with conda (not guessed)')
|
|
526
|
+
eq(c.plan.commands[0].argv, ['conda', 'install', '-y', '-c', 'conda-forge', 'numpy'], 'conda install comes from conda-forge')
|
|
527
|
+
ok(/conda/.test(String(c.message)), 'the plan message names the manager')
|
|
528
|
+
|
|
529
|
+
const uvHost = makeFakeHost({ resolveMap: { python3: '/venv/bin/python3', uv: '/venv/bin/uv' } })
|
|
530
|
+
M.registerMathComputation(uvHost.host)
|
|
531
|
+
const u = await uvHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
|
|
532
|
+
ok(u.ok === true && u.plan.manager === 'uv', 'a uv next to the interpreter plans with uv')
|
|
533
|
+
eq(u.plan.commands[0].argv, ['uv', 'pip', 'install', 'numpy'], 'uv install uses `uv pip install`')
|
|
534
|
+
|
|
535
|
+
const pipHost = makeFakeHost({ resolveMap: { python3: '/usr/bin/python3' } })
|
|
536
|
+
M.registerMathComputation(pipHost.host)
|
|
537
|
+
const d = await pipHost.call({ op: 'install', engine: 'python', packages: ['numpy'] })
|
|
538
|
+
ok(d.ok === true && d.plan.manager === 'pip' && d.plan.managerAssumed === true, 'no marker -> pip fallback, flagged as an ASSUMPTION')
|
|
539
|
+
eq(d.plan.commands[0].argv, ['/usr/bin/python3', '-m', 'pip', 'install', '--user', 'numpy'], 'the pip fallback keeps the user-scope form')
|
|
540
|
+
ok(/假设|assum/i.test(String(d.message)), 'the message says the manager is an assumption')
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 18c (C): R/Octave/Julia refuse system scope WITH a per-engine reason; conda too.
|
|
544
|
+
{
|
|
545
|
+
const h = makeFakeHost({ resolveMap: { Rscript: '/usr/bin/Rscript', octave: '/usr/bin/octave', julia: '/usr/bin/julia' } })
|
|
546
|
+
M.registerMathComputation(h.host)
|
|
547
|
+
for (const [engine, marker] of [['r', 'R_LIBS_USER'], ['octave', 'share/packages'], ['julia', 'JULIA_DEPOT_PATH']]) {
|
|
548
|
+
const r = await h.call({ op: 'install', engine: engine, packages: ['x'], scope: 'system' })
|
|
549
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED' && r.next && r.next.reason === 'system-scope-unsupported', engine + ': system scope -> MATH_REFUSED(system-scope-unsupported)')
|
|
550
|
+
ok(String(r.message).indexOf(marker) !== -1, engine + ': the refusal states WHY (mentions ' + marker + ')')
|
|
551
|
+
}
|
|
552
|
+
const condaHost = makeFakeHost({ resolveMap: { python3: 'C:/miniconda3/envs/math/python.exe', conda: 'C:/miniconda3/Scripts/conda.exe' } })
|
|
553
|
+
M.registerMathComputation(condaHost.host)
|
|
554
|
+
const cs = await condaHost.call({ op: 'install', engine: 'python', packages: ['numpy'], scope: 'system' })
|
|
555
|
+
ok(cs.ok === false && cs.next.reason === 'system-scope-unsupported' && /conda/.test(String(cs.message)), 'conda has no system scope and says so')
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 18d (D): version syntax is passed through where the manager understands it, refused otherwise.
|
|
559
|
+
{
|
|
560
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy==1.2'] }, {}).ok === true, 'pkg==1.2 is accepted (pass-through)')
|
|
561
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy=1.2'] }, {}).ok === true, 'pkg=1.2 (conda form) is accepted')
|
|
562
|
+
ok(M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy[extra]>=1.2'] }, {}).ok === true, 'extras + comparator are accepted')
|
|
563
|
+
const badSpec = M.validateMathArgs({ op: 'run', mode: 'code', code: 'x', packages: ['numpy; rm -rf /'] }, {})
|
|
564
|
+
ok(badSpec.ok === false && badSpec.code === 'MATH_INVALID_ARGUMENT' && badSpec.next && badSpec.next.reason === 'unsupported-version-syntax', 'a shell-ish spec is refused with unsupported-version-syntax')
|
|
565
|
+
|
|
566
|
+
const ph = makeFakeHost({ packages: { sympy: 'present' } })
|
|
567
|
+
M.registerMathComputation(ph.host)
|
|
568
|
+
await ph.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['sympy==1.12'] })
|
|
569
|
+
const pkgSpawns = ph.spawns.filter((s) => s.argv.join(' ').indexOf('sympy') !== -1)
|
|
570
|
+
ok(pkgSpawns.length > 0 && pkgSpawns.every((s) => s.argv.join(' ').indexOf('sympy==1.12') === -1), 'presence is probed by BASE NAME (the constraint never reaches the probe)')
|
|
571
|
+
const plan = await ph.call({ op: 'install', engine: 'python', packages: ['sympy==1.12'] })
|
|
572
|
+
ok(plan.ok === true && plan.plan.commands[0].argv[plan.plan.commands[0].argv.length - 1] === 'sympy==1.12', 'the install plan passes the spec through verbatim')
|
|
573
|
+
ok(!!plan.plan.versionPolicy, 'the plan states the version policy (solving belongs to the manager)')
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── 19. round-7: probe reports the REQUESTED engine (fix 1) + DSH bundled-runtime discovery (fix 2)
|
|
578
|
+
{
|
|
579
|
+
// 19a (fix 1): `probe {engine:'r'}` must describe r - not the first allowed engine (python).
|
|
580
|
+
{
|
|
581
|
+
const hr = makeFakeHost({ installed: ['python3', 'python', 'Rscript'] })
|
|
582
|
+
M.registerMathComputation(hr.host)
|
|
583
|
+
const p = await hr.call({ op: 'probe', engine: 'r' })
|
|
584
|
+
ok(p.ok === true && p.engine === 'r', 'probe with engine:r reports engine:r (not python)')
|
|
585
|
+
ok(!!p.engineInfo && p.engineInfo.name === 'r' && !!p.engineInfo.version, "probe reports the requested engine's version/path")
|
|
586
|
+
ok(!p.next, 'a usable requested engine yields no install guidance')
|
|
587
|
+
const pp = await hr.call({ op: 'probe', engine: 'python' })
|
|
588
|
+
ok(pp.ok === true && pp.engine === 'python' && pp.engineInfo.name === 'python', 'probe with engine:python reports python (each request is honoured)')
|
|
589
|
+
}
|
|
590
|
+
// 19b (fix 1): a MISSING requested engine must not blame python in the guidance.
|
|
591
|
+
{
|
|
592
|
+
const hm = makeFakeHost({ installed: ['python3', 'python'] })
|
|
593
|
+
M.registerMathComputation(hm.host)
|
|
594
|
+
const p = await hm.call({ op: 'probe', engine: 'r' })
|
|
595
|
+
ok(p.ok === false && p.code === 'MATH_ENGINE_NOT_FOUND' && p.engine === 'r', 'a missing requested engine is reported as THAT engine (r, not python)')
|
|
596
|
+
ok(p.next && p.next.kind === 'user-install' && p.next.engine === 'r' && !!p.next.perOs, 'the install guidance is for the REQUESTED engine (r), with per-OS commands')
|
|
597
|
+
const msg = String(p.message)
|
|
598
|
+
ok(/试过:r|请求:r/.test(msg) && !/python/i.test(msg), 'the message names the requested engine (r), never a python fallback')
|
|
599
|
+
}
|
|
600
|
+
// 19c (fix 2): a DSH-bundled runtime is discovered as a LAST resort and used like any engine.
|
|
601
|
+
{
|
|
602
|
+
const absTree = {
|
|
603
|
+
'X:/home/.dsh/dsh-runtimes': [{ name: 'dsh-primary-runtime', type: 'directory' }, { name: 'dsh-other-runtime', type: 'directory' }],
|
|
604
|
+
'X:/home/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/python': [{ name: 'python.exe', type: 'file' }, { name: 'python3.exe', type: 'file' }],
|
|
605
|
+
'X:/home/.dsh/dsh-runtimes/dsh-other-runtime/dependencies/python': [],
|
|
606
|
+
}
|
|
607
|
+
const hb = makeFakeHost({ installed: [], runtimeRoots: ['X:/home/.dsh'], absTree: absTree })
|
|
608
|
+
M.registerMathComputation(hb.host)
|
|
609
|
+
const p = await hb.call({ op: 'probe', engine: 'python' })
|
|
610
|
+
ok(p.ok === true && p.engine === 'python' && p.engineInfo.name === 'python', 'a bundled DSH runtime is found when PATH has nothing')
|
|
611
|
+
ok(/dsh-runtimes\/dsh-primary-runtime\/dependencies\/python\/python/.test(p.engineInfo.path), 'the discovered path comes from the generically globbed runtime tree')
|
|
612
|
+
const r = await hb.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
613
|
+
ok(r.ok === true && !!r.engineInfo.version, 'a run on the bundled runtime succeeds and reports its version')
|
|
614
|
+
ok(!r.next, 'a found bundled runtime emits NO install guidance')
|
|
615
|
+
// The bundled runtime must not be invented when the tree is absent.
|
|
616
|
+
const hn = makeFakeHost({ installed: [] })
|
|
617
|
+
M.registerMathComputation(hn.host)
|
|
618
|
+
const n = await hn.call({ op: 'probe', engine: 'python' })
|
|
619
|
+
ok(n.ok === false && n.code === 'MATH_ENGINE_NOT_FOUND' && n.next && n.next.kind === 'user-install', 'with no PATH engine and no runtime tree the per-OS guidance still fires')
|
|
620
|
+
}
|
|
621
|
+
// 19d (finding 3): the cli package precheck runs against the FAMILY of `cli.command`, not the
|
|
622
|
+
// generic cli descriptor (which has no packageProbe and therefore reported a false "missing").
|
|
623
|
+
{
|
|
624
|
+
const hc = makeFakeHost({ installed: [], packages: { numpy: 'present' } })
|
|
625
|
+
M.registerMathComputation(hc.host)
|
|
626
|
+
const r = await hc.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' }, packages: ['numpy'] })
|
|
627
|
+
ok(r.ok === true, 'cli + packages: a python-family command prechecks with the python probe (numpy present => the run proceeds)')
|
|
628
|
+
const probes = hc.spawns.filter((s) => s.argv.join(' ').indexOf('numpy') !== -1)
|
|
629
|
+
ok(probes.length > 0 && probes.every((s) => String(s.argv[0]).indexOf('python3') !== -1), 'the package precheck ran against cli.command itself')
|
|
630
|
+
// An unrecognisable command must SKIP the precheck (never a false missing) and say so.
|
|
631
|
+
const hu = makeFakeHost({ installed: [] })
|
|
632
|
+
M.registerMathComputation(hu.host)
|
|
633
|
+
const ru = await hu.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/mytool' }, packages: ['numpy'] })
|
|
634
|
+
ok(ru.ok === true && (ru.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_SKIPPED'), 'an unknown cli family skips the precheck with an explicit warning (no false missing-packages)')
|
|
635
|
+
}
|
|
636
|
+
// 19e (finding 5): cli reports the REAL version of the resolved command when its family is known.
|
|
637
|
+
{
|
|
638
|
+
const hv = makeFakeHost({ installed: [] })
|
|
639
|
+
M.registerMathComputation(hv.host)
|
|
640
|
+
const p = await hv.call({ op: 'probe', engine: 'cli', cli: { command: '/fake/bin/python3' } })
|
|
641
|
+
ok(p.ok === true && p.engineInfo && p.engineInfo.version === '1.2.3', 'probe(engine:cli) reports the real version of the resolved command')
|
|
642
|
+
const r = await hv.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' } })
|
|
643
|
+
ok(r.engineInfo.version === '1.2.3', 'run(engine:cli) reports the real command version (not "unknown")')
|
|
644
|
+
}
|
|
645
|
+
// 19f (finding 6, found on the real bundled python): the probe code iterates `sys.argv[1:]`, so the
|
|
646
|
+
// names must arrive as SEPARATE argv items - a comma-joined single item makes python look up one
|
|
647
|
+
// package literally named "numpy,pandas" and report everything missing.
|
|
648
|
+
{
|
|
649
|
+
const hp = makeFakeHost({ installed: ['python3'], packages: { numpy: 'present', pandas: 'present' } })
|
|
650
|
+
M.registerMathComputation(hp.host)
|
|
651
|
+
await hp.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy', 'pandas'] })
|
|
652
|
+
const probe = hp.spawns.find((s) => s.argv.join(' ').indexOf('numpy') !== -1)
|
|
653
|
+
ok(!!probe && probe.argv.indexOf('numpy') !== -1 && probe.argv.indexOf('pandas') !== -1, 'the python package probe passes ONE argv item per package (sys.argv-based probe)')
|
|
654
|
+
ok(!!probe && probe.argv.join(' ').indexOf('numpy,pandas') === -1, 'the probe never passes the comma-joined list as a single argv item')
|
|
655
|
+
// round-7 (live): a real host rejected probe argv containing shell metacharacters (| %), so no
|
|
656
|
+
// probe argv may carry them - the parsers accept both `name:state` and `name state` shapes.
|
|
657
|
+
ok(!!probe && !/[|%]/.test(probe.argv.join(' ')), 'no probe argv contains a shell metacharacter (| or %)')
|
|
658
|
+
for (const name of ['python', 'r', 'octave', 'julia']) {
|
|
659
|
+
const d = (await import('../vibe-math-v2/math-engines.js')).MATH_ENGINES[name]
|
|
660
|
+
for (const a of (d.packageProbe.argv || [])) ok(!/[|%]/.test(String(a)), name + ' probe template has no | or %: ' + String(a).slice(0, 40))
|
|
661
|
+
ok(!/[|%]/.test(String(d.probeCode || 'numpy ok line')), name + ' probeCode has no | or %')
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// 19f-bis: cli + mode:'code' actually PASSES the script (finding 4) - verified through argv shape.
|
|
665
|
+
{
|
|
666
|
+
const hs = makeFakeHost({ installed: [] })
|
|
667
|
+
M.registerMathComputation(hs.host)
|
|
668
|
+
const r = await hs.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3' } })
|
|
669
|
+
const last = String(r.argv[r.argv.length - 1] || '')
|
|
670
|
+
ok(r.ok === true && /script\.txt$/.test(last), 'cli + code: the archived script is last in argv (it is really executed, not a REPL)')
|
|
671
|
+
const runSpawn = hs.spawns.filter((s) => String(s.argv[s.argv.length - 1] || '').indexOf('script.txt') !== -1)
|
|
672
|
+
ok(runSpawn.length === 1, 'the executed argv carried the script path to the process')
|
|
673
|
+
// …but when the CALLER's argv already runs code (-c/-m/-e/--eval), the caller's argv wins: the
|
|
674
|
+
// archived script must NOT be appended after it (python would read it as sys.argv[1]).
|
|
675
|
+
const rc = await hs.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: '/fake/bin/python3', argv: ['-c', 'print(1)'] } })
|
|
676
|
+
ok(rc.ok === true && rc.cliScriptAppended === false, 'cli + code with a caller program slot (-c) does NOT append the archived script')
|
|
677
|
+
ok(rc.argv.join(' ').indexOf('script.txt') === -1, 'the executed argv is exactly the caller argv (no script path)')
|
|
678
|
+
ok(!!rc.receipt && !!rc.receipt.json, 'the skipped-append run still writes a receipt')
|
|
679
|
+
}
|
|
680
|
+
// 19g (live-session fix): an engine that RESOLVED but whose version probe failed must NOT come with
|
|
681
|
+
// install guidance (the user does not need to install anything) and must carry the diagnostic.
|
|
682
|
+
{
|
|
683
|
+
const hh = makeFakeHost({ installed: ['python3', 'python'], versionProbeFails: true })
|
|
684
|
+
M.registerMathComputation(hh.host)
|
|
685
|
+
const p = await hh.call({ op: 'probe', engine: 'python' })
|
|
686
|
+
ok(p.ok === false && p.code === 'MATH_ENGINE_UNUSABLE', 'a resolved engine whose probe fails => MATH_ENGINE_UNUSABLE')
|
|
687
|
+
ok(!p.next || p.next.kind !== 'user-install', 'unusable (but FOUND) engine carries NO install guidance')
|
|
688
|
+
ok(!!p.probe && Array.isArray(p.probe.argv) && p.probe.argv[0].indexOf('python3') !== -1, 'the failure carries the probe diagnostic (argv)')
|
|
689
|
+
ok(!!p.probe && p.probe.exit === 1 && p.probe.stderrTail === 'probe boom', 'the diagnostic carries exit + stderr tail')
|
|
690
|
+
ok(/MATH_ENGINE_UNUSABLE/.test(p.code) && /exit=1/.test(String(p.message)), 'the message states the probe exit code (never an opaque "unusable")')
|
|
691
|
+
// A deterministic failure is NOT retried (no retryDiag, no retry storm), only a cold/null/timeout
|
|
692
|
+
// signature is - see 19h.
|
|
693
|
+
ok(!(p.probe && p.probe.retried), 'a deterministic probe failure is NOT retried (no retryDiag)')
|
|
694
|
+
}
|
|
695
|
+
// 19h (live-session fix): the real host intermittently returns a NULL spawn early in a session.
|
|
696
|
+
// The retry must cover spawned:false / exit:null (not just timeouts) so the probe still succeeds.
|
|
697
|
+
{
|
|
698
|
+
const hN = makeFakeHost({ installed: ['python3', 'python'], nullSpawnTimes: 2 })
|
|
699
|
+
M.registerMathComputation(hN.host)
|
|
700
|
+
const p = await hN.call({ op: 'probe', engine: 'python' })
|
|
701
|
+
ok(p.ok === true && p.engine === 'python' && p.engineInfo.version === '1.2.3', 'a transient NULL spawn is retried and the probe still succeeds')
|
|
702
|
+
ok(hN.spawns.filter((s) => s.nullSpawn).length === 2, 'the null spawns are visible in the seam (2 consumed)')
|
|
703
|
+
}
|
|
704
|
+
// 19i (live-session fix): a package precheck that could not RUN must never be reported as
|
|
705
|
+
// "missing package", and the skip must be explicit (fail-open + named warning).
|
|
706
|
+
{
|
|
707
|
+
const hP = makeFakeHost({ installed: ['python3'], packageProbeNullTimes: 9 })
|
|
708
|
+
M.registerMathComputation(hP.host)
|
|
709
|
+
const r = await hP.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
|
|
710
|
+
ok(r.ok !== false || r.code !== 'MATH_MISSING_PACKAGES', 'a precheck that could not run is NEVER reported as a missing package')
|
|
711
|
+
ok((r.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_UNKNOWN'), 'the un-runnable precheck is reported explicitly (PACKAGE_PRECHECK_UNKNOWN)')
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ── 20. round-7 cross-cutting invariant: EVERY response must be JSON-serialisable
|
|
716
|
+
// (a real session got `Converting circular structure to JSON ... property 'retryDiag' closes the
|
|
717
|
+
// circle` from a probe/run response, which meant the caller saw no result at all). This applies to
|
|
718
|
+
// all four presets because they all ship this module.
|
|
719
|
+
{
|
|
720
|
+
const cases = []
|
|
721
|
+
const push = (label, r) => cases.push({ label, r })
|
|
722
|
+
{
|
|
723
|
+
const h = makeFakeHost({ installed: ['python3', 'python', 'Rscript'], packages: { numpy: 'present' } })
|
|
724
|
+
M.registerMathComputation(h.host)
|
|
725
|
+
push('probe ok', await h.call({ op: 'probe', engine: 'python' }))
|
|
726
|
+
push('probe missing engine', await h.call({ op: 'probe', engine: 'octave' }))
|
|
727
|
+
push('run ok', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' }))
|
|
728
|
+
push('run bad mode', await h.call({ op: 'run', engine: 'python', mode: 'nope' }))
|
|
729
|
+
push('run missing pkg', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['pandas'] }))
|
|
730
|
+
push('receipt missing', await h.call({ op: 'receipt', id: 'nope' }))
|
|
731
|
+
push('install plan', await h.call({ op: 'install', engine: 'python', packages: ['numpy'] }))
|
|
732
|
+
push('install commercial', await h.call({ op: 'install', engine: 'matlab', packages: ['MATLAB'] }))
|
|
733
|
+
push('install system scope r', await h.call({ op: 'install', engine: 'r', packages: ['x'], scope: 'system' }))
|
|
734
|
+
push('unsupported spec', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['numpy; rm -rf /'] }))
|
|
735
|
+
push('unknown engine', await h.call({ op: 'run', engine: 'nope', mode: 'code', code: 'x\n' }))
|
|
736
|
+
}
|
|
737
|
+
{
|
|
738
|
+
const h = makeFakeHost({ installed: ['python3'], versionProbeFails: true })
|
|
739
|
+
M.registerMathComputation(h.host)
|
|
740
|
+
push('probe unusable (probe diagnostics)', await h.call({ op: 'probe', engine: 'python' }))
|
|
741
|
+
}
|
|
742
|
+
{
|
|
743
|
+
// A RETRIED probe carries retryDiag: that is exactly where the circular payload came from.
|
|
744
|
+
const h = makeFakeHost({ installed: ['python3'], nullSpawnTimes: 40 })
|
|
745
|
+
M.registerMathComputation(h.host)
|
|
746
|
+
push('probe unusable after retries (retryDiag present)', await h.call({ op: 'probe', engine: 'python' }))
|
|
747
|
+
}
|
|
748
|
+
{
|
|
749
|
+
const h = makeFakeHost({ installed: ['python3'], packageProbeNullTimes: 9 })
|
|
750
|
+
M.registerMathComputation(h.host)
|
|
751
|
+
push('run with un-runnable precheck', await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'x\n', packages: ['numpy'] }))
|
|
752
|
+
}
|
|
753
|
+
{
|
|
754
|
+
const h = makeFakeHost({ installed: [] })
|
|
755
|
+
M.registerMathComputation(h.host)
|
|
756
|
+
push('probe nothing', await h.call({ op: 'probe', engine: 'python' }))
|
|
757
|
+
}
|
|
758
|
+
{
|
|
759
|
+
const h = makeFakeHost({ resolveThrows: true })
|
|
760
|
+
M.registerMathComputation(h.host)
|
|
761
|
+
push('probe resolve throws', await h.call({ op: 'probe', engine: 'python' }))
|
|
762
|
+
}
|
|
763
|
+
for (const c of cases) {
|
|
764
|
+
let json = null
|
|
765
|
+
let err = null
|
|
766
|
+
try { json = JSON.stringify(c.r) } catch (e) { err = String((e && e.message) || e) }
|
|
767
|
+
ok(json !== null, '★ response is JSON-serialisable: ' + c.label + (err ? ' (' + err + ')' : ''))
|
|
768
|
+
}
|
|
769
|
+
// The diagnostics themselves must never carry a back-reference.
|
|
770
|
+
{
|
|
771
|
+
const h = makeFakeHost({ installed: ['python3'], nullSpawnTimes: 2 })
|
|
772
|
+
M.registerMathComputation(h.host)
|
|
773
|
+
const r = await h.call({ op: 'probe', engine: 'python' })
|
|
774
|
+
ok(r.ok === true, 'the null-spawn retry recovers (JSON invariant context)')
|
|
775
|
+
const h2 = makeFakeHost({ installed: ['python3'], versionProbeFails: true })
|
|
776
|
+
M.registerMathComputation(h2.host)
|
|
777
|
+
const u = await h2.call({ op: 'probe', engine: 'python' })
|
|
778
|
+
ok(!!u.probe && JSON.stringify(u.probe).indexOf('retryDiag') === -1 ? true : !!u.probe, 'a deterministic failure still exposes its probe diagnostics')
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ── 21. round-7 (live root cause): probes must not depend on the project root EXISTING
|
|
783
|
+
// The real host answers a spawn whose cwd does not exist with `spawned:true, exit:null` (retries
|
|
784
|
+
// cannot help). On a brand-new session the project root does not exist yet, so the version probe and
|
|
785
|
+
// the package precheck must run from a cwd that is guaranteed to exist.
|
|
786
|
+
{
|
|
787
|
+
const FRESH = '/fresh/nonexistent/workspace'
|
|
788
|
+
const h = makeFakeHost({ installed: ['python3', 'python'], projectRoot: FRESH, packages: { numpy: 'present' } })
|
|
789
|
+
M.registerMathComputation(h.host)
|
|
790
|
+
const p = await h.call({ op: 'probe', engine: 'python' })
|
|
791
|
+
ok(p.ok === true && p.engine === 'python' && p.engineInfo.version === '1.2.3', '★ a probe on a FRESH (non-existent) project root still succeeds with a real version')
|
|
792
|
+
const versionSpawns = h.spawns.filter((s) => /--version/.test(s.argv.join(' ')))
|
|
793
|
+
ok(versionSpawns.length > 0, 'the version probe actually spawned')
|
|
794
|
+
ok(versionSpawns.every((s) => String(s.cwd) !== FRESH), '★ no version probe spawn uses the non-existent project root as cwd')
|
|
795
|
+
ok(versionSpawns.every((s) => !s.nullSpawn), 'no version probe spawn reported exit:null (fresh-root signature)')
|
|
796
|
+
const r = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
|
|
797
|
+
const pkgSpawns = h.spawns.filter((s) => s.argv.join(' ').indexOf('numpy') !== -1)
|
|
798
|
+
ok(r.ok === true, 'a run with packages on a FRESH root passes the precheck')
|
|
799
|
+
ok(pkgSpawns.length > 0 && pkgSpawns.every((s) => String(s.cwd) !== FRESH), '★ the package precheck also avoids the non-existent project root')
|
|
800
|
+
ok(!(r.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_UNKNOWN'), 'the precheck RAN (no PACKAGE_PRECHECK_UNKNOWN on a fresh root)')
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log('')
|
|
804
|
+
console.log('=== MATH COMPUTATION SHARED: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
805
|
+
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
806
|
+
process.exit(failed === 0 ? 0 : 1)
|