import assert from 'node:assert/strict' import { testTerminalHostPlugin } from './test-terminal-host-plugin' import { mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' import { build } from 'esbuild' import type { ChatMessage, SessionMeta, ToolCallRecord } from '../src/shared/types' interface SessionStoreApi { listSessions(): Promise createSession(title?: string): Promise appendMessage(sessionId: string, message: ChatMessage): Promise patchMessage(sessionId: string, messageId: string, patch: Partial, options?: { preserveContextBreakdown?: boolean }): Promise } interface IpcModule { registerIpc(getWindow: () => unknown): void sessions: SessionStoreApi } interface RegressionState { userData: string handlers: Map unknown> kernel?: {getContextSnapshotIdentity():unknown;getContextBreakdown():Promise} } const STATE_KEY = '__YCODE_SESSION_STORE_REGRESSION__' function tool(id: string, status: ToolCallRecord['status'] = 'running'): ToolCallRecord { return { id, name: 'shell', args: { command: 'test' }, argsRaw: '{"command":"test"}', status, output: `output-${id}` } } function makeMessage(index: number): ChatMessage { return { id: `assistant-${index}`, role: 'assistant', content: `streaming-${index}`, tools: [tool(`tool-${index}`)], parts: [ { type: 'text', text: `streaming-${index}` }, { type: 'tool', toolId: `tool-${index}` } ], createdAt: index, status: 'streaming' } } async function readStoredSessions(path: string): Promise> { const parsed = JSON.parse(await readFile(path, 'utf8')) as { sessions: Array } assert.ok(Array.isArray(parsed.sessions), 'sessions.json should contain a sessions array') return parsed.sessions } function installHandlers(module: IpcModule, state: RegressionState): void { module.registerIpc(() => ({ webContents: { send: () => undefined } })) const invoke = async (channel: string, ...args: unknown[]): Promise => { const handler = state.handlers.get(channel) if (!handler) throw new Error(`Missing IPC handler: ${channel}`) return handler({}, ...args) } ;(state as RegressionState & { invoke: typeof invoke }).invoke = invoke } async function main(): Promise { const tempRoot = await mkdtemp(join(tmpdir(), 'ycode-session-store-')) const state: RegressionState = { userData: join(tempRoot, 'userData'), handlers: new Map() } const globals = globalThis as typeof globalThis & Record const previous = globals[STATE_KEY] globals[STATE_KEY] = state try { const ipcPath = join(process.cwd(), 'src', 'main', 'ipc.ts') const sessionsPath = join(process.cwd(), 'src', 'main', 'sessions.ts') const outfile = join(tempRoot, 'session-store.mjs') await build({ stdin: { contents: `export { registerIpc } from ${JSON.stringify(ipcPath)};\nexport * as sessions from ${JSON.stringify(sessionsPath)};`, resolveDir: process.cwd(), sourcefile: 'session-store-regression-entry.ts', loader: 'ts' }, outfile, bundle: true, external: ['node-pty'], platform: 'node', format: 'esm', banner: { js: `import { createRequire as createTestRequire } from 'node:module'; const require = createTestRequire(import.meta.url);` }, logLevel: 'silent', plugins: [ testTerminalHostPlugin, { name: 'context-summary-cached-kernel-mock', setup(pluginBuild) { pluginBuild.onResolve({filter:/\/claude-kernel$/},()=>({path:'kernel',namespace:'mock-summary-kernel'})) pluginBuild.onLoad({filter:/.*/,namespace:'mock-summary-kernel'},()=>({loader:'js',contents:` export const cachedClaudeKernel=()=>globalThis.${STATE_KEY}.kernel; export const getClaudeKernel=()=>{throw new Error('context query must not start kernel')}; export const releaseClaudeKernel=async()=>{}; export const claudeRuntimeInfo=()=>({}); export const setSessionVisible=()=>{}; export const removeSessionView=()=>{}; `})) } }, { name: 'session-store-electron-mock', setup(pluginBuild) { pluginBuild.onResolve({ filter: /^electron$/ }, () => ({ path: 'electron', namespace: 'mock-electron' })) pluginBuild.onLoad({ filter: /.*/, namespace: 'mock-electron' }, () => ({ loader: 'js', contents: ` const state = globalThis.${STATE_KEY}; export const app = { getPath: (name) => name === 'userData' ? state.userData : state.userData, getName: () => 'YCode', getVersion: () => '0.1.0' }; export const safeStorage = { isEncryptionAvailable: () => false, encryptString: (value) => Buffer.from(value), decryptString: (value) => value.toString() }; export const ipcMain = { handle: (name, handler) => state.handlers.set(name, handler) }; export const webContents = { getAllWebContents: () => [] }; export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }), showSaveDialog: async () => ({ canceled: true }) }; export const shell = { openPath: async () => '' }; export const powerSaveBlocker = { start: () => 1, stop: () => undefined }; export class BrowserWindow {} export class WebContentsView {} ` })) } } ] }) const invokeState = state as RegressionState & { invoke: (channel: string, ...args: unknown[]) => Promise } const bundleUrl = pathToFileURL(outfile).href const module = (await import(bundleUrl)) as IpcModule installHandlers(module, state) const invoke = invokeState.invoke const sessionsFile = join(state.userData, 'sessions.json') assert.equal(await import('node:fs/promises').then(({ stat }) => stat(state.userData).then(() => true, () => false)), false, 'the test should begin without a user-data directory') const created = await Promise.all( Array.from({ length: 12 }, (_, index) => invoke('session:create', `Concurrent ${index}`) as Promise) ) const listed = (await invoke('session:list')) as SessionMeta[] assert.equal(created.length, 12) assert.equal(new Set(created.map((item) => item.id)).size, 12) assert.equal(listed.length, 12, 'concurrent first creates should all survive in memory') assert.deepEqual(new Set(listed.map((item) => item.title)), new Set(created.map((item) => item.title))) assert.ok(await import('node:fs/promises').then(({ stat }) => stat(state.userData).then(() => true, () => false)), 'the first persist should create the user-data directory') assert.equal((await readStoredSessions(sessionsFile)).length, 12, 'concurrent first creates should all survive on disk') const targetSession = created[0] const messages = Array.from({ length: 10 }, (_, index) => makeMessage(index)) await Promise.all(messages.map((message) => module.sessions.appendMessage(targetSession.id, message))) await Promise.all(messages.map((message, index) => { const terminalStatus = index % 2 === 0 ? 'done' as const : 'cancelled' as const const toolStatus = index % 2 === 0 ? 'ok' as const : 'cancelled' as const return module.sessions.patchMessage(targetSession.id, message.id, { content: `terminal-${index}`, parts: [ { type: 'text', text: `terminal-${index}` }, { type: 'tool', toolId: `tool-${index}` } ], tools: [tool(`tool-${index}`, toolStatus)], status: terminalStatus }) })) const snapshotSession = created[1] const usage = {usage:{inputTokens:25,outputTokens:4,totalTokens:29},requestId:'snapshot-request',modelId:'snapshot-model',contextWindow:1000,sampledAt:5,source:'upstream' as const} const savedBreakdown = {requestId:'snapshot-request',modelId:'snapshot-model',sequence:10,sampledAt:10,method:'native-summary' as const,scope:'prepared-request' as const,counts:{system:10,tools:0,conversation:15,mcp:0,skills:0,other:0},inputTokens:25,partial:true} const snapshotMessage:ChatMessage={id:'snapshot-assistant',role:'assistant',content:'',createdAt:0,status:'streaming',requestId:'snapshot-request',modelId:'snapshot-model',contextUsage:usage} await module.sessions.appendMessage(snapshotSession.id,snapshotMessage) const branchBefore=await invoke('session:messages',snapshotSession.id) assert.equal(await invoke('session:messages',snapshotSession.id),branchBefore,'real store getter returns stable active branch identity') await module.sessions.patchMessage(snapshotSession.id,snapshotMessage.id,{contextBreakdown:savedBreakdown}) await module.sessions.patchMessage(snapshotSession.id,snapshotMessage.id,{requestId:snapshotMessage.requestId,modelId:snapshotMessage.modelId,contextUsage:usage,contextBreakdown:undefined,status:'done'},{preserveContextBreakdown:true}) assert.deepEqual(snapshotMessage.contextBreakdown,savedBreakdown,'terminal checkpoint cannot erase independently saved summary') assert.deepEqual(snapshotMessage.contextUsage,usage,'snapshot persistence and checkpoint keep real usage intact') await module.sessions.patchMessage(snapshotSession.id,snapshotMessage.id,{requestId:snapshotMessage.requestId,modelId:snapshotMessage.modelId,contextBreakdown:{...savedBreakdown,sampledAt:8}},{preserveContextBreakdown:true}) assert.equal(snapshotMessage.contextBreakdown?.sampledAt,10,'older cloned category cannot replace newer summary') await module.sessions.patchMessage(snapshotSession.id,snapshotMessage.id,{requestId:snapshotMessage.requestId,modelId:snapshotMessage.modelId,contextBreakdown:{...savedBreakdown,sampledAt:11}},{preserveContextBreakdown:true}) assert.equal(snapshotMessage.contextBreakdown?.sampledAt,11,'newer same-request category still updates') const instance={} state.kernel={getContextSnapshotIdentity:()=>({instance,requestId:snapshotMessage.requestId,groupKey:'group',closed:false,draining:false}),getContextBreakdown:async()=>({...savedBreakdown,sampledAt:12})} const freshInfo=await invoke('session:context-usage',snapshotSession.id) as import('../src/shared/types').ContextUsageInfo assert.equal(freshInfo.breakdown?.sampledAt,12,'actual IPC adapter persists matching cached summary') assert.deepEqual(freshInfo.sample,usage) const fixedState=state.kernel.getContextSnapshotIdentity state.kernel.getContextBreakdown=async()=>{state.kernel!.getContextSnapshotIdentity=()=>({...fixedState() as object,groupKey:'new-group'});return {...savedBreakdown,sampledAt:13}} assert.equal((await invoke('session:context-usage',snapshotSession.id) as import('../src/shared/types').ContextUsageInfo).breakdown?.sampledAt,12,'group change during summary is rejected under IPC lock') state.kernel=undefined assert.equal((await invoke('session:context-usage',snapshotSession.id) as import('../src/shared/types').ContextUsageInfo).breakdown?.sampledAt,12,'no cached kernel retains last saved categories without starting one') const storedSnapshot=(await readStoredSessions(sessionsFile)).find(session=>session.id===snapshotSession.id)!.messages.find(message=>message.id===snapshotMessage.id)! assert.equal(storedSnapshot.contextBreakdown?.sampledAt,12,'summary is durably stored alongside upstream sample') assert.deepEqual(storedSnapshot.contextUsage,usage) const inMemory = (await invoke('session:messages', targetSession.id)) as ChatMessage[] const diskSessions = await readStoredSessions(sessionsFile) const diskMessages = diskSessions.find((item) => item.id === targetSession.id)?.messages assert.ok(diskMessages) assert.deepEqual(diskMessages, inMemory, 'the latest atomic JSON snapshot should match the session:messages IPC result') assert.equal(inMemory.length, 10) for (const [index, message] of inMemory.entries()) { assert.equal(message.status, index % 2 === 0 ? 'done' : 'cancelled') assert.equal(message.tools?.[0].status, index % 2 === 0 ? 'ok' : 'cancelled') assert.equal(message.tools?.[0].output, `output-tool-${index}`) } const coldModule = (await import(`${bundleUrl}?cold=${Date.now()}`)) as IpcModule installHandlers(coldModule, state) const coldMessages = (await invoke('session:messages', targetSession.id)) as ChatMessage[] const coldSessions = (await invoke('session:list')) as SessionMeta[] assert.deepEqual(coldMessages, inMemory, 'a cold module import should recover saved messages from disk') const coldInfo=await invoke('session:context-usage',snapshotSession.id) as import('../src/shared/types').ContextUsageInfo assert.equal(coldInfo.breakdown?.sampledAt,12,'cold application reload restores persisted category snapshot') assert.deepEqual(coldInfo.sample,usage) assert.equal(coldSessions.length, 12, 'a cold module import should recover all concurrent sessions') await rm(sessionsFile) await mkdir(sessionsFile) state.kernel={getContextSnapshotIdentity:fixedState,getContextBreakdown:async()=>({...savedBreakdown,sampledAt:14})} const failedSave=await invoke('session:context-usage',snapshotSession.id) as import('../src/shared/types').ContextUsageInfo assert.equal(failedSave.breakdown?.sampledAt,12,'failed disk write retains last saved snapshot, not fresh unsaved categories') assert.deepEqual(failedSave.sample,usage) state.kernel=undefined await assert.rejects(coldModule.sessions.createSession('transient write failure'), 'a failed atomic rename must reject its caller') const leftovers = (await readdir(state.userData)).filter((name) => /^\.sessions-.*\.tmp$/.test(name)) assert.deepEqual(leftovers, [], 'a failed write should remove its temporary file') await rm(sessionsFile, { recursive: true }) const recovered = await coldModule.sessions.createSession('queue recovered') assert.equal(recovered.title, 'queue recovered') const recoveredDisk = await readStoredSessions(sessionsFile) assert.ok(recoveredDisk.some((item) => item.id === recovered.id), 'a later write should recover after a transient failure') const afterRecovery = (await invoke('session:messages', targetSession.id)) as ChatMessage[] assert.deepEqual(afterRecovery, inMemory, 'session:messages should remain consistent after the write queue recovers') console.log('session-store-regression: all checks passed') } finally { if (previous === undefined) delete globals[STATE_KEY] else globals[STATE_KEY] = previous await rm(tempRoot, { recursive: true, force: true }) } } void main().catch((error: unknown) => { console.error('session-store-regression failed:', error) process.exitCode = 1 })