diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index f18b70f42..b81136d90 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -24,7 +24,7 @@ import { StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js"; -import { createServerRpc } from "./generated/rpc.js"; +import { createServerRpc, registerClientApiHandlers } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; import { getTraceContext } from "./telemetry.js"; @@ -46,6 +46,7 @@ import type { SessionListFilter, SessionMetadata, SystemMessageCustomizeConfig, + SessionDataStoreConfig, TelemetryConfig, Tool, ToolCallRequestPayload, @@ -216,6 +217,7 @@ export class CopilotClient { | "onListModels" | "telemetry" | "onGetTraceContext" + | "sessionDataStore" > > & { cliPath?: string; @@ -238,6 +240,8 @@ export class CopilotClient { private _rpc: ReturnType | null = null; private processExitPromise: Promise | null = null; // Rejects when CLI process exits private negotiatedProtocolVersion: number | null = null; + /** Connection-level session data store config, set via constructor option. */ + private sessionDataStoreConfig: SessionDataStoreConfig | null = null; /** * Typed server-scoped RPC methods. @@ -307,6 +311,7 @@ export class CopilotClient { this.onListModels = options.onListModels; this.onGetTraceContext = options.onGetTraceContext; + this.sessionDataStoreConfig = options.sessionDataStore ?? null; const effectiveEnv = options.env ?? process.env; this.options = { @@ -399,6 +404,13 @@ export class CopilotClient { // Verify protocol version compatibility await this.verifyProtocolVersion(); + // If a session data store was configured, register as the storage provider + if (this.sessionDataStoreConfig) { + await this.connection!.sendRequest("sessionDataStore.setDataStore", { + descriptor: this.sessionDataStoreConfig.descriptor, + }); + } + this.state = "connected"; } catch (error) { this.state = "error"; @@ -1069,7 +1081,9 @@ export class CopilotClient { throw new Error("Client not connected"); } - const response = await this.connection.sendRequest("session.list", { filter }); + const response = await this.connection.sendRequest("session.list", { + filter, + }); const { sessions } = response as { sessions: Array<{ sessionId: string; @@ -1562,6 +1576,13 @@ export class CopilotClient { await this.handleSystemMessageTransform(params) ); + // Register session data store RPC handlers if configured. + if (this.sessionDataStoreConfig) { + registerClientApiHandlers(this.connection, { + sessionDataStore: this.sessionDataStoreConfig, + }); + } + this.connection.onClose(() => { this.state = "disconnected"; }); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index dadb9e79d..ebfee9bfb 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -179,6 +179,20 @@ export interface AccountGetQuotaResult { }; } +export interface SessionDataStoreSetDataStoreResult { + /** + * Whether the data store was set successfully + */ + success: boolean; +} + +export interface SessionDataStoreSetDataStoreParams { + /** + * Opaque descriptor identifying the storage backend (e.g., 'redis://localhost/sessions') + */ + descriptor: string; +} + export interface SessionModelGetCurrentResult { /** * Currently active model identifier @@ -1050,6 +1064,78 @@ export interface SessionShellKillParams { signal?: "SIGTERM" | "SIGKILL" | "SIGINT"; } +export interface SessionDataStoreLoadResult { + /** + * All persisted events for the session, in order + */ + events: { + [k: string]: unknown; + }[]; +} + +export interface SessionDataStoreLoadParams { + /** + * The session to load events for + */ + sessionId: string; +} + +export interface SessionDataStoreAppendParams { + /** + * The session to append events to + */ + sessionId: string; + /** + * Events to append, in order + */ + events: { + [k: string]: unknown; + }[]; +} + +export interface SessionDataStoreTruncateResult { + /** + * Number of events removed + */ + eventsRemoved: number; + /** + * Number of events kept + */ + eventsKept: number; +} + +export interface SessionDataStoreTruncateParams { + /** + * The session to truncate + */ + sessionId: string; + /** + * Event ID marking the truncation boundary (excluded) + */ + upToEventId: string; +} + +export interface SessionDataStoreListResult { + sessions: { + sessionId: string; + /** + * ISO 8601 timestamp of last modification + */ + mtime: string; + /** + * ISO 8601 timestamp of creation + */ + birthtime: string; + }[]; +} + +export interface SessionDataStoreDeleteParams { + /** + * The session to delete + */ + sessionId: string; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -1067,6 +1153,10 @@ export function createServerRpc(connection: MessageConnection) { getQuota: async (): Promise => connection.sendRequest("account.getQuota", {}), }, + sessionDataStore: { + setDataStore: async (params: SessionDataStoreSetDataStoreParams): Promise => + connection.sendRequest("sessionDataStore.setDataStore", params), + }, }; } @@ -1188,3 +1278,40 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin }, }; } + +/** + * Handler interface for the `sessionDataStore` client API group. + * Implement this to provide a custom sessionDataStore backend. + */ +export interface SessionDataStoreHandler { + load(params: SessionDataStoreLoadParams): Promise; + append(params: SessionDataStoreAppendParams): Promise; + truncate(params: SessionDataStoreTruncateParams): Promise; + list(): Promise; + delete(params: SessionDataStoreDeleteParams): Promise; +} + +/** All client API handler groups. Each group is optional. */ +export interface ClientApiHandlers { + sessionDataStore?: SessionDataStoreHandler; +} + +/** + * Register client API handlers on a JSON-RPC connection. + * The server calls these methods to delegate work to the client. + * Methods for unregistered groups will respond with a standard JSON-RPC + * method-not-found error. + */ +export function registerClientApiHandlers( + connection: MessageConnection, + handlers: ClientApiHandlers, +): void { + if (handlers.sessionDataStore) { + const h = handlers.sessionDataStore!; + connection.onRequest("sessionDataStore.load", (params: SessionDataStoreLoadParams) => h.load(params)); + connection.onRequest("sessionDataStore.append", (params: SessionDataStoreAppendParams) => h.append(params)); + connection.onRequest("sessionDataStore.truncate", (params: SessionDataStoreTruncateParams) => h.truncate(params)); + connection.onRequest("sessionDataStore.list", () => h.list()); + connection.onRequest("sessionDataStore.delete", (params: SessionDataStoreDeleteParams) => h.delete(params)); + } +} diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index c42935a26..5bc273356 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -56,6 +56,9 @@ export type { SessionListFilter, SessionMetadata, SessionUiApi, + SessionDataStoreConfig, + SessionDataStoreHandler, + ClientApiHandlers, SystemMessageAppendConfig, SystemMessageConfig, SystemMessageCustomizeConfig, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 96694137d..84d30470b 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -10,6 +10,21 @@ import type { SessionEvent as GeneratedSessionEvent } from "./generated/session-events.js"; export type SessionEvent = GeneratedSessionEvent; +// Re-export generated client API types +export type { + SessionDataStoreHandler, + SessionDataStoreLoadParams, + SessionDataStoreLoadResult, + SessionDataStoreAppendParams, + SessionDataStoreTruncateParams, + SessionDataStoreTruncateResult, + SessionDataStoreListResult, + SessionDataStoreDeleteParams, + ClientApiHandlers, +} from "./generated/rpc.js"; + +import type { SessionDataStoreHandler } from "./generated/rpc.js"; + /** * Options for creating a CopilotClient */ @@ -171,6 +186,14 @@ export interface CopilotClientOptions { * ``` */ onGetTraceContext?: TraceContextProvider; + + /** + * Custom session data storage backend. + * When provided, the client registers as the session data storage provider + * on connection, routing all event persistence through these callbacks + * instead of the server's default file-based storage. + */ + sessionDataStore?: SessionDataStoreConfig; } /** @@ -1318,6 +1341,20 @@ export interface SessionContext { branch?: string; } +/** + * Configuration for a custom session data store backend. + * + * Extends the generated {@link SessionDataStoreHandler} with a `descriptor` + * that identifies the storage backend for display purposes. + */ +export interface SessionDataStoreConfig extends SessionDataStoreHandler { + /** + * Opaque descriptor identifying this storage backend. + * Used for UI display (e.g., `"redis://localhost/sessions"`). + */ + descriptor: string; +} + /** * Filter options for listing sessions */ diff --git a/nodejs/test/e2e/session_store.test.ts b/nodejs/test/e2e/session_store.test.ts new file mode 100644 index 000000000..b79db0033 --- /dev/null +++ b/nodejs/test/e2e/session_store.test.ts @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { CopilotClient } from "../../src/client.js"; +import { approveAll, type SessionEvent, type SessionDataStoreConfig } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +/** + * In-memory session event store for testing. + * Stores events in a Map keyed by sessionId, and tracks call counts + * for each operation so tests can assert they were invoked. + */ +class InMemorySessionStore { + private sessions = new Map(); + readonly calls = { + load: 0, + append: 0, + truncate: 0, + listSessions: 0, + delete: 0, + }; + + toConfig(descriptor: string): SessionDataStoreConfig { + return { + descriptor, + load: async ({ sessionId }) => { + this.calls.load++; + const events = this.sessions.get(sessionId) ?? []; + return { events: events as Record[] }; + }, + append: async ({ sessionId, events }) => { + this.calls.append++; + const existing = this.sessions.get(sessionId) ?? []; + existing.push(...(events as unknown as SessionEvent[])); + this.sessions.set(sessionId, existing); + }, + truncate: async ({ sessionId, upToEventId }) => { + this.calls.truncate++; + const existing = this.sessions.get(sessionId) ?? []; + const idx = existing.findIndex((e) => e.id === upToEventId); + if (idx === -1) { + return { eventsRemoved: 0, eventsKept: existing.length }; + } + const kept = existing.slice(idx + 1); + this.sessions.set(sessionId, kept); + return { eventsRemoved: idx + 1, eventsKept: kept.length }; + }, + list: async () => { + this.calls.listSessions++; + const now = new Date().toISOString(); + return { + sessions: Array.from(this.sessions.keys()).map((sessionId) => ({ + sessionId, + mtime: now, + birthtime: now, + })), + }; + }, + delete: async ({ sessionId }) => { + this.calls.delete++; + this.sessions.delete(sessionId); + }, + }; + } + + getEvents(sessionId: string): SessionEvent[] { + return this.sessions.get(sessionId) ?? []; + } + + hasSession(sessionId: string): boolean { + return this.sessions.has(sessionId); + } + + get sessionCount(): number { + return this.sessions.size; + } +} + +// These tests require a runtime built with sessionDataStore support. +// Skip when COPILOT_CLI_PATH is not set (CI uses the published CLI which +// doesn't include this feature yet). +const runTests = process.env.COPILOT_CLI_PATH ? describe : describe.skip; + +runTests("Session Data Store", async () => { + const { env } = await createSdkTestContext(); + + it("should persist events to a client-supplied store", async () => { + const store = new InMemorySessionStore(); + const client1 = new CopilotClient({ + env, + logLevel: "error", + cliPath: process.env.COPILOT_CLI_PATH, + sessionDataStore: store.toConfig("memory://test-persist"), + }); + onTestFinished(() => client1.forceStop()); + + const session = await client1.createSession({ + onPermissionRequest: approveAll, + }); + + // Send a message and wait for the response + const msg = await session.sendAndWait({ prompt: "What is 100 + 200?" }); + expect(msg?.data.content).toContain("300"); + + // Verify onAppend was called — events should have been routed to our store. + // The SessionWriter uses debounced flushing, so poll until events arrive. + await vi.waitFor( + () => { + const events = store.getEvents(session.sessionId); + const eventTypes = events.map((e) => e.type); + expect(eventTypes).toContain("session.start"); + expect(eventTypes).toContain("user.message"); + expect(eventTypes).toContain("assistant.message"); + }, + { timeout: 10_000, interval: 200 } + ); + expect(store.calls.append).toBeGreaterThan(0); + }); + + it("should load events from store on resume", async () => { + const store = new InMemorySessionStore(); + + const client2 = new CopilotClient({ + env, + logLevel: "error", + cliPath: process.env.COPILOT_CLI_PATH, + sessionDataStore: store.toConfig("memory://test-resume"), + }); + onTestFinished(() => client2.forceStop()); + + // Create a session and send a message + const session1 = await client2.createSession({ + onPermissionRequest: approveAll, + }); + const sessionId = session1.sessionId; + + const msg1 = await session1.sendAndWait({ prompt: "What is 50 + 50?" }); + expect(msg1?.data.content).toContain("100"); + await session1.disconnect(); + + // Verify onLoad is called when resuming + const loadCountBefore = store.calls.load; + const session2 = await client2.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); + + expect(store.calls.load).toBeGreaterThan(loadCountBefore); + + // Send another message to verify the session is functional + const msg2 = await session2.sendAndWait({ prompt: "What is that times 3?" }); + expect(msg2?.data.content).toContain("300"); + }); + + it("should list sessions from the data store", async () => { + const store = new InMemorySessionStore(); + + const client3 = new CopilotClient({ + env, + logLevel: "error", + cliPath: process.env.COPILOT_CLI_PATH, + sessionDataStore: store.toConfig("memory://test-list"), + }); + onTestFinished(() => client3.forceStop()); + + // Create a session and send a message to trigger event flushing + const session = await client3.createSession({ + onPermissionRequest: approveAll, + }); + await session.sendAndWait({ prompt: "What is 10 + 10?" }); + + // Wait for events to be flushed (debounced) + await vi.waitFor(() => expect(store.hasSession(session.sessionId)).toBe(true), { + timeout: 10_000, + interval: 200, + }); + + // List sessions — should come from our store + const sessions = await client3.listSessions(); + expect(store.calls.listSessions).toBeGreaterThan(0); + expect(sessions.some((s) => s.sessionId === session.sessionId)).toBe(true); + }); + + it("should call onDelete when deleting a session", async () => { + const store = new InMemorySessionStore(); + + const client4 = new CopilotClient({ + env, + logLevel: "error", + cliPath: process.env.COPILOT_CLI_PATH, + sessionDataStore: store.toConfig("memory://test-delete"), + }); + onTestFinished(() => client4.forceStop()); + + const session = await client4.createSession({ + onPermissionRequest: approveAll, + }); + const sessionId = session.sessionId; + + // Send a message to create some events + await session.sendAndWait({ prompt: "What is 7 + 7?" }); + + // Wait for events to flush + await vi.waitFor(() => expect(store.hasSession(sessionId)).toBe(true), { + timeout: 10_000, + interval: 200, + }); + + expect(store.calls.delete).toBe(0); + + // Delete the session + await client4.deleteSession(sessionId); + + // Verify onDelete was called and the session was removed from our store + expect(store.calls.delete).toBeGreaterThan(0); + expect(store.hasSession(sessionId)).toBe(false); + }); + + it("should reject sessionDataStore when sessions already exist", async () => { + // First client uses TCP so a second client can connect to the same runtime + const client5 = new CopilotClient({ + env, + logLevel: "error", + cliPath: process.env.COPILOT_CLI_PATH, + useStdio: false, + }); + onTestFinished(() => client5.forceStop()); + + const session = await client5.createSession({ + onPermissionRequest: approveAll, + }); + await session.sendAndWait({ prompt: "Hello" }); + + // Get the port the first client's runtime is listening on + const port = (client5 as unknown as { actualPort: number }).actualPort; + + // Second client tries to connect with a data store — should fail + // because sessions already exist on the runtime. + const store = new InMemorySessionStore(); + const client6 = new CopilotClient({ + env, + logLevel: "error", + cliUrl: `localhost:${port}`, + sessionDataStore: store.toConfig("memory://too-late"), + }); + onTestFinished(() => client6.forceStop()); + + await expect(client6.start()).rejects.toThrow(); + }); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..c3f458113 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "azure-otter", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 8d23b428f..c8f831c4e 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -86,17 +86,20 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; `); const allMethods = [...collectRpcMethods(schema.server || {}), ...collectRpcMethods(schema.session || {})]; + const clientMethods = collectRpcMethods(schema.client || {}); - for (const method of allMethods) { - const compiled = await compile(method.result, resultTypeName(method.rpcMethod), { - bannerComment: "", - additionalProperties: false, - }); - if (method.stability === "experimental") { - lines.push("/** @experimental */"); + for (const method of [...allMethods, ...clientMethods]) { + if (method.result) { + const compiled = await compile(method.result, resultTypeName(method.rpcMethod), { + bannerComment: "", + additionalProperties: false, + }); + if (method.stability === "experimental") { + lines.push("/** @experimental */"); + } + lines.push(compiled.trim()); + lines.push(""); } - lines.push(compiled.trim()); - lines.push(""); if (method.params?.properties && Object.keys(method.params.properties).length > 0) { const paramsCompiled = await compile(method.params, paramsTypeName(method.rpcMethod), { @@ -132,6 +135,11 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; lines.push(""); } + // Generate client API handler interfaces and registration function + if (schema.client) { + lines.push(...emitClientApiHandlers(schema.client)); + } + const outPath = await writeGeneratedFile("nodejs/src/generated/rpc.ts", lines.join("\n")); console.log(` ✓ ${outPath}`); } @@ -185,6 +193,110 @@ function emitGroup(node: Record, indent: string, isSession: boo return lines; } +// ── Client API Handler Generation ─────────────────────────────────────────── + +/** + * Collect client API methods grouped by their top-level namespace. + * Returns a map like: { sessionStore: [{ rpcMethod, params, result }, ...] } + */ +function collectClientGroups(node: Record): Map { + const groups = new Map(); + for (const [groupName, groupNode] of Object.entries(node)) { + if (typeof groupNode === "object" && groupNode !== null) { + groups.set(groupName, collectRpcMethods(groupNode as Record)); + } + } + return groups; +} + +/** + * Derive the handler method name from the full RPC method name. + * e.g., "sessionStore.load" → "load" + */ +function handlerMethodName(rpcMethod: string): string { + const parts = rpcMethod.split("."); + return parts[parts.length - 1]; +} + +/** + * Generate handler interfaces and a registration function for client API groups. + */ +function emitClientApiHandlers(clientSchema: Record): string[] { + const lines: string[] = []; + const groups = collectClientGroups(clientSchema); + + // Emit a handler interface per group + for (const [groupName, methods] of groups) { + const interfaceName = toPascalCase(groupName) + "Handler"; + lines.push(`/**`); + lines.push(` * Handler interface for the \`${groupName}\` client API group.`); + lines.push(` * Implement this to provide a custom ${groupName} backend.`); + lines.push(` */`); + lines.push(`export interface ${interfaceName} {`); + + for (const method of methods) { + const name = handlerMethodName(method.rpcMethod); + const hasParams = method.params?.properties && Object.keys(method.params.properties).length > 0; + const pType = hasParams ? paramsTypeName(method.rpcMethod) : ""; + const rType = method.result ? resultTypeName(method.rpcMethod) : "void"; + + const sig = hasParams + ? ` ${name}(params: ${pType}): Promise<${rType}>;` + : ` ${name}(): Promise<${rType}>;`; + lines.push(sig); + } + + lines.push(`}`); + lines.push(""); + } + + // Emit combined ClientApiHandlers type + lines.push(`/** All client API handler groups. Each group is optional. */`); + lines.push(`export interface ClientApiHandlers {`); + for (const [groupName] of groups) { + const interfaceName = toPascalCase(groupName) + "Handler"; + lines.push(` ${groupName}?: ${interfaceName};`); + } + lines.push(`}`); + lines.push(""); + + // Emit registration function + lines.push(`/**`); + lines.push(` * Register client API handlers on a JSON-RPC connection.`); + lines.push(` * The server calls these methods to delegate work to the client.`); + lines.push(` * Methods for unregistered groups will respond with a standard JSON-RPC`); + lines.push(` * method-not-found error.`); + lines.push(` */`); + lines.push(`export function registerClientApiHandlers(`); + lines.push(` connection: MessageConnection,`); + lines.push(` handlers: ClientApiHandlers,`); + lines.push(`): void {`); + + for (const [groupName, methods] of groups) { + lines.push(` if (handlers.${groupName}) {`); + lines.push(` const h = handlers.${groupName}!;`); + + for (const method of methods) { + const name = handlerMethodName(method.rpcMethod); + const hasParams = method.params?.properties && Object.keys(method.params.properties).length > 0; + const pType = hasParams ? paramsTypeName(method.rpcMethod) : ""; + + if (hasParams) { + lines.push(` connection.onRequest("${method.rpcMethod}", (params: ${pType}) => h.${name}(params));`); + } else { + lines.push(` connection.onRequest("${method.rpcMethod}", () => h.${name}());`); + } + } + + lines.push(` }`); + } + + lines.push(`}`); + lines.push(""); + + return lines; +} + // ── Main ──────────────────────────────────────────────────────────────────── async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Promise { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 2c13b1d96..bc508e240 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -125,13 +125,14 @@ export async function writeGeneratedFile(relativePath: string, content: string): export interface RpcMethod { rpcMethod: string; params: JSONSchema7 | null; - result: JSONSchema7; + result: JSONSchema7 | null; stability?: string; } export interface ApiSchema { server?: Record; session?: Record; + client?: Record; } export function isRpcMethod(node: unknown): node is RpcMethod { diff --git a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml index cf55fcc17..fdb7ebca0 100644 --- a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml @@ -5,13 +5,13 @@ conversations: - role: system content: ${system} - role: user - content: What is 3+3? Reply with just the number. + content: What is 1+1? Reply with just the number. - role: assistant - content: "6" + content: "2" - messages: - role: system content: ${system} - role: user - content: What is 1+1? Reply with just the number. + content: What is 3+3? Reply with just the number. - role: assistant - content: "2" + content: "6" diff --git a/test/snapshots/session_store/should_call_ondelete_when_deleting_a_session.yaml b/test/snapshots/session_store/should_call_ondelete_when_deleting_a_session.yaml new file mode 100644 index 000000000..2081e76aa --- /dev/null +++ b/test/snapshots/session_store/should_call_ondelete_when_deleting_a_session.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 7 + 7? + - role: assistant + content: 7 + 7 = 14 diff --git a/test/snapshots/session_store/should_list_sessions_from_the_data_store.yaml b/test/snapshots/session_store/should_list_sessions_from_the_data_store.yaml new file mode 100644 index 000000000..3461d8aee --- /dev/null +++ b/test/snapshots/session_store/should_list_sessions_from_the_data_store.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 10 + 10? + - role: assistant + content: 10 + 10 = 20 diff --git a/test/snapshots/session_store/should_list_sessions_from_the_storage_provider.yaml b/test/snapshots/session_store/should_list_sessions_from_the_storage_provider.yaml new file mode 100644 index 000000000..3461d8aee --- /dev/null +++ b/test/snapshots/session_store/should_list_sessions_from_the_storage_provider.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 10 + 10? + - role: assistant + content: 10 + 10 = 20 diff --git a/test/snapshots/session_store/should_load_events_from_a_client_supplied_store_on_resume.yaml b/test/snapshots/session_store/should_load_events_from_a_client_supplied_store_on_resume.yaml new file mode 100644 index 000000000..4744667cd --- /dev/null +++ b/test/snapshots/session_store/should_load_events_from_a_client_supplied_store_on_resume.yaml @@ -0,0 +1,14 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 50 + 50? + - role: assistant + content: 50 + 50 = 100 + - role: user + content: What is that times 3? + - role: assistant + content: 100 × 3 = 300 diff --git a/test/snapshots/session_store/should_load_events_from_store_on_resume.yaml b/test/snapshots/session_store/should_load_events_from_store_on_resume.yaml new file mode 100644 index 000000000..4744667cd --- /dev/null +++ b/test/snapshots/session_store/should_load_events_from_store_on_resume.yaml @@ -0,0 +1,14 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 50 + 50? + - role: assistant + content: 50 + 50 = 100 + - role: user + content: What is that times 3? + - role: assistant + content: 100 × 3 = 300 diff --git a/test/snapshots/session_store/should_persist_events_to_a_client_supplied_store.yaml b/test/snapshots/session_store/should_persist_events_to_a_client_supplied_store.yaml new file mode 100644 index 000000000..455652bfd --- /dev/null +++ b/test/snapshots/session_store/should_persist_events_to_a_client_supplied_store.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 100 + 200? + - role: assistant + content: 100 + 200 = 300 diff --git a/test/snapshots/session_store/should_reject_sessiondatastore_when_sessions_already_exist.yaml b/test/snapshots/session_store/should_reject_sessiondatastore_when_sessions_already_exist.yaml new file mode 100644 index 000000000..fad18cf6f --- /dev/null +++ b/test/snapshots/session_store/should_reject_sessiondatastore_when_sessions_already_exist.yaml @@ -0,0 +1,19 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Hello + - role: assistant + content: |- + Hello! I'm GitHub Copilot CLI, ready to help you with the GitHub Copilot SDK repository. + + I can assist you with: + - Building, testing, and linting across all language SDKs (Node.js, Python, Go, .NET) + - Understanding the codebase architecture and JSON-RPC client implementation + - Adding new SDK features or E2E tests + - Running language-specific tasks or investigating issues + + What would you like to work on today? diff --git a/test/snapshots/session_store/should_reject_setstorageprovider_when_sessions_exist.yaml b/test/snapshots/session_store/should_reject_setstorageprovider_when_sessions_exist.yaml new file mode 100644 index 000000000..09d01531f --- /dev/null +++ b/test/snapshots/session_store/should_reject_setstorageprovider_when_sessions_exist.yaml @@ -0,0 +1,34 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Hello + - role: assistant + content: >- + Hello! I'm GitHub Copilot CLI, powered by claude-sonnet-4.5. I'm here to help you with software engineering + tasks in this repository. + + + I can see you're working in the **copilot-sdk/nodejs** directory, which is part of a monorepo that implements + language SDKs for connecting to the Copilot CLI via JSON-RPC. + + + How can I help you today? I can: + + - Build, test, or lint the codebase + + - Add new SDK features or E2E tests + + - Debug issues or investigate bugs + + - Explore the codebase structure + + - Generate types or run other scripts + + - And more! + + + What would you like to work on? diff --git a/test/snapshots/session_store/should_use_client_supplied_store_for_listsessions.yaml b/test/snapshots/session_store/should_use_client_supplied_store_for_listsessions.yaml new file mode 100644 index 000000000..3461d8aee --- /dev/null +++ b/test/snapshots/session_store/should_use_client_supplied_store_for_listsessions.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 10 + 10? + - role: assistant + content: 10 + 10 = 20