| 1 | import fs from 'fs' |
| 2 | import path from 'path' |
| 3 | import { describe, expect, it } from 'vitest' |
| 4 | import { TypedEventBus } from '../../../src/main/agent-runtime' |
| 5 | import { createRuntimeEmitters } from '../../../src/main/ipc/runtime/runtime-emitters' |
| 6 | import { createSessionProjectResolver } from '../../../src/main/ipc/runtime/session-project' |
| 7 | import { createSessionRunStateStore } from '../../../src/main/ipc/runtime/session-run-state' |
| 8 | |
| 9 | describe('IPC runtime capabilities', () => { |
| 10 | it('keeps generation on its own narrow context instead of the IPC facade', () => { |
| 11 | const generationDirectory = path.resolve('src/main/generation') |
| 12 | const sources = fs |
| 13 | .readdirSync(generationDirectory) |
| 14 | .filter((entry) => entry.endsWith('.ts')) |
| 15 | .map((entry) => ({ |
| 16 | path: path.join(generationDirectory, entry), |
| 17 | source: fs.readFileSync(path.join(generationDirectory, entry), 'utf8') |
| 18 | })) |
| 19 | |
| 20 | for (const source of sources) { |
| 21 | expect(source.source, source.path).not.toContain('IpcContext') |
| 22 | } |
| 23 | const generationContext = sources.find(({ path: filePath }) => filePath.endsWith('/context.ts')) |
| 24 | expect(generationContext?.source).toContain('export type GenerationDbPort') |
| 25 | expect(generationContext?.source).toContain('export type GenerationContext') |
| 26 | }) |
| 27 | |
| 28 | it('keeps session run state separate from lifecycle event emission', () => { |
| 29 | const sessionRuns = createSessionRunStateStore() |
| 30 | const runtimeEvents = new TypedEventBus() |
| 31 | const received: string[] = [] |
| 32 | runtimeEvents.subscribe({ subscriberId: 'test' }, (event) => received.push(event.type)) |
| 33 | const emitters = createRuntimeEmitters({ |
| 34 | mainWindow: { |
| 35 | isDestroyed: () => false, |
| 36 | isVisible: () => true, |
| 37 | show: () => undefined, |
| 38 | focus: () => undefined |
| 39 | } as never, |
| 40 | runtimeEvents, |
| 41 | sessionRuns |
| 42 | }) |
| 43 | |
| 44 | const state = sessionRuns.beginSessionRunState({ |
| 45 | sessionId: 'session-1', |
| 46 | runId: 'run-1', |
| 47 | mode: 'generate', |
| 48 | totalPages: 2 |
| 49 | }) |
| 50 | expect(received).toEqual([]) |
| 51 | |
| 52 | emitters.emitSessionRunLifecycle(state) |
| 53 | emitters.emitGenerateChunk('session-1', { |
| 54 | type: 'page_generated', |
| 55 | payload: { |
| 56 | runId: 'run-1', |
| 57 | stage: 'rendering', |
| 58 | pageId: 'page-1', |
| 59 | pageNumber: 1, |
| 60 | title: 'Overview', |
| 61 | html: '<section>Overview</section>' |
| 62 | } |
| 63 | }) |
| 64 | |
| 65 | expect(received).toEqual(['job.started', 'generation.chunk']) |
| 66 | expect(state.completedPageKeys).toEqual(['page-1']) |
| 67 | expect(state.events[0]).toMatchObject({ |
| 68 | type: 'page_generated', |
| 69 | payload: { html: '' } |
| 70 | }) |
| 71 | }) |
| 72 | |
| 73 | it('builds a session snapshot through the project capability', async () => { |
| 74 | const db = { |
| 75 | getSession: async () => ({ id: 'session-1' }), |
| 76 | getProject: async () => ({ id: 'project-1', root_path: '/tmp/session-1' }), |
| 77 | listSessionPages: async () => [ |
| 78 | { |
| 79 | file_slug: 'page-1', |
| 80 | page_number: 1, |
| 81 | title: 'Overview', |
| 82 | html_path: 'page-1.html', |
| 83 | status: 'completed', |
| 84 | error: null |
| 85 | }, |
| 86 | { |
| 87 | file_slug: 'page-2', |
| 88 | page_number: 2, |
| 89 | title: 'Risks', |
| 90 | html_path: 'page-2.html', |
| 91 | status: 'failed', |
| 92 | error: 'model failed' |
| 93 | } |
| 94 | ] |
| 95 | } |
| 96 | const project = createSessionProjectResolver({ db: db as never }) |
| 97 | |
| 98 | const snapshot = await project.buildSessionGenerationSnapshot( |
| 99 | { id: 'session-1', metadata: '{"existing":true}' }, |
| 100 | { includeHtml: false } |
| 101 | ) |
| 102 | |
| 103 | expect(snapshot.pages).toEqual( |
| 104 | expect.arrayContaining([ |
| 105 | expect.objectContaining({ pageId: 'page-1', html: '', status: 'completed' }), |
| 106 | expect.objectContaining({ pageId: 'page-2', html: '', error: 'model failed' }) |
| 107 | ]) |
| 108 | ) |
| 109 | expect(snapshot.session).toMatchObject({ |
| 110 | page_count: 2, |
| 111 | generated_count: 1, |
| 112 | failed_count: 1 |
| 113 | }) |
| 114 | expect(JSON.parse(String(snapshot.session?.metadata))).toMatchObject({ |
| 115 | existing: true, |
| 116 | entryMode: 'multi_page', |
| 117 | projectId: 'project-1' |
| 118 | }) |
| 119 | }) |
| 120 | }) |
| 121 |