-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathsession_fs_sqlite.e2e.test.ts
More file actions
252 lines (227 loc) · 10.2 KB
/
session_fs_sqlite.e2e.test.ts
File metadata and controls
252 lines (227 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { DatabaseSync } from "node:sqlite";
import { MemoryProvider, VirtualProvider } from "@platformatic/vfs";
import { mkdtempSync, realpathSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, it } from "vitest";
import type { SessionFsReaddirWithTypesEntry } from "../../src/generated/rpc.js";
import {
approveAll,
CopilotSession,
SessionEvent,
type SessionFsConfig,
type SessionFsProvider,
type SessionFsFileInfo,
type SessionFsSqliteQueryResult,
type SessionFsSqliteQueryType,
} from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
const sessionStatePath =
process.platform === "win32"
? "/session-state"
: join(
realpathSync(mkdtempSync(join(tmpdir(), "copilot-sqlite-state-"))),
"session-state"
).replace(/\\/g, "/");
const sessionFsConfig: SessionFsConfig = {
initialCwd: "/",
sessionStatePath,
conventions: "posix",
capabilities: { sqlite: true },
};
describe("Session Fs SQLite", async () => {
const provider = new MemoryProvider();
/** Track which queries were received, per session */
const sqliteCalls: { sessionId: string; queryType: string; query: string }[] = [];
/** Per-session SQLite databases, keyed by session ID.
* Stored at describe scope so the database survives if the CLI
* re-creates the handler (e.g., on reconnect). */
const sessionDbs = new Map<string, DatabaseSync>();
const createSessionFsProvider = (session: CopilotSession) =>
createTestSessionFsHandlerWithSqlite(session, provider, sqliteCalls, sessionDbs);
// Helpers to build session-namespaced paths for direct provider assertions
const p = (sessionId: string, path: string) =>
`/${sessionId}${path.startsWith("/") ? path : "/" + path}`;
const { copilotClient: client } = await createSdkTestContext({
copilotClientOptions: { sessionFs: sessionFsConfig },
});
it(
"should route SQL queries through the sessionFs sqlite handler",
{ timeout: 60000 },
async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
createSessionFsProvider,
});
// Ask the agent to create a table and insert data using the SQL tool
await session.sendAndWait({
prompt:
'Use the sql tool to create a table called "items" with columns id (TEXT PRIMARY KEY) and name (TEXT). ' +
'Then insert a row with id "a1" and name "Widget".',
});
// Verify the sqlite handler was called with the right operations
const sessionCalls = sqliteCalls.filter((c) => c.sessionId === session.sessionId);
expect(sessionCalls.length).toBeGreaterThan(0);
expect(sessionCalls.some((c) => c.query.toUpperCase().includes("CREATE TABLE"))).toBe(
true
);
expect(sessionCalls.some((c) => c.query.toUpperCase().includes("INSERT"))).toBe(true);
// Verify queryType is set correctly
expect(sessionCalls.some((c) => c.queryType === "exec")).toBe(true);
expect(sessionCalls.some((c) => c.queryType === "run")).toBe(true);
await session.disconnect();
}
);
it(
"should allow subagents to use SQL tool via inherited sessionFs",
{ timeout: 60000 },
async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
createSessionFsProvider,
});
const events: SessionEvent[] = [];
session.on((event) => {
events.push(event);
});
// Ask the agent to use the task tool to spawn a subagent that uses SQL
await session.sendAndWait({
prompt:
"Use the task tool to ask a task agent to do the following: " +
"Use the sql tool to run this query: INSERT INTO todos (id, title, status) VALUES ('subagent-test', 'Created by subagent', 'done')",
});
await session.disconnect();
// Verify that the subagent's SQL queries were routed through the sessionFs sqlite handler
const sessionCalls = sqliteCalls.filter((c) => c.sessionId === session.sessionId);
const insertCalls = sessionCalls.filter((c) =>
c.query.toUpperCase().includes("INSERT")
);
expect(insertCalls.length).toBeGreaterThan(0);
// Verify that the sql tool execution in events.jsonl came from the subagent (has agentId)
const buf = await provider.readFile(
p(session.sessionId, `${sessionStatePath}/events.jsonl`)
);
const content = buf.toString("utf8");
const lines = content.split("\n").filter(Boolean);
const parsed = lines.map((line) => JSON.parse(line));
const sqlToolEvents = parsed.filter(
(e: { type?: string; data?: { toolName?: string } }) =>
e.type === "tool.execution_start" && e.data?.toolName === "sql"
);
expect(sqlToolEvents.length).toBeGreaterThan(0);
expect(sqlToolEvents.every((e: { agentId?: string }) => !!e.agentId)).toBe(true);
}
);
});
function createTestSessionFsHandlerWithSqlite(
session: CopilotSession,
provider: VirtualProvider,
sqliteCalls: { sessionId: string; queryType: string; query: string }[],
sessionDbs: Map<string, DatabaseSync>
): SessionFsProvider {
const sp = (path: string) => `/${session.sessionId}${path.startsWith("/") ? path : "/" + path}`;
function getOrCreateDb(): DatabaseSync {
let db = sessionDbs.get(session.sessionId);
if (!db) {
db = new DatabaseSync(":memory:");
db.exec("PRAGMA busy_timeout = 5000");
sessionDbs.set(session.sessionId, db);
}
return db;
}
return {
async readFile(path: string): Promise<string> {
return (await provider.readFile(sp(path), "utf8")) as string;
},
async writeFile(path: string, content: string): Promise<void> {
await provider.writeFile(sp(path), content);
},
async appendFile(path: string, content: string): Promise<void> {
await provider.appendFile(sp(path), content);
},
async exists(path: string): Promise<boolean> {
return provider.exists(sp(path));
},
async stat(path: string): Promise<SessionFsFileInfo> {
const st = await provider.stat(sp(path));
return {
isFile: st.isFile(),
isDirectory: st.isDirectory(),
size: st.size,
mtime: new Date(st.mtimeMs).toISOString(),
birthtime: new Date(st.birthtimeMs).toISOString(),
};
},
async mkdir(path: string, recursive: boolean, mode?: number): Promise<void> {
await provider.mkdir(sp(path), { recursive, mode });
},
async readdir(path: string): Promise<string[]> {
return (await provider.readdir(sp(path))) as string[];
},
async readdirWithTypes(path: string): Promise<SessionFsReaddirWithTypesEntry[]> {
const names = (await provider.readdir(sp(path))) as string[];
return Promise.all(
names.map(async (name) => {
const st = await provider.stat(sp(`${path}/${name}`));
return {
name,
type: st.isDirectory() ? ("directory" as const) : ("file" as const),
};
})
);
},
async rm(path: string): Promise<void> {
await provider.unlink(sp(path));
},
async rename(src: string, dest: string): Promise<void> {
await provider.rename(sp(src), sp(dest));
},
sqlite: {
async query(
queryType: SessionFsSqliteQueryType,
query: string,
params?: Record<string, string | number | null>
): Promise<SessionFsSqliteQueryResult | undefined> {
sqliteCalls.push({ sessionId: session.sessionId, queryType, query });
const database = getOrCreateDb();
const trimmed = query.trim();
if (trimmed.length === 0) {
return undefined;
}
switch (queryType) {
case "exec":
database.exec(trimmed);
return undefined;
case "query": {
const stmt = database.prepare(trimmed);
const rows = (params ? stmt.all(params) : stmt.all()) as Record<
string,
unknown
>[];
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
return { rows, columns, rowsAffected: 0 };
}
case "run": {
const stmt = database.prepare(trimmed);
const result = params ? stmt.run(params) : stmt.run();
return {
rows: [],
columns: [],
rowsAffected: Number(result.changes),
lastInsertRowid:
result.lastInsertRowid !== undefined
? Number(result.lastInsertRowid)
: undefined,
};
}
}
},
async exists(): Promise<boolean> {
return sessionDbs.has(session.sessionId);
},
},
};
}