chatccc 0.2.52 → 0.2.54

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 (42) hide show
  1. package/README.md +95 -256
  2. package/bin/chatccc.mjs +23 -23
  3. package/demo/ilink_echo_probe.ts +222 -222
  4. package/im-skills/feishu-skill/download-video.mjs +162 -162
  5. package/im-skills/feishu-skill/receive-send-file.md +66 -66
  6. package/im-skills/feishu-skill/receive-send-image.md +27 -27
  7. package/im-skills/feishu-skill/send-file.mjs +50 -50
  8. package/im-skills/feishu-skill/send-image.mjs +50 -50
  9. package/im-skills/feishu-skill/skill.md +10 -10
  10. package/package.json +59 -59
  11. package/src/__tests__/agent-image-rpc.test.ts +33 -33
  12. package/src/__tests__/agent-rpc-body.test.ts +41 -41
  13. package/src/__tests__/card-plain-text.test.ts +42 -42
  14. package/src/__tests__/codex-adapter.test.ts +304 -304
  15. package/src/__tests__/config-reload.test.ts +26 -26
  16. package/src/__tests__/config-sample.test.ts +19 -19
  17. package/src/__tests__/crash-logging.test.ts +62 -62
  18. package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
  19. package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
  20. package/src/__tests__/im-skills.test.ts +46 -46
  21. package/src/__tests__/privacy.test.ts +143 -0
  22. package/src/__tests__/session.test.ts +57 -2
  23. package/src/__tests__/sim-agent.test.ts +173 -173
  24. package/src/__tests__/sim-platform.test.ts +76 -76
  25. package/src/__tests__/sim-store.test.ts +213 -213
  26. package/src/__tests__/stream-state.test.ts +134 -134
  27. package/src/__tests__/wechat-platform.test.ts +57 -32
  28. package/src/adapters/codex-adapter.ts +294 -294
  29. package/src/adapters/codex-session-meta-store.ts +130 -130
  30. package/src/agent-file-rpc.ts +156 -156
  31. package/src/agent-image-rpc.ts +152 -152
  32. package/src/agent-rpc-body.ts +48 -48
  33. package/src/card-plain-text.ts +108 -108
  34. package/src/feishu-api.ts +7 -3
  35. package/src/im-skills.ts +109 -109
  36. package/src/platform-adapter.ts +60 -60
  37. package/src/privacy.ts +68 -0
  38. package/src/session-chat-binding.ts +6 -1
  39. package/src/session.ts +40 -35
  40. package/src/sim-agent.ts +167 -167
  41. package/src/trace.ts +50 -50
  42. package/src/wechat-platform.ts +4 -1
@@ -1,305 +1,305 @@
1
- import { describe, it, expect } from "vitest";
2
- import { readFileSync } from "node:fs";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import {
6
- normalizeCodexMessage,
7
- createCodexAdapter,
8
- type CreateCodexAdapterOptions,
9
- } from "../adapters/codex-adapter.ts";
10
- import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
11
- import {
12
- type CodexSessionMeta,
13
- type CodexSessionMetaStore,
14
- } from "../adapters/codex-session-meta-store.ts";
15
- import { accumulateBlockContent, pickFinalReply, type AccumulatorState } from "../session.ts";
16
-
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
-
19
- function readFixture(name: string): unknown[] {
20
- const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
21
- return raw
22
- .split(/\r?\n/)
23
- .filter(Boolean)
24
- .map((line) => JSON.parse(line));
25
- }
26
-
27
- // 进程内内存版 meta store
28
- function createInMemoryMetaStore(
29
- initial: Record<string, CodexSessionMeta> = {},
30
- ): CodexSessionMetaStore & { snapshot(): Record<string, CodexSessionMeta> } {
31
- const map = new Map<string, CodexSessionMeta>(Object.entries(initial));
32
- return {
33
- async get(sid) {
34
- return map.get(sid);
35
- },
36
- async set(sid, partial) {
37
- const existing = map.get(sid) ?? { cwd: "" };
38
- const merged: CodexSessionMeta = { ...existing, ...partial };
39
- if (typeof merged.cwd !== "string" || merged.cwd.length === 0) return;
40
- map.set(sid, merged);
41
- },
42
- async setThreadId(sid, threadId) {
43
- const existing = map.get(sid);
44
- if (existing) {
45
- map.set(sid, { ...existing, threadId });
46
- }
47
- },
48
- snapshot() {
49
- return Object.fromEntries(map);
50
- },
51
- };
52
- }
53
-
54
- // ---------------------------------------------------------------------------
55
- // normalizeCodexMessage — 核心映射逻辑测试(纯函数)
56
- // ---------------------------------------------------------------------------
57
-
58
- describe("normalizeCodexMessage", () => {
59
- it("normalizes agent_message into assistant text block", () => {
60
- const result = normalizeCodexMessage({
61
- type: "item.completed",
62
- item: { id: "item_0", type: "agent_message", text: "hello" },
63
- });
64
- expect(result).not.toBeNull();
65
- expect(result!.type).toBe("assistant");
66
- expect(result!.blocks).toEqual([{ type: "text", text: "hello" }]);
67
- });
68
-
69
- it("normalizes command_execution start into tool_use block", () => {
70
- const result = normalizeCodexMessage({
71
- type: "item.started",
72
- item: {
73
- id: "item_0",
74
- type: "command_execution",
75
- command: "powershell.exe -Command ls",
76
- status: "in_progress",
77
- },
78
- });
79
- expect(result).not.toBeNull();
80
- expect(result!.type).toBe("assistant");
81
- expect(result!.blocks).toEqual([
82
- { type: "tool_use", name: "Bash", input: { command: "powershell.exe -Command ls" } },
83
- ]);
84
- });
85
-
86
- it("normalizes command_execution completion as tool_result (success)", () => {
87
- const result = normalizeCodexMessage({
88
- type: "item.completed",
89
- item: {
90
- id: "item_0",
91
- type: "command_execution",
92
- command: "ls",
93
- aggregated_output: "file1\nfile2\n",
94
- exit_code: 0,
95
- status: "completed",
96
- },
97
- });
98
- expect(result).not.toBeNull();
99
- expect(result!.blocks).toEqual([
100
- {
101
- type: "tool_result",
102
- tool_use_id: "item_0",
103
- content: "file1\nfile2\n",
104
- is_error: undefined,
105
- },
106
- ]);
107
- });
108
-
109
- it("normalizes command_execution completion as tool_result (error)", () => {
110
- const result = normalizeCodexMessage({
111
- type: "item.completed",
112
- item: {
113
- id: "item_err",
114
- type: "command_execution",
115
- command: "nonexistent",
116
- aggregated_output: "command not found",
117
- exit_code: 127,
118
- status: "completed",
119
- },
120
- });
121
- expect(result).not.toBeNull();
122
- expect(result!.blocks).toEqual([
123
- {
124
- type: "tool_result",
125
- tool_use_id: "item_err",
126
- content: "command not found",
127
- is_error: true,
128
- },
129
- ]);
130
- });
131
-
132
- it("returns null for thread.started events", () => {
133
- expect(
134
- normalizeCodexMessage({
135
- type: "thread.started",
136
- thread_id: "abc-123",
137
- }),
138
- ).toBeNull();
139
- });
140
-
141
- it("returns null for turn.started events", () => {
142
- expect(normalizeCodexMessage({ type: "turn.started" })).toBeNull();
143
- });
144
-
145
- it("returns null for turn.completed events", () => {
146
- expect(
147
- normalizeCodexMessage({
148
- type: "turn.completed",
149
- usage: { input_tokens: 100, output_tokens: 50 },
150
- }),
151
- ).toBeNull();
152
- });
153
-
154
- it("returns null for unknown event types", () => {
155
- expect(normalizeCodexMessage({ type: "unknown" })).toBeNull();
156
- expect(normalizeCodexMessage({} as Parameters<typeof normalizeCodexMessage>[0])).toBeNull();
157
- });
158
-
159
- it("returns null for agent_message with empty text", () => {
160
- const result = normalizeCodexMessage({
161
- type: "item.completed",
162
- item: { id: "item_0", type: "agent_message", text: "" },
163
- });
164
- expect(result).toBeNull();
165
- });
166
-
167
- it("returns null for command_execution start without command text", () => {
168
- const result = normalizeCodexMessage({
169
- type: "item.started",
170
- item: { id: "item_0", type: "command_execution", status: "in_progress" },
171
- });
172
- expect(result).toBeNull();
173
- });
174
- });
175
-
176
- // ---------------------------------------------------------------------------
177
- // Fixture 端到端测试
178
- // ---------------------------------------------------------------------------
179
-
180
- describe("Codex stream fixtures", () => {
181
- it("simple text: 流结束后 pickFinalReply 返回正确文本", () => {
182
- const lines = readFixture("codex_simple_text.jsonl");
183
- const state: AccumulatorState = {
184
- accumulatedContent: "",
185
- finalText: "",
186
- finalCompleteText: "",
187
- chunkCount: 0,
188
- };
189
- for (const raw of lines) {
190
- const normalized = normalizeCodexMessage(
191
- raw as Parameters<typeof normalizeCodexMessage>[0],
192
- );
193
- if (!normalized) continue;
194
- for (const block of normalized.blocks) {
195
- accumulateBlockContent(block, state);
196
- }
197
- }
198
-
199
- expect(pickFinalReply(state)).toBe("hello");
200
- });
201
-
202
- it("with tool: 流结束后 pickFinalReply 返回最终文本(不含工具输出在 finalText 中)", () => {
203
- const lines = readFixture("codex_with_tool.jsonl");
204
- const state: AccumulatorState = {
205
- accumulatedContent: "",
206
- finalText: "",
207
- finalCompleteText: "",
208
- chunkCount: 0,
209
- };
210
- for (const raw of lines) {
211
- const normalized = normalizeCodexMessage(
212
- raw as Parameters<typeof normalizeCodexMessage>[0],
213
- );
214
- if (!normalized) continue;
215
- for (const block of normalized.blocks) {
216
- accumulateBlockContent(block, state);
217
- }
218
- }
219
-
220
- // finalText 应该只包含最终的 agent_message
221
- expect(pickFinalReply(state)).toBe("tool_test");
222
- // accumulatedContent 包含工具调用信息
223
- expect(state.accumulatedContent).toContain("Bash");
224
- expect(state.accumulatedContent).toContain("tool_test");
225
- });
226
-
227
- it("with tool: 只映射 agent_message 和 command_execution,不映射元事件", () => {
228
- const lines = readFixture("codex_with_tool.jsonl");
229
- const messages: UnifiedStreamMessage[] = [];
230
- for (const raw of lines) {
231
- const normalized = normalizeCodexMessage(
232
- raw as Parameters<typeof normalizeCodexMessage>[0],
233
- );
234
- if (normalized) messages.push(normalized);
235
- }
236
-
237
- // 应有: tool_use + tool_result + text = 3 条消息
238
- expect(messages.length).toBe(3);
239
- expect(messages[0].blocks[0].type).toBe("tool_use");
240
- expect(messages[1].blocks[0].type).toBe("tool_result");
241
- expect(messages[2].blocks[0].type).toBe("text");
242
- });
243
- });
244
-
245
- // ---------------------------------------------------------------------------
246
- // createCodexAdapter — 工厂函数测试
247
- // ---------------------------------------------------------------------------
248
-
249
- describe("createCodexAdapter", () => {
250
- it("returns adapter with correct displayName and sessionDescPrefix", () => {
251
- const adapter = createCodexAdapter();
252
- expect(adapter.displayName).toBe("Codex");
253
- expect(adapter.sessionDescPrefix).toBe("Codex Session:");
254
- });
255
-
256
- it("closeSession does not throw", async () => {
257
- const adapter = createCodexAdapter();
258
- await expect(adapter.closeSession("any-id")).resolves.toBeUndefined();
259
- });
260
-
261
- it("getSessionInfo: store 中无该 sessionId 时返回 undefined", async () => {
262
- const store = createInMemoryMetaStore();
263
- const adapter = createCodexAdapter({ metaStore: store });
264
- const info = await adapter.getSessionInfo("unknown-sid");
265
- expect(info).toBeUndefined();
266
- });
267
-
268
- it("getSessionInfo: store 有 cwd 时返回 cwd,无 threadId", async () => {
269
- const store = createInMemoryMetaStore({
270
- "sid-known": { cwd: "F:/proj/Foo" },
271
- });
272
- const adapter = createCodexAdapter({ metaStore: store });
273
- const info = await adapter.getSessionInfo("sid-known");
274
- expect(info).toEqual({ sessionId: "sid-known", cwd: "F:/proj/Foo" });
275
- });
276
-
277
- it("getSessionInfo: 有 cwd + threadId 时一并返回", async () => {
278
- const store = createInMemoryMetaStore({
279
- "sid-known": { cwd: "F:/proj/Foo", threadId: "thread-123" },
280
- });
281
- const adapter = createCodexAdapter({ metaStore: store });
282
- const info = await adapter.getSessionInfo("sid-known");
283
- expect(info).toEqual({
284
- sessionId: "sid-known",
285
- cwd: "F:/proj/Foo",
286
- });
287
- });
288
-
289
- it("getSessionInfo: 不同 sessionId 互不影响", async () => {
290
- const store = createInMemoryMetaStore({
291
- "sid-A": { cwd: "/a", threadId: "tA" },
292
- "sid-B": { cwd: "/b" },
293
- });
294
- const adapter = createCodexAdapter({ metaStore: store });
295
- expect(await adapter.getSessionInfo("sid-A")).toEqual({
296
- sessionId: "sid-A",
297
- cwd: "/a",
298
- });
299
- expect(await adapter.getSessionInfo("sid-B")).toEqual({
300
- sessionId: "sid-B",
301
- cwd: "/b",
302
- });
303
- expect(await adapter.getSessionInfo("sid-C")).toBeUndefined();
304
- });
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ normalizeCodexMessage,
7
+ createCodexAdapter,
8
+ type CreateCodexAdapterOptions,
9
+ } from "../adapters/codex-adapter.ts";
10
+ import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
11
+ import {
12
+ type CodexSessionMeta,
13
+ type CodexSessionMetaStore,
14
+ } from "../adapters/codex-session-meta-store.ts";
15
+ import { accumulateBlockContent, pickFinalReply, type AccumulatorState } from "../session.ts";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+
19
+ function readFixture(name: string): unknown[] {
20
+ const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
21
+ return raw
22
+ .split(/\r?\n/)
23
+ .filter(Boolean)
24
+ .map((line) => JSON.parse(line));
25
+ }
26
+
27
+ // 进程内内存版 meta store
28
+ function createInMemoryMetaStore(
29
+ initial: Record<string, CodexSessionMeta> = {},
30
+ ): CodexSessionMetaStore & { snapshot(): Record<string, CodexSessionMeta> } {
31
+ const map = new Map<string, CodexSessionMeta>(Object.entries(initial));
32
+ return {
33
+ async get(sid) {
34
+ return map.get(sid);
35
+ },
36
+ async set(sid, partial) {
37
+ const existing = map.get(sid) ?? { cwd: "" };
38
+ const merged: CodexSessionMeta = { ...existing, ...partial };
39
+ if (typeof merged.cwd !== "string" || merged.cwd.length === 0) return;
40
+ map.set(sid, merged);
41
+ },
42
+ async setThreadId(sid, threadId) {
43
+ const existing = map.get(sid);
44
+ if (existing) {
45
+ map.set(sid, { ...existing, threadId });
46
+ }
47
+ },
48
+ snapshot() {
49
+ return Object.fromEntries(map);
50
+ },
51
+ };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // normalizeCodexMessage — 核心映射逻辑测试(纯函数)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe("normalizeCodexMessage", () => {
59
+ it("normalizes agent_message into assistant text block", () => {
60
+ const result = normalizeCodexMessage({
61
+ type: "item.completed",
62
+ item: { id: "item_0", type: "agent_message", text: "hello" },
63
+ });
64
+ expect(result).not.toBeNull();
65
+ expect(result!.type).toBe("assistant");
66
+ expect(result!.blocks).toEqual([{ type: "text", text: "hello" }]);
67
+ });
68
+
69
+ it("normalizes command_execution start into tool_use block", () => {
70
+ const result = normalizeCodexMessage({
71
+ type: "item.started",
72
+ item: {
73
+ id: "item_0",
74
+ type: "command_execution",
75
+ command: "powershell.exe -Command ls",
76
+ status: "in_progress",
77
+ },
78
+ });
79
+ expect(result).not.toBeNull();
80
+ expect(result!.type).toBe("assistant");
81
+ expect(result!.blocks).toEqual([
82
+ { type: "tool_use", name: "Bash", input: { command: "powershell.exe -Command ls" } },
83
+ ]);
84
+ });
85
+
86
+ it("normalizes command_execution completion as tool_result (success)", () => {
87
+ const result = normalizeCodexMessage({
88
+ type: "item.completed",
89
+ item: {
90
+ id: "item_0",
91
+ type: "command_execution",
92
+ command: "ls",
93
+ aggregated_output: "file1\nfile2\n",
94
+ exit_code: 0,
95
+ status: "completed",
96
+ },
97
+ });
98
+ expect(result).not.toBeNull();
99
+ expect(result!.blocks).toEqual([
100
+ {
101
+ type: "tool_result",
102
+ tool_use_id: "item_0",
103
+ content: "file1\nfile2\n",
104
+ is_error: undefined,
105
+ },
106
+ ]);
107
+ });
108
+
109
+ it("normalizes command_execution completion as tool_result (error)", () => {
110
+ const result = normalizeCodexMessage({
111
+ type: "item.completed",
112
+ item: {
113
+ id: "item_err",
114
+ type: "command_execution",
115
+ command: "nonexistent",
116
+ aggregated_output: "command not found",
117
+ exit_code: 127,
118
+ status: "completed",
119
+ },
120
+ });
121
+ expect(result).not.toBeNull();
122
+ expect(result!.blocks).toEqual([
123
+ {
124
+ type: "tool_result",
125
+ tool_use_id: "item_err",
126
+ content: "command not found",
127
+ is_error: true,
128
+ },
129
+ ]);
130
+ });
131
+
132
+ it("returns null for thread.started events", () => {
133
+ expect(
134
+ normalizeCodexMessage({
135
+ type: "thread.started",
136
+ thread_id: "abc-123",
137
+ }),
138
+ ).toBeNull();
139
+ });
140
+
141
+ it("returns null for turn.started events", () => {
142
+ expect(normalizeCodexMessage({ type: "turn.started" })).toBeNull();
143
+ });
144
+
145
+ it("returns null for turn.completed events", () => {
146
+ expect(
147
+ normalizeCodexMessage({
148
+ type: "turn.completed",
149
+ usage: { input_tokens: 100, output_tokens: 50 },
150
+ }),
151
+ ).toBeNull();
152
+ });
153
+
154
+ it("returns null for unknown event types", () => {
155
+ expect(normalizeCodexMessage({ type: "unknown" })).toBeNull();
156
+ expect(normalizeCodexMessage({} as Parameters<typeof normalizeCodexMessage>[0])).toBeNull();
157
+ });
158
+
159
+ it("returns null for agent_message with empty text", () => {
160
+ const result = normalizeCodexMessage({
161
+ type: "item.completed",
162
+ item: { id: "item_0", type: "agent_message", text: "" },
163
+ });
164
+ expect(result).toBeNull();
165
+ });
166
+
167
+ it("returns null for command_execution start without command text", () => {
168
+ const result = normalizeCodexMessage({
169
+ type: "item.started",
170
+ item: { id: "item_0", type: "command_execution", status: "in_progress" },
171
+ });
172
+ expect(result).toBeNull();
173
+ });
174
+ });
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Fixture 端到端测试
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe("Codex stream fixtures", () => {
181
+ it("simple text: 流结束后 pickFinalReply 返回正确文本", () => {
182
+ const lines = readFixture("codex_simple_text.jsonl");
183
+ const state: AccumulatorState = {
184
+ accumulatedContent: "",
185
+ finalText: "",
186
+ finalCompleteText: "",
187
+ chunkCount: 0,
188
+ };
189
+ for (const raw of lines) {
190
+ const normalized = normalizeCodexMessage(
191
+ raw as Parameters<typeof normalizeCodexMessage>[0],
192
+ );
193
+ if (!normalized) continue;
194
+ for (const block of normalized.blocks) {
195
+ accumulateBlockContent(block, state);
196
+ }
197
+ }
198
+
199
+ expect(pickFinalReply(state)).toBe("hello");
200
+ });
201
+
202
+ it("with tool: 流结束后 pickFinalReply 返回最终文本(不含工具输出在 finalText 中)", () => {
203
+ const lines = readFixture("codex_with_tool.jsonl");
204
+ const state: AccumulatorState = {
205
+ accumulatedContent: "",
206
+ finalText: "",
207
+ finalCompleteText: "",
208
+ chunkCount: 0,
209
+ };
210
+ for (const raw of lines) {
211
+ const normalized = normalizeCodexMessage(
212
+ raw as Parameters<typeof normalizeCodexMessage>[0],
213
+ );
214
+ if (!normalized) continue;
215
+ for (const block of normalized.blocks) {
216
+ accumulateBlockContent(block, state);
217
+ }
218
+ }
219
+
220
+ // finalText 应该只包含最终的 agent_message
221
+ expect(pickFinalReply(state)).toBe("tool_test");
222
+ // accumulatedContent 包含工具调用信息
223
+ expect(state.accumulatedContent).toContain("Bash");
224
+ expect(state.accumulatedContent).toContain("tool_test");
225
+ });
226
+
227
+ it("with tool: 只映射 agent_message 和 command_execution,不映射元事件", () => {
228
+ const lines = readFixture("codex_with_tool.jsonl");
229
+ const messages: UnifiedStreamMessage[] = [];
230
+ for (const raw of lines) {
231
+ const normalized = normalizeCodexMessage(
232
+ raw as Parameters<typeof normalizeCodexMessage>[0],
233
+ );
234
+ if (normalized) messages.push(normalized);
235
+ }
236
+
237
+ // 应有: tool_use + tool_result + text = 3 条消息
238
+ expect(messages.length).toBe(3);
239
+ expect(messages[0].blocks[0].type).toBe("tool_use");
240
+ expect(messages[1].blocks[0].type).toBe("tool_result");
241
+ expect(messages[2].blocks[0].type).toBe("text");
242
+ });
243
+ });
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // createCodexAdapter — 工厂函数测试
247
+ // ---------------------------------------------------------------------------
248
+
249
+ describe("createCodexAdapter", () => {
250
+ it("returns adapter with correct displayName and sessionDescPrefix", () => {
251
+ const adapter = createCodexAdapter();
252
+ expect(adapter.displayName).toBe("Codex");
253
+ expect(adapter.sessionDescPrefix).toBe("Codex Session:");
254
+ });
255
+
256
+ it("closeSession does not throw", async () => {
257
+ const adapter = createCodexAdapter();
258
+ await expect(adapter.closeSession("any-id")).resolves.toBeUndefined();
259
+ });
260
+
261
+ it("getSessionInfo: store 中无该 sessionId 时返回 undefined", async () => {
262
+ const store = createInMemoryMetaStore();
263
+ const adapter = createCodexAdapter({ metaStore: store });
264
+ const info = await adapter.getSessionInfo("unknown-sid");
265
+ expect(info).toBeUndefined();
266
+ });
267
+
268
+ it("getSessionInfo: store 有 cwd 时返回 cwd,无 threadId", async () => {
269
+ const store = createInMemoryMetaStore({
270
+ "sid-known": { cwd: "F:/proj/Foo" },
271
+ });
272
+ const adapter = createCodexAdapter({ metaStore: store });
273
+ const info = await adapter.getSessionInfo("sid-known");
274
+ expect(info).toEqual({ sessionId: "sid-known", cwd: "F:/proj/Foo" });
275
+ });
276
+
277
+ it("getSessionInfo: 有 cwd + threadId 时一并返回", async () => {
278
+ const store = createInMemoryMetaStore({
279
+ "sid-known": { cwd: "F:/proj/Foo", threadId: "thread-123" },
280
+ });
281
+ const adapter = createCodexAdapter({ metaStore: store });
282
+ const info = await adapter.getSessionInfo("sid-known");
283
+ expect(info).toEqual({
284
+ sessionId: "sid-known",
285
+ cwd: "F:/proj/Foo",
286
+ });
287
+ });
288
+
289
+ it("getSessionInfo: 不同 sessionId 互不影响", async () => {
290
+ const store = createInMemoryMetaStore({
291
+ "sid-A": { cwd: "/a", threadId: "tA" },
292
+ "sid-B": { cwd: "/b" },
293
+ });
294
+ const adapter = createCodexAdapter({ metaStore: store });
295
+ expect(await adapter.getSessionInfo("sid-A")).toEqual({
296
+ sessionId: "sid-A",
297
+ cwd: "/a",
298
+ });
299
+ expect(await adapter.getSessionInfo("sid-B")).toEqual({
300
+ sessionId: "sid-B",
301
+ cwd: "/b",
302
+ });
303
+ expect(await adapter.getSessionInfo("sid-C")).toBeUndefined();
304
+ });
305
305
  });