@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.11-beta.3
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/clients/dsh-remote/agent/README.md +412 -0
- package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/clients/dsh-remote/agent/codex.mjs +736 -0
- package/clients/dsh-remote/agent/detect.mjs +399 -0
- package/clients/dsh-remote/agent/events.mjs +511 -0
- package/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/clients/dsh-remote/agent/index.mjs +339 -0
- package/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/clients/dsh-remote/agent/proc.mjs +179 -0
- package/clients/dsh-remote/agent/version.mjs +148 -0
- package/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/clients/dsh-remote/decision-poller.mjs +235 -0
- package/clients/dsh-remote/dsh-bridge.mjs +214 -1
- package/clients/dsh-remote/push-reporter.mjs +54 -0
- package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
- package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
- package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
- package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
- package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
- package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
- package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
- package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
- package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
- package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
- package/clients/dsh-remote/test/wecom-bot.test.mjs +1438 -0
- package/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/dsh-setup.mjs +18 -3
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +1048 -223
- package/packages/dsh-remote-web/lib/index.js +299 -2
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
- package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
- package/packages/dsh-remote-web/test/im-bots.test.mjs +359 -0
- package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -0
- package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
- package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +10 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
- package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
- package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 事件映射最小集契约测试(M2 第 3 周 B 项)。
|
|
4
|
+
*
|
|
5
|
+
* 守四件事(每条都对应一个真实风险):
|
|
6
|
+
* ① **每类真实 stream-json 行都能映射**(夹具直接抄自第 1 周实测输出形态);
|
|
7
|
+
* ② **未知形状不许崩**:未知 type/subtype、坏 JSON、超长行 → 计数丢弃,不抛错;
|
|
8
|
+
* ③ **不记录命令原文**:tool.call 只带输入字节数;tool.result 只带截断预览;
|
|
9
|
+
* ④ **落盘失败静默**:sink 写盘抛错只计数 + 告警一次,绝不把异常抛回会话。
|
|
10
|
+
*
|
|
11
|
+
* 用法: node --test clients/dsh-remote/test/agent-events.test.mjs
|
|
12
|
+
*/
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import fsSync from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_MAX_LINE_BYTES,
|
|
21
|
+
EVENT_TYPES,
|
|
22
|
+
createEventMapper,
|
|
23
|
+
createJsonlSink,
|
|
24
|
+
mapStreamJsonLine,
|
|
25
|
+
turnReasonOf
|
|
26
|
+
} from "../agent/events.mjs";
|
|
27
|
+
|
|
28
|
+
const NOW = 1_700_000_000_000;
|
|
29
|
+
const at = () => NOW;
|
|
30
|
+
|
|
31
|
+
const line = (obj) => JSON.stringify(obj);
|
|
32
|
+
|
|
33
|
+
/** 真实 claude 2.1.318 的 `system/init` 行(裁剪后,保留关键字段)。 */
|
|
34
|
+
const INIT = {
|
|
35
|
+
type: "system",
|
|
36
|
+
subtype: "init",
|
|
37
|
+
cwd: "/tmp/proj",
|
|
38
|
+
session_id: "sess-1",
|
|
39
|
+
tools: ["Bash", "Read", "Write"],
|
|
40
|
+
mcp_servers: [],
|
|
41
|
+
model: "some-model",
|
|
42
|
+
permissionMode: "default"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
test("status:system/init → status(working) + 会话/权限模式/工具数(不带 model)", () => {
|
|
46
|
+
const r = mapStreamJsonLine(line(INIT), { now: at });
|
|
47
|
+
assert.equal(r.ok, true);
|
|
48
|
+
assert.equal(r.events.length, 1);
|
|
49
|
+
const ev = r.events[0];
|
|
50
|
+
assert.equal(ev.type, EVENT_TYPES.STATUS);
|
|
51
|
+
assert.equal(ev.status, "working");
|
|
52
|
+
assert.equal(ev.sessionId, "sess-1");
|
|
53
|
+
assert.equal(ev.permissionMode, "default");
|
|
54
|
+
assert.equal(ev.toolCount, 3);
|
|
55
|
+
assert.equal(ev.model, undefined, "不落 model(可能暴露网关配置,也不给日志增噪)");
|
|
56
|
+
assert.equal(ev.at, NOW);
|
|
57
|
+
assert.equal(ev.v, 1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("status:钩子生命周期(hook_started / hook_response)→ status(hook),不带 output 内容", () => {
|
|
61
|
+
const a = mapStreamJsonLine(line({
|
|
62
|
+
type: "system", subtype: "hook_started", hook_event: "PermissionRequest", hook_id: "h1", session_id: "s"
|
|
63
|
+
}), { now: at });
|
|
64
|
+
assert.equal(a.events[0].status, "hook");
|
|
65
|
+
assert.equal(a.events[0].hookPhase, "started");
|
|
66
|
+
assert.equal(a.events[0].hookEvent, "PermissionRequest");
|
|
67
|
+
|
|
68
|
+
const b = mapStreamJsonLine(line({
|
|
69
|
+
type: "system", subtype: "hook_response", hook_event: "PermissionRequest",
|
|
70
|
+
output: "{\"hookSpecificOutput\":{\"decision\":\"allow\"}}", session_id: "s"
|
|
71
|
+
}), { now: at });
|
|
72
|
+
assert.equal(b.events[0].hookPhase, "finished");
|
|
73
|
+
assert.equal(JSON.stringify(b.events[0]).includes("hookSpecificOutput"), false, "钩子原样输出不进事件");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("status:api_retry → status(retrying);thinking_tokens 直接忽略(不算 unknown)", () => {
|
|
77
|
+
const retry = mapStreamJsonLine(line({ type: "system", subtype: "api_retry", attempt: 2, retry_delay_ms: 500 }), { now: at });
|
|
78
|
+
assert.equal(retry.events[0].status, "retrying");
|
|
79
|
+
assert.equal(retry.events[0].attempt, 2);
|
|
80
|
+
|
|
81
|
+
const think = mapStreamJsonLine(line({ type: "system", subtype: "thinking_tokens", tokens: 10 }), { now: at });
|
|
82
|
+
assert.deepEqual(think, { ok: true, events: [] }, "thinking_tokens 一条会话几十条,落盘没价值");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("status:未知 system subtype → ok:false(上层计数丢弃,不崩)", () => {
|
|
86
|
+
const r = mapStreamJsonLine(line({ type: "system", subtype: "brand_new_thing" }), { now: at });
|
|
87
|
+
assert.equal(r.ok, false);
|
|
88
|
+
assert.match(r.reason, /unknown-system-subtype/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("status:permission_denied(实测 2.1.318 的拒绝回执)→ status(denied) + 理由,不带命令原文", () => {
|
|
92
|
+
// 夹具抄自真实输出(只裁剪了 uuid/时间相关字段)
|
|
93
|
+
const raw = {
|
|
94
|
+
type: "system",
|
|
95
|
+
subtype: "permission_denied",
|
|
96
|
+
tool_name: "Bash",
|
|
97
|
+
tool_use_id: "call_00_x",
|
|
98
|
+
decision_reason_type: "hook",
|
|
99
|
+
decision_reason: "已按拒绝处理",
|
|
100
|
+
message: "已按拒绝处理",
|
|
101
|
+
session_id: "sess-1"
|
|
102
|
+
};
|
|
103
|
+
const r = mapStreamJsonLine(line(raw), { now: at });
|
|
104
|
+
assert.equal(r.ok, true, "这是实测存在的真实事件,不能算 unknown");
|
|
105
|
+
const ev = r.events[0];
|
|
106
|
+
assert.equal(ev.type, EVENT_TYPES.STATUS);
|
|
107
|
+
assert.equal(ev.status, "denied");
|
|
108
|
+
assert.equal(ev.toolName, "Bash");
|
|
109
|
+
assert.equal(ev.toolCallId, "call_00_x");
|
|
110
|
+
assert.equal(ev.reasonType, "hook");
|
|
111
|
+
assert.match(ev.reason, /拒绝/);
|
|
112
|
+
assert.equal(JSON.stringify(ev).includes("rm -rf"), false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("message.delta:assistant 完整文本 → final:true", () => {
|
|
116
|
+
const r = mapStreamJsonLine(line({
|
|
117
|
+
type: "assistant",
|
|
118
|
+
message: { content: [{ type: "text", text: "命令的原始输出:ok" }] }
|
|
119
|
+
}), { now: at });
|
|
120
|
+
assert.equal(r.events.length, 1);
|
|
121
|
+
assert.equal(r.events[0].type, EVENT_TYPES.MESSAGE_DELTA);
|
|
122
|
+
assert.equal(r.events[0].text, "命令的原始输出:ok");
|
|
123
|
+
assert.equal(r.events[0].final, true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("message.delta:stream_event 的 text_delta → final:false(流式增量)", () => {
|
|
127
|
+
const r = mapStreamJsonLine(line({
|
|
128
|
+
type: "stream_event",
|
|
129
|
+
event: { type: "content_block_delta", delta: { type: "text_delta", text: "命" } }
|
|
130
|
+
}), { now: at });
|
|
131
|
+
assert.equal(r.events[0].type, EVENT_TYPES.MESSAGE_DELTA);
|
|
132
|
+
assert.equal(r.events[0].final, false);
|
|
133
|
+
assert.equal(r.events[0].text, "命");
|
|
134
|
+
|
|
135
|
+
const other = mapStreamJsonLine(line({ type: "stream_event", event: { type: "message_start" } }), { now: at });
|
|
136
|
+
assert.equal(other.ok, false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("tool.call:只带 id/name/kind/输入字节数 —— **绝不带命令原文**", () => {
|
|
140
|
+
const command = "curl -H 'Authorization: Bearer sk-secret' http://example.com";
|
|
141
|
+
const r = mapStreamJsonLine(line({
|
|
142
|
+
type: "assistant",
|
|
143
|
+
message: {
|
|
144
|
+
content: [
|
|
145
|
+
{ type: "text", text: "我来执行" },
|
|
146
|
+
{ type: "tool_use", id: "call_1", name: "Bash", input: { command, description: "fetch" } }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
}), { now: at });
|
|
150
|
+
assert.equal(r.events.length, 2);
|
|
151
|
+
const call = r.events[1];
|
|
152
|
+
assert.equal(call.type, EVENT_TYPES.TOOL_CALL);
|
|
153
|
+
assert.equal(call.toolCallId, "call_1");
|
|
154
|
+
assert.equal(call.name, "Bash");
|
|
155
|
+
assert.equal(call.kind, "execute");
|
|
156
|
+
assert.ok(call.inputBytes > 0);
|
|
157
|
+
const dump = JSON.stringify(call);
|
|
158
|
+
assert.equal(dump.includes("sk-secret"), false, "命令原文绝不能进事件");
|
|
159
|
+
assert.equal(dump.includes("curl"), false, "命令原文绝不能进事件");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("tool.call:文件类工具 → kind=edit/read/search", () => {
|
|
163
|
+
const mk = (name) =>
|
|
164
|
+
mapStreamJsonLine(line({
|
|
165
|
+
type: "assistant",
|
|
166
|
+
message: { content: [{ type: "tool_use", id: "c", name, input: { file_path: "/tmp/a.txt" } }] }
|
|
167
|
+
}), { now: at }).events[0];
|
|
168
|
+
assert.equal(mk("Write").kind, "edit");
|
|
169
|
+
assert.equal(mk("Read").kind, "read");
|
|
170
|
+
assert.equal(mk("Grep").kind, "search");
|
|
171
|
+
assert.equal(mk("SomeNewTool").kind, "other", "未知工具落 other,不猜");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("tool.result:string / 数组 content 都能取文本;is_error 透传;预览截断", () => {
|
|
175
|
+
const str = mapStreamJsonLine(line({
|
|
176
|
+
type: "user",
|
|
177
|
+
message: { content: [{ type: "tool_result", tool_use_id: "call_1", is_error: false, content: "hello" }] }
|
|
178
|
+
}), { now: at });
|
|
179
|
+
assert.equal(str.events[0].type, EVENT_TYPES.TOOL_RESULT);
|
|
180
|
+
assert.equal(str.events[0].toolCallId, "call_1");
|
|
181
|
+
assert.equal(str.events[0].isError, false);
|
|
182
|
+
assert.equal(str.events[0].outputBytes, 5);
|
|
183
|
+
assert.equal(str.events[0].outputPreview, "hello");
|
|
184
|
+
|
|
185
|
+
const arr = mapStreamJsonLine(line({
|
|
186
|
+
type: "user",
|
|
187
|
+
message: {
|
|
188
|
+
content: [{
|
|
189
|
+
type: "tool_result", tool_use_id: "call_2", is_error: true,
|
|
190
|
+
content: [{ type: "text", text: "hook-denied: 验证用拒绝" }]
|
|
191
|
+
}]
|
|
192
|
+
}
|
|
193
|
+
}), { now: at });
|
|
194
|
+
assert.equal(arr.events[0].isError, true);
|
|
195
|
+
assert.match(arr.events[0].outputPreview, /hook-denied/);
|
|
196
|
+
|
|
197
|
+
const long = mapStreamJsonLine(line({
|
|
198
|
+
type: "user",
|
|
199
|
+
message: { content: [{ type: "tool_result", tool_use_id: "c", content: "x".repeat(5000) }] }
|
|
200
|
+
}), { now: at, maxPreviewChars: 100 });
|
|
201
|
+
assert.equal(long.events[0].outputBytes, 5000);
|
|
202
|
+
assert.equal(long.events[0].outputPreview.length, 100, "预览必须截断");
|
|
203
|
+
|
|
204
|
+
const off = mapStreamJsonLine(line({
|
|
205
|
+
type: "user",
|
|
206
|
+
message: { content: [{ type: "tool_result", tool_use_id: "c", content: "secret-output" }] }
|
|
207
|
+
}), { now: at, maxPreviewChars: 0 });
|
|
208
|
+
assert.equal(off.events[0].outputPreview, "", "maxPreviewChars=0 时完全不带输出内容");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("turn.end:success → completed;is_error → error;max_turns / interrupted / aborted", () => {
|
|
212
|
+
assert.equal(turnReasonOf({ subtype: "success", is_error: false }), "completed");
|
|
213
|
+
assert.equal(turnReasonOf({ subtype: "success", terminal_reason: "aborted" }), "aborted");
|
|
214
|
+
assert.equal(turnReasonOf({ subtype: "success", terminal_reason: "interrupted" }), "interrupted");
|
|
215
|
+
assert.equal(turnReasonOf({ subtype: "error_during_execution", is_error: true }), "error");
|
|
216
|
+
assert.equal(turnReasonOf({ subtype: "error_max_turns", is_error: true }), "error");
|
|
217
|
+
assert.equal(turnReasonOf({ subtype: "x", is_error: false }), "x");
|
|
218
|
+
|
|
219
|
+
const r = mapStreamJsonLine(line({
|
|
220
|
+
type: "result", subtype: "success", is_error: false,
|
|
221
|
+
duration_ms: 1234, num_turns: 2, session_id: "sess-1", result: "完成"
|
|
222
|
+
}), { now: at });
|
|
223
|
+
const ev = r.events[0];
|
|
224
|
+
assert.equal(ev.type, EVENT_TYPES.TURN_END);
|
|
225
|
+
assert.equal(ev.reason, "completed");
|
|
226
|
+
assert.equal(ev.durationMs, 1234);
|
|
227
|
+
assert.equal(ev.numTurns, 2);
|
|
228
|
+
assert.equal(ev.sessionId, "sess-1");
|
|
229
|
+
assert.equal(JSON.stringify(ev).includes("完成"), false, "最终文本靠 message.delta,不重复塞进 turn.end");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("坏输入:未知 type / 坏 JSON / 空行 / 超长行 —— 全部返回 ok:false 或空,不抛错", () => {
|
|
233
|
+
assert.equal(mapStreamJsonLine(line({ type: "brand_new" }), { now: at }).reason, "unknown-type:brand_new");
|
|
234
|
+
assert.equal(mapStreamJsonLine("{not json", { now: at }).reason, "bad-json");
|
|
235
|
+
assert.equal(mapStreamJsonLine(" ", { now: at }).reason, "empty");
|
|
236
|
+
assert.equal(mapStreamJsonLine("42", { now: at }).reason, "not-object");
|
|
237
|
+
|
|
238
|
+
const huge = JSON.stringify({ type: "assistant", message: { content: [{ type: "text", text: "x".repeat(1000) }] } });
|
|
239
|
+
const r = mapStreamJsonLine(huge, { now: at, maxLineBytes: 100 });
|
|
240
|
+
assert.equal(r.reason, "too-long");
|
|
241
|
+
assert.ok(DEFAULT_MAX_LINE_BYTES > 100);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("mapper:seq 自增 + byType/byReason 统计 + onEvent 回调", () => {
|
|
245
|
+
const seen = [];
|
|
246
|
+
const m = createEventMapper({ now: at, onEvent: (e) => seen.push(e) });
|
|
247
|
+
m.writeLine(line(INIT));
|
|
248
|
+
m.writeLine(line({ type: "assistant", message: { content: [{ type: "text", text: "hi" }] } }));
|
|
249
|
+
m.writeLine(line({ type: "result", subtype: "success", is_error: false }));
|
|
250
|
+
m.writeLine("{bad");
|
|
251
|
+
m.writeLine(line({ type: "nope" }));
|
|
252
|
+
|
|
253
|
+
const s = m.stats();
|
|
254
|
+
assert.equal(s.lines, 5);
|
|
255
|
+
assert.equal(s.events, 3);
|
|
256
|
+
assert.equal(s.badJson, 1);
|
|
257
|
+
assert.equal(s.unknown, 1);
|
|
258
|
+
assert.deepEqual(s.unknownReasons, { "unknown-type:nope": 1 }, "未识别原因要能排障");
|
|
259
|
+
assert.equal(s.byType[EVENT_TYPES.STATUS], 1);
|
|
260
|
+
assert.equal(s.byType[EVENT_TYPES.MESSAGE_DELTA], 1);
|
|
261
|
+
assert.equal(s.byType[EVENT_TYPES.TURN_END], 1);
|
|
262
|
+
assert.equal(s.byReason.completed, 1);
|
|
263
|
+
assert.deepEqual(seen.map((e) => e.seq), [1, 2, 3], "seq 连续自增");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("mapper:坏行不中断后续行(一行意外输出不能把整条会话采集带死)", () => {
|
|
267
|
+
const m = createEventMapper({ now: at });
|
|
268
|
+
m.writeLine("not json at all");
|
|
269
|
+
m.writeLine(line({ type: "result", subtype: "success", is_error: false }));
|
|
270
|
+
assert.equal(m.stats().events, 1);
|
|
271
|
+
assert.equal(m.stats().badJson, 1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("sink:不给 filePath → 默认不启用,什么都不写", () => {
|
|
275
|
+
const sink = createJsonlSink({});
|
|
276
|
+
assert.equal(sink.enabled, false);
|
|
277
|
+
assert.equal(sink.write({ type: "status" }), false);
|
|
278
|
+
assert.equal(sink.stats().events, 0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("sink:真写 jsonl(append-only,一行一事件,可回读)", () => {
|
|
282
|
+
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "agent-events-"));
|
|
283
|
+
const file = path.join(dir, "events.jsonl");
|
|
284
|
+
try {
|
|
285
|
+
const sink = createJsonlSink({ filePath: file, now: at });
|
|
286
|
+
assert.equal(sink.enabled, true);
|
|
287
|
+
assert.equal(sink.write({ v: 1, seq: 1, type: "status" }), true);
|
|
288
|
+
assert.equal(sink.write({ v: 1, seq: 2, type: "turn.end" }), true);
|
|
289
|
+
const lines = fsSync.readFileSync(file, "utf8").trim().split("\n");
|
|
290
|
+
assert.equal(lines.length, 2);
|
|
291
|
+
assert.deepEqual(JSON.parse(lines[0]), { v: 1, seq: 1, type: "status" });
|
|
292
|
+
assert.equal(sink.stats().events, 2);
|
|
293
|
+
assert.ok(sink.stats().bytes > 0);
|
|
294
|
+
} finally {
|
|
295
|
+
fsSync.rmSync(dir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("sink:落盘失败**静默**(计数 + 只告警一次,绝不抛回会话)", () => {
|
|
300
|
+
const logs = [];
|
|
301
|
+
let writes = 0;
|
|
302
|
+
const sink = createJsonlSink({
|
|
303
|
+
filePath: "/definitely/not/writable/events.jsonl",
|
|
304
|
+
now: at,
|
|
305
|
+
log: (e) => logs.push(e),
|
|
306
|
+
fs: {
|
|
307
|
+
appendFileSync: () => {
|
|
308
|
+
writes += 1;
|
|
309
|
+
throw Object.assign(new Error("ENOSPC: no space left"), { code: "ENOSPC" });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
assert.doesNotThrow(() => sink.write({ type: "status" }));
|
|
314
|
+
assert.doesNotThrow(() => sink.write({ type: "status" }));
|
|
315
|
+
assert.equal(writes, 2);
|
|
316
|
+
const s = sink.stats();
|
|
317
|
+
assert.equal(s.writeErrors, 2);
|
|
318
|
+
assert.equal(s.lastError, "ENOSPC");
|
|
319
|
+
assert.equal(logs.length, 1, "只告警一次,不刷屏");
|
|
320
|
+
assert.equal(logs[0].event, "events.sink-error");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("mapper + sink 串联:写入文件的事件与返回值一致", () => {
|
|
324
|
+
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "agent-events2-"));
|
|
325
|
+
const file = path.join(dir, "e.jsonl");
|
|
326
|
+
try {
|
|
327
|
+
const sink = createJsonlSink({ filePath: file, now: at });
|
|
328
|
+
const m = createEventMapper({ sink, now: at });
|
|
329
|
+
const out = m.writeLine(line({
|
|
330
|
+
type: "assistant",
|
|
331
|
+
message: { content: [{ type: "tool_use", id: "c1", name: "Bash", input: { command: "rm -rf x" } }] }
|
|
332
|
+
}));
|
|
333
|
+
assert.equal(out.length, 1);
|
|
334
|
+
const written = JSON.parse(fsSync.readFileSync(file, "utf8").trim());
|
|
335
|
+
assert.equal(written.toolCallId, "c1");
|
|
336
|
+
assert.equal(JSON.stringify(written).includes("rm -rf"), false);
|
|
337
|
+
} finally {
|
|
338
|
+
fsSync.rmSync(dir, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
});
|