chatccc 0.2.199 → 0.2.200
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 +1 -1
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +277 -277
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -224
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -114
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +87 -45
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/orchestrator.test.ts +227 -202
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +380 -380
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +473 -473
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -1072
- package/src/builtin/index.ts +404 -404
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +125 -125
- package/src/feishu-api.ts +86 -63
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +183 -183
- package/src/privacy.ts +118 -118
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +141 -141
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +205 -205
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const resumeSessionMock = vi.hoisted(() => vi.fn());
|
|
4
|
-
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
5
|
-
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
6
|
-
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
|
|
8
|
-
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
9
|
-
getSessionInfo: vi.fn(),
|
|
10
|
-
unstable_v2_createSession: vi.fn(),
|
|
11
|
-
unstable_v2_resumeSession: resumeSessionMock,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
15
|
-
createRawStreamLog: createRawStreamLogMock,
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
import { config } from "../config.ts";
|
|
19
|
-
import { createClaudeAdapter } from "../adapters/claude-adapter.ts";
|
|
20
|
-
|
|
21
|
-
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
22
|
-
|
|
23
|
-
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
24
|
-
const events: unknown[] = [];
|
|
25
|
-
for await (const event of iterable) events.push(event);
|
|
26
|
-
return events;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
resumeSessionMock.mockReset();
|
|
31
|
-
createRawStreamLogMock.mockReset();
|
|
32
|
-
rawLogWriteLineMock.mockReset();
|
|
33
|
-
rawLogCloseMock.mockReset();
|
|
34
|
-
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("Claude raw stream logs", () => {
|
|
38
|
-
it("writes raw Claude SDK stream messages when enabled", async () => {
|
|
39
|
-
const rawMessages = [
|
|
40
|
-
{ type: "system", subtype: "init", session_id: "sid-raw", cwd: "F:/project", model: "claude-test" },
|
|
41
|
-
{ type: "assistant", message: { content: [{ type: "text", text: "hello" }] } },
|
|
42
|
-
];
|
|
43
|
-
const session = {
|
|
44
|
-
send: vi.fn(async () => {}),
|
|
45
|
-
stream: async function* () {
|
|
46
|
-
for (const msg of rawMessages) yield msg;
|
|
47
|
-
},
|
|
48
|
-
close: vi.fn(),
|
|
49
|
-
};
|
|
50
|
-
resumeSessionMock.mockReturnValueOnce(session);
|
|
51
|
-
createRawStreamLogMock.mockResolvedValueOnce({
|
|
52
|
-
filePath: "claude.jsonl.gz",
|
|
53
|
-
writeLine: rawLogWriteLineMock,
|
|
54
|
-
close: rawLogCloseMock,
|
|
55
|
-
});
|
|
56
|
-
config.rawStreamLogs.claude = {
|
|
57
|
-
enabled: true,
|
|
58
|
-
maxBytesPerTurn: 4096,
|
|
59
|
-
retentionDays: 5,
|
|
60
|
-
keepCompleted: false,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const adapter = createClaudeAdapter({
|
|
64
|
-
model: "claude-test",
|
|
65
|
-
effort: "high",
|
|
66
|
-
isEmpty: (value) => value.trim() === "",
|
|
67
|
-
});
|
|
68
|
-
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
69
|
-
|
|
70
|
-
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
71
|
-
enabled: true,
|
|
72
|
-
tool: "claude",
|
|
73
|
-
sessionId: "sid-raw",
|
|
74
|
-
label: "prompt",
|
|
75
|
-
maxBytesPerTurn: 4096,
|
|
76
|
-
retentionDays: 5,
|
|
77
|
-
}));
|
|
78
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, JSON.stringify(rawMessages[0]));
|
|
79
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, JSON.stringify(rawMessages[1]));
|
|
80
|
-
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
81
|
-
expect(session.close).toHaveBeenCalledOnce();
|
|
82
|
-
expect(events).toContainEqual({
|
|
83
|
-
type: "assistant",
|
|
84
|
-
blocks: [{ type: "text", text: "hello" }],
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const resumeSessionMock = vi.hoisted(() => vi.fn());
|
|
4
|
+
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
5
|
+
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
6
|
+
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
|
|
9
|
+
getSessionInfo: vi.fn(),
|
|
10
|
+
unstable_v2_createSession: vi.fn(),
|
|
11
|
+
unstable_v2_resumeSession: resumeSessionMock,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
15
|
+
createRawStreamLog: createRawStreamLogMock,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { config } from "../config.ts";
|
|
19
|
+
import { createClaudeAdapter } from "../adapters/claude-adapter.ts";
|
|
20
|
+
|
|
21
|
+
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
22
|
+
|
|
23
|
+
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
24
|
+
const events: unknown[] = [];
|
|
25
|
+
for await (const event of iterable) events.push(event);
|
|
26
|
+
return events;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
resumeSessionMock.mockReset();
|
|
31
|
+
createRawStreamLogMock.mockReset();
|
|
32
|
+
rawLogWriteLineMock.mockReset();
|
|
33
|
+
rawLogCloseMock.mockReset();
|
|
34
|
+
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Claude raw stream logs", () => {
|
|
38
|
+
it("writes raw Claude SDK stream messages when enabled", async () => {
|
|
39
|
+
const rawMessages = [
|
|
40
|
+
{ type: "system", subtype: "init", session_id: "sid-raw", cwd: "F:/project", model: "claude-test" },
|
|
41
|
+
{ type: "assistant", message: { content: [{ type: "text", text: "hello" }] } },
|
|
42
|
+
];
|
|
43
|
+
const session = {
|
|
44
|
+
send: vi.fn(async () => {}),
|
|
45
|
+
stream: async function* () {
|
|
46
|
+
for (const msg of rawMessages) yield msg;
|
|
47
|
+
},
|
|
48
|
+
close: vi.fn(),
|
|
49
|
+
};
|
|
50
|
+
resumeSessionMock.mockReturnValueOnce(session);
|
|
51
|
+
createRawStreamLogMock.mockResolvedValueOnce({
|
|
52
|
+
filePath: "claude.jsonl.gz",
|
|
53
|
+
writeLine: rawLogWriteLineMock,
|
|
54
|
+
close: rawLogCloseMock,
|
|
55
|
+
});
|
|
56
|
+
config.rawStreamLogs.claude = {
|
|
57
|
+
enabled: true,
|
|
58
|
+
maxBytesPerTurn: 4096,
|
|
59
|
+
retentionDays: 5,
|
|
60
|
+
keepCompleted: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const adapter = createClaudeAdapter({
|
|
64
|
+
model: "claude-test",
|
|
65
|
+
effort: "high",
|
|
66
|
+
isEmpty: (value) => value.trim() === "",
|
|
67
|
+
});
|
|
68
|
+
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
69
|
+
|
|
70
|
+
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
71
|
+
enabled: true,
|
|
72
|
+
tool: "claude",
|
|
73
|
+
sessionId: "sid-raw",
|
|
74
|
+
label: "prompt",
|
|
75
|
+
maxBytesPerTurn: 4096,
|
|
76
|
+
retentionDays: 5,
|
|
77
|
+
}));
|
|
78
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, JSON.stringify(rawMessages[0]));
|
|
79
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, JSON.stringify(rawMessages[1]));
|
|
80
|
+
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
81
|
+
expect(session.close).toHaveBeenCalledOnce();
|
|
82
|
+
expect(events).toContainEqual({
|
|
83
|
+
type: "assistant",
|
|
84
|
+
blocks: [{ type: "text", text: "hello" }],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { PassThrough } from "node:stream";
|
|
3
|
-
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
const spawnMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
8
|
-
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
9
|
-
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
10
|
-
const killProcessTreeMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
11
|
-
|
|
12
|
-
vi.mock("node:child_process", () => ({
|
|
13
|
-
spawn: spawnMock,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
17
|
-
createRawStreamLog: createRawStreamLogMock,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock("../adapters/proc-tree-kill.ts", () => ({
|
|
21
|
-
killProcessTree: killProcessTreeMock,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
import { config } from "../config.ts";
|
|
25
|
-
import { createCodexAdapter } from "../adapters/codex-adapter.ts";
|
|
26
|
-
import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
|
|
27
|
-
import type { CodexSessionMetaStore } from "../adapters/codex-session-meta-store.ts";
|
|
28
|
-
|
|
29
|
-
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
30
|
-
|
|
31
|
-
function createProc(lines: string[]): EventEmitter & {
|
|
32
|
-
stdout: PassThrough;
|
|
33
|
-
stderr: PassThrough;
|
|
34
|
-
stdin: PassThrough;
|
|
35
|
-
pid: number;
|
|
36
|
-
} {
|
|
37
|
-
const proc = new EventEmitter() as EventEmitter & {
|
|
38
|
-
stdout: PassThrough;
|
|
39
|
-
stderr: PassThrough;
|
|
40
|
-
stdin: PassThrough;
|
|
41
|
-
pid: number;
|
|
42
|
-
};
|
|
43
|
-
proc.stdout = new PassThrough();
|
|
44
|
-
proc.stderr = new PassThrough();
|
|
45
|
-
proc.stdin = new PassThrough();
|
|
46
|
-
proc.pid = 4242;
|
|
47
|
-
queueMicrotask(() => {
|
|
48
|
-
for (const line of lines) proc.stdout.write(`${line}\n`);
|
|
49
|
-
proc.stdout.end();
|
|
50
|
-
proc.stderr.end();
|
|
51
|
-
proc.emit("close", 0);
|
|
52
|
-
});
|
|
53
|
-
return proc;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function createHangingProc(lines: string[]): EventEmitter & {
|
|
57
|
-
stdout: PassThrough;
|
|
58
|
-
stderr: PassThrough;
|
|
59
|
-
stdin: PassThrough;
|
|
60
|
-
pid: number;
|
|
61
|
-
} {
|
|
62
|
-
const proc = new EventEmitter() as EventEmitter & {
|
|
63
|
-
stdout: PassThrough;
|
|
64
|
-
stderr: PassThrough;
|
|
65
|
-
stdin: PassThrough;
|
|
66
|
-
pid: number;
|
|
67
|
-
};
|
|
68
|
-
proc.stdout = new PassThrough();
|
|
69
|
-
proc.stderr = new PassThrough();
|
|
70
|
-
proc.stdin = new PassThrough();
|
|
71
|
-
proc.pid = 4242;
|
|
72
|
-
queueMicrotask(() => {
|
|
73
|
-
for (const line of lines) proc.stdout.write(`${line}\n`);
|
|
74
|
-
});
|
|
75
|
-
return proc;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
79
|
-
const events: unknown[] = [];
|
|
80
|
-
for await (const event of iterable) events.push(event);
|
|
81
|
-
return events;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function metaStore(): CodexSessionMetaStore {
|
|
85
|
-
return {
|
|
86
|
-
async get(sessionId) {
|
|
87
|
-
return sessionId === "sid-raw" ? { cwd: "F:/project" } : undefined;
|
|
88
|
-
},
|
|
89
|
-
async set() {},
|
|
90
|
-
async setThreadId() {},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
vi.useRealTimers();
|
|
96
|
-
spawnMock.mockReset();
|
|
97
|
-
createRawStreamLogMock.mockReset();
|
|
98
|
-
rawLogWriteLineMock.mockReset();
|
|
99
|
-
rawLogCloseMock.mockReset();
|
|
100
|
-
killProcessTreeMock.mockClear();
|
|
101
|
-
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("Codex raw stream logs", () => {
|
|
105
|
-
it("writes raw Codex JSONL stdout lines when enabled", async () => {
|
|
106
|
-
const lines = [
|
|
107
|
-
JSON.stringify({ type: "thread.started", thread_id: "thread-1" }),
|
|
108
|
-
JSON.stringify({ type: "item.completed", item: { id: "item-1", type: "agent_message", text: "hello" } }),
|
|
109
|
-
JSON.stringify({ type: "turn.completed" }),
|
|
110
|
-
];
|
|
111
|
-
spawnMock.mockReturnValueOnce(createProc(lines));
|
|
112
|
-
createRawStreamLogMock.mockResolvedValueOnce({
|
|
113
|
-
filePath: "codex.jsonl.gz",
|
|
114
|
-
writeLine: rawLogWriteLineMock,
|
|
115
|
-
close: rawLogCloseMock,
|
|
116
|
-
});
|
|
117
|
-
config.rawStreamLogs.codex = {
|
|
118
|
-
enabled: true,
|
|
119
|
-
maxBytesPerTurn: 2048,
|
|
120
|
-
retentionDays: 2,
|
|
121
|
-
keepCompleted: false,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const adapter = createCodexAdapter({ metaStore: metaStore() });
|
|
125
|
-
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
126
|
-
|
|
127
|
-
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
128
|
-
enabled: true,
|
|
129
|
-
tool: "codex",
|
|
130
|
-
sessionId: "sid-raw",
|
|
131
|
-
label: "prompt",
|
|
132
|
-
maxBytesPerTurn: 2048,
|
|
133
|
-
retentionDays: 2,
|
|
134
|
-
}));
|
|
135
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, lines[0]);
|
|
136
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, lines[1]);
|
|
137
|
-
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(3, lines[2]);
|
|
138
|
-
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
139
|
-
expect(events).toContainEqual({
|
|
140
|
-
type: "assistant",
|
|
141
|
-
blocks: [{ type: "text", text: "hello" }],
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("fails the turn and kills the process tree when bad JSON is followed by idle stdout", async () => {
|
|
146
|
-
vi.useFakeTimers();
|
|
147
|
-
spawnMock.mockReturnValueOnce(createHangingProc([
|
|
148
|
-
"{\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\"",
|
|
149
|
-
]));
|
|
150
|
-
createRawStreamLogMock.mockResolvedValueOnce(null);
|
|
151
|
-
|
|
152
|
-
const adapter = createCodexAdapter({ metaStore: metaStore() });
|
|
153
|
-
const pending = collect(adapter.prompt("sid-raw", "hi", "F:/project"))
|
|
154
|
-
.catch((error: unknown) => error);
|
|
155
|
-
|
|
156
|
-
await Promise.resolve();
|
|
157
|
-
await vi.advanceTimersByTimeAsync(120_000);
|
|
158
|
-
|
|
159
|
-
const error = await pending;
|
|
160
|
-
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
161
|
-
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const spawnMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
const createRawStreamLogMock = vi.hoisted(() => vi.fn());
|
|
8
|
+
const rawLogWriteLineMock = vi.hoisted(() => vi.fn());
|
|
9
|
+
const rawLogCloseMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const killProcessTreeMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:child_process", () => ({
|
|
13
|
+
spawn: spawnMock,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../adapters/raw-stream-log.ts", () => ({
|
|
17
|
+
createRawStreamLog: createRawStreamLogMock,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../adapters/proc-tree-kill.ts", () => ({
|
|
21
|
+
killProcessTree: killProcessTreeMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { config } from "../config.ts";
|
|
25
|
+
import { createCodexAdapter } from "../adapters/codex-adapter.ts";
|
|
26
|
+
import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
|
|
27
|
+
import type { CodexSessionMetaStore } from "../adapters/codex-session-meta-store.ts";
|
|
28
|
+
|
|
29
|
+
const originalRawStreamLogs = structuredClone(config.rawStreamLogs);
|
|
30
|
+
|
|
31
|
+
function createProc(lines: string[]): EventEmitter & {
|
|
32
|
+
stdout: PassThrough;
|
|
33
|
+
stderr: PassThrough;
|
|
34
|
+
stdin: PassThrough;
|
|
35
|
+
pid: number;
|
|
36
|
+
} {
|
|
37
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
38
|
+
stdout: PassThrough;
|
|
39
|
+
stderr: PassThrough;
|
|
40
|
+
stdin: PassThrough;
|
|
41
|
+
pid: number;
|
|
42
|
+
};
|
|
43
|
+
proc.stdout = new PassThrough();
|
|
44
|
+
proc.stderr = new PassThrough();
|
|
45
|
+
proc.stdin = new PassThrough();
|
|
46
|
+
proc.pid = 4242;
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
for (const line of lines) proc.stdout.write(`${line}\n`);
|
|
49
|
+
proc.stdout.end();
|
|
50
|
+
proc.stderr.end();
|
|
51
|
+
proc.emit("close", 0);
|
|
52
|
+
});
|
|
53
|
+
return proc;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createHangingProc(lines: string[]): EventEmitter & {
|
|
57
|
+
stdout: PassThrough;
|
|
58
|
+
stderr: PassThrough;
|
|
59
|
+
stdin: PassThrough;
|
|
60
|
+
pid: number;
|
|
61
|
+
} {
|
|
62
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
63
|
+
stdout: PassThrough;
|
|
64
|
+
stderr: PassThrough;
|
|
65
|
+
stdin: PassThrough;
|
|
66
|
+
pid: number;
|
|
67
|
+
};
|
|
68
|
+
proc.stdout = new PassThrough();
|
|
69
|
+
proc.stderr = new PassThrough();
|
|
70
|
+
proc.stdin = new PassThrough();
|
|
71
|
+
proc.pid = 4242;
|
|
72
|
+
queueMicrotask(() => {
|
|
73
|
+
for (const line of lines) proc.stdout.write(`${line}\n`);
|
|
74
|
+
});
|
|
75
|
+
return proc;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function collect(iterable: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
79
|
+
const events: unknown[] = [];
|
|
80
|
+
for await (const event of iterable) events.push(event);
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function metaStore(): CodexSessionMetaStore {
|
|
85
|
+
return {
|
|
86
|
+
async get(sessionId) {
|
|
87
|
+
return sessionId === "sid-raw" ? { cwd: "F:/project" } : undefined;
|
|
88
|
+
},
|
|
89
|
+
async set() {},
|
|
90
|
+
async setThreadId() {},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
vi.useRealTimers();
|
|
96
|
+
spawnMock.mockReset();
|
|
97
|
+
createRawStreamLogMock.mockReset();
|
|
98
|
+
rawLogWriteLineMock.mockReset();
|
|
99
|
+
rawLogCloseMock.mockReset();
|
|
100
|
+
killProcessTreeMock.mockClear();
|
|
101
|
+
config.rawStreamLogs = structuredClone(originalRawStreamLogs);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("Codex raw stream logs", () => {
|
|
105
|
+
it("writes raw Codex JSONL stdout lines when enabled", async () => {
|
|
106
|
+
const lines = [
|
|
107
|
+
JSON.stringify({ type: "thread.started", thread_id: "thread-1" }),
|
|
108
|
+
JSON.stringify({ type: "item.completed", item: { id: "item-1", type: "agent_message", text: "hello" } }),
|
|
109
|
+
JSON.stringify({ type: "turn.completed" }),
|
|
110
|
+
];
|
|
111
|
+
spawnMock.mockReturnValueOnce(createProc(lines));
|
|
112
|
+
createRawStreamLogMock.mockResolvedValueOnce({
|
|
113
|
+
filePath: "codex.jsonl.gz",
|
|
114
|
+
writeLine: rawLogWriteLineMock,
|
|
115
|
+
close: rawLogCloseMock,
|
|
116
|
+
});
|
|
117
|
+
config.rawStreamLogs.codex = {
|
|
118
|
+
enabled: true,
|
|
119
|
+
maxBytesPerTurn: 2048,
|
|
120
|
+
retentionDays: 2,
|
|
121
|
+
keepCompleted: false,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const adapter = createCodexAdapter({ metaStore: metaStore() });
|
|
125
|
+
const events = await collect(adapter.prompt("sid-raw", "hi", "F:/project"));
|
|
126
|
+
|
|
127
|
+
expect(createRawStreamLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
128
|
+
enabled: true,
|
|
129
|
+
tool: "codex",
|
|
130
|
+
sessionId: "sid-raw",
|
|
131
|
+
label: "prompt",
|
|
132
|
+
maxBytesPerTurn: 2048,
|
|
133
|
+
retentionDays: 2,
|
|
134
|
+
}));
|
|
135
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(1, lines[0]);
|
|
136
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(2, lines[1]);
|
|
137
|
+
expect(rawLogWriteLineMock).toHaveBeenNthCalledWith(3, lines[2]);
|
|
138
|
+
expect(rawLogCloseMock).toHaveBeenCalledWith({ keep: false });
|
|
139
|
+
expect(events).toContainEqual({
|
|
140
|
+
type: "assistant",
|
|
141
|
+
blocks: [{ type: "text", text: "hello" }],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("fails the turn and kills the process tree when bad JSON is followed by idle stdout", async () => {
|
|
146
|
+
vi.useFakeTimers();
|
|
147
|
+
spawnMock.mockReturnValueOnce(createHangingProc([
|
|
148
|
+
"{\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\"",
|
|
149
|
+
]));
|
|
150
|
+
createRawStreamLogMock.mockResolvedValueOnce(null);
|
|
151
|
+
|
|
152
|
+
const adapter = createCodexAdapter({ metaStore: metaStore() });
|
|
153
|
+
const pending = collect(adapter.prompt("sid-raw", "hi", "F:/project"))
|
|
154
|
+
.catch((error: unknown) => error);
|
|
155
|
+
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
await vi.advanceTimersByTimeAsync(120_000);
|
|
158
|
+
|
|
159
|
+
const error = await pending;
|
|
160
|
+
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
161
|
+
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
|
|
162
|
+
});
|
|
163
|
+
});
|