dsh-vibe-math 2.7.1 → 2.7.3
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 +682 -690
- package/README.md +19 -27
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +744 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +35 -9
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +588 -588
- package/docs/math-computation.md +194 -163
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +49 -0
- package/docs/release-notes/README.md +48 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +72 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- package/docs/release-notes/TEMPLATE.md +126 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +157 -154
- package/installer.js +27 -17
- package/package.json +55 -3
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +305 -113
- package/prompt-corpus-v2/formal-verify-v2.md +2399 -279
- package/prompt-corpus-v3/formal-verify-v3.json +134 -110
- package/prompt-corpus-v3/formal-verify-v3.md +409 -141
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +513 -259
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +52 -4
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +107 -9
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +6 -4
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +14 -0
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +63 -22
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +50 -0
- package/tests/audit-prompt-invariants.mjs +43 -5
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +300 -13
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +15 -8
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +566 -12
- package/tests/formal-verify-v2.test.mjs +91 -5
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +85 -6
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +628 -9
- package/tests/formal-verify-v5.test.mjs +68 -9
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +396 -2
- package/tests/math-computation-v2.test.mjs +767 -0
- package/tests/math-computation-v3.test.mjs +759 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +13 -2
- package/tests/run-tests.mjs +139 -9
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +53 -1
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/v2-fix-probes.mutants.mjs +100 -0
- package/tests/v2-fix-probes.test.mjs +1039 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v3-fix-probes.mutants.mjs +95 -0
- package/tests/v3-fix-probes.test.mjs +891 -0
- package/tests/v5-institute-fixes.mutants.mjs +190 -0
- package/vibe-math-v2/agent.cordis.yml +62 -6
- package/vibe-math-v2/math-computation.js +229 -29
- package/vibe-math-v2/math-engines.js +12 -12
- package/vibe-math-v2/vibe-math-v2.js +458 -119
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +5 -1
- package/vibe-math-v3/agent.cordis.yml +72 -6
- package/vibe-math-v3/math-computation.js +229 -29
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/vibe-math-v3.js +482 -139
- package/vibe-math-v4/agent.cordis.yml +44 -8
- package/vibe-math-v4/math-computation.js +229 -29
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/vibe-math-v4.js +245 -52
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +2 -0
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +229 -29
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1307 -243
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1655
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// Usage: node tests/math-computation-shared.test.mjs
|
|
16
16
|
// ============================================================================================
|
|
17
17
|
import { makeFakeHost } from './helpers/math-computation-fake-seam.mjs'
|
|
18
|
-
import { pathToFileURL } from 'node:url'
|
|
18
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
19
20
|
|
|
20
21
|
// The sensitivity probe (tests/audit-math-computation-sensitivity.mjs) points this at a MUTATED copy
|
|
21
22
|
// of the module pair; the default is the shipped copy in vibe-math-v2/.
|
|
@@ -24,6 +25,97 @@ const MODULE = process.env.MATH_COMPUTATION_MODULE
|
|
|
24
25
|
: new URL('../vibe-math-v2/math-computation.js', import.meta.url).href
|
|
25
26
|
const M = await import(MODULE)
|
|
26
27
|
|
|
28
|
+
// §30 predicates — used by the §30 assertions AND by `--self-probe` case 3, so the shipped proof and the
|
|
29
|
+
// audit cannot drift ("same predicate, broken input"). Unit: descriptor entries carrying the reason.
|
|
30
|
+
const carriesVerifyProvenance = (v) => !!v && v.verify === true && typeof v.verifyReason === 'string' && v.verifyReason.length > 0
|
|
31
|
+
const hasNoVerifySlot = (v) => !!v && !('verify' in v) && !('verifyReason' in v)
|
|
32
|
+
|
|
33
|
+
// ── --self-probe (shipped): prove the §32 evidence assertion can REDDEN, from the tarball alone.
|
|
34
|
+
// It copies the module, removes ONE `fail()` whitelist line, and requires the named §32 assertion
|
|
35
|
+
// to fail in the child. Usage: node tests/math-computation-shared.test.mjs --self-probe
|
|
36
|
+
if (process.argv.includes('--self-probe')) {
|
|
37
|
+
const { mkdtempSync, copyFileSync, readFileSync: rf, writeFileSync: wf, rmSync } = await import('node:fs')
|
|
38
|
+
const { tmpdir } = await import('node:os')
|
|
39
|
+
const { join, dirname } = await import('node:path')
|
|
40
|
+
const { spawnSync } = await import('node:child_process')
|
|
41
|
+
const modPath = process.env.MATH_COMPUTATION_MODULE || fileURLToPath(new URL('../vibe-math-v2/math-computation.js', import.meta.url))
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe-'))
|
|
43
|
+
const enginesSrc = join(dirname(modPath), 'math-engines.js')
|
|
44
|
+
try { copyFileSync(enginesSrc, join(dir, 'math-engines.js')) } catch (e) { /* engines may be resolved elsewhere */ }
|
|
45
|
+
const src = rf(modPath, 'utf8')
|
|
46
|
+
const whitelist = ' if (extra && Array.isArray(extra.absent)) out.absent = extra.absent\n'
|
|
47
|
+
if (src.indexOf(whitelist) === -1) {
|
|
48
|
+
console.log('SELF-PROBE FAIL: the fail() whitelist line for absent[] was not found (the probe cannot mutate what it cannot see)')
|
|
49
|
+
rmSync(dir, { recursive: true, force: true })
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
wf(join(dir, 'math-computation.js'), src.replace(whitelist, ''))
|
|
53
|
+
const child = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir, 'math-computation.js') }) })
|
|
54
|
+
const out = String(child.stdout || '') + String(child.stderr || '')
|
|
55
|
+
const failLines = out.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
|
|
56
|
+
const named = failLines.some((l) => l.indexOf('S2 absence evidence: the failure carries an absent[] list') !== -1)
|
|
57
|
+
const sum = (out.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
|
|
58
|
+
console.log((child.status !== 0 && named ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': removing ONE fail() whitelist line reddens the named §32 assertion (' + sum + ')')
|
|
59
|
+
if (!named) for (const l of failLines.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
|
|
60
|
+
rmSync(dir, { recursive: true, force: true })
|
|
61
|
+
// case 2 (S4/S5): remove the single `out.installedSoFar = …` assignment and require the NAMED §33
|
|
62
|
+
// assertion in the child's FAIL line (a label found anywhere in the output is NOT a red).
|
|
63
|
+
const s45line = ' out.installedSoFar = results.filter((x) => x.exit === 0).map((x) => x.argv)\n'
|
|
64
|
+
const dir2 = mkdtempSync(join(tmpdir(), 'mc-shared-selfprobe2-'))
|
|
65
|
+
const src2 = rf(modPath, 'utf8')
|
|
66
|
+
let ok2 = false
|
|
67
|
+
if (src2.indexOf(s45line) === -1) {
|
|
68
|
+
console.log('SELF-PROBE FAIL: the installedSoFar assignment was not found (cannot mutate what it cannot see)')
|
|
69
|
+
} else {
|
|
70
|
+
try { copyFileSync(join(dirname(modPath), 'math-engines.js'), join(dir2, 'math-engines.js')) } catch (e) { /* optional */ }
|
|
71
|
+
wf(join(dir2, 'math-computation.js'), src2.replace(s45line, ''))
|
|
72
|
+
const child2 = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { encoding: 'utf8', maxBuffer: 1 << 26, env: Object.assign({}, process.env, { MATH_COMPUTATION_MODULE: join(dir2, 'math-computation.js') }) })
|
|
73
|
+
const out2 = String(child2.stdout || '') + String(child2.stderr || '')
|
|
74
|
+
const failLines2 = out2.split('\n').filter((l) => /^\s*FAIL\b/.test(l))
|
|
75
|
+
ok2 = child2.status !== 0 && failLines2.some((l) => l.indexOf('reports what already succeeded (installedSoFar)') !== -1)
|
|
76
|
+
const sum2 = (out2.split('\n').filter((l) => /passed, \d+ failed/.test(l)).slice(-1)[0] || '').trim()
|
|
77
|
+
console.log((ok2 ? 'SELF-PROBE PASS' : 'SELF-PROBE FAIL') + ': removing ONE installedSoFar assignment reddens the named §33 assertion (' + sum2 + ')')
|
|
78
|
+
if (!ok2) for (const l of failLines2.slice(0, 4)) console.log(' child FAIL line: ' + l.trim().slice(0, 140))
|
|
79
|
+
}
|
|
80
|
+
rmSync(dir2, { recursive: true, force: true })
|
|
81
|
+
// case 3 (§30, verify provenance): the SAME predicates the §30 assertions use, fed a broken helper.
|
|
82
|
+
// The real `verifyFields(engine)` helper is extracted from the module under test and run against a stub
|
|
83
|
+
// descriptor table; dropping its single reason line (or the no-empty-slot guard) must flip its predicate.
|
|
84
|
+
const src3 = rf(modPath, 'utf8')
|
|
85
|
+
const hStart = src3.indexOf('function verifyFields(engine) {')
|
|
86
|
+
let helper = ''
|
|
87
|
+
if (hStart !== -1) {
|
|
88
|
+
let d = 0, end = hStart
|
|
89
|
+
for (let i = hStart; i < src3.length; i++) { if (src3[i] === '{') d++; else if (src3[i] === '}') { d--; if (d === 0) { end = i + 1; break } } }
|
|
90
|
+
helper = src3.slice(hStart, end)
|
|
91
|
+
}
|
|
92
|
+
const REASON_LINE = " if (typeof d.verifyReason === 'string' && d.verifyReason) out.verifyReason = d.verifyReason\n"
|
|
93
|
+
const GUARD_LINE = ' if (!d.verify) return {}\n'
|
|
94
|
+
let ok3 = false
|
|
95
|
+
if (helper.indexOf(REASON_LINE) === -1 || helper.indexOf(GUARD_LINE) === -1) {
|
|
96
|
+
console.log('SELF-PROBE FAIL: the §30 helper anchors were not found (the probe cannot mutate what it cannot see)')
|
|
97
|
+
} else {
|
|
98
|
+
const stub = { maple: { verify: true, verifyReason: 'CLI spelling varies by Maple version - confirm on a licensed machine' }, python: { license: 'free' } }
|
|
99
|
+
const make = (s) => new Function('MATH_ENGINES', s + '\nreturn verifyFields;')(stub)
|
|
100
|
+
const good = make(helper)
|
|
101
|
+
const broken = make(helper.replace(REASON_LINE, ''))
|
|
102
|
+
const brokenGuard = make(helper.replace(GUARD_LINE, ''))
|
|
103
|
+
const cases3 = [
|
|
104
|
+
['§30 probe carries WHY (shared predicate carriesVerifyProvenance)', carriesVerifyProvenance(good('maple')), carriesVerifyProvenance(broken('maple'))],
|
|
105
|
+
['§30 no empty slot without a declared reason (hasNoVerifySlot)', hasNoVerifySlot(good('python')), hasNoVerifySlot(brokenGuard('python'))],
|
|
106
|
+
]
|
|
107
|
+
let bad3 = 0
|
|
108
|
+
for (const [name, greenNow, redWhenBroken] of cases3) {
|
|
109
|
+
const okc = greenNow === true && redWhenBroken === false
|
|
110
|
+
if (!okc) bad3++
|
|
111
|
+
console.log((okc ? ' ok ' : ' FAIL ') + name + ' :: green-now=' + greenNow + ' broken-goes-red=' + !redWhenBroken)
|
|
112
|
+
}
|
|
113
|
+
ok3 = bad3 === 0
|
|
114
|
+
console.log(' --- §30 verify-provenance predicates: ' + (cases3.length - bad3) + '/' + cases3.length + ' as required')
|
|
115
|
+
}
|
|
116
|
+
process.exit(child.status !== 0 && named && ok2 && ok3 ? 0 : 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
27
119
|
let passed = 0, failed = 0
|
|
28
120
|
const failures = []
|
|
29
121
|
function ok(cond, label, detail) {
|
|
@@ -116,7 +208,11 @@ console.log('-- math_computation shared contract --')
|
|
|
116
208
|
await withHost({ installed: [] }, async (state) => {
|
|
117
209
|
const r = await state.call({ op: 'probe' })
|
|
118
210
|
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no engine -> ENGINE_NOT_FOUND')
|
|
119
|
-
|
|
211
|
+
// round-9: the guide is per-OS and always carries the SUGGESTED command; `command` itself is only
|
|
212
|
+
// populated when that OS's package manager is actually resolvable on this machine.
|
|
213
|
+
ok(r.next && r.next.kind === 'user-install' && r.next.perOs && r.next.perOs.windows && r.next.perOs.linux, 'ENGINE_NOT_FOUND carries the per-OS user-install guide')
|
|
214
|
+
ok(r.next.suggestedCommand === r.next.perOs[r.next.platform], 'the guide keeps the suggested per-OS command alongside the executable one')
|
|
215
|
+
ok(r.next.packageManagerAvailable === false ? r.next.command === '' : typeof r.next.command === 'string', 'command ⇔ package-manager availability (never a command that cannot run)')
|
|
120
216
|
})
|
|
121
217
|
await withHost({ packages: { numpy: 'present', sympy: 'missing' } }, async (state) => {
|
|
122
218
|
const r = await state.call({ op: 'probe', packages: ['numpy', 'sympy'] })
|
|
@@ -800,7 +896,305 @@ console.log('-- math_computation shared contract --')
|
|
|
800
896
|
ok(!(r.warnings || []).some((w) => w.code === 'PACKAGE_PRECHECK_UNKNOWN'), 'the precheck RAN (no PACKAGE_PRECHECK_UNKNOWN on a fresh root)')
|
|
801
897
|
}
|
|
802
898
|
|
|
899
|
+
// ── 22. round-8 (P0/D3): member-facing artifact paths must be OPENABLE by members
|
|
900
|
+
// The receipt's scriptPath is PROJECT-ROOT relative, but members' file tools resolve against the
|
|
901
|
+
// SESSION CWD. The receipt/return therefore carry the absolute `scriptAbs` and the `cwd` to join with.
|
|
902
|
+
{
|
|
903
|
+
const h = makeFakeHost({ installed: ['python3'] })
|
|
904
|
+
M.registerMathComputation(h.host)
|
|
905
|
+
const r = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n' })
|
|
906
|
+
ok(r.ok === true, 'a run succeeds (P0 context)')
|
|
907
|
+
ok(typeof r.scriptAbs === 'string' && r.scriptAbs.length > 0, '★ the return carries an ABSOLUTE scriptAbs (members can open it directly)')
|
|
908
|
+
ok(typeof r.cwd === 'string' && r.cwd.length > 0, '★ the return carries cwd (the project root to join with)')
|
|
909
|
+
ok(String(r.scriptAbs).replace(/\\/g, '/').endsWith(String(r.scriptPath).replace(/\\/g, '/')), '★ scriptAbs ends with the receipt-relative scriptPath (join consistency)')
|
|
910
|
+
ok(!!h.files && h.files.has(String(r.scriptPath).replace(/\\/g, '/')), '★ the archived script really exists at scriptPath (the path handed to members is real)')
|
|
911
|
+
const rj = JSON.parse(h.files.get(String(r.receipt.json).replace(/\\/g, '/')))
|
|
912
|
+
ok(rj.scriptAbs === r.scriptAbs && rj.cwd === r.cwd, 'the RECEIPT carries scriptAbs + cwd too (a later reader can reconstruct the location)')
|
|
913
|
+
ok(String(M.MATH_ARCHIVE_WORKFLOW_LINE).indexOf('receipt.scriptAbs') !== -1 && String(M.MATH_ARCHIVE_WORKFLOW_LINE_EN).indexOf('receipt.scriptAbs') !== -1, 'the injected archive rule tells members HOW to open the script (zh+en)')
|
|
914
|
+
ok(/会话 cwd/.test(String(M.MATH_ARCHIVE_WORKFLOW_LINE)) && /SESSION CWD/.test(String(M.MATH_ARCHIVE_WORKFLOW_LINE_EN)), 'the rule states the session-cwd resolution fact (zh+en)')
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── 23. round-9 (real-engine): an engine INSTALLED but not on PATH must still be discovered
|
|
918
|
+
// R 4.6.1 went to `C:\Program Files\R\R-4.6.1\bin` without touching PATH, so `probe` used to say R
|
|
919
|
+
// was missing even though it was installed.
|
|
920
|
+
{
|
|
921
|
+
// round-B (F4): the candidate roots are INJECTED through the host seam, so this fixture cannot depend
|
|
922
|
+
// on the ambient machine's ProgramFiles/LOCALAPPDATA (which made it a false red on other layouts).
|
|
923
|
+
const RROOT = 'X:/installs/R'
|
|
924
|
+
const absTree = {
|
|
925
|
+
[RROOT]: [{ name: 'R-4.6.1', type: 'directory' }],
|
|
926
|
+
[RROOT + '/R-4.6.1/bin']: [{ name: 'Rscript.exe', type: 'file' }, { name: 'R.exe', type: 'file' }],
|
|
927
|
+
}
|
|
928
|
+
const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { r: [RROOT] } })
|
|
929
|
+
M.registerMathComputation(h.host)
|
|
930
|
+
const p = await h.call({ op: 'probe', engine: 'r' })
|
|
931
|
+
ok(p.ok === true && p.engine === 'r', '★ a Rscript that exists ONLY in the default install dir is discovered (not on PATH)')
|
|
932
|
+
ok(!!p.engineInfo && /installs\/R\/R-4\.6\.1\/bin\/Rscript/.test(String(p.engineInfo.path)), 'the discovered path is the versioned install dir (' + String(p.engineInfo && p.engineInfo.path).slice(0, 60) + ')')
|
|
933
|
+
}
|
|
934
|
+
// ── 23b. round-B (F3): the SAME walker covers Octave and Julia default dirs (both layout shapes)
|
|
935
|
+
{
|
|
936
|
+
const OROOT = 'X:/installs/GNU Octave'
|
|
937
|
+
const JROOT = 'X:/home/.juliaup/bin'
|
|
938
|
+
const absTree = {
|
|
939
|
+
[OROOT]: [{ name: 'Octave-9.2.0', type: 'directory' }],
|
|
940
|
+
[OROOT + '/Octave-9.2.0']: [{ name: 'mingw64', type: 'directory' }],
|
|
941
|
+
[OROOT + '/Octave-9.2.0/mingw64/bin']: [{ name: 'octave-cli.exe', type: 'file' }],
|
|
942
|
+
[JROOT]: [{ name: 'julia.exe', type: 'file' }],
|
|
943
|
+
}
|
|
944
|
+
const h = makeFakeHost({ installed: [], absTree: absTree, runtimeRoots: [], installRoots: { octave: [OROOT], julia: [JROOT] } })
|
|
945
|
+
M.registerMathComputation(h.host)
|
|
946
|
+
const po = await h.call({ op: 'probe', engine: 'octave' })
|
|
947
|
+
ok(po.ok === true && /Octave-9\.2\.0\/mingw64\/bin\/octave-cli/.test(String(po.engineInfo && po.engineInfo.path)), '★ an Octave installed in its default dir (no PATH) is discovered (versioned + mingw64/bin layout)')
|
|
948
|
+
const pj = await h.call({ op: 'probe', engine: 'julia' })
|
|
949
|
+
ok(pj.ok === true && /\.juliaup\/bin\/julia/.test(String(pj.engineInfo && pj.engineInfo.path)), '★ a juliaup-managed Julia (flat bin dir, no PATH) is discovered by the same walker')
|
|
950
|
+
const pr = await h.call({ op: 'probe' })
|
|
951
|
+
ok((pr.absent || []).some((a) => a.engine === 'r' && a.why === 'not-found-on-this-machine'), 'an engine with no injected roots and no PATH entry is still honestly listed as absent')
|
|
952
|
+
// F3: the per-engine default roots are part of the contract (the release notes claim "installed but not
|
|
953
|
+
// on PATH is found"), so their coverage is asserted directly - not only via an injected fixture.
|
|
954
|
+
for (const e of ['r', 'python', 'octave', 'julia', 'matlab']) {
|
|
955
|
+
ok(typeof M.mathInstallRoots === 'function' && M.mathInstallRoots(e, {}, true).length > 0 && M.mathInstallRoots(e, {}, false).length > 0,
|
|
956
|
+
'★ every engine with a well-defined default install dir declares per-OS roots: ' + e)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
// ── 24. round-9 (real-engine): never suggest an install command that cannot run on this machine
|
|
960
|
+
{
|
|
961
|
+
const h = makeFakeHost({ installed: [] }) // `winget` is NOT resolvable here
|
|
962
|
+
M.registerMathComputation(h.host)
|
|
963
|
+
const p = await h.call({ op: 'probe', engine: 'octave' })
|
|
964
|
+
ok(p.ok === false && p.code === 'MATH_ENGINE_NOT_FOUND', 'a missing engine still fails with ENGINE_NOT_FOUND')
|
|
965
|
+
ok(!!p.next && p.next.kind === 'user-install', 'the guidance is still user-install shaped')
|
|
966
|
+
ok(String(p.next.command) === '', '★ no runnable-looking command when its package manager is absent (command is empty)')
|
|
967
|
+
ok(!/winget|brew|apt|choco|scoop/i.test(String(p.next.command) + ' ' + String(p.message)), '★ no winget/brew/apt suggestion anywhere when that manager is absent')
|
|
968
|
+
ok(p.next.packageManager === 'winget' && p.next.packageManagerAvailable === false, 'the response SAYS which manager was assumed and that it is unavailable')
|
|
969
|
+
ok(typeof p.next.note === 'string' && p.next.note.length > 0, 'an explicit note explains that the suggested command cannot run')
|
|
970
|
+
const h2 = makeFakeHost({ installed: [], resolveMap: { winget: 'C:/fake/winget.exe' } })
|
|
971
|
+
M.registerMathComputation(h2.host)
|
|
972
|
+
const p2 = await h2.call({ op: 'probe', engine: 'octave' })
|
|
973
|
+
ok(/winget/i.test(String(p2.next.command)) && p2.next.packageManagerAvailable === true, 'when the manager IS present the command is offered as before')
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ── 25. round-9 F3/F5/F7: probe completeness, uniform failure evidence, explicit constraint policy
|
|
977
|
+
{
|
|
978
|
+
// F3: a partial availability line must NAME the configured-but-absent engines.
|
|
979
|
+
const h = makeFakeHost({ installed: ['python3'] })
|
|
980
|
+
M.registerMathComputation(h.host)
|
|
981
|
+
const p = await h.call({ op: 'probe', engine: 'python' })
|
|
982
|
+
ok(p.ok === true && Array.isArray(p.absent) && p.absent.length > 0, '★ probe names the configured-but-absent engines instead of being silently partial')
|
|
983
|
+
ok(p.absent.some((a) => a.engine === 'r' && a.why === 'not-found-on-this-machine'), 'the absent list carries a reason (key `why`) per engine')
|
|
984
|
+
ok(p.absent.some((a) => a.engine === 'cli' && /caller-supplied/.test(a.why)), 'cli is explained as needing a caller-supplied command')
|
|
985
|
+
ok(/未发现/.test(String(p.message)), 'the message states which configured engines were not found')
|
|
986
|
+
// F5: MATH_TIMEOUT carries the SAME evidence fields a non-zero exit does.
|
|
987
|
+
const h2 = makeFakeHost({ installed: ['python3'], hang: true })
|
|
988
|
+
M.registerMathComputation(h2.host)
|
|
989
|
+
const t = await h2.call({ op: 'run', engine: 'python', mode: 'code', code: 'while True: pass\n', timeoutMs: 1000 })
|
|
990
|
+
ok(t.ok === false && t.code === 'MATH_TIMEOUT', 'a hanging run reports MATH_TIMEOUT')
|
|
991
|
+
ok(t.timedOut === true && 'exit' in t && t.exit === null, '★ MATH_TIMEOUT carries timedOut:true and an explicit exit field (uniform with NONZERO_EXIT)')
|
|
992
|
+
ok('stderr' in t && typeof t.stderr === 'string', '★ MATH_TIMEOUT carries the captured stderr like NONZERO_EXIT does')
|
|
993
|
+
// F7: the response says the constraints are existence-only, and lists the ones that were NOT checked.
|
|
994
|
+
const h3 = makeFakeHost({ installed: ['python3'], packages: { numpy: 'present' } })
|
|
995
|
+
M.registerMathComputation(h3.host)
|
|
996
|
+
const c = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy>=1'] })
|
|
997
|
+
ok(c.ok === true && /existence-only/.test(String(c.versionPolicy)), '★ the run response states the version constraints are existence-only')
|
|
998
|
+
ok(Array.isArray(c.constraintsNotEnforced) && c.constraintsNotEnforced.indexOf('numpy>=1') !== -1, '★ the response names the constraint it did NOT check (no silent drop)')
|
|
999
|
+
const c2 = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['numpy'] })
|
|
1000
|
+
ok(Array.isArray(c2.constraintsNotEnforced) && c2.constraintsNotEnforced.length === 0, 'an unconstrained request reports an empty not-enforced list')
|
|
1001
|
+
const m = await h3.call({ op: 'run', engine: 'python', mode: 'code', code: 'print(1)\n', packages: ['pandas>=2'] })
|
|
1002
|
+
ok(m.ok === false && /existence-only/.test(String(m.versionPolicy)), 'the missing-package failure also states the constraint policy')
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ── 26. round-9 (N1): edited-archived-script evidence integrity
|
|
1006
|
+
// The documented P2a flow: edit the ORIGINAL source, re-run mode:'file' ⇒ SAME archive id, attempt>=2,
|
|
1007
|
+
// scriptChanged:true, previousReceipt at the prior attempt, prior attempt untouched. Pointing mode:'file'
|
|
1008
|
+
// at an ARCHIVED script is a NEW archive id by design (the id is keyed by the SOURCE PATH) - which must
|
|
1009
|
+
// be SAID (fileIsArchivedScript + a warning), not look like broken change detection.
|
|
1010
|
+
{
|
|
1011
|
+
const h = makeFakeHost({ installed: ['python3'], files: { 'Problems/x.py': 'print(1)\n' } })
|
|
1012
|
+
M.registerMathComputation(h.host)
|
|
1013
|
+
const r1 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
|
|
1014
|
+
ok(r1.ok === true && r1.attempt === 1 && r1.scriptChanged === false, 'first run of a source file: attempt 1, no change')
|
|
1015
|
+
const rj1 = JSON.parse(h.files.get(String(r1.receipt.json).replace(/\\/g, '/')))
|
|
1016
|
+
ok(typeof r1.runId === 'string' && rj1.runId === r1.runId, '★ the run response exposes its archive id (matches the durable receipt.runId) - needed to check "same archive id"')
|
|
1017
|
+
// round-B (F6): the DURABLE receipt must record the constraint policy too (not just the response).
|
|
1018
|
+
ok(typeof rj1.versionPolicy === 'string' && /existence-only/.test(rj1.versionPolicy) && Array.isArray(rj1.constraintsNotEnforced),
|
|
1019
|
+
'★ the durable receipt records versionPolicy + constraintsNotEnforced (the promise is auditable evidence)')
|
|
1020
|
+
h.files.set('Problems/x.py', 'print(2)\n')
|
|
1021
|
+
const r2 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/x.py' })
|
|
1022
|
+
ok(r2.ok === true && r2.runId === r1.runId, '★ editing the ORIGINAL source and re-running lands on the SAME archive id')
|
|
1023
|
+
ok(r2.attempt >= 2 && /\/attempts\/\d+/.test(String(r2.attemptDir || '')), '★ the re-run is attempt>=2 under attempts/<n>/')
|
|
1024
|
+
ok(r2.scriptChanged === true, '★ the re-run reports scriptChanged:true')
|
|
1025
|
+
ok(!!r2.previousReceipt && r2.previousReceipt.attempt === r1.attempt && r2.previousReceipt.scriptHash === r1.scriptHash, '★ previousReceipt points at the prior attempt (attempt + scriptHash)')
|
|
1026
|
+
ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', '★ the prior attempt\'s script is NOT overwritten (append-only)')
|
|
1027
|
+
// A DIFFERENT source path (e.g. a copy of the old script, or a brand-new file) is a NEW archive id BY
|
|
1028
|
+
// DESIGN: the id is keyed by the source path, so there is no history for that id and
|
|
1029
|
+
// scriptChanged/previousReceipt are - correctly - empty. This is the case a finder can mistake for
|
|
1030
|
+
// "broken change detection"; it is not, and the docs now say so.
|
|
1031
|
+
h.files.set('Problems/copy_of_old.py', 'print(1)\n')
|
|
1032
|
+
const r3 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: 'Problems/copy_of_old.py' })
|
|
1033
|
+
ok(r3.ok === true, 'a run of a different source path succeeds (' + JSON.stringify({ ok: r3.ok, code: r3.code }) + ')')
|
|
1034
|
+
ok(r3.runId !== r1.runId, '★ a DIFFERENT source path gets a DIFFERENT archive id')
|
|
1035
|
+
ok(r3.attempt === 1, '★ …and therefore starts at attempt 1 (fresh archive, by design)')
|
|
1036
|
+
ok(r3.scriptChanged === false, '★ …and reports scriptChanged:false (no history FOR THIS id)')
|
|
1037
|
+
ok(r3.previousReceipt === null || r3.previousReceipt === undefined, '★ …and has no previousReceipt (expected, not broken change detection)')
|
|
1038
|
+
ok(h.files.get(String(r1.scriptPath).replace(/\\/g, '/')) === 'print(1)\n', 'the earlier attempt is still untouched after that run')
|
|
1039
|
+
// Review round A (F1/F2): the ARCHIVED copy is REACHABLE (projectRel() is purely lexical - it rejects
|
|
1040
|
+
// absolute paths and escaping `..` only). The shipped guidance must therefore say the truth: pointing
|
|
1041
|
+
// mode:'file' at the archived copy yields a NEW archive (new id, attempt 1, no history), flagged by
|
|
1042
|
+
// fileIsArchivedScript + ARCHIVED_SCRIPT_RERUN - it is NOT "a new attempt of the same archive".
|
|
1043
|
+
const archivedPath = String(r1.scriptPath).replace(/\\/g, '/')
|
|
1044
|
+
const r4 = await h.call({ op: 'run', engine: 'python', mode: 'file', file: archivedPath })
|
|
1045
|
+
ok(r4.ok === true, '★ pointing mode:file at the ARCHIVED copy succeeds (the guard is lexical, it does not refuse Computation/)')
|
|
1046
|
+
ok(r4.fileIsArchivedScript === true, '★ the response flags fileIsArchivedScript for an archived source')
|
|
1047
|
+
ok((r4.warnings || []).some((w) => w.code === 'ARCHIVED_SCRIPT_RERUN'), '★ the response warns ARCHIVED_SCRIPT_RERUN (so it cannot read as broken change detection)')
|
|
1048
|
+
ok(r4.runId !== r1.runId && r4.attempt === 1 && r4.scriptChanged === false && (r4.previousReceipt === null || r4.previousReceipt === undefined),
|
|
1049
|
+
'★ …and it really is a NEW archive (new id, attempt 1, no history) - NOT a new attempt of the same archive')
|
|
1050
|
+
ok(h.files.get(archivedPath) === 'print(1)\n', 'the archived original is still byte-identical after that run')
|
|
1051
|
+
}
|
|
1052
|
+
// ── 27. round-9 (N2): a TIMEOUT carries its partial output in BOTH the response and the receipt
|
|
1053
|
+
{
|
|
1054
|
+
const h = makeFakeHost({ installed: ['python3'], hang: true, stdoutBytes: 16 })
|
|
1055
|
+
M.registerMathComputation(h.host)
|
|
1056
|
+
const t = await h.call({ op: 'run', engine: 'python', mode: 'code', code: 'print("before-timeout")\n', timeoutMs: 1000 })
|
|
1057
|
+
ok(t.ok === false && t.code === 'MATH_TIMEOUT', 'the hanging run reports MATH_TIMEOUT')
|
|
1058
|
+
ok('stdout' in t && 'stderr' in t, '★ the TIMEOUT response carries stdout AND stderr (not just the archived file)')
|
|
1059
|
+
const rj = JSON.parse(h.files.get(String(t.receipt.json).replace(/\\/g, '/')))
|
|
1060
|
+
ok(typeof rj.partialStdout === 'string' && typeof rj.partialStderr === 'string', '★ the TIMEOUT receipt JSON carries the capped partial output (as strings)')
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// ── 28. descriptor sweep: NO spawn may ever carry an unsubstituted argv placeholder
|
|
1064
|
+
// Sweep finding (HIGH, fixed): matlab's scriptArgv EMBEDS the placeholder (`run('<script>')`) while the
|
|
1065
|
+
// module substituted by EXACT element equality, so the literal `run('<script>')` reached the engine and
|
|
1066
|
+
// the archived script never ran. This section walks the whole descriptor table.
|
|
1067
|
+
{
|
|
1068
|
+
const PLACEHOLDER = /<script>|<expr>|<pkgs>|<pkgs\.\.\.>|<probeCode>|<exe>|<pkg>|<cliArgv/
|
|
1069
|
+
for (const name of (M.MATH_PARAM_DEFAULTS.mathEngines || [])) {
|
|
1070
|
+
if (name === 'cli') continue
|
|
1071
|
+
const h = makeFakeHost({ installed: [name], files: {}, licence: true })
|
|
1072
|
+
M.registerMathComputation(h.host)
|
|
1073
|
+
const r = await h.call({ op: 'run', engine: name, mode: 'code', code: 'x=1\n' })
|
|
1074
|
+
const bad = (h.spawns || []).map((s) => (s && s.argv) || []).flat().filter((a) => typeof a === 'string' && PLACEHOLDER.test(a))
|
|
1075
|
+
ok(bad.length === 0, '★ ' + name + ': no spawn carries a literal argv placeholder', JSON.stringify(bad.slice(0, 2)))
|
|
1076
|
+
if (name === 'matlab' && r.ok === true) {
|
|
1077
|
+
const runs = (h.spawns || []).map((s) => s.argv).filter((a) => a.join(' ').indexOf('-batch') !== -1 && a.join(' ').indexOf('disp(version)') === -1)
|
|
1078
|
+
const run = runs.pop() || []
|
|
1079
|
+
ok(run.some((a) => /run\('.*script\.m'\)/.test(a)), '★ matlab: the EMBEDDED placeholder is substituted (run(\'<abs script.m>\'))', JSON.stringify(run))
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ── 29. cli policy is SINGLE-SOURCED (descriptor), not hard-coded in the module
|
|
1085
|
+
// Sweep ruling: `cli.policy.requiresMathMode` / `requiresEngineInList` were declared but never consumed
|
|
1086
|
+
// (the same rule was hard-coded twice), so descriptor and behaviour could drift. The refusals now carry
|
|
1087
|
+
// the DESCRIPTOR value, which is exactly what makes a hard-coded revert observable.
|
|
1088
|
+
{
|
|
1089
|
+
const h = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed' }, cliCommands: ['python3'] })
|
|
1090
|
+
M.registerMathComputation(h.host)
|
|
1091
|
+
const r = await h.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
|
|
1092
|
+
ok(r.ok === false && r.code === 'MATH_REFUSED', 'cli under mathMode=typed is refused')
|
|
1093
|
+
ok(r.next && r.next.reason === 'policy' && r.next.requiresMathMode === 'typed+shell',
|
|
1094
|
+
'★ the refusal carries the DESCRIPTOR policy value (next.requiresMathMode === typed+shell)', JSON.stringify(r.next))
|
|
1095
|
+
const h2 = makeFakeHost({ installed: ['python3'], params: { mathMode: 'typed+shell', mathEngines: ['python', 'r'] }, cliCommands: ['python3'] })
|
|
1096
|
+
M.registerMathComputation(h2.host)
|
|
1097
|
+
const r2 = await h2.call({ op: 'run', engine: 'cli', mode: 'code', code: 'print(1)\n', cli: { command: 'python3' } })
|
|
1098
|
+
ok(r2.ok === false && r2.next && r2.next.reason === 'engine-not-allowed' && r2.next.requiresEngineInList === true,
|
|
1099
|
+
'★ when cli is not in mathEngines the refusal surfaces requiresEngineInList from the descriptor', JSON.stringify(r2.next))
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// ── 30. a commercial template's VERIFY provenance REACHES the user (item 2)
|
|
1103
|
+
// `maple.verify`/`verifyReason` were declared but never reached any response, so a user told "commercial
|
|
1104
|
+
// template needs confirmation" could not see WHY. It is surfaced on the probe result (per-engine entry +
|
|
1105
|
+
// engineInfo) and on the vendor-guidance payloads; engines without a declared reason print no empty slot.
|
|
1106
|
+
{
|
|
1107
|
+
const h = makeFakeHost({ installed: ['maple'], licence: true, files: {} })
|
|
1108
|
+
M.registerMathComputation(h.host)
|
|
1109
|
+
const p = await h.call({ op: 'probe', engine: 'maple' })
|
|
1110
|
+
ok(p.ok === true && !!p.engineInfo && p.engineInfo.verify === true, 'probe reports the verify flag for a commercial template')
|
|
1111
|
+
ok(!!p.engineInfo && typeof p.engineInfo.verifyReason === 'string' && p.engineInfo.verifyReason.length > 0 && /version/i.test(p.engineInfo.verifyReason),
|
|
1112
|
+
'★ probe carries WHY the template needs confirmation (engineInfo.verifyReason)', JSON.stringify(p.engineInfo && p.engineInfo.verifyReason))
|
|
1113
|
+
const entry = (p.engines || []).find((e) => e.name === 'maple')
|
|
1114
|
+
ok(carriesVerifyProvenance(entry), 'the per-engine probe entry carries the same provenance')
|
|
1115
|
+
const hp = makeFakeHost({ installed: ['python3'], files: {} })
|
|
1116
|
+
M.registerMathComputation(hp.host)
|
|
1117
|
+
const pp = await hp.call({ op: 'probe', engine: 'python' })
|
|
1118
|
+
ok(hasNoVerifySlot(pp.engineInfo),
|
|
1119
|
+
'★ an engine without a declared reason prints NO empty slot (assemble-only-when-present)')
|
|
1120
|
+
const hm = makeFakeHost({ installed: [], files: {} })
|
|
1121
|
+
M.registerMathComputation(hm.host)
|
|
1122
|
+
const pm = await hm.call({ op: 'probe', engine: 'maple' })
|
|
1123
|
+
ok(pm.ok === false && !!pm.next && typeof pm.next.verifyReason === 'string' && /version/i.test(pm.next.verifyReason),
|
|
1124
|
+
'★ the not-found guidance also says why the commercial template needs confirmation', JSON.stringify(pm.next && pm.next.verifyReason))
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ── 31. S3 symmetry: EVERY failure kind surfaces partial stdout AND stderr (like the timeout)
|
|
1128
|
+
{
|
|
1129
|
+
const h1 = makeFakeHost({ installed: ['python3'], exit: 2, stdoutBytes: 16 })
|
|
1130
|
+
M.registerMathComputation(h1.host)
|
|
1131
|
+
const r1 = await h1.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
|
|
1132
|
+
ok(r1.ok === false && r1.code === 'MATH_NONZERO_EXIT', 'a non-zero exit reports MATH_NONZERO_EXIT')
|
|
1133
|
+
ok('stdout' in r1 && 'stderr' in r1, '★ S3 symmetry: MATH_NONZERO_EXIT carries BOTH partial stdout and stderr', JSON.stringify({ so: 'stdout' in r1, se: 'stderr' in r1 }))
|
|
1134
|
+
const h2 = makeFakeHost({ installed: ['python3'], argError: 'unknown option', stdoutBytes: 16 })
|
|
1135
|
+
M.registerMathComputation(h2.host)
|
|
1136
|
+
const r2 = await h2.call({ op: 'run', engine: 'python', mode: 'code', code: 'x=1\n' })
|
|
1137
|
+
ok(r2.ok === false && r2.code === 'MATH_ENGINE_BAD_ARGV', 'a usage error reports MATH_ENGINE_BAD_ARGV')
|
|
1138
|
+
ok('stdout' in r2 && 'stderr' in r2, '★ S3 symmetry: MATH_ENGINE_BAD_ARGV carries BOTH partial stdout and stderr')
|
|
1139
|
+
}
|
|
1140
|
+
// ── 32. S2: the failure a user actually hits carries the SAME absence evidence as the success shape
|
|
1141
|
+
{
|
|
1142
|
+
const h = makeFakeHost({ installed: [] })
|
|
1143
|
+
M.registerMathComputation(h.host)
|
|
1144
|
+
const r = await h.call({ op: 'probe' })
|
|
1145
|
+
ok(r.ok === false && r.code === 'MATH_ENGINE_NOT_FOUND', 'no resolvable engine reports MATH_ENGINE_NOT_FOUND')
|
|
1146
|
+
ok(Array.isArray(r.configured) && r.configured.length > 0, '★ S2 absence evidence: the failure names the CONFIGURED engines', JSON.stringify(r.configured ?? null))
|
|
1147
|
+
const absent = Array.isArray(r.absent) ? r.absent : []
|
|
1148
|
+
ok(absent.length > 0, '★ S2 absence evidence: the failure carries an absent[] list (not a silent partial)', JSON.stringify(r.absent ?? null))
|
|
1149
|
+
ok(absent.every((a) => a && typeof a.why === 'string' && a.why.length > 0), 'every absent[] entry carries a why', JSON.stringify(absent))
|
|
1150
|
+
}
|
|
803
1151
|
console.log('')
|
|
1152
|
+
// ── 33. S4/S5: install failures are discriminated WITHOUT a new code (frozen interface). Reads the
|
|
1153
|
+
// ENV-RESOLVED module (MATH_COMPUTATION_MODULE), so the --self-probe case 2 above can reach exactly
|
|
1154
|
+
// what these assertions read (the parity-located version could not be reddened by any mutant).
|
|
1155
|
+
{
|
|
1156
|
+
const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
|
|
1157
|
+
ok(src.indexOf("out.op = 'install'") !== -1, '★ S4/S5 install discrimination: a failed install sets op=install')
|
|
1158
|
+
ok(/out\.timedOut = !!/.test(src), '★ S4/S5 install discrimination: a failed install carries timedOut')
|
|
1159
|
+
ok(/out\.installedSoFar = results\.filter/.test(src), '★ S4/S5 install discrimination: a failed install reports what already succeeded (installedSoFar)')
|
|
1160
|
+
ok(/installedSoFar: commands\.map/.test(src), 'the success path reports installedSoFar too (no special-casing by the caller)')
|
|
1161
|
+
ok(src.indexOf('MATH_INSTALL_FAILED') === -1, 'no 12th failure code was added (MATH_FAILURE_CODES stays frozen)')
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ── 34. S6: the install plan, the run path and the missing-package failure state ONE policy the
|
|
1165
|
+
// same way (behavioural: read from the responses, not from the source text).
|
|
1166
|
+
{
|
|
1167
|
+
const PREFIX = 'version-constraints-are-existence-only: '
|
|
1168
|
+
const h = makeFakeHost({ installed: ['python3'] })
|
|
1169
|
+
M.registerMathComputation(h.host)
|
|
1170
|
+
const plan = await h.call({ op: 'install', packages: ['numpy==1.2.3'] })
|
|
1171
|
+
const planPolicy = String((plan && plan.plan && plan.plan.versionPolicy) || '')
|
|
1172
|
+
ok(plan.ok === true && !!plan.plan, 'S6: an install dry-run returns a plan', JSON.stringify(plan && plan.code))
|
|
1173
|
+
ok(planPolicy.indexOf(PREFIX) === 0, '★ S6: the install plan uses the SAME policy wording as the run path', planPolicy.slice(0, 70))
|
|
1174
|
+
ok(planPolicy.indexOf('numpy==1.2.3') !== -1, '★ S6: the plan names the pinned spec it does not enforce', planPolicy.slice(0, 90))
|
|
1175
|
+
const miss = await h.call({ op: 'run', engine: 'python', mode: 'code', packages: ['numpy==1.2.3'], code: 'x=1\n' })
|
|
1176
|
+
ok(miss.ok === false && miss.code === 'MATH_MISSING_PACKAGES', 'S6: a pinned-but-missing package fails as MATH_MISSING_PACKAGES', JSON.stringify(miss.code))
|
|
1177
|
+
ok(String(miss.versionPolicy || '').indexOf(PREFIX) === 0, '★ S6: the failure path uses the same wording', String(miss.versionPolicy || '').slice(0, 70))
|
|
1178
|
+
ok(String(miss.versionPolicy || '') === planPolicy, '★ S6: plan and failure wording are byte-identical for the same spec', JSON.stringify([planPolicy.slice(0, 40), String(miss.versionPolicy || '').slice(0, 40)]))
|
|
1179
|
+
ok(String(miss.message || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the MATH_MISSING_PACKAGES detail derives the same clause', String(miss.message || '').slice(0, 70))
|
|
1180
|
+
const badSpec = M.parsePackageSpec('numpy@1.2')
|
|
1181
|
+
ok(badSpec && badSpec.ok === false && String(badSpec.problem || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the unsupported-version-syntax message derives the same clause', String((badSpec && badSpec.problem) || '').slice(0, 80))
|
|
1182
|
+
ok(String(planPolicy || '').indexOf('只检查是否存在;版本求解交给包管理器') !== -1, '★ S6: the policy FIELD itself contains that clause (one literal, three surfaces)', planPolicy.slice(0, 80))
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ── 35. S6 derivation is LOAD-BEARING (found by mutant case 2): "the message contains the clause" also
|
|
1186
|
+
// passes when the message RE-INLINES the same words, so the real checks are (a) the clause exists as
|
|
1187
|
+
// exactly ONE literal in the module and (b) the two human messages are built from the constant.
|
|
1188
|
+
{
|
|
1189
|
+
const src = readFileSync(fileURLToPath(new URL(MODULE)), 'utf8')
|
|
1190
|
+
const clause = '只检查是否存在;版本求解交给包管理器'
|
|
1191
|
+
const occurrences = src.split(clause).length - 1
|
|
1192
|
+
ok(occurrences === 1, '★ S6: the policy clause exists as exactly ONE literal in the module (a re-inlined message is caught)', 'occurrences=' + occurrences)
|
|
1193
|
+
ok(/MATH_MISSING_PACKAGES[\s\S]{0,160}MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the missing-package detail is built from the clause constant', 'MATH_MISSING_PACKAGES … MATH_VERSION_POLICY_CLAUSE')
|
|
1194
|
+
ok(/unsupported version syntax[\s\S]{0,160}MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the unsupported-version-syntax message is built from the clause constant')
|
|
1195
|
+
ok(/const MATH_VERSION_POLICY = .*MATH_VERSION_POLICY_CLAUSE/.test(src), '★ S6: the long policy sentence is derived from the clause')
|
|
1196
|
+
}
|
|
1197
|
+
|
|
804
1198
|
console.log('=== MATH COMPUTATION SHARED: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
805
1199
|
if (failures.length) for (const f of failures) console.error(' - ' + f)
|
|
806
1200
|
process.exit(failed === 0 ? 0 : 1)
|