@lqc123qwe/car-runtime 1.9.0 → 1.10.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 (176) hide show
  1. package/README.md +17 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +44 -0
  4. package/dist/authz/authz.js +65 -0
  5. package/dist/car-runtime-1.10.0.tgz +0 -0
  6. package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +79 -0
  44. package/dist/loop/stop.js +151 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +40 -0
  65. package/dist/runtime-core/chatStep.js +104 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +553 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +84 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +21 -0
  86. package/dist/seam.js +20 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/seam.ts +21 -0
  107. package/.gitattributes +0 -11
  108. package/.github/workflows/ci.yml +0 -293
  109. package/.github/workflows/release.yml +0 -54
  110. package/adr/ADR-001.md +0 -4
  111. package/adr/ADR-002.md +0 -5
  112. package/adr/ADR-003.md +0 -35
  113. package/adr/host-onboarding.md +0 -68
  114. package/adr/mlps-audit-checklist.md +0 -41
  115. package/adr/win32-spike.md +0 -45
  116. package/docs/plugin-authoring.md +0 -82
  117. package/native/landlock-run/Makefile +0 -19
  118. package/native/landlock-run/landlock-run.c +0 -162
  119. package/scripts/contract-check.ts +0 -56
  120. package/scripts/invariant-n1.ts +0 -96
  121. package/scripts/package-check.ts +0 -29
  122. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  123. package/scripts/release-artifacts.ts +0 -111
  124. package/scripts/release-pipeline.ts +0 -245
  125. package/scripts/secrets-baseline.ts +0 -33
  126. package/scripts/secrets-scan.ts +0 -56
  127. package/scripts/win32-ffi-debug.ts +0 -143
  128. package/scripts/win32-koffi-spike.ts +0 -537
  129. package/test/contract/eventmap.golden.json +0 -7
  130. package/test/fixtures/hello-plugin.ts +0 -5
  131. package/test/kernel.spec.ts +0 -136
  132. package/test/registry-e2e/run-e2e.ts +0 -213
  133. package/test/s11.spec.ts +0 -129
  134. package/test/s12.spec.ts +0 -135
  135. package/test/s13.spec.ts +0 -117
  136. package/test/s14.spec.ts +0 -117
  137. package/test/s15.spec.ts +0 -80
  138. package/test/s16.spec.ts +0 -133
  139. package/test/s17.spec.ts +0 -102
  140. package/test/s18.spec.ts +0 -81
  141. package/test/s19.spec.ts +0 -138
  142. package/test/s2.spec.ts +0 -245
  143. package/test/s20.spec.ts +0 -241
  144. package/test/s21.spec.ts +0 -231
  145. package/test/s22-m7-store-format.spec.ts +0 -135
  146. package/test/s23-m7-export-index.spec.ts +0 -189
  147. package/test/s24-signature-gate.spec.ts +0 -185
  148. package/test/s25-m8-runtime-core.spec.ts +0 -600
  149. package/test/s26-signature-closeout.spec.ts +0 -349
  150. package/test/s27-host-load-wiring.spec.ts +0 -134
  151. package/test/s28-session-index-followup.spec.ts +0 -236
  152. package/test/s29-otel-producer.spec.ts +0 -252
  153. package/test/s3.spec.ts +0 -179
  154. package/test/s30-release-artifacts.spec.ts +0 -127
  155. package/test/s31-real-model-wiring.spec.ts +0 -329
  156. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  157. package/test/s33-anthropic-adapter.spec.ts +0 -354
  158. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  159. package/test/s35-config-ts-zod.spec.ts +0 -145
  160. package/test/s36-host-tool-surface.spec.ts +0 -250
  161. package/test/s37-projection-dedupe.spec.ts +0 -161
  162. package/test/s38-config-jiti.spec.ts +0 -116
  163. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  164. package/test/s4.spec.ts +0 -169
  165. package/test/s40-metrics-cumulative.spec.ts +0 -67
  166. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  167. package/test/s42-traceparent-propagation.spec.ts +0 -415
  168. package/test/s43-remote-http-traceparent.spec.ts +0 -262
  169. package/test/s5.spec.ts +0 -95
  170. package/test/s6.spec.ts +0 -169
  171. package/test/s7.spec.ts +0 -136
  172. package/test/s7b-baseline.spec.ts +0 -20
  173. package/test/s9.spec.ts +0 -89
  174. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  175. package/test/sandbox-escape/run-matrix.sh +0 -98
  176. package/tsconfig.json +0 -18
package/test/s21.spec.ts DELETED
@@ -1,231 +0,0 @@
1
- /**
2
- * M6 验收补充测试(QA 严过关 · s20 缺口补齐)
3
- *
4
- * 覆盖 s20 未触及的边界:
5
- * - discover:空目录 FAIL / 路径不存在 FAIL / *.spec.ts、*.d.ts 排除规则
6
- * - parse:模块导入失败(语法坏文件)→ CAR-E-PARSE
7
- * - validate:optional peer 缺失不 FAIL
8
- * - topo:peer 依赖环 → CAR-E-DEPCYCLE(register SKIPPED)
9
- * - register:DUP 重复插件名 → CAR-E-DUP;erasable 违规 → CAR-E-PTC
10
- * - 热重载:epoch 连续 3 次递增;reload 失败后旧实例不复活(不静默回退的另一半);
11
- * 并发 reload 基本不变量(epoch 计数 / history / 实例均未失效)
12
- * - doctor:CAR_OFFLINE=1 显式离线路径(非仅 skip 注入)
13
- */
14
- import { test } from 'node:test'
15
- import assert from 'node:assert/strict'
16
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
17
- import { tmpdir } from 'node:os'
18
- import { join } from 'node:path'
19
- import { loadPlugins, ReloadManager } from '../src/load/report.ts'
20
- import { doctorConnectivity } from '../src/dx/doctor.ts'
21
- import type { LoadReportVO } from '../src/load/report.ts'
22
-
23
- const PLUGIN_BODY = (tool: string) =>
24
- `export default function apply(api) {\n api.registerTool({ name: '${tool}', run: async () => 'ok' })\n}\n`
25
-
26
- function makePlugin(dir: string, file: string, manifest: Record<string, unknown>, tool: string): string {
27
- const p = join(dir, file)
28
- writeFileSync(p, `export const manifest = ${JSON.stringify(manifest)}\n${PLUGIN_BODY(tool)}\n`)
29
- return p
30
- }
31
-
32
- function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
33
- const dir = mkdtempSync(join(tmpdir(), `car-s21-${name}-`))
34
- return Promise.resolve()
35
- .then(() => fn(dir))
36
- .finally(() => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } })
37
- }
38
-
39
- const stageOf = (r: LoadReportVO, s: string) => r.stages.find(x => x.stage === s)!
40
- const stageNames = (r: LoadReportVO) => r.stages.map(s => `${s.stage}:${s.status}`)
41
-
42
- // ==================== discover 边界 ====================
43
-
44
- test('M6-s21: 空目录 discover FAIL + 后续阶段 SKIPPED', async () => {
45
- await withTempDir('empty', async (dir) => {
46
- const { report } = await loadPlugins({ source: dir })
47
- assert.deepEqual(stageNames(report), [
48
- 'discover:FAIL', 'verify:SKIPPED', 'parse:SKIPPED', 'validate:SKIPPED', 'topo:SKIPPED', 'register:SKIPPED',
49
- ])
50
- assert.match(stageOf(report, 'discover').error!.reason, /no \*\.ts plugin files found/)
51
- })
52
- })
53
-
54
- test('M6-s21: 源路径不存在 discover FAIL(文件定位=source)', async () => {
55
- await withTempDir('missing', async (dir) => {
56
- const missing = join(dir, 'no-such-dir')
57
- const { report } = await loadPlugins({ source: missing })
58
- assert.equal(stageOf(report, 'discover').status, 'FAIL')
59
- assert.match(stageOf(report, 'discover').error!.reason, /source not found/)
60
- })
61
- })
62
-
63
- test('M6-s21: discover 排除规则——*.spec.ts / *.d.ts / 非 .ts 不进装配', async () => {
64
- await withTempDir('exclude', async (dir) => {
65
- makePlugin(dir, 'real.ts', { name: 'real', version: '1.0.0' }, 't')
66
- writeFileSync(join(dir, 'real.spec.ts'), `export const manifest = { name: 'real-spec', version: '1.0.0' }\n${PLUGIN_BODY('x')}\n`)
67
- writeFileSync(join(dir, 'types.d.ts'), `export type X = 1\n`)
68
- writeFileSync(join(dir, 'notes.txt'), 'not a plugin')
69
- const { report, plugins, order } = await loadPlugins({ source: dir })
70
- assert.deepEqual(stageNames(report), ['discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:PASS'])
71
- assert.deepEqual(order.map(m => m.name), ['real'])
72
- assert.equal(plugins.length, 1)
73
- })
74
- })
75
-
76
- // ==================== parse / validate / topo / register 边界 ====================
77
-
78
- test('M6-s21: 模块导入失败(语法坏文件)→ parse FAIL CAR-E-PARSE + 文件定位', async () => {
79
- await withTempDir('syntax', async (dir) => {
80
- const bad = join(dir, 'broken.ts')
81
- writeFileSync(bad, `export const manifest = { name: 'broken', version: '1.0.0' }\nexport default function( {\n`)
82
- const { report } = await loadPlugins({ source: bad })
83
- assert.deepEqual(stageNames(report), [
84
- 'discover:PASS', 'verify:PASS', 'parse:FAIL', 'validate:SKIPPED', 'topo:SKIPPED', 'register:SKIPPED',
85
- ])
86
- const parse = stageOf(report, 'parse')
87
- assert.match(parse.error!.file, /broken\.ts$/)
88
- assert.match(parse.error!.reason, /CAR-E-PARSE/)
89
- })
90
- })
91
-
92
- test('M6-s21: optional peer 缺失 → validate PASS 不 FAIL', async () => {
93
- await withTempDir('optional-peer', async (dir) => {
94
- makePlugin(dir, 'opt.ts', { name: 'opt', version: '1.0.0', peers: [{ peer: 'ghost', range: '^1.0.0', optional: true }] }, 't')
95
- const { report, plugins } = await loadPlugins({ source: dir })
96
- assert.equal(stageOf(report, 'validate').status, 'PASS')
97
- assert.equal(plugins.length, 1)
98
- assert.equal(stageOf(report, 'validate').conflicts, undefined)
99
- })
100
- })
101
-
102
- test('M6-s21: topo 依赖环 → topo FAIL CAR-E-DEPCYCLE + register SKIPPED', async () => {
103
- await withTempDir('cycle', async (dir) => {
104
- makePlugin(dir, 'a.ts', { name: 'a', version: '1.0.0', peers: [{ peer: 'b', range: '^1.0.0' }] }, 'ta')
105
- makePlugin(dir, 'b.ts', { name: 'b', version: '1.0.0', peers: [{ peer: 'a', range: '^1.0.0' }] }, 'tb')
106
- const { report, plugins } = await loadPlugins({ source: dir })
107
- assert.deepEqual(stageNames(report), [
108
- 'discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:FAIL', 'register:SKIPPED',
109
- ])
110
- assert.match(stageOf(report, 'topo').error!.reason, /CAR-E-DEPCYCLE/)
111
- assert.equal(plugins.length, 0)
112
- })
113
- })
114
-
115
- test('M6-s21: 重复插件名 DUP → register FAIL CAR-E-DUP + 前缀阶段已装配不回滚静默', async () => {
116
- await withTempDir('dup', async (dir) => {
117
- makePlugin(dir, 'one.ts', { name: 'dup', version: '1.0.0' }, 't1')
118
- makePlugin(dir, 'two.ts', { name: 'dup', version: '2.0.0' }, 't2')
119
- const { report, plugins } = await loadPlugins({ source: dir })
120
- assert.deepEqual(stageNames(report), [
121
- 'discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:FAIL',
122
- ])
123
- const reg = stageOf(report, 'register')
124
- assert.match(reg.error!.reason, /CAR-E-DUP/)
125
- assert.match(reg.error!.reason, /"dup"/)
126
- // FAIL 短路:已装配前缀保留(不静默吞掉),后续 DUP 项不进
127
- assert.ok(plugins.length <= 1)
128
- })
129
- })
130
-
131
- test('M6-s21: erasable 违规(enum)→ register FAIL CAR-E-PTC + 文件定位', async () => {
132
- await withTempDir('erasable', async (dir) => {
133
- const bad = join(dir, 'non-erasable.ts')
134
- writeFileSync(bad, `export const manifest = { name: 'ne', version: '1.0.0' }\nexport enum Mode { A = 'a' }\nexport default function apply(api) {}\n`)
135
- const { report } = await loadPlugins({ source: bad })
136
- assert.deepEqual(stageNames(report), [
137
- 'discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:FAIL',
138
- ])
139
- const reg = stageOf(report, 'register')
140
- assert.match(reg.error!.reason, /CAR-E-PTC/)
141
- assert.match(reg.error!.file, /non-erasable\.ts$/)
142
- })
143
- })
144
-
145
- test('M6-s21: manifest 命名导出缺省 → 按文件名合成(name/version 兜底口径)', async () => {
146
- await withTempDir('no-manifest', async (dir) => {
147
- writeFileSync(join(dir, 'anon.ts'), PLUGIN_BODY('t'))
148
- const { report, order } = await loadPlugins({ source: dir })
149
- assert.deepEqual(stageNames(report), ['discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:PASS'])
150
- assert.equal(order[0]!.name, 'anon')
151
- assert.equal(order[0]!.version, '0.0.1')
152
- })
153
- })
154
-
155
- // ==================== 热重载补充(SQ-05) ====================
156
-
157
- test('M6-s21: 连续 3 次 reload——epoch 连续递增 1/2/3、history 累积、当前实例始终可用', async () => {
158
- await withTempDir('epoch3', async (dir) => {
159
- const file = join(dir, 'hot.ts')
160
- const mgr = new ReloadManager()
161
- for (const v of ['1.0.0', '1.1.0', '1.2.0']) {
162
- writeFileSync(file, `export const manifest = { name: 'hot', version: '${v}' }\n${PLUGIN_BODY(`tool-${v}`)}`)
163
- const { report, plugins } = await mgr.reload(file)
164
- assert.deepEqual(stageNames(report), ['discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:PASS'])
165
- assert.equal(plugins[0]!.manifest.version, v)
166
- }
167
- assert.equal(mgr.epoch, 3)
168
- assert.equal(mgr.history.length, 3)
169
- assert.equal(mgr.instances.length, 1)
170
- assert.equal(mgr.instances[0]!.isInvalidated(), false)
171
- })
172
- })
173
-
174
- test('M6-s21: reload 失败后——旧实例保持 invalidated 不复活(不静默回退)、epoch 仍递增、无残留实例', async () => {
175
- await withTempDir('no-fallback', async (dir) => {
176
- const file = join(dir, 'hot.ts')
177
- writeFileSync(file, `export const manifest = { name: 'hot', version: '1.0.0' }\n${PLUGIN_BODY('t')}`)
178
- const mgr = new ReloadManager()
179
- const ok = await mgr.reload(file)
180
- const oldApi = ok.plugins[0]!.api
181
- assert.equal(mgr.epoch, 1)
182
- // 重写为非法 manifest → reload FAIL
183
- writeFileSync(file, `export const manifest = { name: 'hot', version: 'bad' }\n${PLUGIN_BODY('t')}`)
184
- const fail = await mgr.reload(file)
185
- assert.equal(stageOf(fail.report, 'parse').status, 'FAIL')
186
- assert.equal(mgr.epoch, 2, '失败 reload 也消耗 epoch(缓存键单调,不留脏缓存)')
187
- assert.equal(mgr.instances.length, 0, '失败后不静默回退到旧实例集')
188
- // 旧句柄不复活:保持 CAR-INVALIDATED(红线 4:显式失败禁止静默)
189
- assert.throws(() => oldApi.getRegisteredTools(), /CAR-INVALIDATED/)
190
- // 修复后可恢复
191
- writeFileSync(file, `export const manifest = { name: 'hot', version: '1.1.0' }\n${PLUGIN_BODY('t2')}`)
192
- const fixed = await mgr.reload(file)
193
- assert.equal(fixed.plugins[0]!.manifest.version, '1.1.0')
194
- assert.equal(mgr.epoch, 3)
195
- })
196
- })
197
-
198
- test('M6-s21: 并发 reload 基本不变量——epoch=2、history=2、终态实例未失效可用', async () => {
199
- await withTempDir('concurrent', async (dir) => {
200
- const file = join(dir, 'hot.ts')
201
- writeFileSync(file, `export const manifest = { name: 'hot', version: '1.0.0' }\n${PLUGIN_BODY('t')}`)
202
- const mgr = new ReloadManager()
203
- const [r1, r2] = await Promise.all([mgr.reload(file), mgr.reload(file)])
204
- assert.equal(mgr.epoch, 2)
205
- assert.equal(mgr.history.length, 2)
206
- for (const r of [r1, r2]) {
207
- assert.deepEqual(stageNames(r.report), ['discover:PASS', 'verify:PASS', 'parse:PASS', 'validate:PASS', 'topo:PASS', 'register:PASS'])
208
- }
209
- assert.equal(mgr.instances.length, 1)
210
- assert.equal(mgr.instances[0]!.isInvalidated(), false, '终态实例必须可用(不被并发残留失效)')
211
- const host: Array<{ name: string }> = []
212
- mgr.instances[0]!.bindCore({ registerTool: t => { host.push(t) }, getRegisteredTools: () => host })
213
- assert.deepEqual(mgr.instances[0]!.api.getRegisteredTools().map(t => t.name), ['t'])
214
- })
215
- })
216
-
217
- // ==================== doctor CAR_OFFLINE=1 路径 ====================
218
-
219
- test('M6-s21: CAR_OFFLINE=1 → connectivity 显式 SKIPPED(环境变量路径,非仅 skip 注入)', async () => {
220
- const prev = process.env.CAR_OFFLINE
221
- process.env.CAR_OFFLINE = '1'
222
- try {
223
- const r = await doctorConnectivity({ registryUrl: 'https://should-never-be-fetched.example' })
224
- assert.equal(r.status, 'SKIPPED')
225
- assert.match(r.detail, /CAR_OFFLINE=1/)
226
- assert.equal(r.latencyMs, 0)
227
- } finally {
228
- if (prev === undefined) delete process.env.CAR_OFFLINE
229
- else process.env.CAR_OFFLINE = prev
230
- }
231
- })
@@ -1,135 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { mkdtempSync, writeFileSync, appendFileSync, rmSync, statSync } from 'node:fs'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { SessionLog, loadSessionLog, deriveSessionId } from '../src/session/log.ts'
7
- import { SessionFileStore } from '../src/session/store.ts'
8
- import { compressJsonlZstd, decodeLogBuffer, splitJsonlLines, zstdAvailable } from '../src/session/format.ts'
9
-
10
- // ==================== fixture 工具(红线 8:清理 try/catch 容错) ====================
11
-
12
- function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
13
- const dir = mkdtempSync(join(tmpdir(), `car-s22-${name}-`))
14
- return Promise.resolve()
15
- .then(() => fn(dir))
16
- .finally(() => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8:safe-delete 拦截容错 */ } })
17
- }
18
-
19
- function appendConversation(log: SessionLog, n: number): void {
20
- for (let i = 0; i < n; i++) {
21
- log.append('user', 'user', 'T0', `请处理第 ${i} 项任务并按标准流程汇报结果`)
22
- log.append('model', 'assistant', 'T0', `第 ${i} 项任务已完成:读取输入 → 执行标准流程 → 输出结果摘要(含耗时与状态码)`)
23
- }
24
- }
25
-
26
- // ==================== W1 落盘存储层(§3.2.M7.5 Step 1-2) ====================
27
-
28
- test('M7-W1: attachSink 逐事件落盘——append 返回即已持久,读回与内存逐字节一致且链完整', () => {
29
- withTempDir('sink', (dir) => {
30
- const file = join(dir, 'session-S-m7sink.jsonl')
31
- const log = new SessionLog('S-m7sink')
32
- const store = new SessionFileStore(file)
33
- log.attachSink(line => store.append(line))
34
- appendConversation(log, 5)
35
- store.close()
36
- assert.equal(statSync(file).size > 0, true)
37
- const { log: reloaded, brokenAt, tornTail } = loadSessionLog(file)
38
- assert.equal(tornTail, false)
39
- assert.equal(brokenAt, null)
40
- assert.equal(reloaded.events.length, log.events.length)
41
- assert.deepEqual(reloaded.events.map(e => e.hash), log.events.map(e => e.hash))
42
- // sessionId 经文件名推导还原(审计定位口径)
43
- assert.equal(reloaded.sessionId, 'S-m7sink')
44
- })
45
- })
46
-
47
- test('M7-W1: sink 写失败 fail-fast——事件不入内存、链尾不推进(内存与盘面永不失配)', () => {
48
- withTempDir('failfast', (dir) => {
49
- const file = join(dir, 'log.jsonl')
50
- const store = new SessionFileStore(file)
51
- store.append('{"primed":true}')
52
- store.close()
53
- const log = new SessionLog('S-ff')
54
- log.attachSink(line => store.append(line))
55
- assert.throws(() => log.append('user', 'user', 'T0', 'x'), /CAR-E-STORE/)
56
- assert.equal(log.events.length, 0)
57
- assert.equal(log.stats().tailHash, 'GENESIS')
58
- })
59
- })
60
-
61
- // ==================== W2 撕裂尾显式格式校验(§3.2.M7.1 第 4 点) ====================
62
-
63
- test('M7-W2: 撕裂尾显式校验——崩溃半行显式丢弃报告,前缀链完整(不算断链)', () => {
64
- withTempDir('torn', (dir) => {
65
- const file = join(dir, 'log.jsonl')
66
- writeFileSync(file, (() => { const l = new SessionLog('S-torn'); appendConversation(l, 4); return l.exportJSONL() })(), 'utf-8')
67
- appendFileSync(file, '{"seq":8,"ts":123,"actor":"model","kind":"assistant"') // 崩溃半行(无换行)
68
- const { log, brokenAt, tornTail, tornTailBytes } = loadSessionLog(file)
69
- assert.equal(brokenAt, null)
70
- assert.equal(tornTail, true)
71
- assert.ok(tornTailBytes > 0)
72
- assert.equal(log.events.length, 8)
73
- })
74
- })
75
-
76
- test('M7-W2: 完整行非法 JSON = CAR-E-FORMAT 拒绝装载(与撕裂尾/断链三语义互斥)', () => {
77
- withTempDir('badline', (dir) => {
78
- const file = join(dir, 'log.jsonl')
79
- const l = new SessionLog('S-bad'); appendConversation(l, 2)
80
- writeFileSync(file, l.exportJSONL() + 'not-json\n', 'utf-8')
81
- assert.throws(() => loadSessionLog(file), /CAR-E-FORMAT/)
82
- })
83
- })
84
-
85
- test('M7-W2: splitJsonlLines 边界——空文件/正常收尾/空行过滤/撕裂尾残留不入 lines', () => {
86
- assert.deepEqual(splitJsonlLines(''), { lines: [], tornTail: false, tornTailBytes: 0 })
87
- assert.deepEqual(splitJsonlLines('a\nb\n'), { lines: ['a', 'b'], tornTail: false, tornTailBytes: 0 })
88
- assert.deepEqual(splitJsonlLines('a\n\nb\n'), { lines: ['a', 'b'], tornTail: false, tornTailBytes: 0 }) // 中间空行=噪声过滤
89
- const r = splitJsonlLines('a\n\nb')
90
- assert.equal(r.tornTail, true)
91
- assert.deepEqual(r.lines, ['a']) // 'b' 为撕裂尾残留(显式丢弃,不入 lines)
92
- assert.equal(r.tornTailBytes, 1)
93
- })
94
-
95
- test('M7-W2: deriveSessionId 推导规则——session- 前缀剥离 / events.jsonl 取父目录 / zstd 后缀', () => {
96
- assert.equal(deriveSessionId('x/session-S-abc.jsonl'), 'S-abc')
97
- assert.equal(deriveSessionId('x/S-abc.jsonl.zstd'), 'S-abc')
98
- assert.equal(deriveSessionId('root/sessions/S-xyz/events.jsonl'), 'S-xyz')
99
- assert.equal(deriveSessionId('events.jsonl'), 'events') // 无父目录兜底
100
- })
101
-
102
- // ==================== W3 zstd 存储增强(M2演进 #14③ / M3演进「M7+ 选项」正主) ====================
103
-
104
- const ZSTD_OFF = zstdAvailable() ? false : 'zstd 能力不在场(Node <22.15 无此能力)——CI 22.19 原生在场真跑'
105
-
106
- test('M7-W3: zstd 归档压缩 + magic 嗅探 layout-blind 直读(能力在场时)', { skip: ZSTD_OFF }, () => {
107
- withTempDir('zstd', (dir) => {
108
- const log = new SessionLog('S-m7zstd')
109
- appendConversation(log, 400) // 800 事件:真实会话量级
110
- const plain = log.exportJSONL()
111
- assert.ok(plain.length > 200_000, `fixture 量级不足:${plain.length}`)
112
- const zbuf = compressJsonlZstd(plain)
113
- // 规格口径 ≥3x(§3.2.M7.1 第 4 点):实测 6.21x(800 事件 261213→42035B,libzstd 1.5.5 level 3,WSL 实测 2026-09-19——
114
- // Node zstdCompressSync 同库同级别同 fixture,断言留 2x 余量防 libzstd 版本漂移)
115
- const ratio = plain.length / zbuf.length
116
- assert.ok(ratio >= 3, `压缩比低于规格 3x:${ratio.toFixed(2)}x(${plain.length} → ${zbuf.length})`)
117
- const file = join(dir, 'session-S-m7zstd.jsonl.zstd')
118
- writeFileSync(file, zbuf)
119
- // layout-blind:读取方不感知后缀,magic 嗅探直读
120
- const { log: reloaded, brokenAt, encoding, tornTail } = loadSessionLog(file)
121
- assert.equal(encoding, 'zstd')
122
- assert.equal(brokenAt, null)
123
- assert.equal(tornTail, false)
124
- assert.equal(reloaded.events.length, log.events.length)
125
- assert.deepEqual(reloaded.events.map(e => e.hash), log.events.map(e => e.hash))
126
- assert.equal(decodeLogBuffer(Buffer.from(plain, 'utf-8')).encoding, 'plain')
127
- })
128
- })
129
-
130
- test('M7-W3: zstd 能力缺席 = 显式拒绝(CAR-E-ZSTD),不静默降级(能力缺席环境实跑)', { skip: zstdAvailable() ? 'zstd 能力在场(22.15+ 原生)——缺席路径由更早版本环境覆盖' : false }, () => {
131
- // 能力缺席环境(如 22.19 无 flag / 23.5):magic 命中后必须显式 throw,不得静默按明文装载
132
- const fakeZstd = Buffer.from([0x28, 0xb5, 0x2f, 0xfd, 0x00, 0x00])
133
- assert.throws(() => decodeLogBuffer(fakeZstd), /CAR-E-ZSTD/)
134
- assert.throws(() => compressJsonlZstd('x'), /CAR-E-ZSTD/)
135
- })
@@ -1,189 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { spawn } from 'node:child_process'
4
- import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, mkdirSync, statSync } from 'node:fs'
5
- import { tmpdir } from 'node:os'
6
- import { join, dirname } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { createHash } from 'node:crypto'
9
- import { SessionLog, loadSessionLog } from '../src/session/log.ts'
10
- import { zstdAvailable } from '../src/session/format.ts'
11
- import { rebuildIndex, sqliteAvailable } from '../src/session/indexStore.ts'
12
- import { verifyBundle, type ForensicsBundle } from '../src/session/export.ts'
13
-
14
- const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
15
- const ZSTD_OFF = zstdAvailable() ? false : 'zstd 能力不在场(Node <22.15 无此能力)——CI 22.19 原生在场真跑'
16
-
17
- // ==================== fixture 工具(红线 8:清理 try/catch 容错) ====================
18
-
19
- function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
20
- const dir = mkdtempSync(join(tmpdir(), `car-s23-${name}-`))
21
- return Promise.resolve()
22
- .then(() => fn(dir))
23
- .finally(() => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8:safe-delete 拦截容错 */ } })
24
- }
25
-
26
- /** 真实子进程跑 CLI(语义化退出码断言:0=成功 1=数据校验失败 2=用法/输入不可用) */
27
- function car(args: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> {
28
- return new Promise((resolve, reject) => {
29
- const child = spawn(process.execPath, ['--experimental-transform-types', CLI, ...args], { stdio: ['ignore', 'pipe', 'pipe'] })
30
- let out = '', err = ''
31
- child.stdout.on('data', d => { out += d })
32
- child.stderr.on('data', d => { err += d })
33
- child.on('error', reject)
34
- child.on('exit', code => resolve({ code, stdout: out, stderr: err }))
35
- })
36
- }
37
-
38
- function writeSessionLog(dir: string, fileName: string, sessionId: string, n: number, corrupt = false): string {
39
- const log = new SessionLog(sessionId)
40
- for (let i = 0; i < n; i++) {
41
- log.append('user', 'user', 'T0', `任务 ${i}:请按标准流程处理并汇报`)
42
- log.append('model', 'assistant', 'T0', `任务 ${i} 已完成:读取输入 → 执行 → 汇报状态码与耗时`)
43
- }
44
- const file = join(dir, fileName)
45
- let text = log.exportJSONL()
46
- if (corrupt) {
47
- const lines = text.split('\n').filter(Boolean)
48
- const ev = JSON.parse(lines[2]!)
49
- ev.payload = '篡改后的内容'
50
- lines[2] = JSON.stringify(ev)
51
- text = lines.join('\n') + '\n'
52
- }
53
- writeFileSync(file, text, 'utf-8')
54
- return file
55
- }
56
-
57
- // ==================== W4 取证包导出(§3.2.M7.2 导出行 + SQ-06) ====================
58
-
59
- test('M7-W4: session export 全流程——断链前置校验、落盘读回重验、离线自证(删源后可 verify)', async () => {
60
- await withTempDir('export', async (dir) => {
61
- const file = writeSessionLog(dir, 'session-S-export1.jsonl', 'S-export1', 6)
62
- const out = join(dir, 'bundle')
63
- const r = await car(['session', 'export', file, '--out', out])
64
- assert.equal(r.code, 0, r.stderr)
65
- // 目录结构:manifest.json + sessions/<id>/events.jsonl
66
- const mfPath = join(out, 'manifest.json')
67
- const eventsPath = join(out, 'sessions', 'S-export1', 'events.jsonl')
68
- assert.ok(existsSync(mfPath))
69
- assert.ok(existsSync(eventsPath))
70
- // manifest 自证:链锚点 + 事件文件哈希一致(明文口径)
71
- const mf = JSON.parse(readFileSync(mfPath, 'utf-8')) as ForensicsBundle['manifest']
72
- assert.equal(mf.verifyChainAtExport, 'PASS')
73
- assert.equal(mf.eventCount, 12)
74
- assert.ok(mf.chainHead && mf.chainTail)
75
- const eventsContent = readFileSync(eventsPath, 'utf-8')
76
- assert.equal(createHash('sha256').update(eventsContent).digest('hex'), mf.files[0]!.sha256)
77
- assert.ok(verifyBundle({ files: [{ name: mf.files[0]!.name, content: eventsContent, sha256: mf.files[0]!.sha256 }], manifest: mf }).ok)
78
- // 离线回放:删源文件后,仅凭导出目录 verify/replay 不依赖运行时
79
- rmSync(file)
80
- const v = await car(['session', 'verify', eventsPath])
81
- assert.equal(v.code, 0, v.stderr)
82
- assert.match(v.stdout, /哈希链完整:12 事件/)
83
- const { log: replayed } = loadSessionLog(eventsPath)
84
- assert.equal(replayed.deriveMessages().length, 12)
85
- })
86
- })
87
-
88
- test('M7-W4: 断链中止——exit 1 + 位置报告 + 不产出导出目录(SQ-06 不出带病审计包)', async () => {
89
- await withTempDir('broken-export', async (dir) => {
90
- const file = writeSessionLog(dir, 'session-S-bad.jsonl', 'S-bad', 4, true) // 篡改第 3 行 payload
91
- const out = join(dir, 'bundle')
92
- const r = await car(['session', 'export', file, '--out', out])
93
- assert.equal(r.code, 1)
94
- assert.match(r.stderr, /断链 @ seq=/)
95
- assert.equal(existsSync(out), false)
96
- })
97
- })
98
-
99
- test('M7-W4: export --zstd——物理布局 .jsonl.zstd + storage 登记 + 读回重验三方一致', { skip: ZSTD_OFF }, async () => {
100
- await withTempDir('export-zstd', async (dir) => {
101
- const file = writeSessionLog(dir, 'session-S-z1.jsonl', 'S-z1', 6)
102
- const out = join(dir, 'bundle')
103
- const r = await car(['session', 'export', file, '--out', out, '--zstd'])
104
- assert.equal(r.code, 0, r.stderr)
105
- const eventsPath = join(out, 'sessions', 'S-z1', 'events.jsonl.zstd')
106
- assert.ok(existsSync(eventsPath), '物理文件应为 .jsonl.zstd')
107
- const mf = JSON.parse(readFileSync(join(out, 'manifest.json'), 'utf-8')) as ForensicsBundle['manifest']
108
- assert.equal(mf.storage?.encoding, 'zstd')
109
- assert.equal(mf.storage.storedAs, 'sessions/S-z1/events.jsonl.zstd')
110
- // 三方一致:物理压缩字节哈希 = storage 登记;解码明文哈希 = files 逻辑口径
111
- const diskBytes = readFileSync(eventsPath)
112
- assert.equal(createHash('sha256').update(diskBytes).digest('hex'), mf.storage.physicalSha256)
113
- const { log: reloaded, brokenAt, encoding } = loadSessionLog(eventsPath)
114
- assert.equal(encoding, 'zstd')
115
- assert.equal(brokenAt, null)
116
- assert.equal(reloaded.events.length, 12)
117
- })
118
- })
119
-
120
- test('M7-W4: 用法守卫——缺 --out / 输入不可用(CAR-E-FORMAT)= exit 2', async () => {
121
- await withTempDir('usage', async (dir) => {
122
- const r1 = await car(['session', 'export', join(dir, 'whatever.jsonl')])
123
- assert.equal(r1.code, 2)
124
- assert.match(r1.stderr, /--out/)
125
- const bad = join(dir, 'bad.jsonl')
126
- writeFileSync(bad, 'not-json\n', 'utf-8')
127
- const r2 = await car(['session', 'verify', bad])
128
- assert.equal(r2.code, 2)
129
- assert.match(r2.stderr, /CAR-E-FORMAT/)
130
- })
131
- })
132
-
133
- // ==================== W5 C-02 SQLite 索引(§3.2.M7.5 Step 3) ====================
134
-
135
- test('M7-W5: rebuild-index 幂等重建——断链/坏格式文件入 errors 不中断,索引只收链完整会话', async () => {
136
- await withTempDir('index', async (dir) => {
137
- const root = join(dir, 'sessions-root')
138
- mkdirSync(join(root, 'sessions', 'S-nest'), { recursive: true })
139
- writeSessionLog(root, 'session-S-a1.jsonl', 'S-a1', 3)
140
- writeSessionLog(root, 'session-S-a2.jsonl', 'S-a2', 2)
141
- writeSessionLog(join(root, 'sessions', 'S-nest'), 'events.jsonl', 'S-nest', 1)
142
- writeSessionLog(root, 'session-S-broken.jsonl', 'S-broken', 2, true)
143
- const bad = join(root, 'session-S-format.jsonl')
144
- writeFileSync(bad, 'not-json\n', 'utf-8')
145
- const db = join(dir, 'idx.db')
146
- const r1 = await rebuildIndex(root, db)
147
- assert.equal(r1.rows.length, 3)
148
- assert.equal(r1.errors.length, 2)
149
- assert.ok(r1.errors.some(e => /哈希链断裂/.test(e.reason)))
150
- assert.ok(r1.errors.some(e => /CAR-E-FORMAT/.test(e.reason)))
151
- // 排序确定 + 嵌套 events.jsonl 的 sessionId 取父目录
152
- assert.deepEqual(r1.rows.map(r => r.sessionId), ['S-a1', 'S-a2', 'S-nest'])
153
- assert.equal(r1.rows.find(r => r.sessionId === 'S-nest')!.file, join(root, 'sessions', 'S-nest', 'events.jsonl'))
154
- // 幂等:重跑结果一致(单事务 DELETE+INSERT 覆盖)
155
- const r2 = await rebuildIndex(root, db)
156
- assert.deepEqual(r2.rows, r1.rows)
157
- // 盘上 SQLite 内容抽查:行数与 rows 一致
158
- const mod = await import('node:sqlite')
159
- const dbHandle = new (mod as unknown as { DatabaseSync: new (p: string) => { prepare(s: string): { get(): Record<string, unknown> }; close(): void } }).DatabaseSync(db)
160
- try {
161
- const cnt = dbHandle.prepare('SELECT COUNT(*) AS n FROM sessions').get() as { n: number }
162
- assert.equal(cnt.n, 3)
163
- const nest = dbHandle.prepare("SELECT eventCount FROM sessions WHERE sessionId = 'S-nest'").get() as { eventCount: number }
164
- assert.equal(nest.eventCount, 2)
165
- } finally { dbHandle.close() }
166
- })
167
- })
168
-
169
- test('M7-W5: sqlite 能力探测在场(CI 22.19 经 NODE_OPTIONS,23.4+ 原生)', async () => {
170
- // 显式登记:能力缺席环境的 rebuildIndex 拒绝路径由 CAR-E-SQLITE 文案覆盖(format.ts 同款三语义口径)
171
- assert.equal(await sqliteAvailable(), true)
172
- })
173
-
174
- test('M7-W5: rebuild-index 对 zstd 归档文件 layout-blind 入索引(能力在场时)', { skip: ZSTD_OFF }, async () => {
175
- await withTempDir('index-zstd', async (dir) => {
176
- const { compressJsonlZstd } = await import('../src/session/format.ts')
177
- const root = join(dir, 'root')
178
- mkdirSync(root, { recursive: true })
179
- const log = new SessionLog('S-arch')
180
- for (let i = 0; i < 3; i++) log.append('user', 'user', 'T0', `q${i}`)
181
- writeFileSync(join(root, 'S-arch.jsonl.zstd'), compressJsonlZstd(log.exportJSONL()))
182
- const r = await rebuildIndex(root, join(dir, 'idx.db'))
183
- assert.equal(r.rows.length, 1)
184
- assert.equal(r.rows[0]!.encoding, 'zstd')
185
- assert.equal(r.rows[0]!.sessionId, 'S-arch')
186
- assert.equal(r.rows[0]!.eventCount, 3)
187
- assert.equal(r.errors.length, 0)
188
- })
189
- })