@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -1,721 +0,0 @@
1
- // identity-router v0 单测。零依赖:node test/run.mjs
2
- // 覆盖:STATE 缺失/字段缺失、多卡取最新、超长截断、中文路径、空内容、
3
- // 插件注入(pre-step)、reject 透传、子代理跳过、周期/销毁写草稿。
4
-
5
- import assert from 'node:assert/strict';
6
- import fs from 'node:fs';
7
- import os from 'node:os';
8
- import path from 'node:path';
9
-
10
- import {
11
- DEFAULT_MAX_CHARS,
12
- buildOpenCard,
13
- charLength,
14
- collectOpenCard,
15
- extractSections,
16
- findSection,
17
- normalizeHeading,
18
- parseState,
19
- selectLatestHandover,
20
- sortHandoverCards,
21
- summarizeHandover,
22
- truncateChars,
23
- } from '../../src/identity/handover.js';
24
- import {
25
- buildHandoverDraft,
26
- normalizeItems,
27
- slugifyTopic,
28
- writeHandoverDraft,
29
- writeHandoverFromState,
30
- } from '../../src/identity/writeback.js';
31
- import { apply, normalizeConfig } from '../../src/identity/index.js';
32
- import {
33
- insertionIndex,
34
- placeAfterFirstHuman,
35
- } from '../../src/identity/inject-position.js';
36
-
37
- const tests = [];
38
- const test = (name, fn) => tests.push({ name, fn });
39
- const tempDirs = [];
40
-
41
- function makeTemp(prefix = 'identity-router-') {
42
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
43
- tempDirs.push(dir);
44
- return dir;
45
- }
46
-
47
- const STATE_FIXTURE = `# 听澜状态卡
48
-
49
- ## 2026-11-03 21:47 · rc.9 真机 + scope 落卡
50
- **下一步(最紧三条)**
51
- - 这是粗体小标题下的内容,不该被当成「我在哪」
52
-
53
- ## 我在哪(2026-11-03 19:55 更新)
54
- - 在 C:\\work\\identity-router 写插件
55
- - 卡在 agent/pre-step 注入
56
-
57
- ## 在等什么
58
- - 等父代理验收
59
-
60
- ## 最近三件
61
- 1. 读完宿主钩子
62
- 2. 写完 handover.js
63
- 3. 准备写测试
64
-
65
- ## 我现在在哪
66
- - 干扰项:不该被精确匹配取到
67
-
68
- ## 最近三件未完成
69
- - 干扰项:不该被精确匹配取到
70
- `;
71
-
72
- const HANDOVER_FIXTURE = `# longlive 0.2 设计要点
73
-
74
- ## 一句话
75
-
76
- 把长期记忆做成会话心跳。
77
-
78
- ## 三层
79
-
80
- - 状态卡
81
- - 交接卡
82
- - 流程卡
83
- `;
84
-
85
- function makeCtx() {
86
- const handlers = new Map();
87
- return {
88
- handlers,
89
- logger: { info() {}, warn() {} },
90
- on(event, fn) {
91
- handlers.set(event, fn);
92
- },
93
- };
94
- }
95
-
96
- function makeAgent(id = 'sess-test-0001', origin) {
97
- return {
98
- id,
99
- session: {
100
- id,
101
- header: origin ? { origin } : {},
102
- },
103
- };
104
- }
105
-
106
- // ---------------------------------------------------------------------------
107
- // handover.js
108
- // ---------------------------------------------------------------------------
109
-
110
- test('STATE 正常:三节都抓到,卡片含关键文本且不超 800 字', () => {
111
- const card = buildOpenCard({
112
- stateText: STATE_FIXTURE,
113
- handover: { name: '2026-11-01-longlive.md', text: HANDOVER_FIXTURE },
114
- now: new Date(2026, 10, 4, 9, 30),
115
- });
116
- assert.equal(card.truncated, false);
117
- assert.ok(card.chars <= DEFAULT_MAX_CHARS);
118
- assert.match(card.text, /【开机卡】2026-11-04 09:30/);
119
- assert.match(card.text, /在 C:\\work\\identity-router 写插件/);
120
- assert.match(card.text, /等父代理验收/);
121
- assert.match(card.text, /读完宿主钩子/);
122
- assert.match(card.text, /把长期记忆做成会话心跳/);
123
- assert.deepEqual(card.found, { where: true, waiting: true, recent: true, handover: true });
124
- assert.equal(card.warnings.length, 0);
125
- });
126
-
127
- test('STATE 缺失:不抛,给占位并记 STATE_MISSING', () => {
128
- const card = buildOpenCard({
129
- stateText: null,
130
- handover: null,
131
- now: new Date(2026, 10, 4, 9, 30),
132
- });
133
- assert.ok(card.text.length > 0);
134
- assert.ok(card.warnings.includes('STATE_MISSING'));
135
- assert.ok(card.warnings.includes('HANDOVER_MISSING'));
136
- assert.match(card.text, /STATE\.md 里没有这一节/);
137
- assert.equal(card.found.where, false);
138
- });
139
-
140
- test('字段缺失:只给「我在哪」,其余节用占位,不编造', () => {
141
- const card = buildOpenCard({
142
- stateText: '# 卡\n\n## 我在哪\n- 只在写测试\n',
143
- handover: { name: '2026-11-02-x.md', text: '# X\n\n## 一句话\n测试卡。\n' },
144
- now: new Date(2026, 10, 4),
145
- });
146
- assert.equal(card.found.where, true);
147
- assert.equal(card.found.waiting, false);
148
- assert.equal(card.found.recent, false);
149
- assert.ok(card.warnings.includes('SECTION_MISSING:在等什么'));
150
- assert.ok(card.warnings.includes('SECTION_MISSING:最近三件'));
151
- assert.match(card.text, /只在写测试/);
152
- assert.match(card.text, /STATE\.md 里没有这一节/);
153
- });
154
-
155
- test('近似标题不误匹配:「我现在在哪」「最近三件未完成」不算', () => {
156
- const parsed = parseState(STATE_FIXTURE);
157
- assert.ok(findSection(parsed.sections, '我在哪'));
158
- assert.ok(findSection(parsed.sections, '最近三件'));
159
- const where = findSection(parsed.sections, '我在哪');
160
- assert.match(where.content, /identity-router/);
161
- assert.doesNotMatch(where.content, /干扰项/);
162
- const recent = findSection(parsed.sections, '最近三件');
163
- assert.match(recent.content, /准备写测试/);
164
- assert.doesNotMatch(recent.content, /干扰项/);
165
- });
166
-
167
- test('标题归一化剥括号注释,但不等于近似标题', () => {
168
- assert.equal(normalizeHeading('## 我在哪(2026-11-03 更新)'), '我在哪');
169
- assert.equal(normalizeHeading('## 在等什么'), '在等什么');
170
- assert.notEqual(normalizeHeading('## 我现在在哪'), '我在哪');
171
- assert.notEqual(normalizeHeading('## 最近三件未完成'), '最近三件');
172
- });
173
-
174
- test('代码围栏里的 ## 不当标题', () => {
175
- const markdown = [
176
- '# 卡',
177
- '',
178
- '```md',
179
- '## 我在哪',
180
- '这是假货',
181
- '```',
182
- '',
183
- '## 我在哪',
184
- '- 这是真的',
185
- ].join('\n');
186
- const sections = extractSections(markdown);
187
- const where = sections.filter((section) => section.title === '我在哪');
188
- assert.equal(where.length, 1);
189
- assert.match(where[0].content, /这是真的/);
190
- assert.doesNotMatch(where[0].content, /假货/);
191
- });
192
-
193
- test('BOM + CRLF 也能正常解析', () => {
194
- const markdown = '\uFEFF# 标题\r\n\r\n## 我在哪\r\n- Windows 换行\r\n';
195
- const parsed = parseState(markdown);
196
- assert.ok(parsed.found.where);
197
- assert.match(findSection(parsed.sections, '我在哪').content, /Windows 换行/);
198
- });
199
-
200
- test('多张交接卡按文件名日期取最新(mtime 更新的旧卡不抢位)', () => {
201
- const entries = [
202
- { name: '2026-10-31-surgery.md', mtimeMs: 9_999_999_999_999 },
203
- { name: '2026-11-01-longlive-0.2.md', mtimeMs: 1 },
204
- { name: '2026-10-27-old.md', mtimeMs: 5 },
205
- ];
206
- const sorted = sortHandoverCards(entries);
207
- assert.equal(sorted[0].name, '2026-11-01-longlive-0.2.md');
208
- assert.equal(selectLatestHandover(entries).name, '2026-11-01-longlive-0.2.md');
209
- });
210
-
211
- test('无日期卡片按 mtime 排序并排在日期卡之后', () => {
212
- const sorted = sortHandoverCards([
213
- { name: 'note.md', mtimeMs: 100 },
214
- { name: '2026-11-01-a.md', mtimeMs: 1 },
215
- { name: 'scratch.md', mtimeMs: 200 },
216
- ]);
217
- assert.equal(sorted[0].name, '2026-11-01-a.md');
218
- assert.deepEqual(sorted.slice(1).map((item) => item.name), ['scratch.md', 'note.md']);
219
- });
220
-
221
- test('超长截断:300 字上限下长度不超,且标记 truncated', () => {
222
- const long = Array.from({ length: 80 }, (_, index) => `- 第 ${index + 1} 条很长很长的进度描述`).join('\n');
223
- const card = buildOpenCard({
224
- stateText: `# 卡\n\n## 我在哪\n${long}\n\n## 在等什么\n${long}\n\n## 最近三件\n${long}\n`,
225
- handover: { name: '2026-11-01-x.md', text: `# 卡\n\n## 一句话\n${'交接内容'.repeat(200)}` },
226
- maxChars: 300,
227
- now: new Date(2026, 10, 4),
228
- });
229
- assert.ok(charLength(card.text) <= 300, `实际 ${charLength(card.text)}`);
230
- assert.equal(card.truncated, true);
231
- assert.ok(card.warnings.includes('CARD_TRUNCATED') || card.warnings.includes('SECTION_TRUNCATED'));
232
- });
233
-
234
- test('极小上限 120 也不越界', () => {
235
- const card = buildOpenCard({
236
- stateText: STATE_FIXTURE,
237
- handover: { name: '2026-11-01-x.md', text: HANDOVER_FIXTURE },
238
- maxChars: 120,
239
- });
240
- assert.ok(card.chars <= 120, `实际 ${card.chars}`);
241
- });
242
-
243
- test('中文路径:STATE 与 handover 都能读', () => {
244
- const root = path.join(makeTemp('身份路由-'), '记忆 根');
245
- const handoverDir = path.join(root, 'handover');
246
- fs.mkdirSync(handoverDir, { recursive: true });
247
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
248
- fs.writeFileSync(
249
- path.join(handoverDir, '2026-11-01-中文卡.md'),
250
- '# 中文交接卡\n\n## 一句话\n中文交接内容。\n',
251
- 'utf8',
252
- );
253
-
254
- const card = collectOpenCard({ memoryRoot: root, now: new Date(2026, 10, 4) });
255
- assert.deepEqual(card.errors, []);
256
- assert.match(card.text, /中文交接内容/);
257
- assert.ok(card.handoverPath.endsWith('2026-11-01-中文卡.md'));
258
- });
259
-
260
- test('空内容:空 STATE + 空 handover 不抛,全占位', () => {
261
- const card = buildOpenCard({ stateText: '', handover: { name: 'empty.md', text: '' } });
262
- assert.ok(card.warnings.includes('STATE_EMPTY'));
263
- assert.ok(card.warnings.includes('HANDOVER_EMPTY'));
264
- assert.ok(card.chars > 0);
265
- assert.equal(card.truncated, false);
266
- });
267
-
268
- test('空 handover 目录:HANDOVER_MISSING,但卡照常生成', () => {
269
- const root = makeTemp();
270
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
271
- fs.mkdirSync(path.join(root, 'handover'), { recursive: true });
272
- const card = collectOpenCard({ memoryRoot: root });
273
- assert.ok(card.warnings.includes('HANDOVER_MISSING'));
274
- assert.equal(card.handoverPath, null);
275
- assert.match(card.text, /没有找到交接卡/);
276
- });
277
-
278
- test('最新交接卡是空文件:回退到旧卡并告警', () => {
279
- const root = makeTemp();
280
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
281
- const handoverDir = path.join(root, 'handover');
282
- fs.mkdirSync(handoverDir, { recursive: true });
283
- fs.writeFileSync(path.join(handoverDir, '2026-11-01-empty.md'), '', 'utf8');
284
- fs.writeFileSync(
285
- path.join(handoverDir, '2026-10-27-old.md'),
286
- '# 旧卡\n\n## 一句话\n旧卡也要能接上。\n',
287
- 'utf8',
288
- );
289
- const card = collectOpenCard({ memoryRoot: root });
290
- assert.ok(card.warnings.includes('HANDOVER_LATEST_EMPTY'));
291
- assert.match(card.text, /旧卡也要能接上/);
292
- assert.ok(card.handoverPath.endsWith('2026-10-27-old.md'));
293
- });
294
-
295
- test('交接卡摘要优先取「一句话」,带标题', () => {
296
- const summary = summarizeHandover(HANDOVER_FIXTURE, 200);
297
- assert.match(summary.text, /^《longlive 0\.2 设计要点》/);
298
- assert.match(summary.text, /把长期记忆做成会话心跳/);
299
- assert.equal(summary.truncated, false);
300
- });
301
-
302
- test('交接卡有标题时不再重复文件名标签', () => {
303
- const card = buildOpenCard({
304
- stateText: STATE_FIXTURE,
305
- handover: { name: '2026-11-01-longlive-0.2-设计要点.md', text: HANDOVER_FIXTURE },
306
- now: new Date(2026, 10, 4),
307
- });
308
- const occurrences = card.text.match(/longlive 0\.2 设计要点/gu) ?? [];
309
- assert.equal(occurrences.length, 1);
310
- assert.doesNotMatch(card.text, /2026-11-01-longlive/);
311
- });
312
-
313
- test('truncateChars 按码点计数,emoji 不被劈成两半', () => {
314
- const cut = truncateChars('鲸鱼🐳鲸鱼🐳鲸鱼', 6);
315
- assert.equal(charLength(cut.text), 6);
316
- assert.equal(cut.truncated, true);
317
- assert.ok(cut.text.endsWith('…'));
318
- });
319
-
320
- // ---------------------------------------------------------------------------
321
- // writeback.js
322
- // ---------------------------------------------------------------------------
323
-
324
- test('从 STATE 生成草稿:进度与下一步来自三节,不编造', () => {
325
- const draft = buildHandoverDraft({
326
- topic: 'identity-router v0',
327
- stateText: STATE_FIXTURE,
328
- now: new Date(2026, 10, 4, 10, 5),
329
- });
330
- assert.ok(draft.text.includes('# 交接卡草稿:identity-router v0'));
331
- assert.ok(draft.text.includes('## 当前进度'));
332
- assert.ok(draft.text.includes('【我在哪】'));
333
- assert.ok(draft.text.includes('【最近三件】'));
334
- assert.ok(draft.text.includes('## 下一步'));
335
- assert.ok(draft.text.includes('等父代理验收'));
336
- assert.ok(draft.text.includes('## 风险'));
337
- assert.ok(draft.text.includes('尚未写入 memoryRoot'));
338
- assert.equal(draft.filename, '2026-11-04-identity-router-v0.md');
339
- assert.ok(draft.chars <= 1600);
340
- assert.ok(!draft.warnings.includes('DRAFT_USED_STATE_FALLBACK'));
341
- });
342
-
343
- test('显式 progress/next/risks 覆盖 STATE 推导', () => {
344
- const draft = buildHandoverDraft({
345
- topic: '手动卡',
346
- stateText: STATE_FIXTURE,
347
- progress: ['改完 handover.js', '写完 writeback.js'],
348
- next: '补事件注入',
349
- risks: ['钩子没在真机验证'],
350
- now: new Date(2026, 10, 4),
351
- });
352
- assert.match(draft.text, /- 改完 handover\.js/);
353
- assert.match(draft.text, /- 补事件注入/);
354
- assert.match(draft.text, /- 钩子没在真机验证/);
355
- assert.doesNotMatch(draft.text, /【我在哪】/);
356
- });
357
-
358
- test('中文 topic 的 slug 回退为 session,文件名仍是合法 ASCII', () => {
359
- assert.equal(slugifyTopic('身份路由 v0'), 'v0');
360
- assert.equal(slugifyTopic('完全中文'), 'session');
361
- const draft = buildHandoverDraft({ topic: '完全中文', progress: ['x'], next: ['y'], now: new Date(2026, 0, 2) });
362
- assert.equal(draft.filename, '2026-01-02-session.md');
363
- });
364
-
365
- test('写草稿:创建 outbox,重名自动加 -2 不覆盖', () => {
366
- const outbox = path.join(makeTemp(), 'outbox');
367
- const draft = buildHandoverDraft({
368
- topic: 'collision',
369
- progress: ['第一版'],
370
- next: ['继续'],
371
- risks: ['无'],
372
- now: new Date(2026, 10, 4),
373
- });
374
- const first = writeHandoverDraft(draft, { outboxDir: outbox });
375
- const second = writeHandoverDraft(draft, { outboxDir: outbox });
376
- assert.equal(first.ok, true);
377
- assert.equal(second.ok, true);
378
- assert.ok(first.path.endsWith('2026-11-04-collision.md'));
379
- assert.ok(second.path.endsWith('2026-11-04-collision-2.md'));
380
- assert.equal(fs.readFileSync(first.path, 'utf8'), draft.text);
381
- });
382
-
383
- test('writeHandoverFromState:中文路径下读 STATE、写 outbox', () => {
384
- const root = path.join(makeTemp('身份路由-'), '记忆 根');
385
- fs.mkdirSync(root, { recursive: true });
386
- const statePath = path.join(root, 'STATE.md');
387
- fs.writeFileSync(statePath, STATE_FIXTURE, 'utf8');
388
- const outbox = path.join(root, 'outbox 目录');
389
-
390
- const result = writeHandoverFromState({
391
- statePath,
392
- outboxDir: outbox,
393
- topic: '中文路径测试',
394
- now: new Date(2026, 10, 4, 11, 0),
395
- });
396
- assert.equal(result.ok, true);
397
- assert.ok(result.path.startsWith(outbox));
398
- const text = fs.readFileSync(result.path, 'utf8');
399
- assert.match(text, /【我在哪】/);
400
- assert.match(text, /2026-11-04 11:00/);
401
- });
402
-
403
- test('STATE 读不到时草稿仍可写,但带占位与警告', () => {
404
- const outbox = makeTemp();
405
- const result = writeHandoverFromState({
406
- statePath: path.join(outbox, 'missing-STATE.md'),
407
- outboxDir: path.join(outbox, 'out'),
408
- topic: 'missing',
409
- now: new Date(2026, 10, 4),
410
- });
411
- assert.equal(result.ok, true);
412
- assert.ok(result.warnings.includes('STATE_READ_FAILED:ENOENT'));
413
- assert.ok(result.warnings.includes('DRAFT_USED_STATE_FALLBACK'));
414
- assert.match(result.text, /未提供 STATE\.md/);
415
- });
416
-
417
- test('normalizeItems 去 bullet、拆多行、过滤空项', () => {
418
- assert.deepEqual(normalizeItems('- a\n2. b\n\n* c'), ['a', 'b', 'c']);
419
- assert.deepEqual(normalizeItems(['x', ['y', 'z']]), ['x', 'y', 'z']);
420
- assert.deepEqual(normalizeItems(''), []);
421
- });
422
-
423
- // ---------------------------------------------------------------------------
424
- // index.js(插件外壳)
425
- // ---------------------------------------------------------------------------
426
-
427
- test('normalizeConfig 钳制坏值,默认值稳定', () => {
428
- const config = normalizeConfig({
429
- maxChars: 10,
430
- writeEveryTurns: -5,
431
- memoryRoot: ' ',
432
- log: 'yes',
433
- });
434
- assert.equal(config.maxChars, 120);
435
- assert.equal(config.writeEveryTurns, 0);
436
- assert.equal(config.memoryRoot, undefined);
437
- assert.equal(config.log, false);
438
- assert.equal(normalizeConfig({ maxChars: 99999 }).maxChars, 4000);
439
- assert.equal(normalizeConfig({ maxChars: 'abc' }).maxChars, 800);
440
- });
441
-
442
- test('pre-step 第一轮注入一次,第二轮同一会话不再注入', async () => {
443
- const root = makeTemp();
444
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
445
- fs.mkdirSync(path.join(root, 'handover'), { recursive: true });
446
- fs.writeFileSync(
447
- path.join(root, 'handover', '2026-11-01-x.md'),
448
- HANDOVER_FIXTURE,
449
- 'utf8',
450
- );
451
-
452
- const ctx = makeCtx();
453
- const injected = [];
454
- apply(ctx, {
455
- memoryRoot: root,
456
- outboxDir: path.join(root, 'outbox'),
457
- createUserMessage: (input) => {
458
- injected.push(input);
459
- return { role: 'user', ...input };
460
- },
461
- writeOnTurnEnd: false,
462
- writeOnSessionEnd: false,
463
- });
464
-
465
- const agent = makeAgent('sess-inject-01');
466
- const decision = { kind: 'enter', messages: [] };
467
- const preStep = ctx.handlers.get('agent/pre-step');
468
- const first = await preStep({ agent, step: 1, turn: 1 }, async () => decision);
469
- assert.equal(first.messages.length, 1);
470
- assert.equal(injected.length, 1);
471
- assert.match(injected[0].content[0].text, /【开机卡】/);
472
- assert.equal(injected[0].source.kind, 'session-reference');
473
- assert.equal(injected[0].source.form, 'recall');
474
- assert.equal(injected[0].source.version, 1);
475
- assert.deepEqual(injected[0].source.references, []);
476
- assert.equal(injected[0].source.sections, undefined);
477
-
478
- const second = await preStep({ agent, step: 1, turn: 2 }, async () => ({ kind: 'enter', messages: [] }));
479
- assert.equal(second.messages.length, 0);
480
- assert.equal(injected.length, 1);
481
- });
482
-
483
- test('pre-step 拒绝坏 source.kind:工厂产物不进 decision,也不写日志', async () => {
484
- const root = makeTemp();
485
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
486
- fs.mkdirSync(path.join(root, 'handover'), { recursive: true });
487
- fs.writeFileSync(path.join(root, 'handover', '2026-11-01-x.md'), HANDOVER_FIXTURE, 'utf8');
488
-
489
- const ctx = makeCtx();
490
- const injected = [];
491
- apply(ctx, {
492
- memoryRoot: root,
493
- outboxDir: path.join(root, 'outbox'),
494
- createUserMessage: (input) => {
495
- const message = { role: 'user', ...input, source: { kind: 'plugin:bogus' } };
496
- injected.push(message);
497
- return message;
498
- },
499
- writeOnTurnEnd: false,
500
- writeOnSessionEnd: false,
501
- });
502
-
503
- const agent = makeAgent('sess-bad-kind');
504
- const decision = { kind: 'enter', messages: [] };
505
- const preStep = ctx.handlers.get('agent/pre-step');
506
- const out = await preStep({ agent, step: 1, turn: 1 }, async () => decision);
507
- assert.equal(injected.length, 1, '工厂确实被调用过');
508
- assert.equal(out.messages.length, 0, '坏 kind 注入必须被拒绝,不能 append');
509
- assert.equal(out.messages.some((m) => m.source?.kind === 'plugin:bogus'), false);
510
- });
511
-
512
- test('inject-position:插到第一条真实 user 之后,且不把 runtime-context 当人话', () => {
513
- const user1 = { role: 'user', content: [{ type: 'text', text: '一' }], source: { kind: 'user' } };
514
- const runtime = {
515
- role: 'user',
516
- content: [{ type: 'text', text: 'runtime' }],
517
- source: { kind: 'runtime-context' },
518
- };
519
- const user2 = { role: 'user', content: [{ type: 'text', text: '二' }], source: { kind: 'user' } };
520
- assert.equal(insertionIndex([user1, user2]), 1);
521
- assert.equal(insertionIndex([user1, runtime, user2]), 1);
522
- assert.equal(insertionIndex([runtime]), 0, '没有真实用户消息时退回最前面');
523
-
524
- const boot = { role: 'user', content: [{ type: 'text', text: '【开机卡】' }], source: { kind: 'user' } };
525
- assert.deepEqual(placeAfterFirstHuman([user1, user2], boot), [user1, boot, user2]);
526
- // 幂等:已经就位时不重复插入。
527
- assert.deepEqual(placeAfterFirstHuman([user1, boot, user2], boot), [user1, boot, user2]);
528
- // 被旧实现 unshift 到最前的 boot 会被搬到 user 后面。
529
- assert.deepEqual(placeAfterFirstHuman([boot, user1, user2], boot), [user1, boot, user2]);
530
- });
531
-
532
- test('pre-step 把开机卡插在第一条真实 user 之后(claimed 路径)', async () => {
533
- const root = makeTemp();
534
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
535
- fs.mkdirSync(path.join(root, 'handover'), { recursive: true });
536
- fs.writeFileSync(path.join(root, 'handover', '2026-11-01-x.md'), HANDOVER_FIXTURE, 'utf8');
537
-
538
- const ctx = makeCtx();
539
- const injected = [];
540
- apply(ctx, {
541
- memoryRoot: root,
542
- outboxDir: path.join(root, 'outbox'),
543
- createUserMessage: (input) => {
544
- const message = { role: 'user', ...input };
545
- injected.push(message);
546
- return message;
547
- },
548
- writeOnTurnEnd: false,
549
- writeOnSessionEnd: false,
550
- });
551
-
552
- const agent = makeAgent('sess-inject-claimed');
553
- const user1 = { role: 'user', content: [{ type: 'text', text: '真实提问一' }], source: { kind: 'user' } };
554
- const user2 = { role: 'user', content: [{ type: 'text', text: '真实提问二' }], source: { kind: 'user' } };
555
- const runtime = {
556
- role: 'user',
557
- content: [{ type: 'text', text: 'runtime context' }],
558
- source: { kind: 'runtime-context' },
559
- };
560
- const claimed = [user1, user2];
561
- const preStep = ctx.handlers.get('agent/pre-step');
562
- const decision = await preStep(
563
- { agent, messages: claimed, step: 1, turn: 1 },
564
- async () => ({ kind: 'enter', messages: [...claimed, runtime] }),
565
- );
566
-
567
- assert.equal(injected.length, 1);
568
- assert.deepEqual(decision.messages, [user1, injected[0], user2, runtime]);
569
- // TurnNavigator 取该轮第一个 kind==='user' 节点 → 必须是用户原话。
570
- assert.equal(decision.messages[0], user1);
571
- assert.equal(decision.messages[0].content[0].text, '真实提问一');
572
- assert.match(injected[0].content[0].text, /【开机卡】/);
573
- });
574
-
575
- test('pre-step 对 reject 原样透传,不注入', async () => {
576
- const ctx = makeCtx();
577
- apply(ctx, {
578
- statePath: path.join(makeTemp(), 'STATE.md'),
579
- createUserMessage: () => {
580
- throw new Error('不该被调用');
581
- },
582
- writeOnTurnEnd: false,
583
- writeOnSessionEnd: false,
584
- });
585
- const reject = { kind: 'reject' };
586
- const result = await ctx.handlers.get('agent/pre-step')(
587
- { agent: makeAgent('sess-reject'), step: 1, turn: 1 },
588
- async () => reject,
589
- );
590
- assert.equal(result, reject);
591
- });
592
-
593
- test('子代理默认跳过注入', async () => {
594
- const ctx = makeCtx();
595
- let called = 0;
596
- apply(ctx, {
597
- statePath: path.join(makeTemp(), 'STATE.md'),
598
- createUserMessage: () => {
599
- called += 1;
600
- return { role: 'user' };
601
- },
602
- writeOnTurnEnd: false,
603
- writeOnSessionEnd: false,
604
- });
605
- const agent = makeAgent('sess-sub', 'subagent');
606
- const result = await ctx.handlers.get('agent/pre-step')(
607
- { agent, step: 1, turn: 1 },
608
- async () => ({ kind: 'enter', messages: [] }),
609
- );
610
- assert.equal(result.messages.length, 0);
611
- assert.equal(called, 0);
612
- });
613
-
614
- test('turn/end 每 N 轮写一次草稿,不重复', () => {
615
- const root = makeTemp();
616
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
617
- const outbox = path.join(root, 'outbox');
618
- const ctx = makeCtx();
619
- apply(ctx, {
620
- memoryRoot: root,
621
- outboxDir: outbox,
622
- writeEveryTurns: 2,
623
- writeOnSessionEnd: false,
624
- });
625
-
626
- const session = { id: 'sess-turns-1234', header: {} };
627
- const onEvent = ctx.handlers.get('session/event');
628
- onEvent(session, { type: 'turn/end', data: { turn: 1 } });
629
- assert.equal(fs.existsSync(outbox) ? fs.readdirSync(outbox).length : 0, 0);
630
- onEvent(session, { type: 'turn/end', data: { turn: 2 } });
631
- const files = fs.readdirSync(outbox).filter((file) => file.endsWith('.md'));
632
- assert.equal(files.length, 1);
633
- assert.match(files[0], /identity-router-1234/);
634
- onEvent(session, { type: 'turn/end', data: { turn: 3 } });
635
- assert.equal(fs.readdirSync(outbox).filter((file) => file.endsWith('.md')).length, 1);
636
- });
637
-
638
- test('writeback 也尊重 statePath 配置,不只 memoryRoot', () => {
639
- const root = makeTemp();
640
- const statePath = path.join(root, 'STATE.md');
641
- fs.writeFileSync(statePath, STATE_FIXTURE, 'utf8');
642
- const outbox = path.join(root, 'outbox');
643
- const ctx = makeCtx();
644
- apply(ctx, {
645
- statePath,
646
- outboxDir: outbox,
647
- writeEveryTurns: 1,
648
- writeOnSessionEnd: false,
649
- });
650
- ctx.handlers.get('session/event')(
651
- { id: 'sess-statepath-77', header: {} },
652
- { type: 'turn/end', data: { turn: 1 } },
653
- );
654
- const files = fs.readdirSync(outbox).filter((file) => file.endsWith('.md'));
655
- assert.equal(files.length, 1);
656
- const text = fs.readFileSync(path.join(outbox, files[0]), 'utf8');
657
- assert.match(text, /在 C:\\work\\identity-router 写插件/);
658
- });
659
-
660
- test('session/disposed 写最后一张草稿,且只写一次', () => {
661
- const root = makeTemp();
662
- fs.writeFileSync(path.join(root, 'STATE.md'), STATE_FIXTURE, 'utf8');
663
- const outbox = path.join(root, 'outbox');
664
- const ctx = makeCtx();
665
- apply(ctx, {
666
- memoryRoot: root,
667
- outboxDir: outbox,
668
- writeOnTurnEnd: false,
669
- writeOnSessionEnd: true,
670
- });
671
- const session = { id: 'sess-final-9999', header: {} };
672
- const disposed = ctx.handlers.get('session/disposed');
673
- disposed(session);
674
- disposed(session);
675
- const files = fs.readdirSync(outbox).filter((file) => file.endsWith('.md'));
676
- assert.equal(files.length, 1);
677
- });
678
-
679
- test('注入工厂不可用时跳过注入但不影响 decision', async () => {
680
- const ctx = makeCtx();
681
- apply(ctx, {
682
- statePath: path.join(makeTemp(), 'STATE.md'),
683
- writeOnTurnEnd: false,
684
- writeOnSessionEnd: false,
685
- });
686
- const decision = { kind: 'enter', messages: [] };
687
- const result = await ctx.handlers.get('agent/pre-step')(
688
- { agent: makeAgent('sess-no-factory'), step: 1, turn: 1 },
689
- async () => decision,
690
- );
691
- assert.equal(result, decision);
692
- });
693
-
694
- // ---------------------------------------------------------------------------
695
- // runner
696
- // ---------------------------------------------------------------------------
697
-
698
- let passed = 0;
699
- const failures = [];
700
-
701
- for (const item of tests) {
702
- try {
703
- await item.fn();
704
- passed += 1;
705
- process.stdout.write(`ok ${passed + failures.length} - ${item.name}\n`);
706
- } catch (error) {
707
- failures.push({ name: item.name, error });
708
- process.stdout.write(`not ok - ${item.name}\n ${error?.message ?? error}\n`);
709
- }
710
- }
711
-
712
- for (const dir of tempDirs) {
713
- try {
714
- fs.rmSync(dir, { recursive: true, force: true });
715
- } catch {
716
- // 临时目录清不掉不影响测试结论
717
- }
718
- }
719
-
720
- process.stdout.write(`\n${passed} passed, ${failures.length} failed, ${tests.length} total\n`);
721
- if (failures.length > 0) process.exitCode = 1;