@jjh32456/dsh-longlive 1.6.7 → 1.8.0
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/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/live.js +22 -2
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +105 -16
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -204
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -568
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1020
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -182
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1044
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
// WP5 K10/K14 — owner CLI shares the proposal JSON/state machine with the
|
|
2
|
-
// tools, is the only P0 channel, and never re-implements WP4 transitions.
|
|
3
|
-
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import crypto from 'node:crypto';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import test from 'node:test';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
import { auditKinds, makeService, readJson, tempRoot, writeMemory } from './helpers.mjs';
|
|
13
|
-
|
|
14
|
-
function sha256Buffer(buffer) {
|
|
15
|
-
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const packageRoot = path.resolve(fileURLToPath(new URL('../../', import.meta.url)));
|
|
19
|
-
const cli = path.join(packageRoot, 'scripts', 'router-proposals.mjs');
|
|
20
|
-
|
|
21
|
-
function runCli(args) {
|
|
22
|
-
const child = spawnSync(process.execPath, [cli, ...args, '--json'], {
|
|
23
|
-
cwd: packageRoot,
|
|
24
|
-
encoding: 'utf8',
|
|
25
|
-
timeout: 30000,
|
|
26
|
-
});
|
|
27
|
-
const stdout = String(child.stdout ?? '').trim();
|
|
28
|
-
const stderr = String(child.stderr ?? '').trim();
|
|
29
|
-
let json = null;
|
|
30
|
-
for (const text of [stdout, stderr]) {
|
|
31
|
-
if (!text) continue;
|
|
32
|
-
try { json = JSON.parse(text); break; } catch { /* try the other stream */ }
|
|
33
|
-
}
|
|
34
|
-
return { status: child.status, stdout, stderr, json };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function fixture(prefix) {
|
|
38
|
-
const temp = tempRoot(prefix);
|
|
39
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
40
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
41
|
-
writeMemory(memoryRoot);
|
|
42
|
-
const vaultDir = path.join(temp, 'vault');
|
|
43
|
-
return { temp, memoryRoot, vaultDir, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function proposalFiles(vaultDir) {
|
|
47
|
-
const dir = path.join(vaultDir, 'proposals');
|
|
48
|
-
if (!fs.existsSync(dir)) return [];
|
|
49
|
-
return fs.readdirSync(dir).filter((name) => name.endsWith('.json') && name !== 'queue.json').sort();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
test('CLI propose --priority P0 stamps the owner channel and shares the WP3 proposal JSON', () => {
|
|
53
|
-
const env = fixture('wp5-cli-p0-');
|
|
54
|
-
try {
|
|
55
|
-
const result = runCli([
|
|
56
|
-
'propose',
|
|
57
|
-
'--memory-root', env.memoryRoot,
|
|
58
|
-
'--vault-dir', env.vaultDir,
|
|
59
|
-
'--op', 'journal.append',
|
|
60
|
-
'--target', 'journal/2026-11.md',
|
|
61
|
-
'--title', 'Owner P0 request',
|
|
62
|
-
'--content', 'owner content body',
|
|
63
|
-
'--priority', 'P0',
|
|
64
|
-
'--priority-reason', 'owner asked for it',
|
|
65
|
-
]);
|
|
66
|
-
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
67
|
-
assert.equal(result.json.ok, true);
|
|
68
|
-
assert.equal(result.json.ticket, 1);
|
|
69
|
-
assert.equal(result.json.ticketName, 'q-0001');
|
|
70
|
-
|
|
71
|
-
const files = proposalFiles(env.vaultDir);
|
|
72
|
-
assert.equal(files.length, 1);
|
|
73
|
-
const proposal = readJson(path.join(env.vaultDir, 'proposals', files[0]));
|
|
74
|
-
assert.equal(proposal.origin.kind, 'owner-cli');
|
|
75
|
-
assert.equal(proposal.origin.sessionId, 'owner-cli');
|
|
76
|
-
assert.equal(proposal.queue.priority, 'P0');
|
|
77
|
-
assert.equal(proposal.queue.priorityBy, 'owner-cli');
|
|
78
|
-
assert.equal(proposal.stateLog[0].by, 'owner:cli');
|
|
79
|
-
assert.equal(proposal.status, 'pending');
|
|
80
|
-
|
|
81
|
-
const queue = readJson(path.join(env.vaultDir, 'proposals', 'queue.json'));
|
|
82
|
-
assert.equal(queue.entries.length, 1);
|
|
83
|
-
assert.equal(queue.entries[0].ticket, 'q-0001');
|
|
84
|
-
|
|
85
|
-
const kinds = auditKinds(path.join(env.vaultDir, 'audit.jsonl'));
|
|
86
|
-
assert.ok(kinds.includes('proposal.create'));
|
|
87
|
-
const wal = fs.readFileSync(path.join(env.vaultDir, 'proposals', 'wal.jsonl'), 'utf8')
|
|
88
|
-
.trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
89
|
-
assert.ok(wal.some((entry) => entry.ev === 'queue.assign'));
|
|
90
|
-
} finally {
|
|
91
|
-
env.cleanup();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('CLI list/show/queue read the same JSON as the tools', () => {
|
|
96
|
-
const env = fixture('wp5-cli-read-');
|
|
97
|
-
try {
|
|
98
|
-
const proposed = runCli([
|
|
99
|
-
'propose', '--memory-root', env.memoryRoot, '--vault-dir', env.vaultDir,
|
|
100
|
-
'--op', 'topic.append', '--target', 'topics/test.md', '--title', 'Read me',
|
|
101
|
-
'--content', 'read body',
|
|
102
|
-
]);
|
|
103
|
-
assert.equal(proposed.status, 0, proposed.stderr || proposed.stdout);
|
|
104
|
-
|
|
105
|
-
const list = runCli(['list', '--vault-dir', env.vaultDir]);
|
|
106
|
-
assert.equal(list.status, 0);
|
|
107
|
-
assert.equal(list.json.mode, 'list');
|
|
108
|
-
assert.equal(list.json.proposals.length, 1);
|
|
109
|
-
assert.equal(list.json.proposals[0].ticket, 'q-0001');
|
|
110
|
-
|
|
111
|
-
const queue = runCli(['queue', '--vault-dir', env.vaultDir]);
|
|
112
|
-
assert.equal(queue.status, 0);
|
|
113
|
-
assert.equal(queue.json.mode, 'queue');
|
|
114
|
-
assert.equal(queue.json.depth, 1);
|
|
115
|
-
|
|
116
|
-
const show = runCli(['show', '--vault-dir', env.vaultDir, '--id', proposed.json.id]);
|
|
117
|
-
assert.equal(show.status, 0);
|
|
118
|
-
assert.equal(show.json.mode, 'detail');
|
|
119
|
-
assert.equal(show.json.proposal.id, proposed.json.id);
|
|
120
|
-
assert.match(show.json.proposal.content, /read body/);
|
|
121
|
-
assert.match(show.json.proposal.content, /BEGIN PROPOSAL|PROPOSAL/i);
|
|
122
|
-
} finally {
|
|
123
|
-
env.cleanup();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('K14: CLI transitions run the real WP4 state machine through lease/WAL', () => {
|
|
128
|
-
const env = fixture('wp5-cli-seam-');
|
|
129
|
-
try {
|
|
130
|
-
const proposed = runCli([
|
|
131
|
-
'propose', '--memory-root', env.memoryRoot, '--vault-dir', env.vaultDir,
|
|
132
|
-
'--op', 'journal.append', '--target', 'journal/2026-11.md',
|
|
133
|
-
'--title', 'Seam', '--content', 'seam body', '--priority', 'P2',
|
|
134
|
-
]);
|
|
135
|
-
assert.equal(proposed.status, 0);
|
|
136
|
-
const id = proposed.json.id;
|
|
137
|
-
const proposalFile = path.join(env.vaultDir, 'proposals', `${id}.json`);
|
|
138
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
139
|
-
|
|
140
|
-
const wrong = runCli(['approve', '--vault-dir', env.vaultDir, '--id', id, '--confirm', 'xxxxxx']);
|
|
141
|
-
assert.equal(wrong.status, 1);
|
|
142
|
-
assert.equal(wrong.json.code, 'E_CONFIRM_REQUIRED');
|
|
143
|
-
assert.equal(readJson(proposalFile).status, 'pending');
|
|
144
|
-
|
|
145
|
-
const approve = runCli(['approve', '--vault-dir', env.vaultDir, '--memory-root', env.memoryRoot, '--id', id, '--confirm', id.slice(-6)]);
|
|
146
|
-
assert.equal(approve.status, 0, approve.stderr || approve.stdout);
|
|
147
|
-
assert.equal(approve.json.state, 'approved');
|
|
148
|
-
const approved = readJson(proposalFile);
|
|
149
|
-
assert.equal(approved.status, 'approved');
|
|
150
|
-
assert.equal(approved.stateLog.at(-1).by, 'owner:cli');
|
|
151
|
-
assert.equal(fs.existsSync(targetFile), false, 'approve must not write canonical memory');
|
|
152
|
-
|
|
153
|
-
const apply = runCli(['apply', '--vault-dir', env.vaultDir, '--memory-root', env.memoryRoot, '--id', id, '--confirm', id.slice(-6)]);
|
|
154
|
-
assert.equal(apply.status, 0, apply.stderr || apply.stdout);
|
|
155
|
-
assert.equal(apply.json.state, 'done');
|
|
156
|
-
const done = readJson(proposalFile);
|
|
157
|
-
assert.equal(done.status, 'done');
|
|
158
|
-
assert.match(fs.readFileSync(targetFile, 'utf8'), /seam body/);
|
|
159
|
-
const appliedSha = done.result.afterSha;
|
|
160
|
-
assert.equal(sha256Buffer(fs.readFileSync(targetFile)), appliedSha);
|
|
161
|
-
assert.ok(fs.existsSync(path.join(env.vaultDir, done.result.snapshot)), 'snapshot evidence exists');
|
|
162
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'writer.lease.json')), false, 'lease released');
|
|
163
|
-
|
|
164
|
-
const walFile = path.join(env.vaultDir, 'proposals', 'wal.jsonl');
|
|
165
|
-
const kinds = fs.readFileSync(walFile, 'utf8').trim().split('\n').map((line) => JSON.parse(line).ev);
|
|
166
|
-
for (const ev of ['queue.assign', 'queue.approve', 'lease.acquire', 'apply.begin', 'apply.done']) {
|
|
167
|
-
assert.ok(kinds.includes(ev), `WAL has ${ev}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const reject = runCli(['reject', '--vault-dir', env.vaultDir, '--id', id]);
|
|
171
|
-
assert.equal(reject.status, 1);
|
|
172
|
-
assert.equal(reject.json.code, 'E_BAD_STATE');
|
|
173
|
-
|
|
174
|
-
const rollback = runCli(['rollback', '--vault-dir', env.vaultDir, '--memory-root', env.memoryRoot, '--id', id, '--confirm', id.slice(-6)]);
|
|
175
|
-
assert.equal(rollback.status, 0, rollback.stderr || rollback.stdout);
|
|
176
|
-
assert.equal(rollback.json.state, 'rolled_back');
|
|
177
|
-
assert.equal(rollback.json.removed, true, 'R-06: a new-file apply is undone by removing the target');
|
|
178
|
-
assert.equal(rollback.json.restoredSha, null);
|
|
179
|
-
const rolledBack = readJson(proposalFile);
|
|
180
|
-
assert.equal(rolledBack.status, 'rolled_back');
|
|
181
|
-
assert.equal(fs.existsSync(targetFile), false, 'R-06: rollback removes the file the apply created');
|
|
182
|
-
assert.equal(rolledBack.result.removed, true);
|
|
183
|
-
assert.equal(rolledBack.result.restoredSha, null);
|
|
184
|
-
assert.ok(fs.existsSync(path.join(env.vaultDir, rolledBack.result.rollbackSnapshot)), 'rollback evidence kept');
|
|
185
|
-
const walKinds = fs.readFileSync(walFile, 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
186
|
-
const doneWal = walKinds.filter((line) => line.ev === 'rollback.done').at(-1);
|
|
187
|
-
assert.equal(doneWal.removed, true, 'WAL records removed:true');
|
|
188
|
-
assert.equal(doneWal.restoredSha, null);
|
|
189
|
-
} finally {
|
|
190
|
-
env.cleanup();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('K10: tool P0 is denied without creating a proposal; the CLI P0 channel is isolated', async () => {
|
|
195
|
-
const temp = tempRoot('wp5-k10-');
|
|
196
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
197
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
198
|
-
writeMemory(memoryRoot);
|
|
199
|
-
const vaultDir = path.join(temp, 'vault');
|
|
200
|
-
const env = makeService({
|
|
201
|
-
temp,
|
|
202
|
-
memoryRoot,
|
|
203
|
-
vaultDir,
|
|
204
|
-
config: { proposalEnabled: true },
|
|
205
|
-
});
|
|
206
|
-
try {
|
|
207
|
-
const started = await env.service.start();
|
|
208
|
-
assert.equal(started.state, 'running');
|
|
209
|
-
|
|
210
|
-
const toolResult = await env.service.proposeProposal({
|
|
211
|
-
op: 'journal.append',
|
|
212
|
-
target: 'journal/2026-11.md',
|
|
213
|
-
title: 'tool wants P0',
|
|
214
|
-
content: 'tool body',
|
|
215
|
-
priority: 'P0',
|
|
216
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
217
|
-
|
|
218
|
-
assert.equal(toolResult.ok, false);
|
|
219
|
-
assert.equal(toolResult.code, 'E_PRIORITY_DENIED');
|
|
220
|
-
assert.equal(proposalFiles(vaultDir).length, 0, 'tool P0 must not create a proposal');
|
|
221
|
-
const kinds = auditKinds(path.join(vaultDir, 'audit.jsonl'));
|
|
222
|
-
assert.ok(kinds.includes('priority.rejected'));
|
|
223
|
-
|
|
224
|
-
const cliResult = runCli([
|
|
225
|
-
'propose', '--memory-root', memoryRoot, '--vault-dir', vaultDir,
|
|
226
|
-
'--op', 'journal.append', '--target', 'journal/2026-11.md',
|
|
227
|
-
'--title', 'owner P0', '--content', 'owner body', '--priority', 'P0',
|
|
228
|
-
]);
|
|
229
|
-
assert.equal(cliResult.status, 0, cliResult.stderr || cliResult.stdout);
|
|
230
|
-
const files = proposalFiles(vaultDir);
|
|
231
|
-
assert.equal(files.length, 1);
|
|
232
|
-
const owner = readJson(path.join(vaultDir, 'proposals', files[0]));
|
|
233
|
-
assert.equal(owner.origin.kind, 'owner-cli');
|
|
234
|
-
assert.equal(owner.queue.priorityBy, 'owner-cli');
|
|
235
|
-
assert.equal(owner.stateLog[0].by, 'owner:cli');
|
|
236
|
-
} finally {
|
|
237
|
-
await env.service.dispose();
|
|
238
|
-
env.cleanup();
|
|
239
|
-
}
|
|
240
|
-
});
|
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
// WP5 (D-WP5-1..6) — model auto resolution, provider fallback, waiting_provider,
|
|
2
|
-
// and host failure mapping. No host, no network, no credentials.
|
|
3
|
-
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import test from 'node:test';
|
|
8
|
-
|
|
9
|
-
import { MockLlm } from '../../src/router/background/mock-llm.mjs';
|
|
10
|
-
import { createRouterService } from '../../src/router/service/service.mjs';
|
|
11
|
-
import {
|
|
12
|
-
MODEL_AUTO_PICKED_LIST_ORDER,
|
|
13
|
-
MODEL_UNLISTED,
|
|
14
|
-
REASONING_EFFORT_UNSUPPORTED,
|
|
15
|
-
mapProviderFailure,
|
|
16
|
-
} from '../../src/router/service/model-resolve.mjs';
|
|
17
|
-
import { appendTask, auditKinds, readJson, taskInput, tempRoot, writeMemory } from './helpers.mjs';
|
|
18
|
-
|
|
19
|
-
class CatalogLlm extends MockLlm {
|
|
20
|
-
constructor({ providers = [{ id: 'mock', name: 'Mock' }], models = [], infos = new Map(), ...rest } = {}) {
|
|
21
|
-
super(rest);
|
|
22
|
-
this.providers = providers;
|
|
23
|
-
this.models = models;
|
|
24
|
-
this.infos = infos;
|
|
25
|
-
this.listModelsCalls = [];
|
|
26
|
-
this.resolveCalls = [];
|
|
27
|
-
this.fullRequests = [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
listProviders() {
|
|
31
|
-
return this.providers;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async listModels(provider) {
|
|
35
|
-
this.listModelsCalls.push(provider);
|
|
36
|
-
return this.models.map((entry) => (typeof entry === 'string'
|
|
37
|
-
? { provider, id: entry, name: entry }
|
|
38
|
-
: { provider, ...entry }));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async resolveModelInfo(provider, model) {
|
|
42
|
-
this.resolveCalls.push([provider, model]);
|
|
43
|
-
return this.infos.get(model) ?? { provider, id: model, name: model, context: { contextWindow: 1048576 } };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
stream(request) {
|
|
47
|
-
this.fullRequests.push(request);
|
|
48
|
-
return super.stream(request);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function logger() {
|
|
53
|
-
return { warn: () => {}, info: () => {} };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function build(ctx, configOverrides = {}, deps = {}) {
|
|
57
|
-
const temp = tempRoot('wp5-model-');
|
|
58
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
59
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
60
|
-
writeMemory(memoryRoot);
|
|
61
|
-
const vaultDir = path.join(temp, 'vault');
|
|
62
|
-
const config = {
|
|
63
|
-
enabled: true,
|
|
64
|
-
provider: 'mock',
|
|
65
|
-
model: 'auto',
|
|
66
|
-
memoryRoot,
|
|
67
|
-
vaultDir,
|
|
68
|
-
tickMs: 60000,
|
|
69
|
-
maxTokensPerRun: 50000,
|
|
70
|
-
maxTokensPerDay: 150000,
|
|
71
|
-
callMaxTokens: 256,
|
|
72
|
-
maxAttempts: 1,
|
|
73
|
-
maxConsecutiveErrors: 3,
|
|
74
|
-
disposeTimeoutMs: 500,
|
|
75
|
-
...configOverrides,
|
|
76
|
-
};
|
|
77
|
-
const service = createRouterService(ctx, config, {
|
|
78
|
-
memoryRoot,
|
|
79
|
-
installTimers: false,
|
|
80
|
-
installListener: false,
|
|
81
|
-
stability: { enabled: false, waitMs: 0, retries: 0 },
|
|
82
|
-
...deps,
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
service,
|
|
86
|
-
temp,
|
|
87
|
-
memoryRoot,
|
|
88
|
-
vaultDir,
|
|
89
|
-
config,
|
|
90
|
-
cleanup: () => fs.rmSync(temp, { recursive: true, force: true }),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function auditFile(vaultDir) {
|
|
95
|
-
return path.join(vaultDir, 'audit.jsonl');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
test('auto picks the first non-preview model from listModels and records the list-order warning', async () => {
|
|
99
|
-
const llm = new CatalogLlm({ models: ['mock-2-preview', 'mock-3', 'mock-4-experimental'] });
|
|
100
|
-
const ctx = { get: (name) => (name === 'llm' ? llm : undefined), logger };
|
|
101
|
-
const env = build(ctx);
|
|
102
|
-
try {
|
|
103
|
-
const result = await env.service.start();
|
|
104
|
-
assert.equal(result.ok, true);
|
|
105
|
-
assert.equal(result.state, 'running');
|
|
106
|
-
assert.equal(env.service.status().model.resolved, 'mock-3');
|
|
107
|
-
assert.ok(env.service.status().warnings.includes(MODEL_AUTO_PICKED_LIST_ORDER));
|
|
108
|
-
const kinds = auditKinds(auditFile(env.vaultDir));
|
|
109
|
-
assert.ok(kinds.includes('model.auto_resolved'));
|
|
110
|
-
assert.ok(kinds.includes('model.auto_list_order'));
|
|
111
|
-
|
|
112
|
-
appendTask(env.service, taskInput('auto-1'));
|
|
113
|
-
await env.service.tick({ manual: true });
|
|
114
|
-
assert.equal(llm.fullRequests.length, 1);
|
|
115
|
-
assert.equal(llm.fullRequests[0].provider, 'mock');
|
|
116
|
-
assert.equal(llm.fullRequests[0].model, 'mock-3');
|
|
117
|
-
assert.equal(llm.fullRequests[0].reasoningEffort, undefined);
|
|
118
|
-
assert.deepEqual(llm.listModelsCalls, ['mock']);
|
|
119
|
-
} finally {
|
|
120
|
-
await env.service.dispose();
|
|
121
|
-
env.cleanup();
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('auto follows agentDefaultModel.currentSelection and inherits reasoningEffort', async () => {
|
|
126
|
-
const llm = new CatalogLlm({ models: ['mock-3'] });
|
|
127
|
-
const ctx = {
|
|
128
|
-
get: (name) => {
|
|
129
|
-
if (name === 'llm') return llm;
|
|
130
|
-
if (name === 'agentDefaultModel') {
|
|
131
|
-
return { currentSelection: () => ({ provider: 'mock', model: 'mock-host', reasoningEffort: 'max' }) };
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
},
|
|
135
|
-
logger,
|
|
136
|
-
};
|
|
137
|
-
const env = build(ctx);
|
|
138
|
-
try {
|
|
139
|
-
const result = await env.service.start();
|
|
140
|
-
assert.equal(result.state, 'running');
|
|
141
|
-
assert.equal(env.service.status().model.resolved, 'mock-host');
|
|
142
|
-
assert.equal(llm.listModelsCalls.length, 0, 'host selection must win without a catalog scan');
|
|
143
|
-
|
|
144
|
-
appendTask(env.service, taskInput('host-1'));
|
|
145
|
-
await env.service.tick({ manual: true });
|
|
146
|
-
assert.equal(llm.fullRequests[0].model, 'mock-host');
|
|
147
|
-
assert.equal(llm.fullRequests[0].reasoningEffort, 'max');
|
|
148
|
-
} finally {
|
|
149
|
-
await env.service.dispose();
|
|
150
|
-
env.cleanup();
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('K15: an explicit provider must be registered; an empty provider list blocks E_NO_PROVIDER', async () => {
|
|
155
|
-
const twoProviders = new CatalogLlm({
|
|
156
|
-
providers: [{ id: 'alpha' }, { id: 'beta' }],
|
|
157
|
-
models: ['alpha-1'],
|
|
158
|
-
});
|
|
159
|
-
const ctx = { get: (name) => (name === 'llm' ? twoProviders : undefined), logger };
|
|
160
|
-
const env = build(ctx, { provider: 'mock', model: 'mock-1' });
|
|
161
|
-
try {
|
|
162
|
-
const result = await env.service.start();
|
|
163
|
-
assert.equal(result.state, 'blocked');
|
|
164
|
-
const status = env.service.status();
|
|
165
|
-
assert.equal(status.lastError.code, 'E_NO_PROVIDER');
|
|
166
|
-
assert.match(status.lastError.message, /provider "mock" is not registered/);
|
|
167
|
-
assert.match(status.lastError.message, /alpha, beta/);
|
|
168
|
-
assert.equal(twoProviders.listModelsCalls.length, 0, 'an unregistered explicit provider must not be silently swapped');
|
|
169
|
-
const blockedFile = path.join(env.vaultDir, 'status.json');
|
|
170
|
-
assert.equal(fs.existsSync(blockedFile), true, 'blocked E_NO_PROVIDER must write status.json');
|
|
171
|
-
assert.equal(JSON.parse(fs.readFileSync(blockedFile, 'utf8')).state, 'blocked');
|
|
172
|
-
} finally {
|
|
173
|
-
await env.service.dispose();
|
|
174
|
-
env.cleanup();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const emptyProviders = new CatalogLlm({ providers: [], models: ['mock-1'] });
|
|
178
|
-
const emptyCtx = { get: (name) => (name === 'llm' ? emptyProviders : undefined), logger };
|
|
179
|
-
const emptyEnv = build(emptyCtx, { provider: 'mock', model: 'mock-1' });
|
|
180
|
-
try {
|
|
181
|
-
const result = await emptyEnv.service.start();
|
|
182
|
-
assert.equal(result.state, 'blocked');
|
|
183
|
-
assert.equal(emptyEnv.service.status().lastError.code, 'E_NO_PROVIDER');
|
|
184
|
-
assert.match(emptyEnv.service.status().lastError.message, /listProviders\(\) returned no providers/);
|
|
185
|
-
assert.equal(fs.existsSync(path.join(emptyEnv.vaultDir, 'status.json')), true);
|
|
186
|
-
} finally {
|
|
187
|
-
await emptyEnv.service.dispose();
|
|
188
|
-
emptyEnv.cleanup();
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('auto with an empty catalog blocks E_NO_PROVIDER and keeps resolved=null', async () => {
|
|
193
|
-
const llm = new CatalogLlm({ models: [] });
|
|
194
|
-
const ctx = { get: (name) => (name === 'llm' ? llm : undefined), logger };
|
|
195
|
-
const env = build(ctx);
|
|
196
|
-
try {
|
|
197
|
-
const result = await env.service.start();
|
|
198
|
-
assert.equal(result.state, 'blocked');
|
|
199
|
-
const status = env.service.status();
|
|
200
|
-
assert.equal(status.lastError.code, 'E_NO_PROVIDER');
|
|
201
|
-
assert.deepEqual(status.model, { auto: true, resolved: null, resolvedAt: null });
|
|
202
|
-
assert.match(status.lastError.message, /listModels\("mock"\) returned no models/);
|
|
203
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'proposals')), false);
|
|
204
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'status.json')), true);
|
|
205
|
-
} finally {
|
|
206
|
-
await env.service.dispose();
|
|
207
|
-
env.cleanup();
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('an explicit but unlisted model runs with MODEL_UNLISTED and a model.unlisted audit', async () => {
|
|
212
|
-
const llm = new CatalogLlm({ models: ['mock-3'] });
|
|
213
|
-
const ctx = { get: (name) => (name === 'llm' ? llm : undefined), logger };
|
|
214
|
-
const env = build(ctx, { model: 'mock-unlisted' });
|
|
215
|
-
try {
|
|
216
|
-
const result = await env.service.start();
|
|
217
|
-
assert.equal(result.state, 'running');
|
|
218
|
-
assert.equal(env.service.status().model.resolved, 'mock-unlisted');
|
|
219
|
-
assert.ok(env.service.status().warnings.includes(MODEL_UNLISTED));
|
|
220
|
-
assert.ok(auditKinds(auditFile(env.vaultDir)).includes('model.unlisted'));
|
|
221
|
-
} finally {
|
|
222
|
-
await env.service.dispose();
|
|
223
|
-
env.cleanup();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('provider: explicit wins, otherwise the host current selection is used', async () => {
|
|
228
|
-
const explicitLlm = new CatalogLlm({ providers: [{ id: 'mock' }, { id: 'other' }], models: ['mock-1'] });
|
|
229
|
-
const explicitCtx = {
|
|
230
|
-
get: (name) => {
|
|
231
|
-
if (name === 'llm') return explicitLlm;
|
|
232
|
-
if (name === 'agentDefaultModel') return { currentSelection: () => ({ provider: 'other', model: 'other-1' }) };
|
|
233
|
-
return undefined;
|
|
234
|
-
},
|
|
235
|
-
logger,
|
|
236
|
-
};
|
|
237
|
-
const explicitEnv = build(explicitCtx, { provider: 'mock', model: 'mock-1' });
|
|
238
|
-
try {
|
|
239
|
-
await explicitEnv.service.start();
|
|
240
|
-
const start = explicitEnv.service.status();
|
|
241
|
-
assert.equal(start.provider.configured, true);
|
|
242
|
-
assert.equal(start.model.resolved, 'mock-1');
|
|
243
|
-
const records = fs.readFileSync(auditFile(explicitEnv.vaultDir), 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
244
|
-
const startRecord = records.find((record) => record.kind === 'router.start');
|
|
245
|
-
assert.equal(startRecord.provider, 'mock');
|
|
246
|
-
assert.equal(startRecord.provider_source, 'explicit');
|
|
247
|
-
} finally {
|
|
248
|
-
await explicitEnv.service.dispose();
|
|
249
|
-
explicitEnv.cleanup();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const inheritedLlm = new CatalogLlm({ providers: [{ id: 'mock' }, { id: 'other' }], models: ['host-1'] });
|
|
253
|
-
const inheritedCtx = {
|
|
254
|
-
get: (name) => {
|
|
255
|
-
if (name === 'llm') return inheritedLlm;
|
|
256
|
-
if (name === 'agentDefaultModel') return { currentSelection: () => ({ provider: 'other', model: 'host-1' }) };
|
|
257
|
-
return undefined;
|
|
258
|
-
},
|
|
259
|
-
logger,
|
|
260
|
-
};
|
|
261
|
-
const inheritedEnv = build(inheritedCtx, { provider: 'deepseek-official', providerExplicit: false, model: 'auto' });
|
|
262
|
-
try {
|
|
263
|
-
await inheritedEnv.service.start();
|
|
264
|
-
const records = fs.readFileSync(auditFile(inheritedEnv.vaultDir), 'utf8').trim().split('\n').map((line) => JSON.parse(line));
|
|
265
|
-
const startRecord = records.find((record) => record.kind === 'router.start');
|
|
266
|
-
assert.equal(startRecord.provider, 'other');
|
|
267
|
-
assert.equal(startRecord.provider_source, 'host-default');
|
|
268
|
-
assert.equal(startRecord.model, 'host-1');
|
|
269
|
-
} finally {
|
|
270
|
-
await inheritedEnv.service.dispose();
|
|
271
|
-
inheritedEnv.cleanup();
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('missing llm enters waiting_provider and self-heals on retry without a permanent block', async () => {
|
|
276
|
-
let current = null;
|
|
277
|
-
const ctx = { get: (name) => (name === 'llm' ? current : undefined), logger };
|
|
278
|
-
const env = build(ctx);
|
|
279
|
-
try {
|
|
280
|
-
const result = await env.service.start();
|
|
281
|
-
assert.equal(result.state, 'waiting_provider');
|
|
282
|
-
assert.equal(result.lastError, null);
|
|
283
|
-
const waitingStatus = env.service.status();
|
|
284
|
-
assert.equal(waitingStatus.provider.missing, true);
|
|
285
|
-
assert.equal(typeof waitingStatus.provider.nextRetryAt, 'string');
|
|
286
|
-
// 观测性:waiting_provider 也必须落 vault + status.json(面板契约 v2.3 §6.5)
|
|
287
|
-
assert.equal(fs.existsSync(env.vaultDir), true);
|
|
288
|
-
const waitingFile = path.join(env.vaultDir, 'status.json');
|
|
289
|
-
assert.equal(fs.existsSync(waitingFile), true, 'waiting_provider must write status.json');
|
|
290
|
-
const waitingOnDisk = JSON.parse(fs.readFileSync(waitingFile, 'utf8'));
|
|
291
|
-
assert.equal(waitingOnDisk.state, 'waiting_provider');
|
|
292
|
-
assert.equal(waitingOnDisk.provider.missing, true);
|
|
293
|
-
const tick = await env.service.tick({ manual: true });
|
|
294
|
-
assert.equal(tick.code, 'E_NOT_RUNNING');
|
|
295
|
-
|
|
296
|
-
current = new CatalogLlm({ models: ['mock-3'] });
|
|
297
|
-
await env.service.retryProviderNow();
|
|
298
|
-
assert.equal(env.service.status().state, 'running');
|
|
299
|
-
assert.equal(env.service.status().model.resolved, 'mock-3');
|
|
300
|
-
assert.deepEqual(env.service.status().provider, { configured: true, missing: false });
|
|
301
|
-
} finally {
|
|
302
|
-
await env.service.dispose();
|
|
303
|
-
env.cleanup();
|
|
304
|
-
}
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('a throwing ctx.llm getter must not crash start; ctx.get is the only probe', async () => {
|
|
308
|
-
const ctx = {
|
|
309
|
-
get: (name) => (name === 'llm' ? undefined : undefined),
|
|
310
|
-
logger,
|
|
311
|
-
get llm() {
|
|
312
|
-
throw new Error('ctx.llm must never be read');
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
const env = build(ctx);
|
|
316
|
-
try {
|
|
317
|
-
const result = await env.service.start();
|
|
318
|
-
assert.equal(result.state, 'waiting_provider');
|
|
319
|
-
} finally {
|
|
320
|
-
await env.service.dispose();
|
|
321
|
-
env.cleanup();
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test('reasoningEffort is passed only when explicitly supplied and unsupported values warn', async () => {
|
|
326
|
-
const unsupported = new CatalogLlm({
|
|
327
|
-
models: ['mock-3'],
|
|
328
|
-
infos: new Map([['mock-3', { provider: 'mock', id: 'mock-3', reasoning: { efforts: ['low', 'high'] } }]]),
|
|
329
|
-
});
|
|
330
|
-
const ctx = { get: (name) => (name === 'llm' ? unsupported : undefined), logger };
|
|
331
|
-
const env = build(ctx, { model: 'mock-3' }, { reasoningEffort: 'max' });
|
|
332
|
-
try {
|
|
333
|
-
const result = await env.service.start();
|
|
334
|
-
assert.equal(result.state, 'running');
|
|
335
|
-
assert.ok(env.service.status().warnings.includes(REASONING_EFFORT_UNSUPPORTED));
|
|
336
|
-
assert.ok(auditKinds(auditFile(env.vaultDir)).includes('model.reasoning_effort_unsupported'));
|
|
337
|
-
|
|
338
|
-
appendTask(env.service, taskInput('effort-1'));
|
|
339
|
-
await env.service.tick({ manual: true });
|
|
340
|
-
assert.equal(unsupported.fullRequests[0].reasoningEffort, undefined);
|
|
341
|
-
} finally {
|
|
342
|
-
await env.service.dispose();
|
|
343
|
-
env.cleanup();
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test('mapProviderFailure implements D-WP5-5', () => {
|
|
348
|
-
assert.equal(mapProviderFailure('NO_ADAPTER').routerCode, 'E_NO_PROVIDER');
|
|
349
|
-
assert.equal(mapProviderFailure('NO_ADAPTER').terminal, true);
|
|
350
|
-
assert.equal(mapProviderFailure('MISSING_CREDENTIAL').routerCode, 'E_NO_PROVIDER');
|
|
351
|
-
assert.equal(mapProviderFailure('INVALID_CREDENTIAL').routerCode, 'E_NO_PROVIDER');
|
|
352
|
-
assert.equal(mapProviderFailure('AUTH').routerCode, 'E_NO_PROVIDER');
|
|
353
|
-
assert.equal(mapProviderFailure('RATE_LIMIT').routerCode, 'E_RATE_LIMITED');
|
|
354
|
-
assert.equal(mapProviderFailure('RATE_LIMIT').rateLimited, true);
|
|
355
|
-
assert.equal(mapProviderFailure('ACCOUNT_QUOTA').routerCode, 'E_RATE_LIMITED');
|
|
356
|
-
assert.equal(mapProviderFailure('QUOTA').routerCode, 'E_QUOTA');
|
|
357
|
-
assert.equal(mapProviderFailure('QUOTA').terminal, true);
|
|
358
|
-
assert.equal(mapProviderFailure('CONTEXT_WINDOW_EXCEEDED').routerCode, 'E_CONTEXT_WINDOW');
|
|
359
|
-
assert.equal(mapProviderFailure('INVALID_CATALOG').routerCode, 'E_PROVIDER_FAILED');
|
|
360
|
-
assert.equal(mapProviderFailure('INVALID_CATALOG').terminal, true);
|
|
361
|
-
assert.equal(mapProviderFailure('TIMEOUT').routerCode, 'E_PROVIDER_FAILED');
|
|
362
|
-
assert.equal(mapProviderFailure('TIMEOUT').terminal, false);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
for (const [hostCode, expected] of [
|
|
366
|
-
['MISSING_CREDENTIAL', 'E_NO_PROVIDER'],
|
|
367
|
-
['AUTH', 'E_NO_PROVIDER'],
|
|
368
|
-
['QUOTA', 'E_QUOTA'],
|
|
369
|
-
['CONTEXT_WINDOW_EXCEEDED', 'E_CONTEXT_WINDOW'],
|
|
370
|
-
['INVALID_CATALOG', 'E_PROVIDER_FAILED'],
|
|
371
|
-
]) {
|
|
372
|
-
test(`terminal stream failure ${hostCode} blocks with ${expected}`, async () => {
|
|
373
|
-
const failOn = new Map([['t1', hostCode]]);
|
|
374
|
-
const llm = new MockLlm({ failOn });
|
|
375
|
-
const ctx = { get: (name) => (name === 'llm' ? llm : undefined), logger };
|
|
376
|
-
const env = build(ctx, { model: 'mock-1' });
|
|
377
|
-
try {
|
|
378
|
-
await env.service.start();
|
|
379
|
-
appendTask(env.service, taskInput('t1'));
|
|
380
|
-
await env.service.tick({ manual: true });
|
|
381
|
-
const status = env.service.status();
|
|
382
|
-
assert.equal(status.state, 'blocked');
|
|
383
|
-
assert.equal(status.lastError.code, expected);
|
|
384
|
-
const record = fs.readFileSync(auditFile(env.vaultDir), 'utf8').trim().split('\n')
|
|
385
|
-
.map((line) => JSON.parse(line)).find((entry) => entry.kind === 'task.error');
|
|
386
|
-
assert.equal(record.code, hostCode);
|
|
387
|
-
assert.equal(record.router_code, expected);
|
|
388
|
-
} finally {
|
|
389
|
-
await env.service.dispose();
|
|
390
|
-
env.cleanup();
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const hostCode of ['RATE_LIMIT', 'ACCOUNT_QUOTA']) {
|
|
396
|
-
test(`retryable stream failure ${hostCode} backs off as E_RATE_LIMITED without a permanent block`, async () => {
|
|
397
|
-
const failOn = new Map([['t1', { code: hostCode, providerRetryAfterMs: 1 }]]);
|
|
398
|
-
const llm = new MockLlm({ failOn });
|
|
399
|
-
const ctx = { get: (name) => (name === 'llm' ? llm : undefined), logger };
|
|
400
|
-
const env = build(ctx, { model: 'mock-1' });
|
|
401
|
-
try {
|
|
402
|
-
await env.service.start();
|
|
403
|
-
appendTask(env.service, taskInput('t1'));
|
|
404
|
-
await env.service.tick({ manual: true });
|
|
405
|
-
const status = env.service.status();
|
|
406
|
-
assert.equal(status.state, 'running');
|
|
407
|
-
assert.equal(status.lastError, null);
|
|
408
|
-
assert.equal(typeof status.nextTickAt, 'string');
|
|
409
|
-
const record = fs.readFileSync(auditFile(env.vaultDir), 'utf8').trim().split('\n')
|
|
410
|
-
.map((line) => JSON.parse(line)).find((entry) => entry.kind === 'task.error');
|
|
411
|
-
assert.equal(record.router_code, 'E_RATE_LIMITED');
|
|
412
|
-
assert.equal(record.rate_limited, true);
|
|
413
|
-
} finally {
|
|
414
|
-
await env.service.dispose();
|
|
415
|
-
env.cleanup();
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
}
|