dsh-vibe-math 2.7.3 → 2.8.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/LICENSE +21 -21
- package/README.en.md +682 -682
- package/README.md +683 -683
- package/docs/AUDIT-CHECKLIST.md +758 -744
- package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -194
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
- package/docs/release-playbook.md +111 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/test-timing.md +158 -157
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +792 -792
- package/package.json +18 -3
- package/prompt-corpus-v2/formal-verify-v2.json +2 -2
- package/prompt-corpus-v2/formal-verify-v2.md +4 -0
- package/prompt-corpus-v3/formal-verify-v3.json +7 -7
- package/prompt-corpus-v3/formal-verify-v3.md +14 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
- package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/release-check.mjs +181 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-compat.test.mjs +256 -256
- package/tests/audit-math-computation-sensitivity.mjs +1 -1
- package/tests/audit-persona-sensitivity.mjs +323 -323
- package/tests/audit-persona-surface.test.mjs +431 -431
- package/tests/audit-prompt-duplication.mutants.mjs +3 -2
- package/tests/audit-prompt-invariants.mjs +589 -546
- package/tests/audit-registration.mjs +78 -78
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -172
- package/tests/audit-v5-integrity.mjs +795 -795
- package/tests/audit-v5-sensitivity.mjs +433 -432
- package/tests/e2e-v5-round2.test.mjs +2453 -2426
- package/tests/formal-verify-v2.test.mjs +1806 -1800
- package/tests/formal-verify-v3.test.mjs +1757 -1750
- package/tests/formal-verify-v4.test.mjs +2114 -2106
- package/tests/formal-verify-v5.test.mjs +774 -767
- package/tests/math-computation-discovery.mutants.mjs +3 -3
- package/tests/math-computation-shared.test.mjs +122 -5
- package/tests/math-computation-v2.test.mjs +14 -2
- package/tests/math-computation-v3.test.mjs +14 -2
- package/tests/math-computation-v4.test.mjs +739 -739
- package/tests/prompt-v5-integrity.test.mjs +1291 -1291
- package/tests/release-check.mutants.mjs +78 -0
- package/tests/run-tests.mjs +393 -332
- package/tests/selfdrive-v5.mjs +560 -560
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +4 -0
- package/tests/v2-fix-probes.test.mjs +1034 -1033
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -95
- package/tests/v3-fix-probes.test.mjs +891 -890
- package/tests/v4-final-paper.mutants.mjs +116 -0
- package/tests/v4-final-paper.test.mjs +628 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -190
- package/vibe-math-v2/agent.cordis.yml +376 -376
- package/vibe-math-v2/math-computation.js +17 -15
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5078 -5011
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
- package/vibe-math-v3/agent.cordis.yml +420 -420
- package/vibe-math-v3/math-computation.js +17 -15
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6215 -6149
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -438
- package/vibe-math-v4/math-computation.js +17 -15
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4520 -4422
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
- package/vibe-math-v5/math-computation.js +17 -15
- package/vibe-math-v5/vibe-math-v5.js +93 -11
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
// ============================================================================================
|
|
2
|
-
// INSTALLER HOST-COMPATIBILITY AUDIT
|
|
3
|
-
//
|
|
4
|
-
// The installer's host self-check warns when the running DSH is outside the range this package
|
|
5
|
-
// declares. That verdict must come from the SAME declaration dsh-market renders on the plugin card
|
|
6
|
-
// (`engines.dsh` / `dsh.engines.dsh`) and not from a second source, or the two disagree: the card
|
|
7
|
-
// says "compatible" while the log warns, or the reverse. This suite pins that:
|
|
8
|
-
//
|
|
9
|
-
// · the range matcher reproduces semver 7.8.5 exactly — the expectations below were generated with
|
|
10
|
-
// npm's own bundled semver (`require('…/npm/node_modules/semver')`) and re-checked against the
|
|
11
|
-
// shipped matcher over 320 (range, version, includePrerelease) pairs, 0 mismatches;
|
|
12
|
-
// · `dshVersionVerdict` prefers the declared range and falls back to `dsh.compatibility`;
|
|
13
|
-
// · every release our own compatibility matrix calls `compatible` is NOT judged incompatible by
|
|
14
|
-
// the declared range — the two sources cannot drift apart.
|
|
15
|
-
//
|
|
16
|
-
// Run: node tests/audit-installer-compat.test.mjs (part of `node tests/run-tests.mjs`)
|
|
17
|
-
// ============================================================================================
|
|
18
|
-
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from 'node:fs'
|
|
19
|
-
import { tmpdir } from 'node:os'
|
|
20
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
|
-
import { join, resolve } from 'node:path'
|
|
22
|
-
import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
|
|
23
|
-
|
|
24
|
-
const HERE = fileURLToPath(new URL('../', import.meta.url))
|
|
25
|
-
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
26
|
-
// §4 drives the real apply(); INSTALLER_JS lets the sensitivity probe run it against a reverted copy
|
|
27
|
-
const INSTALLER_SRC = process.env.INSTALLER_JS ? resolve(process.env.INSTALLER_JS) : join(HERE, 'installer.js')
|
|
28
|
-
|
|
29
|
-
let passed = 0, failed = 0
|
|
30
|
-
const failures = []
|
|
31
|
-
const ok = (cond, label, detail) => {
|
|
32
|
-
if (cond) { passed++; console.log(' ok ' + label); return true }
|
|
33
|
-
failed++; failures.push(label + (detail ? ' — ' + detail : ''))
|
|
34
|
-
console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
|
|
35
|
-
return false
|
|
36
|
-
}
|
|
37
|
-
const eq = (actual, expected, label) => ok(actual === expected, label, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected))
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------------------------
|
|
40
|
-
// 1. the matcher reproduces semver 7.8.5 (expectations generated with npm's bundled semver)
|
|
41
|
-
// ---------------------------------------------------------------------------------------------
|
|
42
|
-
const CASES = [
|
|
43
|
-
// [range, version, includePrerelease, expected]
|
|
44
|
-
['*', '1.2.3', false, true],
|
|
45
|
-
['*', '0.1.5-rc.2', false, false], // npm: `*` excludes prereleases unless opted in
|
|
46
|
-
['*', '0.1.5-rc.2', true, true],
|
|
47
|
-
['1.2.3', '1.2.3', false, true],
|
|
48
|
-
['1.2.3', '1.2.4', false, false],
|
|
49
|
-
['^0.1.0', '0.1.4', false, true],
|
|
50
|
-
['^0.1.0', '0.2.0-rc.1', true, false], // 0.x caret never reaches the next minor
|
|
51
|
-
['^0.1.0', '0.1.0-rc.5', true, false], // ...nor a prerelease below its own floor
|
|
52
|
-
['^0.1.0-rc.7', '0.1.0-rc.7', false, true],
|
|
53
|
-
['^0.1.0-rc.7', '0.1.5-rc.2', true, true],
|
|
54
|
-
['^0.0.1', '0.0.1', false, true],
|
|
55
|
-
['^0.0.1', '0.0.2', false, false],
|
|
56
|
-
['~0.1.2', '0.1.4', false, true],
|
|
57
|
-
['~0.1.2', '0.2.0', false, false],
|
|
58
|
-
['>0.1.0 <=0.1.5', '0.1.5', false, true],
|
|
59
|
-
['>=0.1.5-rc.1', '0.1.5-rc.2', false, true],
|
|
60
|
-
['>=0.1.5-rc.1', '0.2.0', false, true], // a floor-only range keeps admitting newer hosts
|
|
61
|
-
// the trap contributing.md warns about: no comparator on the 0.1.5 tuple carrying a prerelease
|
|
62
|
-
['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', false, false],
|
|
63
|
-
['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', true, true],
|
|
64
|
-
// the market-style chain
|
|
65
|
-
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.2-rc.1', false, true],
|
|
66
|
-
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', false, false],
|
|
67
|
-
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', true, true],
|
|
68
|
-
]
|
|
69
|
-
for (const [range, version, pre, expected] of CASES) {
|
|
70
|
-
eq(satisfiesDshRange(version, range, { includePrerelease: pre }), expected,
|
|
71
|
-
'matcher: ' + JSON.stringify(range) + ' vs ' + version + (pre ? ' (includePrerelease)' : ''))
|
|
72
|
-
}
|
|
73
|
-
// our own declaration, both semantics (the exact chain the market and the installer share)
|
|
74
|
-
// The window is per-tuple: every supported line has its own prerelease branch, because npm's
|
|
75
|
-
// prerelease rule only admits a version when a comparator in the SAME [major,minor,patch] tuple also
|
|
76
|
-
// carries a prerelease. 0.2.0 joined the window with the DSH 0.2.0 adaptation.
|
|
77
|
-
for (const v of ['0.1.2-alpha.4', '0.1.2-rc.1', '0.1.3-alpha.2', '0.1.5-rc.1', '0.1.5-rc.2', '0.1.6-alpha.2', '0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2']) {
|
|
78
|
-
eq(satisfiesDshRange(v, pkg.engines.dsh), true, 'own range admits ' + v + ' (default semantics)')
|
|
79
|
-
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits ' + v + ' (includePrerelease)')
|
|
80
|
-
}
|
|
81
|
-
for (const v of ['0.2.0']) {
|
|
82
|
-
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits the released ' + v)
|
|
83
|
-
}
|
|
84
|
-
for (const v of ['0.1.1-rc.1', '0.3.0-rc.1', '0.3.0', '1.0.0']) {
|
|
85
|
-
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), false, 'own range rejects ' + v)
|
|
86
|
-
}
|
|
87
|
-
// unsupported forms are UNKNOWN (null) — reported, never asserted incompatible
|
|
88
|
-
for (const r of ['not-a-range', '>=1.2', '1.2.x', '']) {
|
|
89
|
-
eq(satisfiesDshRange('1.2.5', r, { includePrerelease: true }), null, 'unsupported range is unknown: ' + JSON.stringify(r))
|
|
90
|
-
}
|
|
91
|
-
eq(satisfiesDshRange('not-a-version', pkg.engines.dsh), null, 'an unparseable host version is unknown, not incompatible')
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------------------------
|
|
94
|
-
// 2. the verdict prefers the declared range, falls back to the matrix
|
|
95
|
-
// ---------------------------------------------------------------------------------------------
|
|
96
|
-
{
|
|
97
|
-
const a = dshVersionVerdict('0.1.5-rc.2', pkg)
|
|
98
|
-
ok(a.status === 'compatible' && a.basis === 'engines', 'our own host line is compatible via the declared range', JSON.stringify(a))
|
|
99
|
-
eq(a.requirement, pkg.engines.dsh, 'the verdict surfaces the declared range (so the log and the market card say the same thing)')
|
|
100
|
-
const b = dshVersionVerdict('0.3.0', pkg)
|
|
101
|
-
ok(b.status === 'incompatible' && b.basis === 'engines', 'a host outside the declared range is incompatible via the range', JSON.stringify(b))
|
|
102
|
-
const c = dshVersionVerdict('0.1.0-rc.6', pkg)
|
|
103
|
-
ok(c.status === 'incompatible', 'a host below the declared floor is incompatible', JSON.stringify(c))
|
|
104
|
-
// dsh.engines.dsh is the second position the ecosystem (and the market) reads
|
|
105
|
-
const onlyNested = { dsh: { engines: { dsh: '>=0.1.0-rc.6' } } }
|
|
106
|
-
const d = dshVersionVerdict('0.1.0-rc.6', onlyNested)
|
|
107
|
-
ok(d.status === 'compatible' && d.basis === 'engines', 'dsh.engines.dsh is honoured when engines.dsh is absent', JSON.stringify(d))
|
|
108
|
-
// top-level wins when both are present (mirrors dsh-market's manifestFacts)
|
|
109
|
-
const both = { engines: { dsh: '>=0.2.0' }, dsh: { engines: { dsh: '>=0.1.0' } } }
|
|
110
|
-
eq(dshVersionVerdict('0.1.5-rc.2', both).status, 'incompatible', 'engines.dsh wins when both positions are declared')
|
|
111
|
-
// fallback: a manifest that declares no range keeps the dshReleases behaviour
|
|
112
|
-
const mapOnly = {
|
|
113
|
-
dsh: { compatibility: { dshReleases: { '0.1.5-rc.2': 'compatible', '0.1.0-rc.1': 'incompatible', '0.1.9': 'unknown' } } },
|
|
114
|
-
}
|
|
115
|
-
const e = dshVersionVerdict('0.1.5-rc.2', mapOnly)
|
|
116
|
-
ok(e.status === 'compatible' && e.basis === 'dshReleases', 'fallback: the matrix still decides when no range is declared', JSON.stringify(e))
|
|
117
|
-
eq(dshVersionVerdict('0.1.0-rc.1', mapOnly).status, 'incompatible', 'fallback: an explicitly incompatible release')
|
|
118
|
-
eq(dshVersionVerdict('0.1.7', mapOnly).status, 'undeclared', 'fallback: an unlisted release is undeclared (soft warning)')
|
|
119
|
-
eq(dshVersionVerdict('0.1.9', mapOnly).status, 'unknown', 'fallback: an explicitly unknown release')
|
|
120
|
-
// a range the matcher cannot parse must not become "incompatible"
|
|
121
|
-
const unparsable = { engines: { dsh: 'not a range' } }
|
|
122
|
-
const f = dshVersionVerdict('0.1.5-rc.2', unparsable)
|
|
123
|
-
ok(f.status === 'unknown' && f.basis === 'engines', 'an unparseable declared range is unknown, never incompatible', JSON.stringify(f))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------------------------
|
|
127
|
-
// 3. the two sources cannot drift: every release our matrix calls compatible must not be judged
|
|
128
|
-
// incompatible by the declared range (the installer and the market must agree).
|
|
129
|
-
// ---------------------------------------------------------------------------------------------
|
|
130
|
-
{
|
|
131
|
-
const matrix = Object.entries((pkg.dsh.compatibility || {}).dshReleases || {})
|
|
132
|
-
const contradicted = matrix.filter(([ver, status]) => {
|
|
133
|
-
const v = dshVersionVerdict(ver, pkg)
|
|
134
|
-
return status === 'compatible' && v.status === 'incompatible'
|
|
135
|
-
}).map(([ver]) => ver)
|
|
136
|
-
ok(contradicted.length === 0, 'no release marked compatible in dsh.compatibility is rejected by engines.dsh',
|
|
137
|
-
contradicted.join(', '))
|
|
138
|
-
const missedByRange = matrix.filter(([ver]) => dshVersionVerdict(ver, pkg).status === 'unknown').map(([ver]) => ver)
|
|
139
|
-
ok(missedByRange.length === 0, 'the declared range can be evaluated for every release in the matrix', missedByRange.join(', '))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------------------------
|
|
143
|
-
// 4. the version probe must not be able to hold the installer ROW's activation
|
|
144
|
-
//
|
|
145
|
-
// `detectDshVersion` asks `pluginManager.listBundles()` for the runtime version, and it runs at the
|
|
146
|
-
// top of apply(). The result is diagnostic only — the preset mechanism is decided by the loader tree
|
|
147
|
-
// / the service's `register`, never by the version — so a slow or stuck plugin manager must fall
|
|
148
|
-
// through to the next source instead of stalling the boot. (Round B, machinery audit.)
|
|
149
|
-
// ---------------------------------------------------------------------------------------------
|
|
150
|
-
console.log('=== 4. a pluginManager that never answers must not hold the row ===')
|
|
151
|
-
{
|
|
152
|
-
const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-timeout-'))
|
|
153
|
-
const pkg = join(tmp, 'pkg')
|
|
154
|
-
mkdirSync(pkg, { recursive: true })
|
|
155
|
-
cpSync(INSTALLER_SRC, join(pkg, 'installer.js'))
|
|
156
|
-
writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name: 'dsh-vibe-math', version: '9.9.9', type: 'module' }, null, 2))
|
|
157
|
-
for (const p of PRESETS) {
|
|
158
|
-
mkdirSync(join(pkg, p.src), { recursive: true })
|
|
159
|
-
for (const f of p.files) cpSync(join(HERE, p.src, f), join(pkg, p.src, f))
|
|
160
|
-
}
|
|
161
|
-
const home = join(tmp, 'dshhome')
|
|
162
|
-
mkdirSync(home, { recursive: true })
|
|
163
|
-
process.env.DSH_HOME = home
|
|
164
|
-
delete process.env.DSH_VERSION // leave the pluginManager as the only source
|
|
165
|
-
const presetRoot = join(home, '.agent-presets')
|
|
166
|
-
// import the COPY, so `here` inside installer.js is the throwaway package (manifest + preset dirs)
|
|
167
|
-
const mod = await import(pathToFileURL(join(pkg, 'installer.js')).href + '?t=' + Date.now())
|
|
168
|
-
|
|
169
|
-
const run = (services) => {
|
|
170
|
-
const logs = []
|
|
171
|
-
const ctx = {
|
|
172
|
-
get: (n) => (n === 'pluginManager' ? services : undefined),
|
|
173
|
-
logger: { info: (m) => logs.push('info: ' + m), warn: (m) => logs.push('warn: ' + m), error: (m) => logs.push('error: ' + m) },
|
|
174
|
-
}
|
|
175
|
-
return { ctx, logs }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// a promise that never settles: without a timeout apply() would await it forever
|
|
179
|
-
const HARD_LIMIT_MS = 8000
|
|
180
|
-
const { ctx, logs } = run({ listBundles: () => new Promise(() => {}) })
|
|
181
|
-
let verdict = 'hung'
|
|
182
|
-
const t0 = Date.now()
|
|
183
|
-
let hardTimer
|
|
184
|
-
await Promise.race([
|
|
185
|
-
mod.apply(ctx).then(() => { verdict = 'returned' }, (e) => { verdict = 'threw: ' + String(e && e.message) }),
|
|
186
|
-
new Promise((resolve) => { hardTimer = setTimeout(resolve, HARD_LIMIT_MS) }),
|
|
187
|
-
])
|
|
188
|
-
clearTimeout(hardTimer) // the shipped installer wins the race in ~1.5 s; do not hold the suite open
|
|
189
|
-
const elapsed = Date.now() - t0
|
|
190
|
-
ok(verdict === 'returned', 'apply() returns instead of waiting on a pluginManager that never answers', verdict)
|
|
191
|
-
ok(elapsed < 6000, '...within a short timeout, not at the hard test limit', elapsed + 'ms')
|
|
192
|
-
const copied = PRESETS.every((p) => p.files.every((f) => existsSync(join(presetRoot, p.dst, f))))
|
|
193
|
-
ok(copied, '...and the preset copy still happens (the version probe gates nothing)')
|
|
194
|
-
ok(!logs.some((l) => l.includes('preset install/update failed')), 'the timeout is handled as a fallback, not as an error')
|
|
195
|
-
|
|
196
|
-
// the timeout must NOT replace a working probe: a prompt service still supplies the version
|
|
197
|
-
const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
|
|
198
|
-
await mod.apply(quick.ctx)
|
|
199
|
-
// A7 (assertion quality): the version+source claim is asserted on the STRUCTURED result - a bare
|
|
200
|
-
// `logs.some(l => l.includes('0.2.0-rc.2'))` would also pass if the string appeared anywhere for any
|
|
201
|
-
// reason, and `l.includes('source')` would pass on the English word alone. The log keeps ONE
|
|
202
|
-
// shape-anchored check.
|
|
203
|
-
const probeQuick = (await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())).detectDshVersion || detectDshVersion
|
|
204
|
-
const detQuick = await probeQuick(quick.ctx)
|
|
205
|
-
ok(detQuick && detQuick.version === '0.2.0-rc.2',
|
|
206
|
-
'★ a prompt listBundles() still supplies the version (STRUCTURED field: version)', JSON.stringify(detQuick && { version: detQuick.version, source: detQuick.source }))
|
|
207
|
-
ok(!!detQuick && typeof detQuick.source === 'string' && detQuick.source.indexOf('pluginManager') === 0,
|
|
208
|
-
'★ ...and the winning SOURCE is a structured field naming the service (not the word "source" in a log line)', JSON.stringify(detQuick && detQuick.source))
|
|
209
|
-
ok(quick.logs.some((l) => /v?0\.2\.0-rc\.2/.test(l)),
|
|
210
|
-
'...and a log line SHAPE carries that version (shape-anchored, not a bare substring)')
|
|
211
|
-
rmSync(tmp, { recursive: true, force: true })
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Installer review (finding): several sources can report the host's version and "first available wins"
|
|
215
|
-
// hid a conflicting host state. The probe records EVERY value+source, keeps the first available
|
|
216
|
-
// authoritative (priority unchanged), and assembles `disagreement` ONLY when one exists.
|
|
217
|
-
{
|
|
218
|
-
const prev = process.env.DSH_VERSION
|
|
219
|
-
try {
|
|
220
|
-
// Import through INSTALLER_SRC (env-overridable) so a single-site mutant on a COPY of installer.js
|
|
221
|
-
// reaches these assertions - that is what makes the mutant below able to bite.
|
|
222
|
-
const mod = await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())
|
|
223
|
-
const probe = mod.detectDshVersion || detectDshVersion
|
|
224
|
-
process.env.DSH_VERSION = '9.9.9'
|
|
225
|
-
const disagreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '8.8.8' }] } : undefined) }
|
|
226
|
-
const d1 = await probe(disagreeing)
|
|
227
|
-
ok(d1 && d1.version === '9.9.9' && d1.source === 'DSH_VERSION', 'the FIRST available source stays authoritative (which version wins is unchanged)', JSON.stringify(d1))
|
|
228
|
-
ok(d1 && Array.isArray(d1.probes) && d1.probes.length >= 2 && d1.probes.some((p) => p.source.indexOf('pluginManager') === 0 && p.version === '8.8.8'),
|
|
229
|
-
'★ every probe value+source is recorded (nothing is silently dropped)', JSON.stringify(d1 && d1.probes))
|
|
230
|
-
ok(!!(d1 && d1.disagreement) && d1.disagreement.primary === 'DSH_VERSION' && d1.disagreement.conflicts.some((c) => c.version === '8.8.8'),
|
|
231
|
-
'★ DISAGREEING probes surface a disagreement slot naming the conflict', JSON.stringify(d1 && d1.disagreement))
|
|
232
|
-
const agreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '9.9.9' }] } : undefined) }
|
|
233
|
-
const d2 = await probe(agreeing)
|
|
234
|
-
ok(d2 && d2.version === '9.9.9' && !('disagreement' in d2), '★ AGREEING probes print NO disagreement slot (assemble-only-on-conflict)', JSON.stringify(d2))
|
|
235
|
-
// The `%s` placeholder that could reach the user: every installer log line must pre-concatenate.
|
|
236
|
-
const isrc = readFileSync(INSTALLER_SRC, 'utf8')
|
|
237
|
-
const printf = isrc.split('\n').filter((l) => /logger|console/.test(l) && /%[sd]/.test(l))
|
|
238
|
-
ok(printf.length === 0, '★ no installer log line carries a bare printf placeholder (%s/%d)', JSON.stringify(printf.slice(0, 2)))
|
|
239
|
-
} finally {
|
|
240
|
-
if (prev === undefined) delete process.env.DSH_VERSION; else process.env.DSH_VERSION = prev
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
console.log('')
|
|
245
|
-
// The self-check proves service/API SHAPE only - never runtime SEMANTICS. The caveat must be on the
|
|
246
|
-
// USER-VISIBLE pass line, so a passing self-check is not read as a compatibility claim.
|
|
247
|
-
{
|
|
248
|
-
const src = readFileSync(INSTALLER_SRC, 'utf8')
|
|
249
|
-
const line = src.split(/\r?\n/).find((l) => l.indexOf('自检通过') !== -1 && l.indexOf('logger') !== -1) || ''
|
|
250
|
-
ok(line.length > 0, 'the installer reports a passing host self-check')
|
|
251
|
-
ok(line.indexOf('形状') !== -1 && line.indexOf('行为') !== -1, '★ self-check caveat: the PASS line states that SHAPE is proven and BEHAVIOUR is not', JSON.stringify(line.slice(0, 90)))
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
255
|
-
if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
256
|
-
console.log('ALL GREEN')
|
|
1
|
+
// ============================================================================================
|
|
2
|
+
// INSTALLER HOST-COMPATIBILITY AUDIT
|
|
3
|
+
//
|
|
4
|
+
// The installer's host self-check warns when the running DSH is outside the range this package
|
|
5
|
+
// declares. That verdict must come from the SAME declaration dsh-market renders on the plugin card
|
|
6
|
+
// (`engines.dsh` / `dsh.engines.dsh`) and not from a second source, or the two disagree: the card
|
|
7
|
+
// says "compatible" while the log warns, or the reverse. This suite pins that:
|
|
8
|
+
//
|
|
9
|
+
// · the range matcher reproduces semver 7.8.5 exactly — the expectations below were generated with
|
|
10
|
+
// npm's own bundled semver (`require('…/npm/node_modules/semver')`) and re-checked against the
|
|
11
|
+
// shipped matcher over 320 (range, version, includePrerelease) pairs, 0 mismatches;
|
|
12
|
+
// · `dshVersionVerdict` prefers the declared range and falls back to `dsh.compatibility`;
|
|
13
|
+
// · every release our own compatibility matrix calls `compatible` is NOT judged incompatible by
|
|
14
|
+
// the declared range — the two sources cannot drift apart.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/audit-installer-compat.test.mjs (part of `node tests/run-tests.mjs`)
|
|
17
|
+
// ============================================================================================
|
|
18
|
+
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
21
|
+
import { join, resolve } from 'node:path'
|
|
22
|
+
import { satisfiesDshRange, dshVersionVerdict, detectDshVersion, PRESETS } from '../installer.js'
|
|
23
|
+
|
|
24
|
+
const HERE = fileURLToPath(new URL('../', import.meta.url))
|
|
25
|
+
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
26
|
+
// §4 drives the real apply(); INSTALLER_JS lets the sensitivity probe run it against a reverted copy
|
|
27
|
+
const INSTALLER_SRC = process.env.INSTALLER_JS ? resolve(process.env.INSTALLER_JS) : join(HERE, 'installer.js')
|
|
28
|
+
|
|
29
|
+
let passed = 0, failed = 0
|
|
30
|
+
const failures = []
|
|
31
|
+
const ok = (cond, label, detail) => {
|
|
32
|
+
if (cond) { passed++; console.log(' ok ' + label); return true }
|
|
33
|
+
failed++; failures.push(label + (detail ? ' — ' + detail : ''))
|
|
34
|
+
console.error(' FAIL ' + label + (detail ? ' — ' + detail : ''))
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
const eq = (actual, expected, label) => ok(actual === expected, label, 'got ' + JSON.stringify(actual) + ', expected ' + JSON.stringify(expected))
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------------------------
|
|
40
|
+
// 1. the matcher reproduces semver 7.8.5 (expectations generated with npm's bundled semver)
|
|
41
|
+
// ---------------------------------------------------------------------------------------------
|
|
42
|
+
const CASES = [
|
|
43
|
+
// [range, version, includePrerelease, expected]
|
|
44
|
+
['*', '1.2.3', false, true],
|
|
45
|
+
['*', '0.1.5-rc.2', false, false], // npm: `*` excludes prereleases unless opted in
|
|
46
|
+
['*', '0.1.5-rc.2', true, true],
|
|
47
|
+
['1.2.3', '1.2.3', false, true],
|
|
48
|
+
['1.2.3', '1.2.4', false, false],
|
|
49
|
+
['^0.1.0', '0.1.4', false, true],
|
|
50
|
+
['^0.1.0', '0.2.0-rc.1', true, false], // 0.x caret never reaches the next minor
|
|
51
|
+
['^0.1.0', '0.1.0-rc.5', true, false], // ...nor a prerelease below its own floor
|
|
52
|
+
['^0.1.0-rc.7', '0.1.0-rc.7', false, true],
|
|
53
|
+
['^0.1.0-rc.7', '0.1.5-rc.2', true, true],
|
|
54
|
+
['^0.0.1', '0.0.1', false, true],
|
|
55
|
+
['^0.0.1', '0.0.2', false, false],
|
|
56
|
+
['~0.1.2', '0.1.4', false, true],
|
|
57
|
+
['~0.1.2', '0.2.0', false, false],
|
|
58
|
+
['>0.1.0 <=0.1.5', '0.1.5', false, true],
|
|
59
|
+
['>=0.1.5-rc.1', '0.1.5-rc.2', false, true],
|
|
60
|
+
['>=0.1.5-rc.1', '0.2.0', false, true], // a floor-only range keeps admitting newer hosts
|
|
61
|
+
// the trap contributing.md warns about: no comparator on the 0.1.5 tuple carrying a prerelease
|
|
62
|
+
['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', false, false],
|
|
63
|
+
['>=0.1.2-rc.1 <0.2.0', '0.1.5-rc.2', true, true],
|
|
64
|
+
// the market-style chain
|
|
65
|
+
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.2-rc.1', false, true],
|
|
66
|
+
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', false, false],
|
|
67
|
+
['^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2', '0.1.3-alpha.2', true, true],
|
|
68
|
+
]
|
|
69
|
+
for (const [range, version, pre, expected] of CASES) {
|
|
70
|
+
eq(satisfiesDshRange(version, range, { includePrerelease: pre }), expected,
|
|
71
|
+
'matcher: ' + JSON.stringify(range) + ' vs ' + version + (pre ? ' (includePrerelease)' : ''))
|
|
72
|
+
}
|
|
73
|
+
// our own declaration, both semantics (the exact chain the market and the installer share)
|
|
74
|
+
// The window is per-tuple: every supported line has its own prerelease branch, because npm's
|
|
75
|
+
// prerelease rule only admits a version when a comparator in the SAME [major,minor,patch] tuple also
|
|
76
|
+
// carries a prerelease. 0.2.0 joined the window with the DSH 0.2.0 adaptation.
|
|
77
|
+
for (const v of ['0.1.2-alpha.4', '0.1.2-rc.1', '0.1.3-alpha.2', '0.1.5-rc.1', '0.1.5-rc.2', '0.1.6-alpha.2', '0.1.7-rc.2', '0.2.0-rc.1', '0.2.0-rc.2']) {
|
|
78
|
+
eq(satisfiesDshRange(v, pkg.engines.dsh), true, 'own range admits ' + v + ' (default semantics)')
|
|
79
|
+
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits ' + v + ' (includePrerelease)')
|
|
80
|
+
}
|
|
81
|
+
for (const v of ['0.2.0']) {
|
|
82
|
+
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), true, 'own range admits the released ' + v)
|
|
83
|
+
}
|
|
84
|
+
for (const v of ['0.1.1-rc.1', '0.3.0-rc.1', '0.3.0', '1.0.0']) {
|
|
85
|
+
eq(satisfiesDshRange(v, pkg.engines.dsh, { includePrerelease: true }), false, 'own range rejects ' + v)
|
|
86
|
+
}
|
|
87
|
+
// unsupported forms are UNKNOWN (null) — reported, never asserted incompatible
|
|
88
|
+
for (const r of ['not-a-range', '>=1.2', '1.2.x', '']) {
|
|
89
|
+
eq(satisfiesDshRange('1.2.5', r, { includePrerelease: true }), null, 'unsupported range is unknown: ' + JSON.stringify(r))
|
|
90
|
+
}
|
|
91
|
+
eq(satisfiesDshRange('not-a-version', pkg.engines.dsh), null, 'an unparseable host version is unknown, not incompatible')
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------------------------
|
|
94
|
+
// 2. the verdict prefers the declared range, falls back to the matrix
|
|
95
|
+
// ---------------------------------------------------------------------------------------------
|
|
96
|
+
{
|
|
97
|
+
const a = dshVersionVerdict('0.1.5-rc.2', pkg)
|
|
98
|
+
ok(a.status === 'compatible' && a.basis === 'engines', 'our own host line is compatible via the declared range', JSON.stringify(a))
|
|
99
|
+
eq(a.requirement, pkg.engines.dsh, 'the verdict surfaces the declared range (so the log and the market card say the same thing)')
|
|
100
|
+
const b = dshVersionVerdict('0.3.0', pkg)
|
|
101
|
+
ok(b.status === 'incompatible' && b.basis === 'engines', 'a host outside the declared range is incompatible via the range', JSON.stringify(b))
|
|
102
|
+
const c = dshVersionVerdict('0.1.0-rc.6', pkg)
|
|
103
|
+
ok(c.status === 'incompatible', 'a host below the declared floor is incompatible', JSON.stringify(c))
|
|
104
|
+
// dsh.engines.dsh is the second position the ecosystem (and the market) reads
|
|
105
|
+
const onlyNested = { dsh: { engines: { dsh: '>=0.1.0-rc.6' } } }
|
|
106
|
+
const d = dshVersionVerdict('0.1.0-rc.6', onlyNested)
|
|
107
|
+
ok(d.status === 'compatible' && d.basis === 'engines', 'dsh.engines.dsh is honoured when engines.dsh is absent', JSON.stringify(d))
|
|
108
|
+
// top-level wins when both are present (mirrors dsh-market's manifestFacts)
|
|
109
|
+
const both = { engines: { dsh: '>=0.2.0' }, dsh: { engines: { dsh: '>=0.1.0' } } }
|
|
110
|
+
eq(dshVersionVerdict('0.1.5-rc.2', both).status, 'incompatible', 'engines.dsh wins when both positions are declared')
|
|
111
|
+
// fallback: a manifest that declares no range keeps the dshReleases behaviour
|
|
112
|
+
const mapOnly = {
|
|
113
|
+
dsh: { compatibility: { dshReleases: { '0.1.5-rc.2': 'compatible', '0.1.0-rc.1': 'incompatible', '0.1.9': 'unknown' } } },
|
|
114
|
+
}
|
|
115
|
+
const e = dshVersionVerdict('0.1.5-rc.2', mapOnly)
|
|
116
|
+
ok(e.status === 'compatible' && e.basis === 'dshReleases', 'fallback: the matrix still decides when no range is declared', JSON.stringify(e))
|
|
117
|
+
eq(dshVersionVerdict('0.1.0-rc.1', mapOnly).status, 'incompatible', 'fallback: an explicitly incompatible release')
|
|
118
|
+
eq(dshVersionVerdict('0.1.7', mapOnly).status, 'undeclared', 'fallback: an unlisted release is undeclared (soft warning)')
|
|
119
|
+
eq(dshVersionVerdict('0.1.9', mapOnly).status, 'unknown', 'fallback: an explicitly unknown release')
|
|
120
|
+
// a range the matcher cannot parse must not become "incompatible"
|
|
121
|
+
const unparsable = { engines: { dsh: 'not a range' } }
|
|
122
|
+
const f = dshVersionVerdict('0.1.5-rc.2', unparsable)
|
|
123
|
+
ok(f.status === 'unknown' && f.basis === 'engines', 'an unparseable declared range is unknown, never incompatible', JSON.stringify(f))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------------------------
|
|
127
|
+
// 3. the two sources cannot drift: every release our matrix calls compatible must not be judged
|
|
128
|
+
// incompatible by the declared range (the installer and the market must agree).
|
|
129
|
+
// ---------------------------------------------------------------------------------------------
|
|
130
|
+
{
|
|
131
|
+
const matrix = Object.entries((pkg.dsh.compatibility || {}).dshReleases || {})
|
|
132
|
+
const contradicted = matrix.filter(([ver, status]) => {
|
|
133
|
+
const v = dshVersionVerdict(ver, pkg)
|
|
134
|
+
return status === 'compatible' && v.status === 'incompatible'
|
|
135
|
+
}).map(([ver]) => ver)
|
|
136
|
+
ok(contradicted.length === 0, 'no release marked compatible in dsh.compatibility is rejected by engines.dsh',
|
|
137
|
+
contradicted.join(', '))
|
|
138
|
+
const missedByRange = matrix.filter(([ver]) => dshVersionVerdict(ver, pkg).status === 'unknown').map(([ver]) => ver)
|
|
139
|
+
ok(missedByRange.length === 0, 'the declared range can be evaluated for every release in the matrix', missedByRange.join(', '))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------------------------
|
|
143
|
+
// 4. the version probe must not be able to hold the installer ROW's activation
|
|
144
|
+
//
|
|
145
|
+
// `detectDshVersion` asks `pluginManager.listBundles()` for the runtime version, and it runs at the
|
|
146
|
+
// top of apply(). The result is diagnostic only — the preset mechanism is decided by the loader tree
|
|
147
|
+
// / the service's `register`, never by the version — so a slow or stuck plugin manager must fall
|
|
148
|
+
// through to the next source instead of stalling the boot. (Round B, machinery audit.)
|
|
149
|
+
// ---------------------------------------------------------------------------------------------
|
|
150
|
+
console.log('=== 4. a pluginManager that never answers must not hold the row ===')
|
|
151
|
+
{
|
|
152
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vibe-installer-timeout-'))
|
|
153
|
+
const pkg = join(tmp, 'pkg')
|
|
154
|
+
mkdirSync(pkg, { recursive: true })
|
|
155
|
+
cpSync(INSTALLER_SRC, join(pkg, 'installer.js'))
|
|
156
|
+
writeFileSync(join(pkg, 'package.json'), JSON.stringify({ name: 'dsh-vibe-math', version: '9.9.9', type: 'module' }, null, 2))
|
|
157
|
+
for (const p of PRESETS) {
|
|
158
|
+
mkdirSync(join(pkg, p.src), { recursive: true })
|
|
159
|
+
for (const f of p.files) cpSync(join(HERE, p.src, f), join(pkg, p.src, f))
|
|
160
|
+
}
|
|
161
|
+
const home = join(tmp, 'dshhome')
|
|
162
|
+
mkdirSync(home, { recursive: true })
|
|
163
|
+
process.env.DSH_HOME = home
|
|
164
|
+
delete process.env.DSH_VERSION // leave the pluginManager as the only source
|
|
165
|
+
const presetRoot = join(home, '.agent-presets')
|
|
166
|
+
// import the COPY, so `here` inside installer.js is the throwaway package (manifest + preset dirs)
|
|
167
|
+
const mod = await import(pathToFileURL(join(pkg, 'installer.js')).href + '?t=' + Date.now())
|
|
168
|
+
|
|
169
|
+
const run = (services) => {
|
|
170
|
+
const logs = []
|
|
171
|
+
const ctx = {
|
|
172
|
+
get: (n) => (n === 'pluginManager' ? services : undefined),
|
|
173
|
+
logger: { info: (m) => logs.push('info: ' + m), warn: (m) => logs.push('warn: ' + m), error: (m) => logs.push('error: ' + m) },
|
|
174
|
+
}
|
|
175
|
+
return { ctx, logs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// a promise that never settles: without a timeout apply() would await it forever
|
|
179
|
+
const HARD_LIMIT_MS = 8000
|
|
180
|
+
const { ctx, logs } = run({ listBundles: () => new Promise(() => {}) })
|
|
181
|
+
let verdict = 'hung'
|
|
182
|
+
const t0 = Date.now()
|
|
183
|
+
let hardTimer
|
|
184
|
+
await Promise.race([
|
|
185
|
+
mod.apply(ctx).then(() => { verdict = 'returned' }, (e) => { verdict = 'threw: ' + String(e && e.message) }),
|
|
186
|
+
new Promise((resolve) => { hardTimer = setTimeout(resolve, HARD_LIMIT_MS) }),
|
|
187
|
+
])
|
|
188
|
+
clearTimeout(hardTimer) // the shipped installer wins the race in ~1.5 s; do not hold the suite open
|
|
189
|
+
const elapsed = Date.now() - t0
|
|
190
|
+
ok(verdict === 'returned', 'apply() returns instead of waiting on a pluginManager that never answers', verdict)
|
|
191
|
+
ok(elapsed < 6000, '...within a short timeout, not at the hard test limit', elapsed + 'ms')
|
|
192
|
+
const copied = PRESETS.every((p) => p.files.every((f) => existsSync(join(presetRoot, p.dst, f))))
|
|
193
|
+
ok(copied, '...and the preset copy still happens (the version probe gates nothing)')
|
|
194
|
+
ok(!logs.some((l) => l.includes('preset install/update failed')), 'the timeout is handled as a fallback, not as an error')
|
|
195
|
+
|
|
196
|
+
// the timeout must NOT replace a working probe: a prompt service still supplies the version
|
|
197
|
+
const quick = run({ listBundles: async () => [{ name: '@deepseek-ai/dsh-base', version: '0.2.0-rc.2' }] })
|
|
198
|
+
await mod.apply(quick.ctx)
|
|
199
|
+
// A7 (assertion quality): the version+source claim is asserted on the STRUCTURED result - a bare
|
|
200
|
+
// `logs.some(l => l.includes('0.2.0-rc.2'))` would also pass if the string appeared anywhere for any
|
|
201
|
+
// reason, and `l.includes('source')` would pass on the English word alone. The log keeps ONE
|
|
202
|
+
// shape-anchored check.
|
|
203
|
+
const probeQuick = (await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())).detectDshVersion || detectDshVersion
|
|
204
|
+
const detQuick = await probeQuick(quick.ctx)
|
|
205
|
+
ok(detQuick && detQuick.version === '0.2.0-rc.2',
|
|
206
|
+
'★ a prompt listBundles() still supplies the version (STRUCTURED field: version)', JSON.stringify(detQuick && { version: detQuick.version, source: detQuick.source }))
|
|
207
|
+
ok(!!detQuick && typeof detQuick.source === 'string' && detQuick.source.indexOf('pluginManager') === 0,
|
|
208
|
+
'★ ...and the winning SOURCE is a structured field naming the service (not the word "source" in a log line)', JSON.stringify(detQuick && detQuick.source))
|
|
209
|
+
ok(quick.logs.some((l) => /v?0\.2\.0-rc\.2/.test(l)),
|
|
210
|
+
'...and a log line SHAPE carries that version (shape-anchored, not a bare substring)')
|
|
211
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Installer review (finding): several sources can report the host's version and "first available wins"
|
|
215
|
+
// hid a conflicting host state. The probe records EVERY value+source, keeps the first available
|
|
216
|
+
// authoritative (priority unchanged), and assembles `disagreement` ONLY when one exists.
|
|
217
|
+
{
|
|
218
|
+
const prev = process.env.DSH_VERSION
|
|
219
|
+
try {
|
|
220
|
+
// Import through INSTALLER_SRC (env-overridable) so a single-site mutant on a COPY of installer.js
|
|
221
|
+
// reaches these assertions - that is what makes the mutant below able to bite.
|
|
222
|
+
const mod = await import(pathToFileURL(INSTALLER_SRC).href + '?t=' + Date.now())
|
|
223
|
+
const probe = mod.detectDshVersion || detectDshVersion
|
|
224
|
+
process.env.DSH_VERSION = '9.9.9'
|
|
225
|
+
const disagreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '8.8.8' }] } : undefined) }
|
|
226
|
+
const d1 = await probe(disagreeing)
|
|
227
|
+
ok(d1 && d1.version === '9.9.9' && d1.source === 'DSH_VERSION', 'the FIRST available source stays authoritative (which version wins is unchanged)', JSON.stringify(d1))
|
|
228
|
+
ok(d1 && Array.isArray(d1.probes) && d1.probes.length >= 2 && d1.probes.some((p) => p.source.indexOf('pluginManager') === 0 && p.version === '8.8.8'),
|
|
229
|
+
'★ every probe value+source is recorded (nothing is silently dropped)', JSON.stringify(d1 && d1.probes))
|
|
230
|
+
ok(!!(d1 && d1.disagreement) && d1.disagreement.primary === 'DSH_VERSION' && d1.disagreement.conflicts.some((c) => c.version === '8.8.8'),
|
|
231
|
+
'★ DISAGREEING probes surface a disagreement slot naming the conflict', JSON.stringify(d1 && d1.disagreement))
|
|
232
|
+
const agreeing = { get: (n) => (n === 'pluginManager' ? { listBundles: async () => [{ name: '@deepseek-ai/dsh', version: '9.9.9' }] } : undefined) }
|
|
233
|
+
const d2 = await probe(agreeing)
|
|
234
|
+
ok(d2 && d2.version === '9.9.9' && !('disagreement' in d2), '★ AGREEING probes print NO disagreement slot (assemble-only-on-conflict)', JSON.stringify(d2))
|
|
235
|
+
// The `%s` placeholder that could reach the user: every installer log line must pre-concatenate.
|
|
236
|
+
const isrc = readFileSync(INSTALLER_SRC, 'utf8')
|
|
237
|
+
const printf = isrc.split('\n').filter((l) => /logger|console/.test(l) && /%[sd]/.test(l))
|
|
238
|
+
ok(printf.length === 0, '★ no installer log line carries a bare printf placeholder (%s/%d)', JSON.stringify(printf.slice(0, 2)))
|
|
239
|
+
} finally {
|
|
240
|
+
if (prev === undefined) delete process.env.DSH_VERSION; else process.env.DSH_VERSION = prev
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log('')
|
|
245
|
+
// The self-check proves service/API SHAPE only - never runtime SEMANTICS. The caveat must be on the
|
|
246
|
+
// USER-VISIBLE pass line, so a passing self-check is not read as a compatibility claim.
|
|
247
|
+
{
|
|
248
|
+
const src = readFileSync(INSTALLER_SRC, 'utf8')
|
|
249
|
+
const line = src.split(/\r?\n/).find((l) => l.indexOf('自检通过') !== -1 && l.indexOf('logger') !== -1) || ''
|
|
250
|
+
ok(line.length > 0, 'the installer reports a passing host self-check')
|
|
251
|
+
ok(line.indexOf('形状') !== -1 && line.indexOf('行为') !== -1, '★ self-check caveat: the PASS line states that SHAPE is proven and BEHAVIOUR is not', JSON.stringify(line.slice(0, 90)))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('=== INSTALLER COMPAT: ' + passed + ' passed, ' + failed + ' failed ===')
|
|
255
|
+
if (failed) { for (const f of failures) console.error(' - ' + f); process.exit(1) }
|
|
256
|
+
console.log('ALL GREEN')
|
|
@@ -210,7 +210,7 @@ function runSuite(modulePath) {
|
|
|
210
210
|
for (const p of PROBES) {
|
|
211
211
|
const dir = mkdtempSync(join(tmpdir(), 'mc-sens-' + p.name + '-'))
|
|
212
212
|
copyFileSync(join(SRC, 'math-engines.js'), join(dir, 'math-engines.js'))
|
|
213
|
-
const src = readFileSync(join(SRC, p.file), 'utf8')
|
|
213
|
+
const src = readFileSync(join(SRC, p.file), 'utf8').replace(/\r\n/g, '\n') // anchors are LF: normalize CRLF checkouts
|
|
214
214
|
if (src.indexOf(p.from) === -1) {
|
|
215
215
|
bad++
|
|
216
216
|
console.log(' FAIL ' + p.name + ' — mutation anchor no longer applies (drift): ' + JSON.stringify(p.from.slice(0, 60)))
|