@sudocode-ai/claude-code-acp 0.12.10 → 0.13.1
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 +3 -9
- package/dist/acp-agent.d.ts +194 -0
- package/dist/acp-agent.d.ts.map +1 -0
- package/dist/acp-agent.js +48 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib.d.ts +7 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/mcp-server.d.ts +21 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +214 -158
- package/dist/settings.d.ts +123 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/tools.d.ts +50 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +3 -1
- package/dist/utils.d.ts +32 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +22 -12
- package/dist/tests/acp-agent-fork.test.js +0 -338
- package/dist/tests/acp-agent.test.js +0 -753
- package/dist/tests/extract-lines.test.js +0 -79
- package/dist/tests/fork-session.test.js +0 -83
- package/dist/tests/replace-and-calculate-location.test.js +0 -266
- package/dist/tests/settings.test.js +0 -462
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import { WritableStream, ReadableStream } from "node:stream/web";
|
|
3
|
+
import { Logger } from "./acp-agent.js";
|
|
4
|
+
import { ClaudeCodeSettings } from "./settings.js";
|
|
5
|
+
export declare class Pushable<T> implements AsyncIterable<T> {
|
|
6
|
+
private queue;
|
|
7
|
+
private resolvers;
|
|
8
|
+
private done;
|
|
9
|
+
push(item: T): void;
|
|
10
|
+
end(): void;
|
|
11
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
12
|
+
}
|
|
13
|
+
export declare function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array>;
|
|
14
|
+
export declare function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array>;
|
|
15
|
+
export declare function unreachable(value: never, logger?: Logger): void;
|
|
16
|
+
export declare function sleep(time: number): Promise<void>;
|
|
17
|
+
export declare function loadManagedSettings(): ClaudeCodeSettings | null;
|
|
18
|
+
export declare function applyEnvironmentSettings(settings: ClaudeCodeSettings): void;
|
|
19
|
+
export interface ExtractLinesResult {
|
|
20
|
+
content: string;
|
|
21
|
+
wasLimited: boolean;
|
|
22
|
+
linesRead: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts lines from file content with byte limit enforcement.
|
|
26
|
+
*
|
|
27
|
+
* @param fullContent - The complete file content
|
|
28
|
+
* @param maxContentLength - Maximum number of UTF-16 Code Units to return
|
|
29
|
+
* @returns Object containing extracted content and metadata
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractLinesWithByteLimit(fullContent: string, maxContentLength: number): ExtractLinesResult;
|
|
32
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,kBAAkB,EAA0B,MAAM,eAAe,CAAC;AAG3E,qBAAa,QAAQ,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,IAAI,CAAS;IAErB,IAAI,CAAC,IAAI,EAAE,CAAC;IASZ,GAAG;IAQH,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;CAgB3C;AAGD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAclF;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,QAAQ,GAAG,cAAc,CAAC,UAAU,CAAC,CAUlF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,GAAE,MAAgB,QAQjE;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjD;AAED,wBAAgB,mBAAmB,IAAI,kBAAkB,GAAG,IAAI,CAM/D;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAM3E;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,kBAAkB,CA8CpB"}
|
package/package.json
CHANGED
|
@@ -3,15 +3,24 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.13.1",
|
|
7
7
|
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
|
8
8
|
"main": "dist/lib.js",
|
|
9
|
+
"types": "dist/lib.d.ts",
|
|
9
10
|
"bin": {
|
|
10
11
|
"claude-code-acp": "./dist/index.js"
|
|
11
12
|
},
|
|
12
13
|
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/lib.d.ts",
|
|
17
|
+
"import": "./dist/lib.js"
|
|
18
|
+
},
|
|
19
|
+
"./*": "./*"
|
|
20
|
+
},
|
|
13
21
|
"files": [
|
|
14
22
|
"dist/",
|
|
23
|
+
"!dist/tests/",
|
|
15
24
|
"README.md",
|
|
16
25
|
"LICENSE",
|
|
17
26
|
"package.json"
|
|
@@ -25,6 +34,7 @@
|
|
|
25
34
|
"format": "prettier --write .",
|
|
26
35
|
"format:check": "prettier --check .",
|
|
27
36
|
"check": "npm run lint && npm run format:check",
|
|
37
|
+
"sync-upstream": "git merge upstream/main",
|
|
28
38
|
"test": "vitest",
|
|
29
39
|
"test:integration": "RUN_INTEGRATION_TESTS=true vitest run",
|
|
30
40
|
"test:run": "vitest run",
|
|
@@ -56,23 +66,23 @@
|
|
|
56
66
|
"author": "Sudocode AI",
|
|
57
67
|
"license": "Apache-2.0",
|
|
58
68
|
"dependencies": {
|
|
59
|
-
"@agentclientprotocol/sdk": "0.
|
|
60
|
-
"@anthropic-ai/claude-agent-sdk": "0.
|
|
61
|
-
"@modelcontextprotocol/sdk": "1.25.
|
|
62
|
-
"diff": "8.0.
|
|
63
|
-
"minimatch": "
|
|
69
|
+
"@agentclientprotocol/sdk": "0.13.0",
|
|
70
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.7",
|
|
71
|
+
"@modelcontextprotocol/sdk": "1.25.2",
|
|
72
|
+
"diff": "8.0.3",
|
|
73
|
+
"minimatch": "10.1.1"
|
|
64
74
|
},
|
|
65
75
|
"devDependencies": {
|
|
66
76
|
"@anthropic-ai/sdk": "0.71.2",
|
|
67
|
-
"@types/node": "25.0.
|
|
68
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
69
|
-
"@typescript-eslint/parser": "8.
|
|
77
|
+
"@types/node": "25.0.8",
|
|
78
|
+
"@typescript-eslint/eslint-plugin": "8.53.0",
|
|
79
|
+
"@typescript-eslint/parser": "8.53.0",
|
|
70
80
|
"eslint": "9.39.2",
|
|
71
81
|
"eslint-config-prettier": "10.1.8",
|
|
72
|
-
"globals": "
|
|
73
|
-
"prettier": "3.
|
|
82
|
+
"globals": "17.0.0",
|
|
83
|
+
"prettier": "3.8.0",
|
|
74
84
|
"ts-node": "10.9.2",
|
|
75
85
|
"typescript": "5.9.3",
|
|
76
|
-
"vitest": "4.0.
|
|
86
|
+
"vitest": "4.0.17"
|
|
77
87
|
}
|
|
78
88
|
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for fork-related helper methods in ClaudeAcpAgent.
|
|
3
|
-
*
|
|
4
|
-
* Since the methods are private, we test them by:
|
|
5
|
-
* 1. Creating a test subclass that exposes the private methods
|
|
6
|
-
* 2. Testing the file manipulation logic directly
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
9
|
-
import { ClaudeAcpAgent } from "../acp-agent.js";
|
|
10
|
-
import * as fs from "node:fs";
|
|
11
|
-
import * as path from "node:path";
|
|
12
|
-
import * as os from "node:os";
|
|
13
|
-
// Create a test subclass that exposes private methods for testing
|
|
14
|
-
class TestableClaudeAcpAgent extends ClaudeAcpAgent {
|
|
15
|
-
// Expose private methods for testing
|
|
16
|
-
testExtractInternalSessionId(filePath) {
|
|
17
|
-
return this.extractInternalSessionId(filePath);
|
|
18
|
-
}
|
|
19
|
-
testPromoteToFullSession(filePath) {
|
|
20
|
-
return this.promoteToFullSession(filePath);
|
|
21
|
-
}
|
|
22
|
-
testUpdateSessionIdInFile(filePath, newSessionId) {
|
|
23
|
-
return this.updateSessionIdInFile(filePath, newSessionId);
|
|
24
|
-
}
|
|
25
|
-
testGetSessionDirPath(cwd) {
|
|
26
|
-
return this.getSessionDirPath(cwd);
|
|
27
|
-
}
|
|
28
|
-
testGetSessionFilePath(sessionId, cwd) {
|
|
29
|
-
return this.getSessionFilePath(sessionId, cwd);
|
|
30
|
-
}
|
|
31
|
-
async testDiscoverCliSessionId(sessionDir, beforeFiles, fallbackId, timeout) {
|
|
32
|
-
return this.discoverCliSessionId(sessionDir, beforeFiles, fallbackId, timeout);
|
|
33
|
-
}
|
|
34
|
-
async testWaitForSessionFile(filePath, timeout) {
|
|
35
|
-
return this.waitForSessionFile(filePath, timeout);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
describe("ClaudeAcpAgent fork helpers", () => {
|
|
39
|
-
let tempDir;
|
|
40
|
-
let agent;
|
|
41
|
-
const mockLogger = {
|
|
42
|
-
log: vi.fn(),
|
|
43
|
-
error: vi.fn(),
|
|
44
|
-
};
|
|
45
|
-
// Create a minimal mock client
|
|
46
|
-
const mockClient = {
|
|
47
|
-
sessionUpdate: vi.fn(),
|
|
48
|
-
readTextFile: vi.fn(),
|
|
49
|
-
writeTextFile: vi.fn(),
|
|
50
|
-
requestPermission: vi.fn(),
|
|
51
|
-
};
|
|
52
|
-
beforeEach(async () => {
|
|
53
|
-
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "acp-agent-fork-test-"));
|
|
54
|
-
agent = new TestableClaudeAcpAgent(mockClient, mockLogger);
|
|
55
|
-
vi.clearAllMocks();
|
|
56
|
-
});
|
|
57
|
-
afterEach(async () => {
|
|
58
|
-
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
59
|
-
});
|
|
60
|
-
describe("extractInternalSessionId", () => {
|
|
61
|
-
it("should extract UUID sessionId from valid JSONL file", async () => {
|
|
62
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
63
|
-
const sessionId = "12345678-1234-1234-1234-123456789abc";
|
|
64
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ sessionId, type: "init" }) + "\n");
|
|
65
|
-
const result = agent.testExtractInternalSessionId(filePath);
|
|
66
|
-
expect(result).toBe(sessionId);
|
|
67
|
-
});
|
|
68
|
-
it("should return null for non-existent file", () => {
|
|
69
|
-
const result = agent.testExtractInternalSessionId(path.join(tempDir, "nonexistent.jsonl"));
|
|
70
|
-
expect(result).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
it("should return null for file without sessionId", async () => {
|
|
73
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
74
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ type: "init" }) + "\n");
|
|
75
|
-
const result = agent.testExtractInternalSessionId(filePath);
|
|
76
|
-
expect(result).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
it("should return null for non-UUID sessionId", async () => {
|
|
79
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
80
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ sessionId: "not-a-uuid", type: "init" }) + "\n");
|
|
81
|
-
const result = agent.testExtractInternalSessionId(filePath);
|
|
82
|
-
expect(result).toBeNull();
|
|
83
|
-
});
|
|
84
|
-
it("should handle empty file", async () => {
|
|
85
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
86
|
-
await fs.promises.writeFile(filePath, "");
|
|
87
|
-
const result = agent.testExtractInternalSessionId(filePath);
|
|
88
|
-
expect(result).toBeNull();
|
|
89
|
-
});
|
|
90
|
-
it("should find sessionId from first non-empty line", async () => {
|
|
91
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
92
|
-
const sessionId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
93
|
-
await fs.promises.writeFile(filePath, "\n\n" + JSON.stringify({ sessionId, type: "init" }) + "\n");
|
|
94
|
-
const result = agent.testExtractInternalSessionId(filePath);
|
|
95
|
-
expect(result).toBe(sessionId);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe("promoteToFullSession", () => {
|
|
99
|
-
it("should change isSidechain from true to false", async () => {
|
|
100
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
101
|
-
const lines = [
|
|
102
|
-
JSON.stringify({ sessionId: "test-id", isSidechain: true }),
|
|
103
|
-
JSON.stringify({ type: "message", content: "hello" }),
|
|
104
|
-
];
|
|
105
|
-
await fs.promises.writeFile(filePath, lines.join("\n"));
|
|
106
|
-
const result = agent.testPromoteToFullSession(filePath);
|
|
107
|
-
expect(result).toBe(true);
|
|
108
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
109
|
-
const parsedLines = content.split("\n").map(line => JSON.parse(line));
|
|
110
|
-
expect(parsedLines[0].isSidechain).toBe(false);
|
|
111
|
-
expect(parsedLines[1].content).toBe("hello");
|
|
112
|
-
});
|
|
113
|
-
it("should return false for non-existent file", () => {
|
|
114
|
-
const result = agent.testPromoteToFullSession(path.join(tempDir, "nonexistent.jsonl"));
|
|
115
|
-
expect(result).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
it("should preserve other fields when promoting", async () => {
|
|
118
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
119
|
-
const original = {
|
|
120
|
-
sessionId: "test-id",
|
|
121
|
-
isSidechain: true,
|
|
122
|
-
cwd: "/some/path",
|
|
123
|
-
model: "claude-3",
|
|
124
|
-
};
|
|
125
|
-
await fs.promises.writeFile(filePath, JSON.stringify(original));
|
|
126
|
-
agent.testPromoteToFullSession(filePath);
|
|
127
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
128
|
-
const parsed = JSON.parse(content);
|
|
129
|
-
expect(parsed.sessionId).toBe("test-id");
|
|
130
|
-
expect(parsed.isSidechain).toBe(false);
|
|
131
|
-
expect(parsed.cwd).toBe("/some/path");
|
|
132
|
-
expect(parsed.model).toBe("claude-3");
|
|
133
|
-
});
|
|
134
|
-
it("should handle file without isSidechain field", async () => {
|
|
135
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
136
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ sessionId: "test-id", type: "init" }));
|
|
137
|
-
const result = agent.testPromoteToFullSession(filePath);
|
|
138
|
-
expect(result).toBe(true);
|
|
139
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
140
|
-
const parsed = JSON.parse(content);
|
|
141
|
-
expect(parsed.isSidechain).toBeUndefined();
|
|
142
|
-
});
|
|
143
|
-
it("should log success message", async () => {
|
|
144
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
145
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ sessionId: "test-id", isSidechain: true }));
|
|
146
|
-
agent.testPromoteToFullSession(filePath);
|
|
147
|
-
expect(mockLogger.log).toHaveBeenCalledWith(expect.stringContaining("Promoted sidechain to full session"));
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
describe("updateSessionIdInFile", () => {
|
|
151
|
-
it("should update sessionId in all lines", async () => {
|
|
152
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
153
|
-
const oldId = "old-session-id";
|
|
154
|
-
const newId = "new-session-id";
|
|
155
|
-
const lines = [
|
|
156
|
-
JSON.stringify({ sessionId: oldId, type: "init" }),
|
|
157
|
-
JSON.stringify({ sessionId: oldId, type: "message", content: "hello" }),
|
|
158
|
-
JSON.stringify({ sessionId: oldId, type: "tool_use", name: "Read" }),
|
|
159
|
-
];
|
|
160
|
-
await fs.promises.writeFile(filePath, lines.join("\n"));
|
|
161
|
-
const result = agent.testUpdateSessionIdInFile(filePath, newId);
|
|
162
|
-
expect(result).toBe(true);
|
|
163
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
164
|
-
const parsedLines = content.split("\n").map(line => JSON.parse(line));
|
|
165
|
-
for (const line of parsedLines) {
|
|
166
|
-
expect(line.sessionId).toBe(newId);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
it("should return false for non-existent file", () => {
|
|
170
|
-
const result = agent.testUpdateSessionIdInFile(path.join(tempDir, "nonexistent.jsonl"), "new-id");
|
|
171
|
-
expect(result).toBe(false);
|
|
172
|
-
});
|
|
173
|
-
it("should preserve lines without sessionId", async () => {
|
|
174
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
175
|
-
const lines = [
|
|
176
|
-
JSON.stringify({ sessionId: "old-id", type: "init" }),
|
|
177
|
-
JSON.stringify({ type: "comment", text: "no sessionId here" }),
|
|
178
|
-
];
|
|
179
|
-
await fs.promises.writeFile(filePath, lines.join("\n"));
|
|
180
|
-
agent.testUpdateSessionIdInFile(filePath, "new-id");
|
|
181
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
182
|
-
const parsedLines = content.split("\n").map(line => JSON.parse(line));
|
|
183
|
-
expect(parsedLines[0].sessionId).toBe("new-id");
|
|
184
|
-
expect(parsedLines[1].sessionId).toBeUndefined();
|
|
185
|
-
expect(parsedLines[1].text).toBe("no sessionId here");
|
|
186
|
-
});
|
|
187
|
-
it("should handle empty lines gracefully", async () => {
|
|
188
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
189
|
-
const content = JSON.stringify({ sessionId: "old-id" }) + "\n\n" + JSON.stringify({ sessionId: "old-id" });
|
|
190
|
-
await fs.promises.writeFile(filePath, content);
|
|
191
|
-
const result = agent.testUpdateSessionIdInFile(filePath, "new-id");
|
|
192
|
-
expect(result).toBe(true);
|
|
193
|
-
const newContent = await fs.promises.readFile(filePath, "utf-8");
|
|
194
|
-
const lines = newContent.split("\n");
|
|
195
|
-
expect(JSON.parse(lines[0]).sessionId).toBe("new-id");
|
|
196
|
-
expect(lines[1]).toBe(""); // Empty line preserved
|
|
197
|
-
expect(JSON.parse(lines[2]).sessionId).toBe("new-id");
|
|
198
|
-
});
|
|
199
|
-
it("should log success message", async () => {
|
|
200
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
201
|
-
await fs.promises.writeFile(filePath, JSON.stringify({ sessionId: "old-id" }));
|
|
202
|
-
agent.testUpdateSessionIdInFile(filePath, "new-id");
|
|
203
|
-
expect(mockLogger.log).toHaveBeenCalledWith(expect.stringContaining("Updated session ID in file"));
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
describe("getSessionDirPath", () => {
|
|
207
|
-
it("should compute correct path with cwd hash", () => {
|
|
208
|
-
const result = agent.testGetSessionDirPath("/private/tmp");
|
|
209
|
-
const homeDir = os.homedir();
|
|
210
|
-
expect(result).toBe(`${homeDir}/.claude/projects/-private-tmp`);
|
|
211
|
-
});
|
|
212
|
-
it("should replace both / and _ with -", () => {
|
|
213
|
-
// Use tempDir which exists
|
|
214
|
-
const testDir = path.join(tempDir, "my_project");
|
|
215
|
-
fs.mkdirSync(testDir, { recursive: true });
|
|
216
|
-
const result = agent.testGetSessionDirPath(testDir);
|
|
217
|
-
const realPath = fs.realpathSync(testDir);
|
|
218
|
-
const expectedHash = realPath.replace(/[/_]/g, "-");
|
|
219
|
-
expect(result).toBe(`${os.homedir()}/.claude/projects/${expectedHash}`);
|
|
220
|
-
});
|
|
221
|
-
it("should resolve symlinks", () => {
|
|
222
|
-
// /var on macOS is a symlink to /private/var
|
|
223
|
-
const result = agent.testGetSessionDirPath("/var/tmp");
|
|
224
|
-
const homeDir = os.homedir();
|
|
225
|
-
// Should use the resolved path
|
|
226
|
-
expect(result).toBe(`${homeDir}/.claude/projects/-private-var-tmp`);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("getSessionFilePath", () => {
|
|
230
|
-
it("should append sessionId.jsonl to session dir", () => {
|
|
231
|
-
const result = agent.testGetSessionFilePath("my-session-id", "/private/tmp");
|
|
232
|
-
const homeDir = os.homedir();
|
|
233
|
-
expect(result).toBe(`${homeDir}/.claude/projects/-private-tmp/my-session-id.jsonl`);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
describe("waitForSessionFile", () => {
|
|
237
|
-
it("should return true immediately if file exists", async () => {
|
|
238
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
239
|
-
await fs.promises.writeFile(filePath, "{}");
|
|
240
|
-
const start = Date.now();
|
|
241
|
-
const result = await agent.testWaitForSessionFile(filePath, 1000);
|
|
242
|
-
const elapsed = Date.now() - start;
|
|
243
|
-
expect(result).toBe(true);
|
|
244
|
-
expect(elapsed).toBeLessThan(200);
|
|
245
|
-
});
|
|
246
|
-
it("should return true when file appears before timeout", async () => {
|
|
247
|
-
const filePath = path.join(tempDir, "session.jsonl");
|
|
248
|
-
// Create file after 200ms
|
|
249
|
-
setTimeout(async () => {
|
|
250
|
-
await fs.promises.writeFile(filePath, "{}");
|
|
251
|
-
}, 200);
|
|
252
|
-
const start = Date.now();
|
|
253
|
-
const result = await agent.testWaitForSessionFile(filePath, 2000);
|
|
254
|
-
const elapsed = Date.now() - start;
|
|
255
|
-
expect(result).toBe(true);
|
|
256
|
-
expect(elapsed).toBeGreaterThanOrEqual(200);
|
|
257
|
-
expect(elapsed).toBeLessThan(2000);
|
|
258
|
-
});
|
|
259
|
-
it("should return false when timeout expires", async () => {
|
|
260
|
-
const filePath = path.join(tempDir, "nonexistent.jsonl");
|
|
261
|
-
const start = Date.now();
|
|
262
|
-
const result = await agent.testWaitForSessionFile(filePath, 300);
|
|
263
|
-
const elapsed = Date.now() - start;
|
|
264
|
-
expect(result).toBe(false);
|
|
265
|
-
expect(elapsed).toBeGreaterThanOrEqual(300);
|
|
266
|
-
expect(elapsed).toBeLessThan(500);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
describe("discoverCliSessionId", () => {
|
|
270
|
-
it("should find new agent-xxx file", async () => {
|
|
271
|
-
const sessionDir = path.join(tempDir, "sessions");
|
|
272
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
273
|
-
const beforeFiles = new Set();
|
|
274
|
-
// Create an agent-xxx file
|
|
275
|
-
const agentFile = "agent-abc123.jsonl";
|
|
276
|
-
await fs.promises.writeFile(path.join(sessionDir, agentFile), "{}");
|
|
277
|
-
const result = await agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback", 1000);
|
|
278
|
-
expect(result).toBe("agent-abc123");
|
|
279
|
-
});
|
|
280
|
-
it("should ignore non-agent files", async () => {
|
|
281
|
-
const sessionDir = path.join(tempDir, "sessions");
|
|
282
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
283
|
-
const beforeFiles = new Set();
|
|
284
|
-
// Create a UUID-named file (not agent-xxx)
|
|
285
|
-
const uuidFile = "12345678-1234-1234-1234-123456789abc.jsonl";
|
|
286
|
-
await fs.promises.writeFile(path.join(sessionDir, uuidFile), "{}");
|
|
287
|
-
const result = await agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback", 500);
|
|
288
|
-
expect(result).toBe("fallback");
|
|
289
|
-
});
|
|
290
|
-
it("should return fallback when no new files found", async () => {
|
|
291
|
-
const sessionDir = path.join(tempDir, "sessions");
|
|
292
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
293
|
-
const beforeFiles = new Set();
|
|
294
|
-
const result = await agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback-id", 300);
|
|
295
|
-
expect(result).toBe("fallback-id");
|
|
296
|
-
});
|
|
297
|
-
it("should ignore files that existed before", async () => {
|
|
298
|
-
const sessionDir = path.join(tempDir, "sessions");
|
|
299
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
300
|
-
// Create file before
|
|
301
|
-
const existingFile = "agent-existing.jsonl";
|
|
302
|
-
await fs.promises.writeFile(path.join(sessionDir, existingFile), "{}");
|
|
303
|
-
const beforeFiles = new Set([existingFile]);
|
|
304
|
-
const result = await agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback", 300);
|
|
305
|
-
expect(result).toBe("fallback");
|
|
306
|
-
});
|
|
307
|
-
it("should return newest file when multiple agent files appear", async () => {
|
|
308
|
-
const sessionDir = path.join(tempDir, "sessions");
|
|
309
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
310
|
-
const beforeFiles = new Set();
|
|
311
|
-
// Create first file - use hex chars to match agent-[a-f0-9]+ pattern
|
|
312
|
-
const firstPath = path.join(sessionDir, "agent-aaa111.jsonl");
|
|
313
|
-
await fs.promises.writeFile(firstPath, "{}");
|
|
314
|
-
// Wait a bit to ensure different mtime, then create second file
|
|
315
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
|
-
const secondPath = path.join(sessionDir, "agent-bbb222.jsonl");
|
|
317
|
-
await fs.promises.writeFile(secondPath, "{}");
|
|
318
|
-
// Verify files exist before calling discover
|
|
319
|
-
expect(fs.existsSync(firstPath)).toBe(true);
|
|
320
|
-
expect(fs.existsSync(secondPath)).toBe(true);
|
|
321
|
-
const result = await agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback", 1000);
|
|
322
|
-
expect(result).toBe("agent-bbb222");
|
|
323
|
-
});
|
|
324
|
-
it("should handle non-existent session directory initially", async () => {
|
|
325
|
-
const sessionDir = path.join(tempDir, "nonexistent-dir");
|
|
326
|
-
const beforeFiles = new Set();
|
|
327
|
-
// Start the discovery in parallel with file creation
|
|
328
|
-
const discoverPromise = agent.testDiscoverCliSessionId(sessionDir, beforeFiles, "fallback", 2000);
|
|
329
|
-
// Create directory and file after 200ms (within the 2000ms timeout)
|
|
330
|
-
// Use hex chars to match agent-[a-f0-9]+ pattern
|
|
331
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
332
|
-
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
333
|
-
await fs.promises.writeFile(path.join(sessionDir, "agent-ccc333.jsonl"), "{}");
|
|
334
|
-
const result = await discoverPromise;
|
|
335
|
-
expect(result).toBe("agent-ccc333");
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|