dsh-memu-memory 1.1.3 → 1.2.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/AGENTS.md +1 -1
- package/README.md +39 -6
- package/README.zh.md +39 -7
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +3 -3
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +8 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +396 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +23 -6
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +335 -35
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +144 -0
- package/lib/curator.js +86 -31
- package/lib/cycle.js +40 -6
- package/lib/graph.js +139 -5
- package/lib/index.js +33 -1
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +534 -88
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +430 -43
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +54 -5
- package/lib/state.js +29 -12
- package/lib/text-similarity.js +106 -0
- package/lib/tools.js +113 -10
- package/lib/vector-migration.js +28 -3
- package/lib/vector-profile.js +51 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/scripts/replay.mjs +573 -0
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +216 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +331 -0
- package/tests/consolidation-feed.test.mjs +96 -0
- package/tests/consolidation-fork.test.mjs +489 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +360 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-commit.test.mjs +277 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-cascade.test.mjs +190 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +94 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +122 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +591 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/recall-dedup.test.mjs +417 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +212 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +790 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +165 -0
- package/tests/windows-env.test.mjs +89 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { execFileSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
10
|
+
const SCRIPT = path.join(ROOT, 'scripts', 'active-memory-ab.mjs')
|
|
11
|
+
const ab = await import(pathToFileURL(SCRIPT).href)
|
|
12
|
+
const activeMemory = await import(pathToFileURL(path.join(ROOT, 'lib', 'active-memory.js')).href)
|
|
13
|
+
const { estTokens } = await import(pathToFileURL(path.join(ROOT, 'lib', 'budget.js')).href)
|
|
14
|
+
|
|
15
|
+
const tempRoots = []
|
|
16
|
+
function tempRoot(label) {
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-ab-' + label + '-'))
|
|
18
|
+
tempRoots.push(dir)
|
|
19
|
+
return dir
|
|
20
|
+
}
|
|
21
|
+
test.after(() => {
|
|
22
|
+
for (const dir of tempRoots.splice(0)) {
|
|
23
|
+
try { fs.rmSync(dir, { recursive: true, force: true }) } catch {}
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const reportPromise = ab.runAb({})
|
|
28
|
+
const report = await reportPromise
|
|
29
|
+
|
|
30
|
+
function stable(value) {
|
|
31
|
+
return JSON.parse(JSON.stringify(value, (key, item) => {
|
|
32
|
+
if (key === 'measuredAdapterMs' || key === 'startedAt') return undefined
|
|
33
|
+
return item
|
|
34
|
+
}))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runOf(caseId, rowId) {
|
|
38
|
+
const found = report.cases.find(run => run.caseId === caseId && run.rowId === rowId)
|
|
39
|
+
assert.ok(found, 'missing run ' + caseId + '/' + rowId)
|
|
40
|
+
return found
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function coveredLabelCount(run) {
|
|
44
|
+
return run.labels.filter(label => label.covered).length
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ---------------- 数据集、固定快照与 pin ---------------- */
|
|
48
|
+
|
|
49
|
+
test('fixture dataset loads with every case pinned by throughSeq and snapshot sha256', () => {
|
|
50
|
+
const dataset = ab.loadDataset({})
|
|
51
|
+
assert.equal(dataset.status, 'synthetic-proxy-not-gold')
|
|
52
|
+
assert.equal(dataset.draft, true)
|
|
53
|
+
assert.equal(dataset.notGold, true)
|
|
54
|
+
assert.equal(dataset.gold, false)
|
|
55
|
+
assert.equal(dataset.review, 'pending')
|
|
56
|
+
assert.equal(dataset.cases.length, 14)
|
|
57
|
+
assert.match(dataset.digest, /^[a-f0-9]{64}$/)
|
|
58
|
+
for (const entry of dataset.cases) {
|
|
59
|
+
assert.match(entry.snapshot.digest, /^[a-f0-9]{64}$/)
|
|
60
|
+
assert.equal(entry.snapshot.digest, entry.manifest.snapshotSha256, entry.id + ' digest must equal the manifest pin')
|
|
61
|
+
assert.equal(entry.snapshot.throughSeq, entry.manifest.throughSeq, entry.id + ' throughSeq must equal the manifest pin')
|
|
62
|
+
const expectedTail = entry.def.snapshot.pendingCheck === true ? entry.snapshot.tailEvents : 0
|
|
63
|
+
assert.equal(entry.snapshot.events.length, entry.snapshot.throughSeq + 1 + expectedTail, entry.id + ' only a pendingCheck case may carry a tail past the pinned snapshot')
|
|
64
|
+
for (let index = 0; index < entry.snapshot.events.length; index += 1) {
|
|
65
|
+
assert.equal(entry.snapshot.events[index].seq, index, entry.id + ' seq must equal the assembled position')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('editing a case without re-pinning the digest fails loudly', () => {
|
|
71
|
+
const copy = tempRoot('tamper')
|
|
72
|
+
fs.cpSync(ab.FIXTURE_DIR, copy, { recursive: true })
|
|
73
|
+
const file = path.join(copy, 'cases', '03-clean-starved-small.json')
|
|
74
|
+
const caseDef = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
75
|
+
caseDef.snapshot.events[1].data.content[0].text = caseDef.snapshot.events[1].data.content[0].text + '(改过)'
|
|
76
|
+
fs.writeFileSync(file, JSON.stringify(caseDef, null, 2), 'utf8')
|
|
77
|
+
assert.throws(() => ab.loadDataset({ fixtureDir: copy }), /does not match the manifest pin/)
|
|
78
|
+
const lenient = ab.loadDataset({ fixtureDir: copy, verifyPins: false })
|
|
79
|
+
assert.equal(lenient.cases.length, 14, 'a digest review pass can still read the dataset')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('snapshot digest ignores events beyond throughSeq and reacts to real edits', () => {
|
|
83
|
+
const dataset = ab.loadDataset({})
|
|
84
|
+
const entry = dataset.cases[0]
|
|
85
|
+
const events = entry.snapshot.events.map(event => ({ ...event }))
|
|
86
|
+
const base = ab.computeSnapshotDigest({ fromSeq: entry.snapshot.fromSeq, throughSeq: entry.snapshot.throughSeq, events })
|
|
87
|
+
assert.equal(base, entry.snapshot.digest)
|
|
88
|
+
const tailOnly = events.concat([{ type: 'turn/end', seq: events.length, at: events[events.length - 1].at }])
|
|
89
|
+
assert.equal(ab.computeSnapshotDigest({ fromSeq: 0, throughSeq: entry.snapshot.throughSeq, events: tailOnly }), base, 'a future event must not change the fixed snapshot')
|
|
90
|
+
const edited = events.map((event, index) => index === 1 ? { ...event, at: '2026-10-30T03:00:00.000Z' } : event)
|
|
91
|
+
assert.notEqual(ab.computeSnapshotDigest({ fromSeq: 0, throughSeq: entry.snapshot.throughSeq, events: edited }), base)
|
|
92
|
+
assert.notEqual(ab.computeSnapshotDigest({ fromSeq: 0, throughSeq: entry.snapshot.throughSeq - 1, events }), base)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('canonical serialization is key-order stable so digests survive formatting changes', () => {
|
|
96
|
+
assert.equal(ab.canonicalJson({ b: 1, a: [2, { d: 3, c: 4 }] }), ab.canonicalJson({ a: [2, { c: 4, d: 3 }], b: 1 }))
|
|
97
|
+
assert.notEqual(ab.canonicalJson({ a: [1, 2] }), ab.canonicalJson({ a: [2, 1] }))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('the padding generator is deterministic and its polling text dedupes to one work unit', () => {
|
|
101
|
+
const dataset = ab.loadDataset({})
|
|
102
|
+
const entry = dataset.cases.find(item => item.id === 'clean-large-thin')
|
|
103
|
+
assert.ok(entry.snapshot.eventCount > 2000, 'large case must cross the large-transcript threshold, got ' + entry.snapshot.eventCount)
|
|
104
|
+
const first = ab.expandCaseSnapshot(entry.def)
|
|
105
|
+
const second = ab.expandCaseSnapshot(entry.def)
|
|
106
|
+
assert.equal(first.digest, second.digest, 'padding expansion must be reproducible')
|
|
107
|
+
const toolCalls = first.events.filter(event => event.type === 'tool/call').length
|
|
108
|
+
assert.ok(toolCalls >= 520, 'padding should produce tool calls, got ' + toolCalls)
|
|
109
|
+
const evidence = activeMemory.collectEvidence(first.events)
|
|
110
|
+
assert.ok(evidence.toolUnits <= 3, 'identical polling results must collapse, got toolUnits=' + evidence.toolUnits)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/* ---------------- 不做金标、不做真实模型调用 ---------------- */
|
|
114
|
+
|
|
115
|
+
test('every fixture is an unconfirmed synthetic proxy, never a gold label', () => {
|
|
116
|
+
const dataset = ab.loadDataset({})
|
|
117
|
+
for (const entry of dataset.cases) {
|
|
118
|
+
assert.equal(entry.def.annotationSource, 'synthetic-proxy', entry.id)
|
|
119
|
+
assert.equal(entry.def.review, 'pending', entry.id)
|
|
120
|
+
assert.equal(entry.def.gold, false, entry.id)
|
|
121
|
+
assert.equal(entry.def.draft, true, entry.id)
|
|
122
|
+
assert.equal(entry.def.notGold, true, entry.id)
|
|
123
|
+
assert.match(entry.def.annotationNotes, /合成代理/, entry.id + ' must say the label is a synthetic proxy')
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('gold gating rejects a synthetic label promoted to gold or a human label without review', () => {
|
|
128
|
+
const dataset = ab.loadDataset({})
|
|
129
|
+
const caseDef = dataset.cases[0].def
|
|
130
|
+
assert.throws(() => ab.validateCaseShape({ ...caseDef, gold: true }, 'x'), /gold must be false/)
|
|
131
|
+
assert.throws(() => ab.validateCaseShape({ ...caseDef, gold: true, annotationSource: 'human' }, 'x'), /gold must be false/)
|
|
132
|
+
assert.throws(() => ab.validateCaseShape({ ...caseDef, annotationSource: 'human' }, 'x'), /human annotation requires review/)
|
|
133
|
+
assert.throws(() => ab.validateCaseShape({ ...caseDef, review: 'approved' }, 'x'), /unknown review status/)
|
|
134
|
+
const manifest = dataset.manifest
|
|
135
|
+
assert.throws(() => ab.validateManifest({ ...manifest, gold: true }), /gold must be false/)
|
|
136
|
+
assert.throws(() => ab.validateManifest({ ...manifest, status: 'gold-reviewed' }), /status must be/)
|
|
137
|
+
assert.throws(() => ab.validateManifest({ ...manifest, notGold: false }), /notGold must be true/)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('the report states that no model ran and keeps gold gating separate', () => {
|
|
141
|
+
assert.equal(report.gold, false)
|
|
142
|
+
assert.equal(report.model.executed, false)
|
|
143
|
+
assert.equal(report.model.networkCalls, 0)
|
|
144
|
+
assert.equal(report.model.realModelCalls, 0)
|
|
145
|
+
assert.equal(report.model.requiresUserOptIn, true)
|
|
146
|
+
assert.equal(report.goldGating.humanReviewRequired, true)
|
|
147
|
+
assert.equal(report.goldGating.goldAllowed, false)
|
|
148
|
+
assert.equal(report.goldGating.reviewedCases, 0)
|
|
149
|
+
assert.equal(report.goldGating.candidateCases, 14)
|
|
150
|
+
assert.ok(report.unmeasured.length >= 4)
|
|
151
|
+
assert.match(report.pipeline.note, /调度/)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('the harness source contains no network or model client code', () => {
|
|
155
|
+
const source = fs.readFileSync(SCRIPT, 'utf8')
|
|
156
|
+
assert.ok(!/node:https?/.test(source), 'no http client')
|
|
157
|
+
assert.ok(!/node:net/.test(source), 'no raw sockets')
|
|
158
|
+
assert.ok(!/\bfetch\s*\(/.test(source), 'no fetch calls')
|
|
159
|
+
assert.ok(!/openai|deepseek-ai\/dsh-llm|api\./i.test(source), 'no model client')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
/* ---------------- 触发计划:旧行为 / guide / guide+weighted ---------------- */
|
|
163
|
+
|
|
164
|
+
test('a dense turn wakes the weighted gate on score while the idle-only path never checks', () => {
|
|
165
|
+
const weighted = runOf('clean-dense-turn', 'guide-weighted:oracle')
|
|
166
|
+
const legacy = runOf('clean-dense-turn', 'legacy')
|
|
167
|
+
assert.ok(weighted.calls.backgroundChecks >= 1, 'weighted must check')
|
|
168
|
+
assert.ok(weighted.checks.some(check => check.trigger === 'score'), 'wake reason must be score')
|
|
169
|
+
assert.ok(weighted.checks[0].score >= activeMemory.ACTIVE_MEMORY_POLICY.minScore)
|
|
170
|
+
assert.equal(legacy.calls.backgroundChecks, 0, 'no idle gap means the old path never checks')
|
|
171
|
+
assert.ok(legacy.blocked.every(item => item.reason === 'no-idle-gap'))
|
|
172
|
+
assert.equal(weighted.labels.filter(label => label.covered).length, 4)
|
|
173
|
+
assert.equal(legacy.labels.filter(label => label.covered).length, 0)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('guide delivery is recorded but does not change the schedule in these fixtures', () => {
|
|
177
|
+
const legacy = runOf('clean-dense-turn', 'legacy')
|
|
178
|
+
const guide = runOf('clean-dense-turn', 'guide')
|
|
179
|
+
assert.equal(legacy.guide.delivered, false)
|
|
180
|
+
assert.equal(legacy.guide.rulesKey, null)
|
|
181
|
+
assert.equal(guide.guide.delivered, true)
|
|
182
|
+
assert.match(guide.guide.rulesKey, /^[a-f0-9]{16}$/)
|
|
183
|
+
assert.equal(guide.guide.deliveries > 0, true)
|
|
184
|
+
assert.equal(guide.calls.backgroundChecks, legacy.calls.backgroundChecks, 'guide alone must not change when a check happens')
|
|
185
|
+
assert.deepEqual(guide.blocked.map(item => item.reason), legacy.blocked.map(item => item.reason))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('a thin turn stays unchecked, and pure tool traffic has no human turn to trigger on', () => {
|
|
189
|
+
const thin = runOf('clean-thin-turn', 'guide-weighted:oracle')
|
|
190
|
+
assert.equal(thin.calls.backgroundChecks, 0)
|
|
191
|
+
assert.deepEqual(Object.keys(thin.blocked), ['0', '1'])
|
|
192
|
+
assert.ok(thin.blocked.every(item => item.reason === 'low-score'))
|
|
193
|
+
const noHuman = runOf('no-human-turn', 'guide-weighted:oracle')
|
|
194
|
+
assert.equal(noHuman.calls.backgroundChecks, 0)
|
|
195
|
+
assert.ok(noHuman.blocked.some(item => item.reason === 'no-human-turn'))
|
|
196
|
+
assert.equal(noHuman.labelsTotal, 0)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('starvation wakes a small session but not a large transcript', () => {
|
|
200
|
+
const small = runOf('clean-starved-small', 'guide-weighted:oracle')
|
|
201
|
+
assert.ok(small.checks.some(check => check.trigger === 'starved'))
|
|
202
|
+
const large = runOf('clean-large-thin', 'guide-weighted:oracle')
|
|
203
|
+
assert.equal(large.calls.backgroundChecks, 0)
|
|
204
|
+
assert.ok(large.blocked.some(item => item.reason === 'large-transcript-thin'))
|
|
205
|
+
const largeLegacy = runOf('clean-large-thin', 'legacy')
|
|
206
|
+
assert.equal(largeLegacy.calls.backgroundChecks, 1, 'the idle-only path still forks')
|
|
207
|
+
assert.ok(largeLegacy.cost.seedTokensTotal > small.cost.seedTokensTotal * 10, 'the large full-prefix seed costs more than the small full-prefix seed')
|
|
208
|
+
assert.equal(largeLegacy.blocking.mainSessionWaitForCheck, null)
|
|
209
|
+
assert.equal(large.blocking.mainSessionWaitForCheck, null)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('a recent recall injection suppresses the check instead of recording no_value', () => {
|
|
213
|
+
const weighted = runOf('clean-recent-recall', 'guide-weighted:oracle')
|
|
214
|
+
assert.equal(weighted.calls.backgroundChecks, 0, 'suppression is not a check')
|
|
215
|
+
assert.ok(weighted.blocked.some(item => item.reason === 'recent-recall'))
|
|
216
|
+
assert.equal(weighted.outcomeCounts.deferred, 0)
|
|
217
|
+
assert.equal(weighted.cursor.finalSeq, 0)
|
|
218
|
+
const legacy = runOf('clean-recent-recall', 'legacy')
|
|
219
|
+
assert.equal(legacy.calls.backgroundChecks, 1, 'the old path has no suppression')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
/* ---------------- 凭证、游标与半步 ---------------- */
|
|
223
|
+
|
|
224
|
+
test('a half-step snapshot defers and never advances the cursor', () => {
|
|
225
|
+
const run = runOf('clean-half-step', 'guide-weighted:oracle')
|
|
226
|
+
assert.equal(run.checks.length, 1)
|
|
227
|
+
assert.equal(run.checks[0].halfStep, true)
|
|
228
|
+
assert.equal(run.checks[0].status, 'deferred')
|
|
229
|
+
assert.equal(run.checks[0].reasonCode, 'half-step')
|
|
230
|
+
assert.equal(run.checks[0].cursorAdvanced, false)
|
|
231
|
+
assert.deepEqual(run.checks[0].createdIds, [])
|
|
232
|
+
assert.equal(run.cursor.finalSeq, 0, 'a deferred range must not move the cursor')
|
|
233
|
+
assert.equal(run.labels.filter(label => label.covered).length, 0)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('the worker receives the logical range and a complete contiguous seed, never the undelivered tail', () => {
|
|
237
|
+
const seen = []
|
|
238
|
+
return ab.runAb({
|
|
239
|
+
rowIds: ['guide-weighted:no-value'],
|
|
240
|
+
workers: {
|
|
241
|
+
'no-value': ctx => {
|
|
242
|
+
seen.push({
|
|
243
|
+
caseId: ctx.caseId,
|
|
244
|
+
fromSeq: ctx.fromSeq,
|
|
245
|
+
throughSeq: ctx.throughSeq,
|
|
246
|
+
events: ctx.events.length,
|
|
247
|
+
lastSeq: ctx.events[ctx.events.length - 1].seq,
|
|
248
|
+
seedEvents: ctx.seedEvents.length,
|
|
249
|
+
seedLastSeq: ctx.seedEvents[ctx.seedEvents.length - 1].seq,
|
|
250
|
+
})
|
|
251
|
+
return { status: 'completed', outcome: 'no_value', createdIds: [], updatedIds: [], heldIds: [], reasonCode: 'no-candidate', retryable: false }
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}).then(inner => {
|
|
255
|
+
assert.ok(seen.length > 0)
|
|
256
|
+
for (const call of seen) {
|
|
257
|
+
assert.equal(call.events, call.throughSeq - call.fromSeq + 1, 'ctx.events must cover exactly the requested logical range')
|
|
258
|
+
assert.equal(call.lastSeq, call.throughSeq)
|
|
259
|
+
assert.equal(call.seedEvents, call.throughSeq + 1, 'ctx.seedEvents must be the complete contiguous prefix')
|
|
260
|
+
assert.equal(call.seedLastSeq, call.throughSeq, 'the complete prefix must end at the checked boundary')
|
|
261
|
+
const entry = inner.cases.find(run => run.caseId === call.caseId && run.rowId === 'guide-weighted:no-value')
|
|
262
|
+
assert.ok(entry.snapshot.tailEvents >= 0)
|
|
263
|
+
if (call.caseId === 'clean-half-step') assert.ok(entry.snapshot.tailEvents > 0, 'the half-step case must keep a tail outside the snapshot')
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('later logical checks still seed and estimate the full prefix in every arm', async () => {
|
|
269
|
+
const dir = tempRoot('full-prefix-seed')
|
|
270
|
+
fs.cpSync(ab.FIXTURE_DIR, dir, { recursive: true })
|
|
271
|
+
const manifestFile = path.join(dir, 'manifest.json')
|
|
272
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'))
|
|
273
|
+
manifest.cases = manifest.cases.filter(entry => entry.id === 'clean-starved-small')
|
|
274
|
+
const entry = manifest.cases[0]
|
|
275
|
+
const caseFile = path.join(dir, entry.file)
|
|
276
|
+
const caseDef = JSON.parse(fs.readFileSync(caseFile, 'utf8'))
|
|
277
|
+
const originalEvents = caseDef.snapshot.events
|
|
278
|
+
const laterEvents = originalEvents.map(event => ({
|
|
279
|
+
...event, at: new Date(Date.parse(event.at) + 60 * 60 * 1000).toISOString(),
|
|
280
|
+
}))
|
|
281
|
+
caseDef.snapshot.events = originalEvents.concat(laterEvents)
|
|
282
|
+
caseDef.labels.expected.push({
|
|
283
|
+
...caseDef.labels.expected[0], id: 'mem-ab-later-rule', sourceSeq: originalEvents.length + 1,
|
|
284
|
+
})
|
|
285
|
+
delete caseDef.expect
|
|
286
|
+
const snapshot = ab.expandCaseSnapshot(caseDef)
|
|
287
|
+
entry.throughSeq = snapshot.throughSeq
|
|
288
|
+
entry.snapshotSha256 = snapshot.digest
|
|
289
|
+
fs.writeFileSync(caseFile, JSON.stringify(caseDef), 'utf8')
|
|
290
|
+
fs.writeFileSync(manifestFile, JSON.stringify(manifest), 'utf8')
|
|
291
|
+
|
|
292
|
+
const seen = []
|
|
293
|
+
const capture = worker => ctx => { seen.push(ctx); return worker(ctx) }
|
|
294
|
+
const result = await ab.runAb({ fixtureDir: dir, workers: {
|
|
295
|
+
oracle: capture(ab.oracleWorker), 'no-value': capture(ab.noValueWorker),
|
|
296
|
+
} })
|
|
297
|
+
for (const run of result.cases) {
|
|
298
|
+
assert.equal(run.checks.length, 2, run.rowId)
|
|
299
|
+
const contexts = seen.filter(ctx => ctx.rowId === run.rowId)
|
|
300
|
+
assert.equal(contexts.length, 2, run.rowId)
|
|
301
|
+
for (const [index, check] of run.checks.entries()) {
|
|
302
|
+
const ctx = contexts[index]
|
|
303
|
+
const prefix = snapshot.events.slice(0, check.throughSeq + 1)
|
|
304
|
+
assert.deepEqual(ctx.seedEvents, prefix, run.rowId + ' actual seed')
|
|
305
|
+
assert.equal(check.seedEvents, prefix.length)
|
|
306
|
+
assert.equal(check.seedTokens, estTokens(prefix.map(ab.eventText).join(' ')))
|
|
307
|
+
assert.deepEqual(ctx.events, snapshot.events.slice(check.fromSeq, check.throughSeq + 1))
|
|
308
|
+
assert.deepEqual(ctx.evidence, activeMemory.collectEvidence(ctx.events))
|
|
309
|
+
assert.deepEqual(ctx.labelsInRange.map(label => label.id), caseDef.labels.expected
|
|
310
|
+
.filter(label => label.sourceSeq >= check.fromSeq && label.sourceSeq <= check.throughSeq)
|
|
311
|
+
.map(label => label.id))
|
|
312
|
+
}
|
|
313
|
+
const last = contexts[1]
|
|
314
|
+
if (run.path === 'snapshot-worker') {
|
|
315
|
+
assert.equal(last.fromSeq, originalEvents.length)
|
|
316
|
+
assert.ok(last.seedEvents.length > last.events.length, 'not a windowed seed')
|
|
317
|
+
assert.deepEqual(last.labelsInRange.map(label => label.id), ['mem-ab-later-rule'])
|
|
318
|
+
assert.deepEqual(last.existingIds, [], 'old seed content is not newly eligible')
|
|
319
|
+
assert.ok(run.checks[1].seedTokens > estTokens(last.events.map(ab.eventText).join(' ')))
|
|
320
|
+
} else {
|
|
321
|
+
assert.equal(last.fromSeq, 0)
|
|
322
|
+
assert.deepEqual(last.seedEvents, last.events)
|
|
323
|
+
}
|
|
324
|
+
assert.equal(run.cost.seedTokensTotal, run.checks.reduce((sum, check) => sum + check.seedTokens, 0))
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('both fork paths leave parent blocking and wait counts unmeasured', () => {
|
|
329
|
+
assert.deepEqual(new Set(report.cases.map(run => run.path)), new Set(['full-prefix-fork', 'snapshot-worker']))
|
|
330
|
+
for (const run of report.cases) {
|
|
331
|
+
assert.equal(run.blocking.structural, 'parent-blocking-not-measured')
|
|
332
|
+
assert.equal(run.blocking.mainSessionWaitForCheck, null)
|
|
333
|
+
assert.match(run.blocking.note, /not measured/)
|
|
334
|
+
}
|
|
335
|
+
for (const summary of report.mainAxis.rows) {
|
|
336
|
+
assert.equal(summary.blocking.mainSessionWaitForCheck, null)
|
|
337
|
+
assert.match(summary.blocking.note, /not measured/)
|
|
338
|
+
}
|
|
339
|
+
for (const item of report.mainAxis.deltas) assert.equal(Object.hasOwn(item.delta, 'mainSessionWaitForCheck'), false)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('credential validation rejects mismatched identity and contradictory buckets', () => {
|
|
343
|
+
const request = { fromSeq: 4, throughSeq: 9, snapshotSha256: 'a'.repeat(64), scopeGeneration: 1 }
|
|
344
|
+
const good = {
|
|
345
|
+
checked: { fromSeq: 4, throughSeq: 9, snapshotSha256: 'a'.repeat(64), scopeGeneration: 1 },
|
|
346
|
+
status: 'completed',
|
|
347
|
+
outcome: 'created',
|
|
348
|
+
createdIds: ['mem-ab-x'],
|
|
349
|
+
updatedIds: [],
|
|
350
|
+
heldIds: [],
|
|
351
|
+
reasonCode: 'created',
|
|
352
|
+
retryable: false,
|
|
353
|
+
}
|
|
354
|
+
assert.deepEqual(ab.validateCredential(good, request), [])
|
|
355
|
+
assert.equal(ab.canAdvance(good), true)
|
|
356
|
+
|
|
357
|
+
const wrongDigest = { ...good, checked: { ...good.checked, snapshotSha256: 'b'.repeat(64) } }
|
|
358
|
+
assert.match(ab.validateCredential(wrongDigest, request).join(';'), /snapshotSha256/)
|
|
359
|
+
const wrongRange = { ...good, checked: { ...good.checked, throughSeq: 10 } }
|
|
360
|
+
assert.match(ab.validateCredential(wrongRange, request).join(';'), /throughSeq/)
|
|
361
|
+
const wrongScope = { ...good, checked: { ...good.checked, scopeGeneration: 2 } }
|
|
362
|
+
assert.match(ab.validateCredential(wrongScope, request).join(';'), /scopeGeneration/)
|
|
363
|
+
|
|
364
|
+
const badStatus = { ...good, status: 'done' }
|
|
365
|
+
assert.match(ab.validateCredential(badStatus, request).join(';'), /unknown status/)
|
|
366
|
+
const doubleBucket = { ...good, heldIds: ['mem-ab-x'] }
|
|
367
|
+
assert.match(ab.validateCredential(doubleBucket, request).join(';'), /more than one bucket/)
|
|
368
|
+
const deferredWithIds = { ...good, status: 'deferred' }
|
|
369
|
+
assert.match(ab.validateCredential(deferredWithIds, request).join(';'), /must not claim written ids/)
|
|
370
|
+
const emptyNoValue = { ...good, outcome: 'no_value' }
|
|
371
|
+
assert.match(ab.validateCredential(emptyNoValue, request).join(';'), /no_value with created/)
|
|
372
|
+
const claimedButEmpty = { ...good, createdIds: [] }
|
|
373
|
+
assert.match(ab.validateCredential(claimedButEmpty, request).join(';'), /without any ids/)
|
|
374
|
+
|
|
375
|
+
assert.equal(ab.canAdvance({ ...good, status: 'deferred' }), false)
|
|
376
|
+
assert.equal(ab.canAdvance({ ...good, status: 'failed' }), false)
|
|
377
|
+
assert.equal(ab.canAdvance({ ...good, status: 'skipped' }), false)
|
|
378
|
+
assert.equal(ab.canAdvance({ ...good, outcome: 'no_value', createdIds: [], reasonCode: 'no-candidate' }), true, 'a completed no_value is a normal result')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('an injected worker that reports skipped tools keeps the cursor where it was', async () => {
|
|
382
|
+
const inner = await ab.runAb({
|
|
383
|
+
rowIds: ['guide-weighted:no-value'],
|
|
384
|
+
workers: {
|
|
385
|
+
'no-value': () => ({ status: 'skipped', outcome: 'no_value', createdIds: [], updatedIds: [], heldIds: [], reasonCode: 'unsupported-tools', retryable: true }),
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
const skipped = inner.cases.filter(run => run.checks.length > 0)
|
|
389
|
+
assert.ok(skipped.length > 0)
|
|
390
|
+
for (const run of skipped) {
|
|
391
|
+
for (const check of run.checks) {
|
|
392
|
+
if (check.halfStep) continue
|
|
393
|
+
assert.equal(check.status, 'skipped')
|
|
394
|
+
assert.equal(check.cursorAdvanced, false)
|
|
395
|
+
assert.deepEqual(check.problems, [])
|
|
396
|
+
}
|
|
397
|
+
assert.equal(run.cursor.finalSeq, 0)
|
|
398
|
+
if (run.checks.some(check => !check.halfStep)) assert.ok(run.outcomeCounts.skipped > 0, 'the injected worker reported skipped')
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('checked ranges are contiguous and every credential is valid', () => {
|
|
403
|
+
assert.deepEqual(report.structureChecks, [])
|
|
404
|
+
assert.equal(report.ok, true)
|
|
405
|
+
for (const run of report.cases) {
|
|
406
|
+
let expectedFrom = run.cursor.startedAtSeq
|
|
407
|
+
for (const check of run.checks) {
|
|
408
|
+
assert.equal(check.fromSeq, expectedFrom, run.caseId + '/' + run.rowId + ' range must continue from the cursor')
|
|
409
|
+
assert.equal(check.credentialValid, true)
|
|
410
|
+
if (check.cursorAdvanced) expectedFrom = check.throughSeq + 1
|
|
411
|
+
}
|
|
412
|
+
if (run.cursor.mode === 'snapshot-credit-contiguous') {
|
|
413
|
+
assert.equal(run.cursor.finalSeq, expectedFrom)
|
|
414
|
+
if (run.checks.length > 0 && run.checks[run.checks.length - 1].cursorAdvanced) {
|
|
415
|
+
assert.equal(run.cursor.finalSeq, run.checks[run.checks.length - 1].throughSeq + 1)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
/* ---------------- 覆盖、延迟、重复、held、成本 ---------------- */
|
|
422
|
+
|
|
423
|
+
test('coverage is a bounded interval, not a model measurement', () => {
|
|
424
|
+
const rows = report.mainAxis.rows
|
|
425
|
+
const oracle = rows.find(row => row.row === 'guide-weighted:oracle')
|
|
426
|
+
const lower = rows.find(row => row.row === 'guide-weighted:no-value')
|
|
427
|
+
const legacy = rows.find(row => row.row === 'legacy')
|
|
428
|
+
assert.ok(oracle.coverage.covered >= lower.coverage.covered)
|
|
429
|
+
assert.equal(lower.coverage.background, 0, 'a worker that always answers no_value adds no background coverage')
|
|
430
|
+
assert.equal(lower.coverage.covered, lower.coverage.mainProactive, 'only main-model writes remain covered under the lower bound')
|
|
431
|
+
// The scale gradient (clean-scale-100/1000/5000/20000) shows the guard trades
|
|
432
|
+
// coverage for cost above largeTranscriptEvents: below it the weighted path checks
|
|
433
|
+
// once like legacy does, above it the thin turn is suppressed entirely and only the
|
|
434
|
+
// old idle path still forks. So there is no global "weighted covers more than
|
|
435
|
+
// legacy" ordering — asserting one was only ever true of the original 8-case mix.
|
|
436
|
+
for (const id of ['clean-scale-100', 'clean-scale-1000']) {
|
|
437
|
+
assert.equal(runOf(id, 'guide-weighted:oracle').calls.backgroundChecks, 1, id + ' is still under the large-transcript guard')
|
|
438
|
+
assert.equal(runOf(id, 'legacy').calls.backgroundChecks, 1, id + ' forks on the idle path too')
|
|
439
|
+
assert.equal(coveredLabelCount(runOf(id, 'guide-weighted:oracle')), coveredLabelCount(runOf(id, 'legacy')), id + ' covers the same under both conditions')
|
|
440
|
+
}
|
|
441
|
+
for (const id of ['clean-scale-5000', 'clean-scale-20000']) {
|
|
442
|
+
assert.equal(runOf(id, 'guide-weighted:oracle').calls.backgroundChecks, 0, id + ' is suppressed by the large-transcript guard')
|
|
443
|
+
assert.equal(runOf(id, 'legacy').calls.backgroundChecks, 1, id + ' still forks: the old path has no guard')
|
|
444
|
+
assert.ok(coveredLabelCount(runOf(id, 'guide-weighted:oracle')) < coveredLabelCount(runOf(id, 'legacy')), id + ' is the measured cost of the guard')
|
|
445
|
+
}
|
|
446
|
+
assert.equal(oracle.coverage.labelsTotal, lower.coverage.labelsTotal)
|
|
447
|
+
assert.match(report.bounds.coverageUpper, /oracle/)
|
|
448
|
+
assert.match(report.bounds.coverageLower, /no-value/)
|
|
449
|
+
assert.match(oracle.coverage.interpretation, /上界/)
|
|
450
|
+
assert.equal(oracle.cost.estimateOnly, true)
|
|
451
|
+
assert.match(oracle.cost.method, /estTokens/)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('coverage counts main-model writes separately and never as background coverage', () => {
|
|
455
|
+
const run = runOf('clean-duplicate-reread', 'guide-weighted:oracle')
|
|
456
|
+
const main = run.labels.find(label => label.id === 'mem-ab-dup-no-window')
|
|
457
|
+
assert.equal(main.coveredBy, 'main')
|
|
458
|
+
const background = run.labels.find(label => label.id === 'mem-ab-dup-credential-fields')
|
|
459
|
+
assert.equal(background.coveredBy, 'background')
|
|
460
|
+
assert.ok(run.checks[0].reproposed >= 1, 'a full-prefix replay re-proposes what main already wrote')
|
|
461
|
+
assert.equal(run.calls.memoryToolCalls, 1)
|
|
462
|
+
assert.equal(run.calls.memoryWriteCalls, 1)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('held conflicts are reported separately and excluded from coverage', () => {
|
|
466
|
+
const run = runOf('global-irrelevant-conflict', 'guide-weighted:oracle')
|
|
467
|
+
const conflict = run.labels.find(label => label.id === 'mem-ab-global-backup-change')
|
|
468
|
+
assert.equal(conflict.coveredBy, 'background-held')
|
|
469
|
+
assert.equal(conflict.covered, false)
|
|
470
|
+
assert.equal(conflict.latency, null)
|
|
471
|
+
assert.ok(run.checks[0].heldIds.includes('mem-ab-global-backup-change'))
|
|
472
|
+
assert.equal(run.checks[0].outcome, 'mixed')
|
|
473
|
+
const port = run.labels.find(label => label.id === 'mem-ab-global-port')
|
|
474
|
+
assert.equal(port.coveredBy, 'background')
|
|
475
|
+
assert.ok(port.latency.humanTurns >= 0)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('latency, no_value rate, calls per step and cost are all recorded', () => {
|
|
479
|
+
const run = runOf('clean-starved-small', 'guide-weighted:oracle')
|
|
480
|
+
assert.equal(run.checks.length, 1)
|
|
481
|
+
assert.equal(run.checks[0].outcome, 'created')
|
|
482
|
+
assert.ok(run.checks[0].evidence.humanTurns >= 1)
|
|
483
|
+
assert.ok(run.checks[0].seedTokens > 0)
|
|
484
|
+
const label = run.labels.find(item => item.covered)
|
|
485
|
+
assert.ok(label.latency && Number.isFinite(label.latency.humanTurns))
|
|
486
|
+
const row = report.mainAxis.rows.find(item => item.row === 'guide-weighted:oracle')
|
|
487
|
+
assert.ok(Number.isFinite(row.noValue.rate))
|
|
488
|
+
assert.ok(Number.isFinite(row.calls.per1000Steps))
|
|
489
|
+
assert.ok(row.calls.backgroundChecks > 0)
|
|
490
|
+
assert.ok(row.evidence.humanTurns > 0)
|
|
491
|
+
assert.ok(row.cost.seedTokensTotal > 0)
|
|
492
|
+
assert.equal(row.statuses.invalidCredential, 0)
|
|
493
|
+
assert.equal(row.statuses.failed, 0)
|
|
494
|
+
assert.match(row.blocking.note, /unknown and not measured/)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('fixture expectations match what the weighted row actually produces', () => {
|
|
498
|
+
const dataset = ab.loadDataset({})
|
|
499
|
+
for (const entry of dataset.cases) {
|
|
500
|
+
const expect = entry.def.expect
|
|
501
|
+
assert.ok(expect, entry.id + ' must declare expect')
|
|
502
|
+
const run = runOf(entry.id, 'guide-weighted:oracle')
|
|
503
|
+
const where = entry.id + ': '
|
|
504
|
+
if (expect.minBackgroundChecks !== undefined) {
|
|
505
|
+
assert.ok(run.calls.backgroundChecks >= expect.minBackgroundChecks, where + 'expected at least ' + expect.minBackgroundChecks + ' checks, got ' + run.calls.backgroundChecks)
|
|
506
|
+
}
|
|
507
|
+
if (expect.maxBackgroundChecks !== undefined) {
|
|
508
|
+
assert.ok(run.calls.backgroundChecks <= expect.maxBackgroundChecks, where + 'expected at most ' + expect.maxBackgroundChecks + ' checks, got ' + run.calls.backgroundChecks)
|
|
509
|
+
}
|
|
510
|
+
if (expect.wakeReasons) {
|
|
511
|
+
for (const reason of expect.wakeReasons) {
|
|
512
|
+
assert.ok(run.checks.some(check => check.trigger === reason), where + 'missing wake reason ' + reason)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (expect.blockedReasons) {
|
|
516
|
+
for (const reason of expect.blockedReasons) {
|
|
517
|
+
assert.ok(run.blocked.some(item => item.reason === reason), where + 'missing blocked reason ' + reason)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (expect.deferredAtLeast !== undefined) {
|
|
521
|
+
assert.ok(run.outcomeCounts.deferred >= expect.deferredAtLeast, where + 'expected deferred decisions')
|
|
522
|
+
}
|
|
523
|
+
if (expect.heldAtLeast !== undefined) {
|
|
524
|
+
const held = run.checks.reduce((sum, check) => sum + check.heldIds.length, 0)
|
|
525
|
+
assert.ok(held >= expect.heldAtLeast, where + 'expected held ids, got ' + held)
|
|
526
|
+
}
|
|
527
|
+
if (expect.duplicatesReproposedAtLeast !== undefined) {
|
|
528
|
+
const reproposed = run.checks.reduce((sum, check) => sum + check.reproposed, 0)
|
|
529
|
+
assert.ok(reproposed >= expect.duplicatesReproposedAtLeast, where + 'expected re-proposed ids, got ' + reproposed)
|
|
530
|
+
}
|
|
531
|
+
for (const id of expect.coveredLabelIds || []) {
|
|
532
|
+
const label = run.labels.find(item => item.id === id)
|
|
533
|
+
assert.ok(label, where + 'unknown label ' + id)
|
|
534
|
+
assert.equal(label.covered, true, where + 'expected ' + id + ' to be covered')
|
|
535
|
+
}
|
|
536
|
+
for (const id of expect.uncoveredLabelIds || []) {
|
|
537
|
+
const label = run.labels.find(item => item.id === id)
|
|
538
|
+
assert.ok(label, where + 'unknown label ' + id)
|
|
539
|
+
assert.equal(label.covered, false, where + 'expected ' + id + ' to stay uncovered')
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
/* ---------------- 全局干扰是独立维度 ---------------- */
|
|
545
|
+
|
|
546
|
+
test('global interference is its own axis and never mixes into the main table', () => {
|
|
547
|
+
const levels = report.interferenceAxis.rows.map(row => row.level)
|
|
548
|
+
assert.deepEqual(levels, ['off', 'relevant', 'irrelevant-conflict'])
|
|
549
|
+
const offCases = report.cases.filter(run => run.interference === 'off' && run.rowId === 'guide-weighted:oracle').length
|
|
550
|
+
const weightedRow = report.mainAxis.rows.find(row => row.row === 'guide-weighted:oracle')
|
|
551
|
+
assert.equal(weightedRow.cases, offCases, 'the main axis must be computed on interference = off only')
|
|
552
|
+
assert.equal(report.cases.filter(run => run.interference !== 'off').length > 0, true)
|
|
553
|
+
const relevant = report.interferenceAxis.rows.find(row => row.level === 'relevant')
|
|
554
|
+
const off = report.interferenceAxis.rows.find(row => row.level === 'off')
|
|
555
|
+
assert.ok(relevant.anchorTokens > off.anchorTokens, 'relevant global entries cost budget')
|
|
556
|
+
assert.equal(off.anchorTokens, 0, 'interference = off injects nothing')
|
|
557
|
+
for (const row of report.interferenceAxis.rows) assert.equal(row.leaks, 0)
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('global entries never leak into private writes', () => {
|
|
561
|
+
for (const run of report.cases) {
|
|
562
|
+
assert.equal(run.interferenceReport.leaks, 0, run.caseId + '/' + run.rowId + ' leaked a global entry')
|
|
563
|
+
const globalIds = new Set((run.def_globalIds || run.interferenceReport.globalIds) || [])
|
|
564
|
+
for (const check of run.checks) {
|
|
565
|
+
for (const id of check.createdIds) assert.ok(!globalIds.has(id))
|
|
566
|
+
for (const id of check.heldIds) assert.ok(!globalIds.has(id))
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const relevant = runOf('global-relevant', 'guide-weighted:oracle')
|
|
570
|
+
assert.deepEqual(relevant.interferenceReport.globalIds, ['mem-gb-relevant-curator', 'mem-gb-relevant-scope'])
|
|
571
|
+
const conflict = runOf('global-irrelevant-conflict', 'guide-weighted:oracle')
|
|
572
|
+
assert.deepEqual(conflict.interferenceReport.conflictLabelIds, ['mem-ab-global-backup-change'])
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
/* ---------------- 可复现与 CLI ---------------- */
|
|
576
|
+
|
|
577
|
+
test('two runs of the harness produce identical reports apart from measured timings', async () => {
|
|
578
|
+
const again = await ab.runAb({})
|
|
579
|
+
assert.deepEqual(stable(again), stable(report))
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('caseLimit and row selection keep the run small and still well formed', async () => {
|
|
583
|
+
const inner = await ab.runAb({ caseLimit: 2, rowIds: ['legacy', 'guide-weighted:no-value'] })
|
|
584
|
+
assert.equal(inner.cases.length, 4)
|
|
585
|
+
assert.deepEqual(inner.mainAxis.rows.map(row => row.row), ['legacy', 'guide-weighted:no-value'])
|
|
586
|
+
assert.equal(inner.cases.filter(run => run.caseId === 'clean-dense-turn').length, 2)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
test('parseArgs accepts the documented flags and refuses anything that would call a model', () => {
|
|
590
|
+
const options = ab.parseArgs(['--cases', '3', '--row', 'legacy', '--out', 'x.json', '--digests'])
|
|
591
|
+
assert.equal(options.caseLimit, 3)
|
|
592
|
+
assert.deepEqual(options.rowIds, ['legacy'])
|
|
593
|
+
assert.equal(options.out, 'x.json')
|
|
594
|
+
assert.equal(options.digests, true)
|
|
595
|
+
assert.throws(() => ab.parseArgs(['--samples', '1']), /unknown argument/)
|
|
596
|
+
for (const flag of ab.REAL_MODEL_FLAGS) {
|
|
597
|
+
assert.throws(() => ab.parseArgs([flag]), /explicit user decision and is not implemented/)
|
|
598
|
+
}
|
|
599
|
+
assert.throws(() => ab.parseArgs(['--row', 'nope']), /unknown row/)
|
|
600
|
+
assert.throws(() => ab.parseArgs(['--cases', '0']), /positive integer/)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
test('the CLI writes a CRLF JSON report and prints the pinned digests on request', () => {
|
|
604
|
+
const dir = tempRoot('cli')
|
|
605
|
+
const out = path.join(dir, 'report.json')
|
|
606
|
+
const stdout = execFileSync(process.execPath, [SCRIPT, '--out', out], { encoding: 'utf8', cwd: ROOT })
|
|
607
|
+
assert.match(stdout, /wrote/)
|
|
608
|
+
const raw = fs.readFileSync(out, 'utf8')
|
|
609
|
+
assert.ok(raw.includes('\r\n'), 'report must be written with CRLF')
|
|
610
|
+
const parsed = JSON.parse(raw)
|
|
611
|
+
assert.equal(parsed.kind, 'active-memory-ab')
|
|
612
|
+
assert.equal(parsed.ok, true)
|
|
613
|
+
assert.equal(parsed.status, 'synthetic-proxy-not-gold')
|
|
614
|
+
assert.equal(parsed.model.executed, false)
|
|
615
|
+
const digests = execFileSync(process.execPath, [SCRIPT, '--digests'], { encoding: 'utf8', cwd: ROOT })
|
|
616
|
+
assert.match(digests, /throughSeq=\d+ {2}sha256=[a-f0-9]{64} {2}\(manifest pin matches\)/)
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
test('the harness never touches a user memory directory', async () => {
|
|
620
|
+
const home = tempRoot('home')
|
|
621
|
+
const sentinel = path.join(home, 'sentinel.txt')
|
|
622
|
+
fs.writeFileSync(sentinel, 'do not touch the user library\n', 'utf8')
|
|
623
|
+
const before = fs.readdirSync(home).sort()
|
|
624
|
+
const previousHome = process.env.DSH_MEMU_HOME
|
|
625
|
+
const previousMemoryHome = process.env.DSH_MEMU_MEMORY_HOME
|
|
626
|
+
process.env.DSH_MEMU_HOME = home
|
|
627
|
+
process.env.DSH_MEMU_MEMORY_HOME = home
|
|
628
|
+
try {
|
|
629
|
+
const inner = await ab.runAb({ caseLimit: 3 })
|
|
630
|
+
assert.equal(inner.ok, true)
|
|
631
|
+
} finally {
|
|
632
|
+
if (previousHome === undefined) delete process.env.DSH_MEMU_HOME
|
|
633
|
+
else process.env.DSH_MEMU_HOME = previousHome
|
|
634
|
+
if (previousMemoryHome === undefined) delete process.env.DSH_MEMU_MEMORY_HOME
|
|
635
|
+
else process.env.DSH_MEMU_MEMORY_HOME = previousMemoryHome
|
|
636
|
+
}
|
|
637
|
+
assert.deepEqual(fs.readdirSync(home).sort(), before)
|
|
638
|
+
assert.equal(fs.readFileSync(sentinel, 'utf8'), 'do not touch the user library\n')
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
/* ---------------- 真 worker 结构行:回执 → 凭证 ---------------- */
|
|
642
|
+
|
|
643
|
+
test('the structural row validates its credential and reproduces the oracle row', async () => {
|
|
644
|
+
const both = await ab.runAb({ rowIds: ['guide-weighted:oracle', 'guide-weighted:structural'] })
|
|
645
|
+
assert.equal(both.ok, true, 'no structural violation in either row')
|
|
646
|
+
assert.deepEqual(both.structureChecks, [], 'both rows keep the checked cursor contiguous')
|
|
647
|
+
|
|
648
|
+
const oracleRow = both.mainAxis.rows.find(row => row.row === 'guide-weighted:oracle')
|
|
649
|
+
const structuralRow = both.mainAxis.rows.find(row => row.row === 'guide-weighted:structural')
|
|
650
|
+
assert.ok(oracleRow && structuralRow)
|
|
651
|
+
// 回执走的是 C 包的 parseReceipt,凭证结论必须与注解预言机逐项相同:
|
|
652
|
+
// 否则说明"真 worker 的产出形状"和 §13.6 的凭证规则对不上。
|
|
653
|
+
assert.equal(structuralRow.coverage.covered, oracleRow.coverage.covered)
|
|
654
|
+
assert.equal(structuralRow.coverage.labelsTotal, oracleRow.coverage.labelsTotal)
|
|
655
|
+
assert.equal(structuralRow.calls.backgroundChecks, oracleRow.calls.backgroundChecks)
|
|
656
|
+
assert.equal(structuralRow.cost.seedTokensTotal, oracleRow.cost.seedTokensTotal)
|
|
657
|
+
|
|
658
|
+
for (const id of new Set(both.cases.map(run => run.caseId))) {
|
|
659
|
+
const oracle = both.cases.find(run => run.caseId === id && run.rowId === 'guide-weighted:oracle')
|
|
660
|
+
const structural = both.cases.find(run => run.caseId === id && run.rowId === 'guide-weighted:structural')
|
|
661
|
+
const covered = run => run.labels.filter(label => label.covered).map(label => label.id).sort()
|
|
662
|
+
assert.deepEqual(covered(structural), covered(oracle), id + ' covers the same labels through the receipt path')
|
|
663
|
+
assert.equal(structural.calls.backgroundChecks, oracle.calls.backgroundChecks, id + ' checks the same number of times')
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
test('a deferred or skipped receipt never claims ids and never advances the cursor', () => {
|
|
668
|
+
const skipped = ab.receiptToCredential({ status: 'skipped', outcome: 'held', reasonCode: 'conflict', createdIds: ['x'], updatedIds: ['y'], heldIds: ['z'] })
|
|
669
|
+
assert.equal(skipped.outcome, 'no_value', 'an outcome outside the enum falls back instead of failing the whole credential')
|
|
670
|
+
assert.deepEqual(skipped.createdIds, [], 'harness rule: a non-completed status claims no ids')
|
|
671
|
+
assert.deepEqual(skipped.updatedIds, [])
|
|
672
|
+
assert.deepEqual(skipped.heldIds, [])
|
|
673
|
+
assert.equal(ab.canAdvance(skipped), false, 'skipped does not advance the cursor in this harness')
|
|
674
|
+
|
|
675
|
+
const deferredNull = ab.receiptToCredential({ status: 'deferred', outcome: null, reasonCode: 'half-step' })
|
|
676
|
+
assert.equal(deferredNull.outcome, 'no_value')
|
|
677
|
+
assert.equal(deferredNull.status, 'deferred')
|
|
678
|
+
assert.equal(ab.canAdvance(deferredNull), false)
|
|
679
|
+
|
|
680
|
+
const completed = ab.receiptToCredential({ status: 'completed', outcome: 'created', createdIds: ['a'], updatedIds: [], heldIds: [] })
|
|
681
|
+
assert.deepEqual(completed.createdIds, ['a'])
|
|
682
|
+
assert.equal(ab.canAdvance(completed), true)
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('a missing, malformed or invalid receipt is a retryable failure, never no_value', () => {
|
|
686
|
+
const missing = ab.workerStructuralFromMessage('整理完了,没有值得记的。')
|
|
687
|
+
assert.equal(missing.status, 'failed')
|
|
688
|
+
assert.equal(missing.reasonCode, 'receipt-missing')
|
|
689
|
+
assert.equal(missing.retryable, true)
|
|
690
|
+
assert.equal(ab.canAdvance(missing), false)
|
|
691
|
+
|
|
692
|
+
const malformed = ab.workerStructuralFromMessage('```memu-consolidation-receipt\n{ not json\n```')
|
|
693
|
+
assert.equal(malformed.status, 'failed')
|
|
694
|
+
assert.equal(malformed.reasonCode, 'receipt-malformed')
|
|
695
|
+
assert.equal(malformed.retryable, true)
|
|
696
|
+
|
|
697
|
+
const invalid = ab.workerStructuralFromMessage('```memu-consolidation-receipt\n' + JSON.stringify({
|
|
698
|
+
version: 1, status: 'completed', outcome: 'created', createdIds: ['a'],
|
|
699
|
+
}) + '\n```')
|
|
700
|
+
assert.equal(invalid.status, 'failed', 'a terminal receipt without a checkedRange is invalid')
|
|
701
|
+
assert.equal(invalid.reasonCode, 'receipt-invalid')
|
|
702
|
+
assert.equal(invalid.retryable, true)
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
test('the structural row is opt-in and does not change the default report', () => {
|
|
706
|
+
assert.deepEqual(ab.DEFAULT_ROW_IDS, ['legacy', 'guide', 'guide-weighted:oracle', 'guide-weighted:no-value'])
|
|
707
|
+
const rows = report.mainAxis.rows.map(row => row.row)
|
|
708
|
+
assert.equal(rows.includes('guide-weighted:structural'), false, 'the structural row must not enter the default report')
|
|
709
|
+
assert.equal(ab.ROWS.length, 5, 'the row exists in the registry for explicit selection')
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('the contract the worker is told to satisfy is the one this harness parses', async () => {
|
|
713
|
+
const worker = await import(pathToFileURL(path.join(ROOT, 'lib', 'consolidation-worker.js')).href)
|
|
714
|
+
const receipt = await import(pathToFileURL(path.join(ROOT, 'lib', 'consolidation-receipt.js')).href)
|
|
715
|
+
|
|
716
|
+
const snapshot = { fromSeq: 0, throughSeq: 97, eventCount: 98, hash: 'a'.repeat(64) }
|
|
717
|
+
const contract = receipt.receiptContractText({ jobId: 'ab-contract', snapshotHash: snapshot.hash })
|
|
718
|
+
const brief = worker.buildWorkerBrief({ snapshot, probe: { tools: ['memu_private_remember', 'memu_private_recall'] }, contract })
|
|
719
|
+
|
|
720
|
+
assert.ok(brief.includes(contract), 'the brief must carry the receipt contract verbatim')
|
|
721
|
+
assert.ok(brief.includes(String(snapshot.throughSeq)), 'the brief must state the snapshot range')
|
|
722
|
+
assert.ok(brief.includes('memu_private_remember'), 'the brief must name the write tool it expects')
|
|
723
|
+
assert.match(brief, /没有回执视为本次整理失败/)
|
|
724
|
+
|
|
725
|
+
// 把 brief 要求的字段原样交回去,必须能被 parseReceipt 接受:
|
|
726
|
+
// 否则 worker 按任务书写出的东西,harness 读不了。
|
|
727
|
+
const text = [receipt.RECEIPT_FENCE, JSON.stringify({
|
|
728
|
+
version: 1,
|
|
729
|
+
jobId: 'ab-contract',
|
|
730
|
+
snapshotHash: snapshot.hash,
|
|
731
|
+
status: 'completed',
|
|
732
|
+
outcome: 'created',
|
|
733
|
+
checkedRange: { fromSeq: snapshot.fromSeq, throughSeq: snapshot.throughSeq },
|
|
734
|
+
createdIds: ['mem-ab-scale-plan-scope'],
|
|
735
|
+
updatedIds: [],
|
|
736
|
+
heldIds: [],
|
|
737
|
+
retryable: false,
|
|
738
|
+
}), receipt.RECEIPT_FENCE].join('\n')
|
|
739
|
+
const parsed = receipt.parseReceipt(text)
|
|
740
|
+
assert.equal(parsed.ok, true, 'a receipt written to the brief contract must parse: ' + (parsed.problems || parsed.detail || ''))
|
|
741
|
+
|
|
742
|
+
const credential = ab.receiptToCredential(parsed.receipt)
|
|
743
|
+
assert.equal(ab.canAdvance(credential), true)
|
|
744
|
+
assert.deepEqual(credential.createdIds, ['mem-ab-scale-plan-scope'])
|
|
745
|
+
})
|