@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,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;