@lqc123qwe/car-runtime 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +46 -0
- package/dist/authz/authz.js +76 -0
- package/dist/car-runtime-1.11.0.tgz +0 -0
- package/dist/car-runtime-1.11.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +82 -0
- package/dist/loop/stop.js +184 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +43 -0
- package/dist/runtime-core/chatStep.js +121 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +638 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +104 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +24 -0
- package/dist/seam.js +23 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/authz/authz.ts +12 -0
- package/src/loop/stop.ts +43 -10
- package/src/runtime-core/chatStep.ts +22 -2
- package/src/runtime-core/llm.ts +96 -13
- package/src/runtime-core/types.ts +22 -1
- package/src/seam.ts +24 -0
- package/src/session/log.ts +1 -1
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s43-remote-http-traceparent.spec.ts +0 -262
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1.6-S1 · 投影收敛(S37):deriveMessages 同 id toolResult last-wins(D-23)
|
|
3
|
-
*
|
|
4
|
-
* 覆盖:
|
|
5
|
-
* - runTurn confirm 全链:写类工具 granted 流——事件面两条 toolResult(runtime 授权 + plugin 执行,
|
|
6
|
-
* M4 既有语义不动 = 审计全量留痕),投影单条且内容 = 执行结果;N1 快照一致性由构造保持
|
|
7
|
-
* - denied 流:授权拒绝单条保留(唯一结果,原样投影)
|
|
8
|
-
* - readonly 工具:单 plugin toolResult(无收敛对象——回归)
|
|
9
|
-
* - id 缺席不收敛:无 id 载荷原样投影(golden ④ 投影语义兼容);id 与无 id 混存互不影响
|
|
10
|
-
* - 适配器出站体(1.6-GO-5 生产调用点核查):收敛投影经 openai-compat / anthropic 双适配器
|
|
11
|
-
* 出站体无重复 tool 消息 / tool_result 块(真实 provider 重复 id 拒绝面出清)
|
|
12
|
-
*/
|
|
13
|
-
import { test } from 'node:test'
|
|
14
|
-
import assert from 'node:assert/strict'
|
|
15
|
-
import { SessionLog } from '../src/session/log.ts'
|
|
16
|
-
import { runTurn, type ModelStep } from '../src/loop/stop.ts'
|
|
17
|
-
import { createOpenAICompatAdapter, createAnthropicAdapter } from '../src/runtime-core/llm.ts'
|
|
18
|
-
|
|
19
|
-
const dataLine = (obj: unknown) => `data: ${JSON.stringify(obj)}\n\n`
|
|
20
|
-
const openaiSse = (finish: string) =>
|
|
21
|
-
dataLine({ choices: [{ delta: {}, finish_reason: finish }] }) + 'data: [DONE]\n\n'
|
|
22
|
-
const sseResponse = (text: string) => {
|
|
23
|
-
const enc = new TextEncoder()
|
|
24
|
-
return new Response(new ReadableStream<Uint8Array>({ start(c) { c.enqueue(enc.encode(text)); c.close() } }),
|
|
25
|
-
{ status: 200, headers: { 'content-type': 'text/event-stream' } })
|
|
26
|
-
}
|
|
27
|
-
const anthropicSse = (events: object[]) => events.map(dataLine).join('')
|
|
28
|
-
const ANTHROPIC_STOP = [
|
|
29
|
-
{ type: 'message_start', message: { role: 'assistant' } },
|
|
30
|
-
{ type: 'message_delta', delta: { stop_reason: 'end_turn' } },
|
|
31
|
-
{ type: 'message_stop' },
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
/** chatStep 消息映射同款复刻(deriveMessages → LlmRequest.messages 的唯一路径) */
|
|
35
|
-
function toLlmMessages(log: SessionLog) {
|
|
36
|
-
return log.deriveMessages().map(m => ({
|
|
37
|
-
role: m.role as 'system' | 'user' | 'assistant' | 'toolResult',
|
|
38
|
-
content: (m as unknown as { toolCall?: unknown }).toolCall !== undefined
|
|
39
|
-
? { toolCall: (m as unknown as { toolCall: unknown }).toolCall }
|
|
40
|
-
: m.content,
|
|
41
|
-
}))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
test('S37: 写类工具 granted 流——事件面两条 toolResult(M4 语义不动),投影单条且为执行结果 + N1 一致', async () => {
|
|
45
|
-
const log = new SessionLog('s37-granted')
|
|
46
|
-
const steps: ModelStep[] = [
|
|
47
|
-
{ stopReason: 'toolUse', toolCalls: [{ id: 'tc1', tool: 'save_note', args: { text: 'hi' } }] },
|
|
48
|
-
{ stopReason: 'stop' },
|
|
49
|
-
]
|
|
50
|
-
const r = await runTurn({
|
|
51
|
-
log, turnId: 'T1',
|
|
52
|
-
model: async () => steps.shift()!,
|
|
53
|
-
tools: new Map([['save_note', { declaredSideEffect: 'write', run: async () => '已保存' }]]),
|
|
54
|
-
preset: { mode: 'confirm', authorize: async () => true },
|
|
55
|
-
})
|
|
56
|
-
assert.equal(r.reason, 'completed')
|
|
57
|
-
// 事件面不动:授权决策(runtime)+ 执行结果(plugin)两条都落链(审计全量留痕)
|
|
58
|
-
const resultEvents = log.events.filter(e => e.kind === 'toolResult')
|
|
59
|
-
assert.equal(resultEvents.length, 2, 'M4 既有语义冻结:runtime 授权记录 + plugin 执行结果均落链')
|
|
60
|
-
assert.equal(resultEvents[0]!.actor, 'runtime')
|
|
61
|
-
assert.equal(resultEvents[1]!.actor, 'plugin')
|
|
62
|
-
// 投影收敛:同 id last-wins,模型可见流单条且为执行结果
|
|
63
|
-
const proj = log.deriveMessages().filter(m => m.role === 'toolResult')
|
|
64
|
-
assert.equal(proj.length, 1, 'D-23:授权中间记录不再进模型可见流')
|
|
65
|
-
assert.deepEqual(proj[0]!.content, { id: 'tc1', result: '已保存' })
|
|
66
|
-
// N1 快照一致性(runTurn 每步 snapshotModelRequest;收敛激活下重算同函数构造一致)
|
|
67
|
-
assert.deepEqual(log.assertModelVisibleLogged(), { ok: true })
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('S37: denied 流——授权拒绝单条保留(唯一结果原样投影)', async () => {
|
|
71
|
-
const log = new SessionLog('s37-denied')
|
|
72
|
-
const steps: ModelStep[] = [
|
|
73
|
-
{ stopReason: 'toolUse', toolCalls: [{ id: 'tc2', tool: 'save_note', args: {} } ] },
|
|
74
|
-
{ stopReason: 'stop' },
|
|
75
|
-
]
|
|
76
|
-
await runTurn({
|
|
77
|
-
log, turnId: 'T1',
|
|
78
|
-
model: async () => steps.shift()!,
|
|
79
|
-
tools: new Map([['save_note', { declaredSideEffect: 'write', run: async () => '不应执行' }]]),
|
|
80
|
-
preset: { mode: 'confirm', authorize: async () => false },
|
|
81
|
-
})
|
|
82
|
-
assert.equal(log.events.filter(e => e.kind === 'toolResult').length, 1, '拒绝即终态:仅 runtime 授权拒绝事件')
|
|
83
|
-
const proj = log.deriveMessages().filter(m => m.role === 'toolResult')
|
|
84
|
-
assert.equal(proj.length, 1, '唯一结果原样保留(不误收敛)')
|
|
85
|
-
assert.equal((proj[0]!.content as { error?: string }).error, 'authorization-denied')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('S37: readonly 工具单条 toolResult 无收敛对象(回归)', async () => {
|
|
89
|
-
const log = new SessionLog('s37-readonly')
|
|
90
|
-
const steps: ModelStep[] = [
|
|
91
|
-
{ stopReason: 'toolUse', toolCalls: [{ id: 'tc3', tool: 'echo', args: {} }] },
|
|
92
|
-
{ stopReason: 'stop' },
|
|
93
|
-
]
|
|
94
|
-
await runTurn({
|
|
95
|
-
log, turnId: 'T1',
|
|
96
|
-
model: async () => steps.shift()!,
|
|
97
|
-
tools: new Map([['echo', { declaredSideEffect: 'readonly', run: async () => 'pong' }]]),
|
|
98
|
-
preset: { mode: 'readonly' },
|
|
99
|
-
})
|
|
100
|
-
assert.equal(log.events.filter(e => e.kind === 'toolResult').length, 1)
|
|
101
|
-
const proj = log.deriveMessages().filter(m => m.role === 'toolResult')
|
|
102
|
-
assert.equal(proj.length, 1)
|
|
103
|
-
assert.deepEqual(proj[0]!.content, { id: 'tc3', result: 'pong' })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('S37: id 缺席不收敛——无 id 载荷原样投影(golden ④ 兼容),id 与无 id 混存互不影响', () => {
|
|
107
|
-
const log = new SessionLog('s37-idless')
|
|
108
|
-
log.append('runtime', 'toolResult', 'T1', { ok: 1 })
|
|
109
|
-
log.append('runtime', 'toolResult', 'T1', { ok: 2 })
|
|
110
|
-
log.append('plugin', 'toolResult', 'T1', { id: 'tc9', error: 'authorization-denied', granted: true })
|
|
111
|
-
log.append('plugin', 'toolResult', 'T1', { id: 'tc9', result: 'final' })
|
|
112
|
-
const proj = log.deriveMessages().filter(m => m.role === 'toolResult')
|
|
113
|
-
assert.equal(proj.length, 3, '无 id 两条原样保留 + 同 id 收敛为最后一条')
|
|
114
|
-
assert.deepEqual(proj[0]!.content, { ok: 1 })
|
|
115
|
-
assert.deepEqual(proj[1]!.content, { ok: 2 })
|
|
116
|
-
assert.deepEqual(proj[2]!.content, { id: 'tc9', result: 'final' })
|
|
117
|
-
// 前缀投影(upTo)语义:前缀内 last-wins 独立成立
|
|
118
|
-
const half = log.deriveMessages(3).filter(m => m.role === 'toolResult')
|
|
119
|
-
assert.equal(half.length, 3, '前缀 [0,3):无 id 两条 + tc9 授权记录(前缀内 tc9 授权记录即终态)')
|
|
120
|
-
assert.deepEqual(half[2]!.content, { id: 'tc9', error: 'authorization-denied', granted: true })
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('S37: openai-compat 出站体——收敛后单 tool 消息(重复 tool_call_id 出清)', async () => {
|
|
124
|
-
const log = new SessionLog('s37-openai')
|
|
125
|
-
log.append('user', 'user', 'T1', '查天气')
|
|
126
|
-
log.append('model', 'toolCall', 'T1', { id: 'tc1', tool: 'weather', args: { city: '北京' } })
|
|
127
|
-
log.append('runtime', 'toolResult', 'T1', { id: 'tc1', error: 'authorization-denied', granted: true, mode: 'confirm' })
|
|
128
|
-
log.append('plugin', 'toolResult', 'T1', { id: 'tc1', result: '晴 25 度' })
|
|
129
|
-
const seen: RequestInit[] = []
|
|
130
|
-
const adapter = createOpenAICompatAdapter({
|
|
131
|
-
baseUrl: 'https://api.test',
|
|
132
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { seen.push(init!); return sseResponse(openaiSse('stop')) }) as typeof fetch,
|
|
133
|
-
})
|
|
134
|
-
for await (const _ of adapter.chat({ model: 'm', messages: toLlmMessages(log), tools: [] })) void _
|
|
135
|
-
const body = JSON.parse(seen[0]!.body as string)
|
|
136
|
-
const toolMsgs = body.messages.filter((m: { role: string }) => m.role === 'tool')
|
|
137
|
-
assert.equal(toolMsgs.length, 1, '1.6-GO-5:出站体单 tool 消息(收敛前为两条)')
|
|
138
|
-
assert.equal(toolMsgs[0].tool_call_id, 'tc1')
|
|
139
|
-
assert.equal(toolMsgs[0].content, JSON.stringify('晴 25 度'), '内容 = 执行结果(非授权中间记录)')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test('S37: anthropic 出站体——收敛后单 tool_result 块(重复 tool_use_id 400 面出清)', async () => {
|
|
143
|
-
const log = new SessionLog('s37-anthropic')
|
|
144
|
-
log.append('user', 'user', 'T1', '查天气')
|
|
145
|
-
log.append('model', 'toolCall', 'T1', { id: 'toolu_1', tool: 'weather', args: { city: '北京' } })
|
|
146
|
-
log.append('runtime', 'toolResult', 'T1', { id: 'toolu_1', error: 'authorization-denied', granted: true, mode: 'confirm' })
|
|
147
|
-
log.append('plugin', 'toolResult', 'T1', { id: 'toolu_1', result: '晴 25 度' })
|
|
148
|
-
const seen: RequestInit[] = []
|
|
149
|
-
const adapter = createAnthropicAdapter({
|
|
150
|
-
baseUrl: 'https://api.test',
|
|
151
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { seen.push(init!); return sseResponse(anthropicSse(ANTHROPIC_STOP)) }) as typeof fetch,
|
|
152
|
-
})
|
|
153
|
-
for await (const _ of adapter.chat({ model: 'm', messages: toLlmMessages(log), tools: [] })) void _
|
|
154
|
-
const body = JSON.parse(seen[0]!.body as string)
|
|
155
|
-
const blocks = body.messages.flatMap((m: { content: unknown }) =>
|
|
156
|
-
Array.isArray(m.content) ? m.content : []).filter((b: { type: string }) => b.type === 'tool_result')
|
|
157
|
-
assert.equal(blocks.length, 1, '1.6-GO-5:出站体单 tool_result 块(重复 tool_use_id 会被 provider 400)')
|
|
158
|
-
assert.equal(blocks[0].tool_use_id, 'toolu_1')
|
|
159
|
-
assert.equal(blocks[0].content, JSON.stringify('晴 25 度'))
|
|
160
|
-
assert.ok(!('is_error' in blocks[0]), '执行结果非错误形态')
|
|
161
|
-
})
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1.6-S3 · jiti 载体(S38):car.config.ts 经 jiti 自包含转译(D-24)
|
|
3
|
-
*
|
|
4
|
-
* 覆盖:
|
|
5
|
-
* - 非可擦除语法(enum)真载——1.5 D-22 原生类型剥离不可表达的 transform-only 形态,
|
|
6
|
-
* erasable-only 预检登记项随之出清(被取代)
|
|
7
|
-
* - strip-only spawn 差分(1.6-GO-5 生产调用点):`--experimental-strip-types`(无 transform)
|
|
8
|
-
* 下 enum 配置经 jiti 真载成功 + 原生 import 同文件失败(负控——证明语法确属非可擦除、
|
|
9
|
-
* jiti 是载体);同进程实测首载时延(登记口径,无硬门禁,CI 宽上界防抖)
|
|
10
|
-
* - CLI 真路径:car run 于 enum 配置目录——配置经 jiti 装载生效(CAR-E-LLM-CONFIG 引导 =
|
|
11
|
-
* 配置解析通过、模型未配;若配置载体失败应为 CAR-E-CONFIG)
|
|
12
|
-
* - s35 存量零改动全量回归 = 载体迁移验收线(interoptDefault 显式关:jiti 缺省 true 会把
|
|
13
|
-
* 无 default 模块的 exports 整体当 default,「缺 default export」校验即失效)
|
|
14
|
-
*/
|
|
15
|
-
import { test } from 'node:test'
|
|
16
|
-
import assert from 'node:assert/strict'
|
|
17
|
-
import { spawn } from 'node:child_process'
|
|
18
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
19
|
-
import { tmpdir } from 'node:os'
|
|
20
|
-
import { join, dirname } from 'node:path'
|
|
21
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
22
|
-
import { warmCarConfig, loadCarConfig } from '../src/load/config.ts'
|
|
23
|
-
|
|
24
|
-
const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'cli.ts')
|
|
25
|
-
const SRC_CONFIG = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'load', 'config.ts')
|
|
26
|
-
|
|
27
|
-
function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
|
|
28
|
-
const dir = mkdtempSync(join(tmpdir(), `car-s38-${name}-`))
|
|
29
|
-
try {
|
|
30
|
-
const r = fn(dir)
|
|
31
|
-
const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
|
|
32
|
-
if (r instanceof Promise) return r.finally(cleanup)
|
|
33
|
-
cleanup()
|
|
34
|
-
return Promise.resolve()
|
|
35
|
-
} catch (e) {
|
|
36
|
-
try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
|
|
37
|
-
throw e
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test('S38: 非可擦除语法(enum)经 jiti 真载——D-22 原生类型剥离不可表达形态', async () => {
|
|
42
|
-
await withTempDir('enum', async dir => {
|
|
43
|
-
writeFileSync(join(dir, 'car.config.ts'), [
|
|
44
|
-
`export enum LlmMode { full = 'full-model' }`,
|
|
45
|
-
`export default { llm: { model: LlmMode.full } }`,
|
|
46
|
-
].join('\n'))
|
|
47
|
-
await warmCarConfig({ cwd: dir })
|
|
48
|
-
const r = loadCarConfig({ cwd: dir })
|
|
49
|
-
assert.equal(r.error, undefined)
|
|
50
|
-
assert.equal(r.config.llm?.model, 'full-model', 'enum 值经 jiti 转译进配置')
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('S38: strip-only spawn 差分——原生 import enum 载体失败(负控)+ jiti 真载成功 + 首载时延实测', async () => {
|
|
55
|
-
await withTempDir('strip', async dir => {
|
|
56
|
-
const configPath = join(dir, 'car.config.ts')
|
|
57
|
-
writeFileSync(configPath, [
|
|
58
|
-
`export enum LlmMode { full = 'strip-model' }`,
|
|
59
|
-
`export default { llm: { model: LlmMode.full } }`,
|
|
60
|
-
].join('\n'))
|
|
61
|
-
// 夹具自身仅可擦除语法(strip-only 可载);非可擦除只出现在被测配置里
|
|
62
|
-
const fixture = join(dir, 'fixture.ts')
|
|
63
|
-
writeFileSync(fixture, [
|
|
64
|
-
`import { warmCarConfig, loadCarConfig } from ${JSON.stringify(pathToFileURL(SRC_CONFIG).href)}`,
|
|
65
|
-
`import { pathToFileURL as toURL } from 'node:url'`,
|
|
66
|
-
`const configPath = process.argv[2]!`,
|
|
67
|
-
`let nativeError = ''`,
|
|
68
|
-
`try { await import(toURL(configPath).href) } catch (e) { nativeError = String((e as Error).message) }`,
|
|
69
|
-
`if (!nativeError) { console.log('NATIVE_UNEXPECTEDLY_OK'); process.exit(1) }`,
|
|
70
|
-
`const t0 = performance.now()`,
|
|
71
|
-
`await warmCarConfig({ explicitPath: configPath })`,
|
|
72
|
-
`const elapsed = Math.round(performance.now() - t0)`,
|
|
73
|
-
`const r = loadCarConfig({ explicitPath: configPath })`,
|
|
74
|
-
`if (r.error) { console.log('JITI_FAIL ' + r.error); process.exit(1) }`,
|
|
75
|
-
`console.log('OK model=' + r.config.llm?.model + ' firstLoadMs=' + elapsed + ' nativeRejected=' + nativeError.slice(0, 60).replace(/\\n/g, ' '))`,
|
|
76
|
-
].join('\n'))
|
|
77
|
-
// --experimental-strip-types:仅类型剥离无转译(22.19+ 可用;enum 属 transform-only 形态)
|
|
78
|
-
const { code, stdout } = await new Promise<{ code: number | null; stdout: string }>(resolve => {
|
|
79
|
-
const child = spawn(process.execPath, ['--experimental-strip-types', fixture, configPath], { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
80
|
-
let out = ''
|
|
81
|
-
child.stdout.on('data', d => { out += d })
|
|
82
|
-
child.on('exit', c => resolve({ code: c, stdout: out }))
|
|
83
|
-
})
|
|
84
|
-
assert.equal(code, 0, 'strip-only 环境下 jiti 真载 enum 配置')
|
|
85
|
-
const m = /OK model=(\S+) firstLoadMs=(\d+) nativeRejected=(.+)/.exec(stdout.trim())!
|
|
86
|
-
assert.equal(m[1], 'strip-model')
|
|
87
|
-
assert.ok(m[2] !== undefined, '首载时延实测出值')
|
|
88
|
-
console.log(` [登记] car.config.ts jiti 首载时延(spawn 冷启,本机实跑):${m[2]} ms`)
|
|
89
|
-
assert.ok(Number(m[2]) < 15_000, '首载时延宽上界(CI 防抖;真实数值进收口报告)')
|
|
90
|
-
assert.ok(m[3]!.length > 0, '负控:原生 import 对非可擦除语法显式失败——jiti 是载体而非原生剥离')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('S38: CLI 真路径——car run 于 enum 配置目录:配置经 jiti 装载生效(模型未配引导而非配置报错)', async () => {
|
|
95
|
-
await withTempDir('cli', async dir => {
|
|
96
|
-
writeFileSync(join(dir, 'car.config.ts'), [
|
|
97
|
-
`export enum LlmMode { full = 'full-model' }`,
|
|
98
|
-
`export default { llm: { model: LlmMode.full } }`,
|
|
99
|
-
].join('\n'))
|
|
100
|
-
const plugin = join(dir, 'noop.ts')
|
|
101
|
-
writeFileSync(plugin, [
|
|
102
|
-
`export const manifest = { name: "s38p", version: "1.0.0" }`,
|
|
103
|
-
`export default function apply(api) { api.registerTool({ name: "noop", declaredSideEffect: "readonly", run: async () => "ok" }) }`,
|
|
104
|
-
].join('\n'))
|
|
105
|
-
const { code, output } = await new Promise<{ code: number | null; output: string }>(resolve => {
|
|
106
|
-
const child = spawn(process.execPath, ['--experimental-transform-types', CLI, 'run', plugin], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
107
|
-
let out = ''
|
|
108
|
-
child.stdout.on('data', d => { out += d })
|
|
109
|
-
child.stderr.on('data', d => { out += d })
|
|
110
|
-
child.on('exit', c => resolve({ code: c, output: out }))
|
|
111
|
-
})
|
|
112
|
-
assert.equal(code, 2, 'D-13:无凭据/端点 → exit 2 显式引导')
|
|
113
|
-
assert.match(output, /CAR-E-LLM-CONFIG/, '配置经 jiti 解析通过(enum 值进 llm.model)走到模型就绪检查')
|
|
114
|
-
assert.doesNotMatch(output, /CAR-E-CONFIG/, '配置载体本身零错误')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1.7-S1 · OTLP span 语义标准化(D-26):exception 标准事件 + status / addEvent 标准 events 编码
|
|
3
|
-
*
|
|
4
|
-
* 覆盖(1.7-迭代规划 TELE-1;1.4 遗留「遥测 OTLP 增强」出清主体):
|
|
5
|
-
* - recordException → OTel 规范形态:span event(name='exception',exception.type/exception.message
|
|
6
|
-
* 事件属性 + timeUnixNano)+ span status → STATUS_CODE_ERROR——替换 1.1 起「仅 exception.message
|
|
7
|
-
* 属性 + 恒 UNSET」非标准形态;stacktrace 缺省不出站(D-26 口径:防路径/源码面外泄)
|
|
8
|
-
* - addEvent → 标准 events 数组编码(name + 原形态属性 + 时刻)——替换 `event.<name>` JSON 属性串
|
|
9
|
-
* - 出站体加法字段:无事件 span 不出 events 键(空形态合法);正常 span 恒 STATUS_CODE_UNSET(行为不变面)
|
|
10
|
-
* - noop 门面零出站不变(默认关三原则;s29/s32 回归由全量保证)
|
|
11
|
-
*/
|
|
12
|
-
import { test } from 'node:test'
|
|
13
|
-
import assert from 'node:assert/strict'
|
|
14
|
-
import { createTelemetryFacade } from '../src/runtime-core/telemetry.ts'
|
|
15
|
-
|
|
16
|
-
const okRes = () => ({ ok: true, status: 200 }) as unknown as Response
|
|
17
|
-
|
|
18
|
-
function capture() {
|
|
19
|
-
const bodies: any[] = []
|
|
20
|
-
const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
|
|
21
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
|
|
22
|
-
})
|
|
23
|
-
return { f, bodies }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const spansOf = (bodies: any[]) => bodies.filter(b => b.scopeSpans).flatMap(b => b.scopeSpans[0].spans)
|
|
27
|
-
|
|
28
|
-
test('S39: recordException → 标准 exception 事件 + STATUS_CODE_ERROR(stacktrace 不出站)', async () => {
|
|
29
|
-
const { f, bodies } = capture()
|
|
30
|
-
const s = f.getTracer().startSpan('car.step', { attributes: { 'car.step': 1 } })
|
|
31
|
-
s.recordException(new TypeError('boom'))
|
|
32
|
-
s.end('error')
|
|
33
|
-
await f.flush()
|
|
34
|
-
const span = spansOf(bodies)[0]
|
|
35
|
-
assert.equal(span.status.code, 'STATUS_CODE_ERROR', 'recordException → status ERROR(1.7 前恒 UNSET)')
|
|
36
|
-
assert.ok(Array.isArray(span.events) && span.events.length === 1, 'exception 为 span event')
|
|
37
|
-
const ev = span.events[0]
|
|
38
|
-
assert.equal(ev.name, 'exception')
|
|
39
|
-
assert.ok(ev.timeUnixNano, '事件时刻出站')
|
|
40
|
-
const attrs = Object.fromEntries(ev.attributes.map((a: any) => [a.key, a.value.stringValue]))
|
|
41
|
-
assert.equal(attrs['exception.type'], 'TypeError', 'err.name → exception.type')
|
|
42
|
-
assert.equal(attrs['exception.message'], 'boom', 'err.message → exception.message')
|
|
43
|
-
const allKeys = [...span.attributes, ...span.events.flatMap((e: any) => e.attributes)].map((a: any) => a.key)
|
|
44
|
-
assert.equal(allKeys.some((k: string) => /stack/i.test(k)), false, 'stacktrace 缺省不出站(D-26)')
|
|
45
|
-
assert.equal(allKeys.includes('exception.message') && span.attributes.some((a: any) => a.key === 'exception.message'), false,
|
|
46
|
-
'exception.message 不再落于 span attributes(1.1 非标准形态退役)')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('S39: recordException 非 Error 值兜底——type=Error / message=String(err)', async () => {
|
|
50
|
-
const { f, bodies } = capture()
|
|
51
|
-
const s = f.getTracer().startSpan('car.step')
|
|
52
|
-
s.recordException('plain failure')
|
|
53
|
-
s.end('error')
|
|
54
|
-
await f.flush()
|
|
55
|
-
const ev = spansOf(bodies)[0].events[0]
|
|
56
|
-
const attrs = Object.fromEntries(ev.attributes.map((a: any) => [a.key, a.value.stringValue]))
|
|
57
|
-
assert.deepEqual(attrs, { 'exception.type': 'Error', 'exception.message': 'plain failure' })
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
test('S39: addEvent → 标准 events 编码(原形态属性 + 时刻;attributes 不再承载事件)', async () => {
|
|
61
|
-
const { f, bodies } = capture()
|
|
62
|
-
const s = f.getTracer().startSpan('car.turn')
|
|
63
|
-
s.addEvent('cache-hit', { key: 'x', n: 1, flag: true })
|
|
64
|
-
s.end('completed')
|
|
65
|
-
await f.flush()
|
|
66
|
-
const span = spansOf(bodies)[0]
|
|
67
|
-
assert.equal(span.status.code, 'STATUS_CODE_UNSET', '正常 span 恒 UNSET(行为不变面)')
|
|
68
|
-
assert.equal(span.events.length, 1)
|
|
69
|
-
const ev = span.events[0]
|
|
70
|
-
assert.equal(ev.name, 'cache-hit')
|
|
71
|
-
assert.ok(ev.timeUnixNano)
|
|
72
|
-
const map = Object.fromEntries(ev.attributes.map((a: any) => [a.key, a.value]))
|
|
73
|
-
assert.deepEqual(map, { key: { stringValue: 'x' }, n: { doubleValue: 1 }, flag: { boolValue: true } },
|
|
74
|
-
'原形态属性出站(1.7 前为 event.<name>=JSON 串)')
|
|
75
|
-
const attrKeys = span.attributes.map((a: any) => a.key)
|
|
76
|
-
assert.equal(attrKeys.includes('event.cache-hit'), false, 'attributes 不再承载事件(非标准形态退役)')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('S39: 无事件 span 不出 events 键(出站体加法字段——空形态合法)', async () => {
|
|
80
|
-
const { f, bodies } = capture()
|
|
81
|
-
f.getTracer().startSpan('plain').end()
|
|
82
|
-
await f.flush()
|
|
83
|
-
const span = spansOf(bodies)[0]
|
|
84
|
-
assert.equal('events' in span, false, '无事件不出 events 键')
|
|
85
|
-
assert.equal(span.status.code, 'STATUS_CODE_UNSET')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('S39: 多事件按序出站(exception 与 addEvent 混排——事件数组保序)', async () => {
|
|
89
|
-
const { f, bodies } = capture()
|
|
90
|
-
const s = f.getTracer().startSpan('car.step')
|
|
91
|
-
s.addEvent('retry')
|
|
92
|
-
s.recordException(new Error('net'))
|
|
93
|
-
s.addEvent('fallback')
|
|
94
|
-
s.end('completed')
|
|
95
|
-
await f.flush()
|
|
96
|
-
const names = spansOf(bodies)[0].events.map((e: any) => e.name)
|
|
97
|
-
assert.deepEqual(names, ['retry', 'exception', 'fallback'])
|
|
98
|
-
// exception 事件混排下 status 仍 ERROR
|
|
99
|
-
assert.equal(spansOf(bodies)[0].status.code, 'STATUS_CODE_ERROR')
|
|
100
|
-
})
|
package/test/s4.spec.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S4 · 端到端串联(E2E)+ N2 基线实测 + Q-06 性能定标
|
|
3
|
-
*
|
|
4
|
-
* E2E 链路(对齐高层架构 §5.3 业务闭环五环节):
|
|
5
|
-
* 触发 → 装配加载(F9+F6)→ 沙箱内事件运行(F7+F4+F3 降级约束)→ 停止收口(F5/ADR-001)
|
|
6
|
-
* → 日志落盘(F2 哈希链)→ 审计回放(deriveMessages + verifyChain + 基础导出)
|
|
7
|
-
* Q-06 定标:装配时延 / 分发时延 / 日志 1 万事件追加+回放+校验耗时
|
|
8
|
-
*/
|
|
9
|
-
import { test } from 'node:test'
|
|
10
|
-
import assert from 'node:assert/strict'
|
|
11
|
-
import { writeFileSync, mkdtempSync, rmSync, readFileSync } from 'node:fs'
|
|
12
|
-
import { tmpdir } from 'node:os'
|
|
13
|
-
import { join } from 'node:path'
|
|
14
|
-
import { Context } from '../src/kernel/context.ts'
|
|
15
|
-
import { declareEvent, EventBus } from '../src/kernel/events.ts'
|
|
16
|
-
import { SessionLog, loadSessionLog } from '../src/session/log.ts'
|
|
17
|
-
import { runTurn } from '../src/loop/stop.ts'
|
|
18
|
-
import { mountPlugin } from '../src/load/loader.ts'
|
|
19
|
-
import { probeCapabilities, SandboxExecutor } from '../src/sandbox/sandbox.ts'
|
|
20
|
-
import { McpGateway, type McpTransport, type JsonRpcRequest, type JsonRpcResponse } from '../src/mcp/gateway.ts'
|
|
21
|
-
import { AuthzService } from '../src/authz/authz.ts'
|
|
22
|
-
|
|
23
|
-
const cleanup = (p: string) => { try { rmSync(p, { force: true, recursive: true }) } catch {} }
|
|
24
|
-
|
|
25
|
-
test('E2E:五环节全链路(装配→运行→收口→落盘→回放+MCP 桥接+授权留痕)', async () => {
|
|
26
|
-
const dir = mkdtempSync(join(tmpdir(), 'car-e2e-'))
|
|
27
|
-
try {
|
|
28
|
-
// —— 触发:插件文件(免编译 TS)——
|
|
29
|
-
const pluginFile = join(dir, 'demo-plugin.ts')
|
|
30
|
-
writeFileSync(pluginFile, `export default function apply(api) {\n api.registerTool({ name: 'e2e_tool', run: async (args) => 'e2e-' + args.tag })\n}\n`)
|
|
31
|
-
const manifest = { name: 'demo', version: '1.0.0' }
|
|
32
|
-
|
|
33
|
-
// —— 环节 1:装配加载(F9 两阶段 + F6 免编译 + F1 依赖推导)——
|
|
34
|
-
const t0 = Date.now()
|
|
35
|
-
const plugin = await mountPlugin({ file: pluginFile, manifest })
|
|
36
|
-
const ctx = new Context()
|
|
37
|
-
const log = new SessionLog('S-E2E')
|
|
38
|
-
const authz = new AuthzService({ log, timeoutMs: 100 })
|
|
39
|
-
declareEvent('tools/around', 'waterfall')
|
|
40
|
-
const bus = new EventBus()
|
|
41
|
-
let auditTrail = ''
|
|
42
|
-
bus.on('tools/around', async (_p, next) => { const r = await next(); auditTrail = 'wrapped:' + r; return r })
|
|
43
|
-
bus.on('tools/around', () => Promise.resolve('e2e-ok')) // 下游结果提供者
|
|
44
|
-
const hostTools: any[] = []
|
|
45
|
-
ctx.plugin({
|
|
46
|
-
name: 'demo',
|
|
47
|
-
apply: (c) => {
|
|
48
|
-
// 插件文件工厂已在 mount 时经 Stub 收集注册;bindCore 冲刷进宿主(勿重复注册)
|
|
49
|
-
plugin.bindCore({ registerTool: (t) => { hostTools.push(t); c.provide('tool:e2e_tool', t) } })
|
|
50
|
-
},
|
|
51
|
-
})
|
|
52
|
-
const mountMs = Date.now() - t0
|
|
53
|
-
assert.ok(hostTools.some(t => t.name === 'e2e_tool'), '装配:插件工具注册进能力矩阵')
|
|
54
|
-
|
|
55
|
-
// —— 环节 2:事件运行(MCP 桥接同审批门 + waterfall 环绕 + 权限门)——
|
|
56
|
-
const gw = new McpGateway()
|
|
57
|
-
const rpc: McpTransport = {
|
|
58
|
-
async send(req: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
59
|
-
if (req.method === 'tools/list') return { jsonrpc: '2.0', id: req.id, result: { tools: [{ name: 'py_tool', sideEffect: 'readonly' }] } }
|
|
60
|
-
if (req.method === 'tools/call') return { jsonrpc: '2.0', id: req.id, result: { content: 'py-ok' } }
|
|
61
|
-
throw new Error('unreachable')
|
|
62
|
-
},
|
|
63
|
-
alive: () => true, close() {},
|
|
64
|
-
}
|
|
65
|
-
await gw.register({ serverId: 'py1', transport: rpc })
|
|
66
|
-
const mcpResult = await gw.callTool('py1', 'py_tool', {})
|
|
67
|
-
assert.equal(mcpResult.ok, true)
|
|
68
|
-
log.append('user', 'user', 'T0', 'run e2e')
|
|
69
|
-
log.snapshotModelRequest()
|
|
70
|
-
log.append('model', 'toolCall', 'T0', { id: 'm1', tool: 'py_tool', args: {} })
|
|
71
|
-
log.append('plugin', 'toolResult', 'T0', { id: 'm1', result: mcpResult.result })
|
|
72
|
-
// 权限门:write 类走 confirm(授权留痕)
|
|
73
|
-
const decision = await authz.decideByUser(
|
|
74
|
-
{ authorizationId: 'A-E2E-1', actor: 'demo', capability: 'fs-write', resource: join(dir, 'out.txt') },
|
|
75
|
-
async () => true,
|
|
76
|
-
)
|
|
77
|
-
assert.equal(decision.decision, 'APPROVED')
|
|
78
|
-
|
|
79
|
-
// —— 环节 3:停止收口(F5/ADR-001)——
|
|
80
|
-
const probe = await probeCapabilities()
|
|
81
|
-
const sandbox = new SandboxExecutor({ probe, audit: () => {}, workspace: dir })
|
|
82
|
-
assert.equal(sandbox.degraded, probe.degraded) // 执行器必须忠实反映探测结论:Windows/裸 CI 必降级,Linux+landlock-run 全执法
|
|
83
|
-
const exec = await sandbox.exec({ argv: ['node', '-e', 'console.log("s")'], capabilities: ['exec'], mode: 'workspace-write', permissionMode: 'confirm', authorize: async () => true })
|
|
84
|
-
assert.equal(exec.ok, true)
|
|
85
|
-
const tools = new Map([['e2e_tool', { declaredSideEffect: 'write', run: hostTools.find(t => t.name === 'e2e_tool')!.run } as any]])
|
|
86
|
-
let n = 0
|
|
87
|
-
const turn = await runTurn({
|
|
88
|
-
log, turnId: 'T0', tools, preset: { mode: 'full' },
|
|
89
|
-
model: async (): Promise<any> => n++ === 0
|
|
90
|
-
? { stopReason: 'toolUse', toolCalls: [{ id: 'c1', tool: 'e2e_tool', args: { tag: 'ok' } }] }
|
|
91
|
-
: { stopReason: 'stop', text: 'final' },
|
|
92
|
-
})
|
|
93
|
-
assert.equal(turn.reason, 'completed')
|
|
94
|
-
// waterfall 环绕实证:分发 → before → next() → after → 结果可改写
|
|
95
|
-
const wrapped = await bus.dispatch<string>('tools/around', {}, () => 'e2e-ok')
|
|
96
|
-
assert.equal(wrapped, 'e2e-ok') // 下游结果透传;环绕 trail 已记录
|
|
97
|
-
assert.deepEqual(auditTrail, 'wrapped:e2e-ok') // handler 侧记录 before/after 顺序
|
|
98
|
-
|
|
99
|
-
// —— 环节 4:日志落盘(F2 哈希链 + N1 断言)——
|
|
100
|
-
const jsonl = join(dir, 'session.jsonl')
|
|
101
|
-
writeFileSync(jsonl, log.exportJSONL())
|
|
102
|
-
assert.equal(log.assertModelVisibleLogged().ok, true)
|
|
103
|
-
|
|
104
|
-
// —— 环节 5:审计回放(只读加载 + 断链校验 + 投影)——
|
|
105
|
-
const { log: reloaded, brokenAt } = loadSessionLog(jsonl)
|
|
106
|
-
assert.equal(brokenAt, null)
|
|
107
|
-
const replayed = reloaded.deriveMessages()
|
|
108
|
-
assert.ok(replayed.length >= 4)
|
|
109
|
-
assert.deepEqual(replayed[0], { role: 'user', content: 'run e2e' })
|
|
110
|
-
// 授权凭据链可回放(US6:谁在何时调用了什么、凭什么)
|
|
111
|
-
assert.ok(reloaded.events.some(e => e.turnId === 'authz:A-E2E-1'))
|
|
112
|
-
void mountMs
|
|
113
|
-
} finally { cleanup(dir) }
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('N2: 首插件跑通耗时实测(目标 ≤300s,实际秒级)', async () => {
|
|
117
|
-
const dir = mkdtempSync(join(tmpdir(), 'car-n2-'))
|
|
118
|
-
try {
|
|
119
|
-
const t0 = Date.now()
|
|
120
|
-
const f = join(dir, 'first.ts')
|
|
121
|
-
writeFileSync(f, `export default function apply(api) { api.registerTool({ name: 'first', run: async () => 42 }) }\n`)
|
|
122
|
-
const p = await mountPlugin({ file: f, manifest: { name: 'first', version: '1.0.0' } })
|
|
123
|
-
let got: unknown
|
|
124
|
-
p.bindCore({ registerTool: async (t) => { got = await t.run({}) } })
|
|
125
|
-
p.api.getRegisteredTools // 能力查询(bind 后可用)
|
|
126
|
-
const ctx = new Context()
|
|
127
|
-
ctx.plugin({ name: 'first', apply: (c) => c.effect(() => async () => {}, 'noop') })
|
|
128
|
-
await ctx.disposeRuntime()
|
|
129
|
-
const wall = Date.now() - t0
|
|
130
|
-
assert.equal(got, 42)
|
|
131
|
-
assert.ok(wall < 300_000, `实测 ${wall}ms`)
|
|
132
|
-
console.log(` [N2 实测] 首插件跑通 = ${wall}ms(含免编译加载+装配+执行+卸载)`)
|
|
133
|
-
} finally { cleanup(dir) }
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
test('Q-06: 性能定标——装配 / 分发 / 日志 1 万事件', async () => {
|
|
137
|
-
// 装配时延:20 插件依赖图
|
|
138
|
-
const ctx = new Context()
|
|
139
|
-
const t0 = Date.now()
|
|
140
|
-
for (let i = 0; i < 20; i++) {
|
|
141
|
-
ctx.plugin({ name: 'p' + i, apply: (c) => c.provide('svc' + i, { i }) })
|
|
142
|
-
}
|
|
143
|
-
for (let i = 0; i < 20; i++) {
|
|
144
|
-
ctx.plugin({ name: 'c' + i, inject: ['svc' + i], apply: () => {} })
|
|
145
|
-
}
|
|
146
|
-
const mountMs = Date.now() - t0
|
|
147
|
-
// 分发时延:serial 1000 次
|
|
148
|
-
declareEvent('bench/e', 'serial')
|
|
149
|
-
const bus = new EventBus()
|
|
150
|
-
bus.on('bench/e', (_p: unknown, next: () => number) => next() + 1)
|
|
151
|
-
const t1 = Date.now()
|
|
152
|
-
for (let i = 0; i < 1000; i++) await bus.dispatch<number>('bench/e', i, () => i)
|
|
153
|
-
const dispatchMs = Date.now() - t1
|
|
154
|
-
// 日志:1 万事件 追加+哈希链+回放+校验
|
|
155
|
-
const log = new SessionLog('S-BENCH')
|
|
156
|
-
const t2 = Date.now()
|
|
157
|
-
for (let i = 0; i < 10_000; i++) log.append('model', 'assistant', 'TB', 'x' + i)
|
|
158
|
-
const appendMs = Date.now() - t2
|
|
159
|
-
const t3 = Date.now()
|
|
160
|
-
const replayed = log.deriveMessages()
|
|
161
|
-
const chainOk = log.verifyChain() === null
|
|
162
|
-
const replayMs = Date.now() - t3
|
|
163
|
-
assert.equal(replayed.length, 10_000)
|
|
164
|
-
assert.equal(chainOk, true)
|
|
165
|
-
const row = { mountMs20Plugins: mountMs, dispatch1000SerialMs: dispatchMs, append10kMs: appendMs, replayAndVerify10kMs: replayMs }
|
|
166
|
-
console.log(' [Q-06 定标]', JSON.stringify(row))
|
|
167
|
-
// 保守上限断言(CI 波动容差):装配 <2s、千次分发 <5s、万事件追加 <10s、回放校验 <10s
|
|
168
|
-
assert.ok(mountMs < 2000 && dispatchMs < 5000 && appendMs < 10_000 && replayMs < 10_000)
|
|
169
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 1.7-S2 · metrics CUMULATIVE 时序语义修正(D-27):累计基数保持
|
|
3
|
-
*
|
|
4
|
-
* 覆盖(1.7-迭代规划 TELE-2;1.2-S3 起 clear() 与 AGGREGATION_TEMPORALITY_CUMULATIVE 标注矛盾出清):
|
|
5
|
-
* - 同计数器双 flush:第二次导出仍为全量累计(5 → 7),非增量(5 → 2)——真 CUMULATIVE;
|
|
6
|
-
* intervalMs>0 自动导出场景下下游(Prometheus 转换器等)不再解读为总量回落;
|
|
7
|
-
* - labels 形态与 temporality 标注不变;DELTA 备选不采纳(D-27:生态主流 + S17 计数语义即累计);
|
|
8
|
-
* - 内存有界由 label 基数保证(零内容枚举红线——AllowedLabels 3 计数器)。
|
|
9
|
-
*/
|
|
10
|
-
import { test } from 'node:test'
|
|
11
|
-
import assert from 'node:assert/strict'
|
|
12
|
-
import { createTelemetryFacade } from '../src/runtime-core/telemetry.ts'
|
|
13
|
-
|
|
14
|
-
const okRes = () => ({ ok: true, status: 200 }) as unknown as Response
|
|
15
|
-
|
|
16
|
-
test('S40: 累计基数保持——同计数器双 flush 导出全量累计快照(真 CUMULATIVE,非增量)', async () => {
|
|
17
|
-
const bodies: any[] = []
|
|
18
|
-
const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
|
|
19
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
|
|
20
|
-
})
|
|
21
|
-
const c = f.getMeter().createCounter('car_test_total')
|
|
22
|
-
c.add(5, { result: 'ok' })
|
|
23
|
-
await f.flush()
|
|
24
|
-
c.add(2, { result: 'ok' })
|
|
25
|
-
await f.flush()
|
|
26
|
-
assert.equal(bodies.length, 2, '两次 flush 各一次 metrics 出站')
|
|
27
|
-
const values = bodies.map(b => b.scopeMetrics[0].metrics[0].sum.dataPoints[0].asInt)
|
|
28
|
-
assert.deepEqual(values, [5, 7], '第二次导出 = 累计 7(1.7 前为增量 2——CUMULATIVE 语义错误)')
|
|
29
|
-
for (const b of bodies) {
|
|
30
|
-
const m = b.scopeMetrics[0].metrics[0]
|
|
31
|
-
assert.equal(m.name, 'car_test_total')
|
|
32
|
-
assert.equal(m.sum.aggregationTemporality, 'AGGREGATION_TEMPORALITY_CUMULATIVE')
|
|
33
|
-
const dp = m.sum.dataPoints[0]
|
|
34
|
-
assert.equal(dp.attributes[0].key, 'result', 'labels 形态不变')
|
|
35
|
-
assert.equal(dp.attributes[0].value.stringValue, 'ok')
|
|
36
|
-
}
|
|
37
|
-
assert.equal(f.stats().metricsExported, 2)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('S40: 无新增计数 interval 下重复 flush——全量快照幂等重出(CUMULATIVE 口径正确形态)', async () => {
|
|
41
|
-
const bodies: any[] = []
|
|
42
|
-
const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
|
|
43
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
|
|
44
|
-
})
|
|
45
|
-
f.getMeter().createCounter('car_load_total').add(3, { result: 'ok' })
|
|
46
|
-
await f.flush()
|
|
47
|
-
await f.flush()
|
|
48
|
-
const values = bodies.map(b => b.scopeMetrics[0].metrics[0].sum.dataPoints[0].asInt)
|
|
49
|
-
assert.deepEqual(values, [3, 3], '无新增 = 同值重出(累计快照,非清零/缺席)')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
test('S40: 多计数器并存——各自独立累计(互不串值)', async () => {
|
|
53
|
-
const bodies: any[] = []
|
|
54
|
-
const f = createTelemetryFacade({ endpoint: 'https://otel' }, {
|
|
55
|
-
fetchImpl: (async (_u: string | URL, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body))); return okRes() }) as typeof fetch,
|
|
56
|
-
})
|
|
57
|
-
const c1 = f.getMeter().createCounter('car_load_total')
|
|
58
|
-
const c2 = f.getMeter().createCounter('car_registry_decision')
|
|
59
|
-
c1.add(1, { result: 'ok' })
|
|
60
|
-
c2.add(2, { source: 'registry' })
|
|
61
|
-
c1.add(1, { result: 'ok' })
|
|
62
|
-
await f.flush()
|
|
63
|
-
const metrics = bodies[0].scopeMetrics[0].metrics
|
|
64
|
-
assert.equal(metrics.length, 2)
|
|
65
|
-
const byName = Object.fromEntries(metrics.map((m: any) => [m.name, m.sum.dataPoints[0].asInt]))
|
|
66
|
-
assert.deepEqual(byName, { car_load_total: 2, car_registry_decision: 2 })
|
|
67
|
-
})
|