@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.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -1,90 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import test from 'node:test';
4
-
5
- import { makeService } from './helpers.mjs';
6
-
7
- function auditRecords(file) {
8
- return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
9
- }
10
-
11
- test('model auto with no host resolver stays unresolved and blocks instead of guessing', async () => {
12
- const env = makeService({ config: { model: 'auto' } });
13
- try {
14
- const result = await env.service.start();
15
- assert.equal(result.ok, false);
16
- assert.equal(result.reason, 'E_NO_PROVIDER');
17
- assert.equal(result.state, 'blocked');
18
- const status = env.service.status();
19
- assert.deepEqual(status.model, { auto: true, resolved: null, resolvedAt: null });
20
- assert.equal(status.state, 'blocked');
21
- // 只在内存里解析,绝不回写配置,也不把字面量 "auto" 当模型发出去。
22
- assert.equal(env.config.model, 'auto');
23
- assert.equal(env.service.config.model, 'auto');
24
- // 启动在 initPaths 之前就 blocked,不留 audit 目录。
25
- assert.equal(status.files.audit, '');
26
- } finally {
27
- await env.service.dispose();
28
- env.cleanup();
29
- }
30
- });
31
-
32
- test('the daily window rolls over at local midnight instead of carrying yesterday forever', async () => {
33
- let clock = new Date('2026-11-05T10:00:00+08:00');
34
- const env = makeService({
35
- config: { maxTokensPerDay: 5000, maxTokensPerRun: 50000 },
36
- deps: { now: () => clock },
37
- });
38
- try {
39
- const result = await env.service.start();
40
- assert.equal(result.state, 'running');
41
- // 模拟当天已经用掉 4900,然后时钟跨到第二天。
42
- env.service.dayUsage = {
43
- day: '2026-11-05',
44
- usedTokens: 4900,
45
- reservedTokens: 0,
46
- capTokens: 5000,
47
- runs: [],
48
- };
49
- clock = new Date('2026-11-06T10:00:00+08:00');
50
- await env.service.tick({ tier: 'reflect', reason: 'test' });
51
- const status = env.service.status();
52
- assert.equal(status.today.day, '2026-11-06');
53
- assert.equal(status.today.usedTokens, 0);
54
- assert.equal(status.state, 'running');
55
- } finally {
56
- await env.service.dispose();
57
- env.cleanup();
58
- }
59
- });
60
-
61
- test('model auto resolves the newest non-preview model into status only', async () => {
62
- const env = makeService({ config: { model: 'auto' } });
63
- env.llm.listModels = async ({ provider }) => {
64
- assert.equal(provider, 'mock');
65
- return {
66
- models: [
67
- { id: 'mock-2-preview' },
68
- { id: 'mock-3' },
69
- { name: 'mock-4-experimental' },
70
- ],
71
- };
72
- };
73
- try {
74
- const result = await env.service.start();
75
- assert.equal(result.ok, true);
76
- assert.equal(result.state, 'running');
77
- const status = env.service.status();
78
- assert.equal(status.model.auto, true);
79
- assert.equal(status.model.resolved, 'mock-3');
80
- assert.equal(typeof status.model.resolvedAt, 'string');
81
- assert.equal(env.config.model, 'auto');
82
- assert.equal(env.service.config.model, 'auto');
83
- const start = auditRecords(status.files.audit).filter((record) => record.kind === 'router.start').pop();
84
- assert.equal(start.model, 'mock-3');
85
- assert.equal(start.model_auto, true);
86
- } finally {
87
- await env.service.dispose();
88
- env.cleanup();
89
- }
90
- });
@@ -1,220 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import test from 'node:test';
6
-
7
- import {
8
- buildStatus,
9
- writeStatus,
10
- storageErrorCode,
11
- STATUS_MAX_BYTES,
12
- STATUS_VERSION,
13
- } from '../../src/router/service/status.mjs';
14
-
15
- const NOW = '2026-11-05T00:30:00+08:00';
16
-
17
- function withTempDir(fn) {
18
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'router-status-'));
19
- try {
20
- return fn(dir);
21
- } finally {
22
- fs.rmSync(dir, { recursive: true, force: true });
23
- }
24
- }
25
-
26
- test('minimal status is a complete v2 read-only projection', () => {
27
- const status = buildStatus({ now: NOW });
28
- assert.equal(status.v, STATUS_VERSION);
29
- assert.equal(status.v, 2);
30
- assert.equal(status.readOnly, true);
31
- assert.equal(status.state, 'stopped');
32
- assert.equal(status.reason, null);
33
- assert.deepEqual(status.provider, { configured: false, missing: true });
34
- assert.deepEqual(status.model, { auto: false, resolved: null, resolvedAt: null });
35
- assert.equal(status.agentMode, false);
36
- assert.deepEqual(status.run, { usedTokens: 0, capTokens: 0 });
37
- assert.deepEqual(status.tickQueue, { pending: 0, invalid: 0, oldestAgeMs: 0 });
38
- assert.deepEqual(status.queue, { pending: 0, queued: 0, applying: 0, blockedOnUser: 0, expired: 0 });
39
- assert.equal(status.board.pulse.unknownSessions, 0);
40
- assert.deepEqual(status.live, { rows: 0, maxRows: 128, unknown: 0 });
41
- assert.deepEqual(status.warnings, []);
42
- assert.deepEqual(status.alerts, []);
43
- assert.equal(status.lastError, null);
44
- assert.equal(status.lastReport, null);
45
- assert.deepEqual(Object.keys(status.files).sort(), ['anchor', 'audit', 'board', 'ledger', 'outbox', 'vault']);
46
- });
47
-
48
- test('provider exposes exactly two booleans and never endpoints or credentials', () => {
49
- const status = buildStatus({
50
- now: NOW,
51
- provider: {
52
- configured: true,
53
- missing: false,
54
- endpoint: 'https://secret.example/v1',
55
- apiKey: 'sk-do-not-leak',
56
- candidates: ['a', 'b'],
57
- },
58
- });
59
- assert.deepEqual(status.provider, { configured: true, missing: false });
60
- assert.equal(JSON.stringify(status).includes('sk-do-not-leak'), false);
61
- assert.equal(JSON.stringify(status).includes('secret.example'), false);
62
-
63
- const empty = buildStatus({ now: NOW, provider: 'mock' });
64
- assert.deepEqual(empty.provider, { configured: true, missing: false });
65
- });
66
-
67
- test('model reports auto and only the resolved identifier', () => {
68
- const auto = buildStatus({ now: NOW, model: { auto: true, resolved: '', resolvedAt: null } });
69
- assert.deepEqual(auto.model, { auto: true, resolved: null, resolvedAt: null });
70
-
71
- const resolved = buildStatus({
72
- now: NOW,
73
- model: { auto: true, resolved: 'deepseek-v4-flash', resolvedAt: NOW, candidates: ['x'] },
74
- });
75
- assert.deepEqual(Object.keys(resolved.model).sort(), ['auto', 'resolved', 'resolvedAt']);
76
- assert.equal(resolved.model.auto, true);
77
- assert.equal(resolved.model.resolved, 'deepseek-v4-flash');
78
- assert.equal(resolved.model.resolvedAt, NOW);
79
-
80
- const plain = buildStatus({ now: NOW, model: 'mock-1' });
81
- assert.deepEqual(plain.model, { auto: false, resolved: 'mock-1', resolvedAt: NOW });
82
- });
83
-
84
- test('tickQueue and the five proposal queue counters are separate', () => {
85
- const status = buildStatus({
86
- now: NOW,
87
- tickQueue: { pending: 2, invalid: 1, oldestAgeMs: 7 },
88
- queueStatus: { pending: 1, queued: 2, applying: 3, blockedOnUser: 4, expired: 5 },
89
- });
90
- assert.deepEqual(status.tickQueue, { pending: 2, invalid: 1, oldestAgeMs: 7 });
91
- assert.deepEqual(status.queue, { pending: 1, queued: 2, applying: 3, blockedOnUser: 4, expired: 5 });
92
-
93
- // 兼容 v1 形状的输入:无 tickQueue 时 queue 充当 tickQueue,queue 五键仍独立为零。
94
- const legacy = buildStatus({ now: NOW, queue: { pending: 9, invalid: 0, oldestAgeMs: 3 } });
95
- assert.deepEqual(legacy.tickQueue, { pending: 9, invalid: 0, oldestAgeMs: 3 });
96
- assert.deepEqual(legacy.queue, { pending: 0, queued: 0, applying: 0, blockedOnUser: 0, expired: 0 });
97
- });
98
-
99
- test('warnings are an open enum: known codes get catalog text, unknown codes stay verbatim', () => {
100
- const status = buildStatus({
101
- now: NOW,
102
- warnings: ['BOARD_STALE', 'board_stale', 'ZZ_CUSTOM'],
103
- });
104
- assert.deepEqual(status.warnings, ['BOARD_STALE', 'ZZ_CUSTOM']);
105
- assert.equal(status.alerts.length, 2);
106
- assert.equal(status.alerts[0].code, 'BOARD_STALE');
107
- assert.equal(status.alerts[0].severity, 'warn');
108
- assert.match(status.alerts[0].message, /看板/);
109
- assert.equal(status.alerts[1].code, 'ZZ_CUSTOM');
110
- assert.match(status.alerts[1].message, /ZZ_CUSTOM/);
111
- });
112
-
113
- test('severity=error alerts are mirrored into warnings, including unknown codes', () => {
114
- const status = buildStatus({
115
- now: NOW,
116
- warnings: ['WAKE_QUOTA'],
117
- alerts: [{ code: 'MY_CUSTOM_FATAL', severity: 'error', message: 'boom' }],
118
- });
119
- assert.ok(status.warnings.includes('MY_CUSTOM_FATAL'));
120
- assert.equal(status.alerts[0].code, 'MY_CUSTOM_FATAL');
121
- assert.equal(status.alerts[0].severity, 'error');
122
- });
123
-
124
- test('warnings and alerts are capped at eight with errors first', () => {
125
- const warnings = Array.from({ length: 12 }, (_, index) => `CUSTOM_${index}`);
126
- const alerts = [
127
- ...Array.from({ length: 6 }, (_, index) => ({ code: `A_${index}`, severity: 'info', message: 'i' })),
128
- ...Array.from({ length: 6 }, (_, index) => ({ code: `E_${index}`, severity: 'error', message: 'e' })),
129
- ];
130
- const status = buildStatus({ now: NOW, warnings, alerts });
131
- assert.equal(status.warnings.length, 8);
132
- assert.equal(status.alerts.length, 8);
133
- assert.equal(status.alerts[0].severity, 'error');
134
- for (const alert of status.alerts.filter((item) => item.severity === 'error')) {
135
- assert.ok(status.warnings.includes(alert.code));
136
- }
137
- });
138
-
139
- test('focus and blocked entries are bounded and never leak proposal bodies', () => {
140
- const focus = Array.from({ length: 12 }, (_, index) => ({
141
- sessionId: `s-${index}`,
142
- goal: 'g',
143
- updatedAt: NOW,
144
- content: 'SECRET BODY',
145
- }));
146
- const blocked = Array.from({ length: 12 }, (_, index) => ({
147
- ticket: `t-${index}`,
148
- reason: 'r',
149
- since: NOW,
150
- content: 'SECRET BODY',
151
- }));
152
- const status = buildStatus({ now: NOW, board: { focus }, proposals: { queue: { blocked } } });
153
- assert.equal(status.board.focus.length, 8);
154
- assert.deepEqual(Object.keys(status.board.focus[0]).sort(), ['goal', 'lastAction', 'queuedCount', 'sessionId', 'updatedAt']);
155
- assert.equal(status.proposals.queue.blocked.length, 8);
156
- assert.deepEqual(Object.keys(status.proposals.queue.blocked[0]).sort(), ['reason', 'since', 'ticket']);
157
- assert.equal(JSON.stringify(status).includes('SECRET BODY'), false);
158
- });
159
-
160
- test('writeStatus writes v2 atomically, reads it back, and classifies storage errors', () => {
161
- withTempDir((dir) => {
162
- const file = path.join(dir, 'vault', 'status.json');
163
- const result = writeStatus(file, {
164
- now: NOW,
165
- state: 'running',
166
- warnings: ['BOARD_STALE'],
167
- lastReport: { summary: 'x'.repeat(2000) },
168
- });
169
- assert.equal(result.ok, true);
170
- assert.ok(result.bytes > 0 && result.bytes <= STATUS_MAX_BYTES);
171
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
172
- assert.equal(parsed.v, 2);
173
- assert.equal(parsed.state, 'running');
174
- assert.equal(parsed.lastReport.summary.length, 200);
175
- const leftovers = fs.readdirSync(path.join(dir, 'vault')).filter((name) => name.includes('.tmp-'));
176
- assert.deepEqual(leftovers, []);
177
-
178
- assert.deepEqual(writeStatus('', {}), { ok: false, error: 'E_STATUS_PATH' });
179
- });
180
-
181
- withTempDir((dir) => {
182
- const blocker = path.join(dir, 'blocker');
183
- fs.writeFileSync(blocker, 'not a directory');
184
- const result = writeStatus(path.join(blocker, 'status.json'), { now: NOW });
185
- assert.equal(result.ok, false);
186
- assert.equal(result.error, 'E_IO');
187
- });
188
-
189
- assert.equal(storageErrorCode({ code: 'ENOSPC' }), 'E_STORAGE');
190
- assert.equal(storageErrorCode({ code: 'EROFS' }), 'E_STORAGE');
191
- assert.equal(storageErrorCode({ code: 'EACCES' }), 'E_STORAGE');
192
- assert.equal(storageErrorCode({ code: 'EPERM' }), 'E_STORAGE');
193
- assert.equal(storageErrorCode({ code: 'ENOTDIR' }), 'E_IO');
194
- assert.equal(storageErrorCode(null), 'E_IO');
195
- });
196
-
197
- test('oversized status shrinks under 16KB without dropping starvation', () => {
198
- withTempDir((dir) => {
199
- const file = path.join(dir, 'status.json');
200
- const alerts = Array.from({ length: 20 }, (_, index) => ({
201
- code: `ERR_${index}`,
202
- severity: index < 10 ? 'error' : 'info',
203
- message: 'm'.repeat(400),
204
- }));
205
- const result = writeStatus(file, {
206
- now: NOW,
207
- alerts,
208
- warnings: alerts.map((alert) => alert.code),
209
- lastError: { code: 'E_BIG', at: NOW, message: 'x'.repeat(5000) },
210
- lastReport: { at: NOW, summary: 'y'.repeat(20000) },
211
- board: { focus: Array.from({ length: 20 }, (_, i) => ({ sessionId: `s${i}`, goal: 'g', updatedAt: NOW })) },
212
- proposals: { queue: { starvation: true, blocked: Array.from({ length: 20 }, () => ({ ticket: 't', reason: 'r', since: NOW })) } },
213
- });
214
- assert.equal(result.ok, true);
215
- assert.ok(result.bytes <= STATUS_MAX_BYTES);
216
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
217
- assert.equal(parsed.proposals.queue.starvation, true);
218
- assert.ok(parsed.alerts.every((alert) => alert.severity === 'error'));
219
- });
220
- });
@@ -1,146 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
-
4
- import { createRouterTools } from '../../src/router/service/tools.mjs';
5
-
6
- function fakeService(config = { enabled: true }, extra = {}) {
7
- const service = {
8
- config,
9
- runId: 'run-1',
10
- panelState: 'running',
11
- status: () => ({ v: 2, state: 'running' }),
12
- requestTick: () => ({ ok: true }),
13
- ask: async () => ({ ok: true }),
14
- reportHistory: () => ({ reports: [] }),
15
- ...extra,
16
- };
17
- return service;
18
- }
19
-
20
- function names(tools) {
21
- return tools.map((tool) => tool.name);
22
- }
23
-
24
- test('closed surface has four tools; proposal surface adds exactly three', () => {
25
- assert.equal(createRouterTools(fakeService()).length, 4);
26
- assert.deepEqual(names(createRouterTools(fakeService())), [
27
- 'router_status',
28
- 'router_tick',
29
- 'router_ask',
30
- 'router_report',
31
- ]);
32
-
33
- for (const config of [
34
- { enabled: true, proposalEnabled: true },
35
- { enabled: true, routerProposalEnabled: true },
36
- { enabled: true, routerProposalsEnabled: true },
37
- ]) {
38
- const tools = createRouterTools(fakeService(config));
39
- assert.equal(tools.length, 7);
40
- assert.deepEqual(names(tools), [
41
- 'router_status',
42
- 'router_tick',
43
- 'router_ask',
44
- 'router_report',
45
- 'router_propose',
46
- 'router_proposals',
47
- 'router_apply',
48
- ]);
49
- }
50
-
51
- // 未开启时不得出现任何未定义工具,字符串 "true" 也不算开启。
52
- assert.equal(createRouterTools(fakeService({ enabled: true, proposalEnabled: 'true' })).length, 4);
53
- assert.equal(createRouterTools(fakeService({ enabled: true, proposalEnabled: 1 })).length, 4);
54
- });
55
-
56
- test('every router tool has a host-shaped parameter map and a text renderer', () => {
57
- const tools = createRouterTools(fakeService({ enabled: true, proposalEnabled: true }));
58
- for (const tool of tools) {
59
- assert.equal(typeof tool.name, 'string');
60
- assert.equal(typeof tool.parameters, 'object');
61
- assert.equal(Array.isArray(tool.parameters), false);
62
- // Host defineTool expects argument-name -> value schema; a full JSON Schema
63
- // object here crashes the host at registration time (UNSUPPORTED_SCHEMA).
64
- assert.equal(Object.hasOwn(tool.parameters, 'type'), false);
65
- assert.equal(Object.hasOwn(tool.parameters, 'properties'), false);
66
- assert.equal(tool.output.schema.type, 'object');
67
- const rendered = tool.output.render({}, { ok: true, message: 'hello' });
68
- assert.deepEqual(rendered, [{ type: 'text', text: 'hello' }]);
69
- }
70
- });
71
-
72
- test('proposal schemas match DESIGN §9.4 within the host value-schema DSL', () => {
73
- const tools = createRouterTools(fakeService({ enabled: true, proposalEnabled: true }));
74
- const byName = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
75
-
76
- const propose = byName.router_propose.parameters;
77
- for (const required of ['op', 'target', 'title', 'content']) {
78
- assert.equal(propose[required].required, true);
79
- }
80
- assert.equal(propose.op.type, 'string');
81
- assert.equal(propose.target.type, 'string');
82
- assert.equal(propose.title.type, 'string');
83
- assert.equal(propose.content.type, 'string');
84
- assert.equal(propose.rationale.type, 'string');
85
- assert.deepEqual(propose.priority.enum, ['P0', 'P1', 'P2', 'P3', 'P4']);
86
- // Length caps live in service validation, not in the host schema DSL.
87
- assert.equal(Object.hasOwn(propose.op, 'maxLength'), false);
88
-
89
- const proposals = byName.router_proposals.parameters;
90
- assert.deepEqual(proposals.mode.enum, ['list', 'queue', 'detail']);
91
- assert.equal(proposals.limit.type, 'integer');
92
-
93
- const apply = byName.router_apply.parameters;
94
- assert.equal(apply.mode.required, true);
95
- assert.deepEqual(apply.mode.enum, ['approve', 'reject', 'apply', 'rollback', 'cancel']);
96
- assert.equal(apply.confirm.type, 'string');
97
- });
98
-
99
- test('router_propose denies P0 before touching the engine, and reports a missing engine honestly', async () => {
100
- const tools = createRouterTools(fakeService({ enabled: true, proposalEnabled: true }));
101
- const propose = tools.find((tool) => tool.name === 'router_propose');
102
- assert.deepEqual(await propose.invoke({ priority: 'P0' }), { ok: false, code: 'E_PRIORITY_DENIED' });
103
- assert.equal((await propose.invoke({ op: 'journal.append' })).code, 'E_NOT_IMPLEMENTED');
104
-
105
- const calls = [];
106
- const withEngine = createRouterTools(fakeService(
107
- { enabled: true, proposalEnabled: true },
108
- { proposeProposal: async (args) => { calls.push(args); return { ok: true, ticket: 't-1' }; } },
109
- ));
110
- const result = await withEngine.find((tool) => tool.name === 'router_propose').invoke({ op: 'journal.append', target: 'x', title: 't', content: 'c' });
111
- assert.equal(result.ticket, 't-1');
112
- assert.equal(calls.length, 1);
113
- });
114
-
115
- test('router_proposals and router_apply validate before delegating', async () => {
116
- const tools = createRouterTools(fakeService({ enabled: true, proposalEnabled: true }));
117
- const proposals = tools.find((tool) => tool.name === 'router_proposals');
118
- const apply = tools.find((tool) => tool.name === 'router_apply');
119
-
120
- assert.equal((await proposals.invoke({ mode: 'queue' })).code, 'E_NOT_IMPLEMENTED');
121
- assert.equal((await apply.invoke({ mode: 'apply' })).code, 'E_APPLY_DISABLED');
122
- assert.equal((await apply.invoke({ mode: 'apply', confirm: 'abcdef' })).code, 'E_APPLY_DISABLED');
123
- assert.equal((await apply.invoke({ mode: 'approve' })).code, 'E_CONFIRM_REQUIRED');
124
- assert.equal((await apply.invoke({ mode: 'rollback' })).code, 'E_APPLY_DISABLED');
125
- assert.equal((await apply.invoke({ mode: 'reject' })).code, 'E_NOT_IMPLEMENTED');
126
-
127
- const withEngine = createRouterTools(fakeService(
128
- { enabled: true, proposalEnabled: true, applyEnabled: true },
129
- {
130
- proposalsView: async (args) => ({ ok: true, mode: args.mode }),
131
- requestApply: async (args) => ({ ok: true, mode: args.mode, queued: true }),
132
- },
133
- ));
134
- assert.deepEqual(
135
- await withEngine.find((tool) => tool.name === 'router_proposals').invoke({ mode: 'queue' }),
136
- { ok: true, mode: 'queue' },
137
- );
138
- assert.deepEqual(
139
- await withEngine.find((tool) => tool.name === 'router_apply').invoke({ mode: 'apply', confirm: 'abcdef' }),
140
- { ok: true, mode: 'apply', queued: true },
141
- );
142
- assert.deepEqual(
143
- await withEngine.find((tool) => tool.name === 'router_apply').invoke({ mode: 'reject' }),
144
- { ok: true, mode: 'reject', queued: true },
145
- );
146
- });