// Actual Electron preload/IPC + installed OMP and HTTP workers, using loopback model responses only. const fs = require('node:fs/promises'), path = require('node:path') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/subagent-app-')) await Promise.all(['data', 'home', 'workspace'].map(name => fs.mkdir(path.join(directory, name)))) await fs.writeFile(path.join(directory, 'workspace', 'fixture.txt'), 'PROJECT_FIXTURE_CONTENT') require('esbuild').buildSync({ stdin: { resolveDir: process.cwd(), contents: [ "export * from './src/main/subagents/scheduler';", "export * from './src/main/subagents/worker';", "export * from './src/main/agent/loop';", "export * from './src/main/gateway';", "export * from './src/shared/message-parts';" ].join('\n') }, bundle: true, platform: 'node', format: 'cjs', external: ['electron'], outfile: path.join(directory, 'host.cjs'), logLevel: 'warning' }) const env = { ...process.env, YCODE_AGENTS_TEST_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_DISABLE_SYSTEM_INTEGRATION: '1', YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '1' } delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL const child = require('node:child_process').spawn(require('electron'), ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let output = '' child.stdout.on('data', data => { output += data; process.stdout.write(data) }) child.stderr.on('data', data => { output += data }) const timer = setTimeout(() => child.kill(), 180000) child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output) const report = JSON.parse(await fs.readFile(path.join(directory, 'report.json'), 'utf8').catch(() => '{"result":"MISSING"}')) console.log(JSON.stringify({ directory, result: report.result, exitCode: code })) if (code || report.result !== 'PASS') { console.error(output.slice(-8000)); process.exitCode = 1 } }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron'), assert = require('node:assert/strict'), http = require('node:http') const directory = process.env.YCODE_AGENTS_TEST_DIRECTORY, workspaceRoot = path.join(directory, 'workspace') app.setPath('userData', path.join(directory, 'data')); app.disableHardwareAcceleration() app.on('window-all-closed', () => {}) BrowserWindow.prototype.show = function () {} const checks = [], requests = [], models = new Map(), rendererErrors = [] let server, main, phase = 'boot', apiBase, host, direct const pause = ms => new Promise(done => setTimeout(done, ms)) function check(label, condition = true) { assert.ok(condition, label); checks.push(label); console.log('AGENT_CHECK ' + label) } function sse(res, model, call, final) { res.writeHead(200, { 'content-type': 'text/event-stream' }) const delta = call ? { role: 'assistant', tool_calls: [{ index: 0, id: model + '-call-' + Math.random(), type: 'function', function: { name: call.name, arguments: JSON.stringify(call.args) } }] } : { role: 'assistant', content: final } res.end([{ id: 'controlled', object: 'chat.completion.chunk', model, choices: [{ index: 0, delta, finish_reason: null }] }, { id: 'controlled', object: 'chat.completion.chunk', model, choices: [{ index: 0, delta: {}, finish_reason: call ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 10, total_tokens: 110 } }] .map(row => 'data: ' + JSON.stringify(row) + '\n\n').join('') + 'data: [DONE]\n\n') } const read = (source, window = main) => window.webContents.executeJavaScript(source) const invoke = (namespace, method, ...args) => read('window.ycode[' + JSON.stringify(namespace) + '][' + JSON.stringify(method) + '](...' + JSON.stringify(args) + ')') async function until(operation, label, attempts = 600) { for (let index = 0; index < attempts; index++) { const value = await operation(); if (value) return value; await pause(30) } throw Error('Timeout: ' + label) } const profile = model => ({ id: model, name: model, model, providerId: 'custom', protocol: 'openai', baseUrl: apiBase + '/v1', apiKey: 'loopback-only', endpoint: { api: 'openai-completions', baseUrl: apiBase + '/v1', checkedAt: Date.now() }, supportsToolCall: true, supportsImages: false, supportsReasoning: false, contextWindowTokens: 32000, maxOutputTokens: 1024, tokenBudgetVersion: 2 }) async function start(model, permissionMode = 'full-access') { models.set(model, { stage: 0, childActive: 0, peak: 0, held: false, rootAfterOff: false }) await invoke('models', 'save', profile(model)) const workspace = await invoke('workspace', 'attach', workspaceRoot) const session = await invoke('session', 'create', model, { modelId: model, workspaceId: workspace.attachedWorkspaceId }) const run = await invoke('chat', 'send', { sessionId: session.id, text: 'ROOT_ONLY_SECRET_MARKER ' + model, permissionMode }) return { session, run } } async function finish(session) { return until(async () => { const assistant = (await invoke('session', 'messages', session.id)).filter(message => message.role === 'assistant').at(-1) return assistant && assistant.status !== 'streaming' && !await invoke('chat', 'activeRun', session.id) ? assistant : null }, 'terminal assistant ' + session.id, 1600) } async function taskWindow(session) { await invoke('session', 'openWindow', session.id) const window = await until(() => BrowserWindow.getAllWindows().find(item => item !== main && item.getTitle() === 'YCode 任务'), 'task window') window.setContentSize(1440, 900) await until(() => read('!!document.querySelector(".composer__area .conversation-team-collaboration__trigger")', window), 'input-area members') window.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push(event.message) }) return window } async function handler(req, res) { try { let raw = ''; for await (const chunk of req) raw += chunk const body = JSON.parse(raw || '{}'), model = body.model, state = models.get(model) assert.ok(state, 'known controlled model') if (req.url !== '/v1/chat/completions') { res.writeHead(404); res.end('{}'); return } const users = body.messages.filter(message => message.role === 'user') const worker = users.some(message => JSON.stringify(message.content).includes('你是 YCode 主智能体委派的执行成员')) const serialized = JSON.stringify(body.messages), toolResults = body.messages.filter(message => message.role === 'tool') requests.push({ model, worker, body }) if (worker) { assert.ok(!serialized.includes('ROOT_ONLY_SECRET_MARKER'), 'child does not inherit private root history') const names = body.tools.map(tool => tool.function.name) assert.ok(!names.includes('spawn_agent') && !names.includes('execute_command') && !names.includes('browser'), 'worker schema is bounded') const isA = serialized.includes('CHILD_A'), write = isA || model.includes('approval') const marker = isA ? 'CHILD_A_RESULT' : 'CHILD_B_RESULT' if (model.includes('held')) { state.held = true return // Held response is aborted by the application's child cancellation. } if (toolResults.length === 0) { state.childActive++; state.peak = Math.max(state.peak, state.childActive) await pause(250) state.childActive-- if (write) { assert.ok(names.includes('write_file'), 'declared writer receives write tools') return sse(res, model, { name: 'write_file', args: { path: model.includes('approval') ? 'approval.txt' : 'agent-a.txt', content: 'WRITTEN_BY_CHILD_A' } }) } assert.ok(!names.includes('write_file'), 'read-only child never receives write tools') return sse(res, model, { name: 'read_file', args: { path: 'fixture.txt' } }) } assert.ok(write ? model.includes('approval') ? serialized.includes('拒绝') || serialized.includes('未批准') : serialized.includes('已写入') : serialized.includes('PROJECT_FIXTURE_CONTENT'), 'actual child tool result returns to model') return sse(res, model, undefined, marker) } if (model.includes('disabled')) { assert.ok(!body.tools.some(tool => tool.function.name === 'spawn_agent'), 'disabled switch hides scheduling') if (state.stage++ === 0) return sse(res, model, { name: 'spawn_agent', args: { name: 'forbidden', task: 'forbidden' } }) assert.ok(serialized.includes('禁用') || serialized.includes('Tool spawn_agent not found'), 'stale spawn name is rejected by host or native tool registry') return sse(res, model, undefined, 'DISABLED_PARENT_COMPLETE') } const held = model.includes('held'), approval = model.includes('approval') if (model.includes('followup')) { const next = state.stage++ if (next === 0) return sse(res, model, { name: 'spawn_agent', args: { name: '持续核对', task: 'CHILD_B 读取 fixture.txt 并报告结果' } }) if (next === 1 || next === 3) return sse(res, model, { name: 'wait_agent', args: { timeoutMs: 30000 } }) if (next === 2) { const content = toolResults[0].content const result = JSON.parse(typeof content === 'string' ? content : content.map(part => part.text || '').join('')) return sse(res, model, { name: 'send_agent_message', args: { id: result.id, message: 'FOLLOWUP_CONTEXT 再核对一次,沿用先前结果' } }) } assert.ok(serialized.includes('CHILD_B_RESULT'), 'follow-up result reaches parent') return sse(res, model, undefined, 'FOLLOWUP_PARENT_COMPLETE') } if (state.stage++ === 0) { assert.ok(body.tools.some(tool => tool.function.name === 'spawn_agent'), 'enabled root sees scheduling') return sse(res, model, { name: 'spawn_agent', args: { name: '文件分析 A', task: 'CHILD_A 独立完成任务并报告验收结果', access: held ? 'read-only' : 'write', ...(held ? {} : { files: [approval ? 'approval.txt' : 'agent-a.txt'] }) } }) } if (held || approval) { if (state.stage === 2) return sse(res, model, { name: 'wait_agent', args: { timeoutMs: 30000 } }) if (model.includes('toggle-held')) { state.rootAfterOff = !body.tools.some(tool => tool.function.name === 'spawn_agent') assert.ok(state.rootAfterOff, 'next native model request loses spawn after disabling') } return sse(res, model, undefined, 'CONTROL_PARENT_COMPLETE') } if (state.stage === 2) return sse(res, model, { name: 'spawn_agent', args: { name: '资料核对 B', task: 'CHILD_B 读取 fixture.txt 并报告结果' } }) if (state.stage === 3) return sse(res, model, undefined, 'EARLY_CANDIDATE') assert.ok(serialized.includes('本地调度器已等待子任务') && serialized.includes('CHILD_A_RESULT') && serialized.includes('CHILD_B_RESULT'), 'host requires synthesis with actual child results') return sse(res, model, undefined, 'MAIN_INTEGRATED_RESULT') } catch (error) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: String(error) } })) } } app.whenReady().then(async () => { host = require(path.join(directory, 'host.cjs')) server = http.createServer(handler) await new Promise(done => server.listen(0, '127.0.0.1', done)) apiBase = 'http://127.0.0.1:' + server.address().port phase = 'http-root-and-workers' const model = 'agent-http', config = { api: 'openai-completions', baseUrl: apiBase + '/v1', model, apiKey: 'loopback-only', temperature: 0, maxToolRounds: 10 } models.set(model, { stage: 0, childActive: 0, peak: 0 }) const controller = new AbortController(), events = [] direct = new host.SubagentScheduler({ runId: 'http-parent', workspaceRoot, modelName: model, enabled: true, signal: controller.signal, emit: event => events.push(event), capabilities: { specs: [], execute: async () => undefined }, worker: (task, tools) => host.createSubagentWorker({ task, tools, model: { profile: profile(model), config }, workspaceRoot, parentRunId: 'http-parent', permissionMode: 'full-access', native: false, emitParent: event => events.push(event) }) }) const result = await host.runTurn({ runId: 'http-parent', sessionId: 'http-session', workspaceRoot, history: [], userText: 'ROOT_ONLY_SECRET_MARKER', gateway: new host.Gateway(config), model, maxToolRounds: 10, signal: controller.signal, permissionMode: 'full-access', capabilityTools: direct.tools, beforeFinish: () => direct.beforeFinish(), emit: event => events.push(event) }) check('HTTP: separate model requests execute and integrate both child results', result.content.includes('MAIN_INTEGRATED_RESULT')) check('HTTP: actual child provider requests overlap', models.get(model).peak >= 2) check('HTTP: real delegated write reaches the declared file', await fs.readFile(path.join(workspaceRoot, 'agent-a.txt'), 'utf8') === 'WRITTEN_BY_CHILD_A') await direct.close(); direct = undefined phase = 'native-cancelled-write-drain' const drainModel = 'agent-drain', drainEvents = [], drainController = new AbortController() models.set(drainModel, { stage: 0, childActive: 0, peak: 0 }) let finishWrite, writeStarted = false, workerSettled = false const drainWorker = await host.createSubagentWorker({ task: { id: 'drain-child', name: 'write drain', task: 'CHILD_A', access: 'write', files: ['agent-a.txt'], modelName: drainModel, status: 'running', createdAt: Date.now(), events: [], tools: [] }, tools: { specs: [], filterSpecs: specs => specs.filter(spec => ['read_file', 'list_dir', 'write_file', 'edit_file'].includes(spec.function.name)), async execute(name, args) { if (name !== 'write_file') return undefined writeStarted = true return new Promise(resolve => { finishWrite = async () => { const target = path.join(workspaceRoot, args.path), oldContent = await fs.readFile(target, 'utf8') await fs.writeFile(target, 'DRAINED_WRITE') resolve({ ok: true, content: '已写入', data: { path: target, oldContent, newContent: 'DRAINED_WRITE' } }) } }) } }, model: { profile: profile(drainModel), config: { ...config, model: drainModel } }, workspaceRoot, parentRunId: 'drain-parent', native: true, permissionMode: 'full-access', emitParent() {} }) const draining = drainWorker.run('CHILD_A 写入后报告', drainController.signal, event => drainEvents.push(event)) draining.then(() => { workerSettled = true }, () => { workerSettled = true }) await until(() => writeStarted, 'write already in progress') drainController.abort(); await pause(80) check('native child cancellation does not release an unfinished local write', !workerSettled) await finishWrite(); await draining check('cancelled native child retains actual file before/after data for rewind', drainEvents.some(event => event.type === 'tool_result' && event.call.result?.newContent === 'DRAINED_WRITE')) await drainWorker.dispose() phase = 'production-boot' delete process.env.YCODE_DISABLE_GPU; delete process.env.YCODE_DISABLE_GPU_SANDBOX await import(require('node:url').pathToFileURL(path.resolve('out/main/index.mjs')).href) main = await until(() => BrowserWindow.getAllWindows().find(window => window.getTitle() === 'YCode' && !window.webContents.isLoading()), 'main app') await until(() => read('!!window.ycode'), 'preload') await read('window.__agentEvents=[];window.ycode.chat.onEvent(event=>window.__agentEvents.push({runId:event.runId,type:event.type,sequence:event.sequence}));true') main.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push(event.message) }) phase = 'production-native' await invoke('agents', 'update', { teamManagement: true }) const normal = await start('agent-native') const assistant = await finish(normal.session) check('APP: installed OMP completes the root and both child model conversations', assistant.status === 'done' && assistant.content.includes('MAIN_INTEGRATED_RESULT')) const emitted = await read('window.__agentEvents.filter(event=>event.runId===' + JSON.stringify(normal.run.runId) + ')') check('APP: terminal event follows every child snapshot with a strictly newer sequence', emitted.at(-1).type === 'done' && emitted.every((event, index) => index === 0 || event.sequence > emitted[index - 1].sequence)) const agents = assistant.parts.filter(part => part.type === 'subagent').map(part => part.agent) check('APP: two completed children retain separate transcripts and tool results', agents.length === 2 && agents.every(agent => agent.status === 'completed' && agent.tools.some(tool => tool.status === 'ok'))) check('APP: root and children keep the same selected custom model', agents.every(agent => agent.modelName === 'agent-native')) const nativeCount = requests.filter(request => request.model === 'agent-native').length check('APP: reported usage sums all root and child requests without replacing context usage', assistant.runUsage?.total.totalTokens === nativeCount * 110 && assistant.runUsage.main.totalTokens > 0 && assistant.runUsage.subagents.totalTokens > 0 && assistant.contextUsage?.usage.totalTokens === 110) check('APP: isolated observer transport still reports prepared request categories', assistant.contextBreakdown?.counts.system > 0 && assistant.contextBreakdown?.counts.tools > 0) const disk = JSON.parse(await fs.readFile(path.join(directory, 'data', 'sessions.json'), 'utf8')) check('APP: agent details and aggregated usage survive disk persistence', disk.sessions.find(session => session.id === normal.session.id).messages.some(message => message.id === assistant.id && message.runUsage?.total.totalTokens === nativeCount * 110 && message.parts.filter(part => part.type === 'subagent').length === 2)) const window = await taskWindow(normal.session) check('UI: completed team has one input-area entry and no reply duplicates', await read('document.querySelectorAll(".composer__area .conversation-team-collaboration__trigger").length===1&&!document.querySelector(".chat-scroll .conversation-team-collaboration")&&document.querySelector(".conversation-team-collaboration__trigger").textContent.includes("2 位团队成员")&&!document.querySelector(".yc-subagent")&&!!document.querySelector(".yc-run-usage")', window)) await read('document.querySelector(".conversation-team-collaboration__trigger").click();document.querySelector(".yc-run-usage>summary").click();true', window) await until(() => read('document.querySelectorAll(".conversation-team-collaboration__member").length===2', window), 'two members') await read('document.querySelector(".conversation-team-collaboration__member").click();true', window) await until(() => read('!!document.querySelector(".conversation-member-detail .msg--assistant")', window), 'member conversation detail') check('UI: selected member opens its child conversation in the existing right detail surface', await read('!!document.querySelector("[data-view-id=detail-panel] .conversation-member-detail")&&document.querySelector(".conversation-team-collaboration__member[aria-current=true]")?.dataset.subagentId===document.querySelector(".conversation-member-detail")?.dataset.subagentId', window)) await read('document.querySelector(".conversation-member-detail__assignment-toggle").click();true', window) check('UI: task assignment is separately expandable and child tools use the normal renderer', await read('document.querySelector(".conversation-member-detail__assignment-detail").textContent.includes("CHILD_A")&&!!document.querySelector(".conversation-member-detail .cr-captured-tool")&&!document.querySelector(".yc-subagent__event")', window)) await pause(350) await fs.writeFile(path.join(directory, 'agents-light.png'), (await window.webContents.capturePage(undefined, { stayHidden: true, stayAwake: true })).toPNG()) await read('localStorage.setItem("ycode:theme","dark");window.dispatchEvent(new StorageEvent("storage",{key:"ycode:theme",newValue:"dark"}));true', window) await until(() => read('document.documentElement.dataset.theme==="dark"&&document.querySelector(".chat-pane")?.dataset.crTheme==="dark"', window), 'dark conversation theme') await pause(400) await fs.writeFile(path.join(directory, 'agents-dark.png'), (await window.webContents.capturePage(undefined, { stayHidden: true, stayAwake: true })).toPNG()) window.destroy() phase = 'disabled-host-gate' await invoke('agents', 'update', { teamManagement: false }) const disabled = await start('agent-disabled') const disabledMessage = await finish(disabled.session) check('APP: disabled switch hides tools and rejects an explicit stale tool name', disabledMessage.status === 'done' && disabledMessage.content.includes('DISABLED_PARENT_COMPLETE') && !disabledMessage.parts.some(part => part.type === 'subagent')) phase = 'toggle-during-run' await invoke('agents', 'update', { teamManagement: true }) const toggled = await start('agent-toggle-held') await until(() => models.get('agent-toggle-held').held, 'held child request') await invoke('agents', 'update', { teamManagement: false }) const toggledMessage = await finish(toggled.session) check('APP: switch-off cancels the running native child while the parent continues', toggledMessage.status === 'done' && toggledMessage.parts.some(part => part.type === 'subagent' && part.agent.status === 'cancelled') && models.get('agent-toggle-held').rootAfterOff) phase = 'parent-cancel' await invoke('agents', 'update', { teamManagement: true }) const cancelled = await start('agent-cancel-held') await until(() => models.get('agent-cancel-held').held, 'cancellable child') await invoke('chat', 'cancel', cancelled.run.runId) const cancelledMessage = await finish(cancelled.session) check('APP: cancelling the parent also cancels its native children', cancelledMessage.status === 'cancelled' && cancelledMessage.parts.some(part => part.type === 'subagent' && part.agent.status === 'cancelled')) phase = 'individual-cancel' const single = await start('agent-single-held') await until(() => models.get('agent-single-held').held, 'individual child') const snapshot = await invoke('chat', 'activeRun', single.session.id) const childId = snapshot.messages.at(-1).parts.find(part => part.type === 'subagent').agent.id check('APP: child cancel IPC rejects an unknown identifier', !(await invoke('chat', 'cancelAgent', single.run.runId, 'unknown')).cancelled) const liveWindow = await taskWindow(single.session) await read('document.querySelector(".conversation-team-collaboration__trigger").click();true', liveWindow) await until(() => read('!!document.querySelector(".conversation-team-collaboration__member[data-status=running]")', liveWindow), 'running member') await read('document.querySelector(".conversation-team-collaboration__member[data-status=running]").click();true', liveWindow) await until(() => read('!!document.querySelector(".conversation-member-detail[data-status=running] .yc-member-stop")', liveWindow), 'live member detail') check('UI: running child is reachable above the input before the parent finishes', await read('!!document.querySelector(".composer__area .conversation-team-collaboration__member[data-status=running]")', liveWindow)) await read('document.querySelector(".yc-member-stop").click();true', liveWindow) const singleMessage = await finish(single.session) check('APP: individual cancellation leaves the parent able to finish', singleMessage.status === 'done' && singleMessage.content.includes('CONTROL_PARENT_COMPLETE')) await until(() => read('!!document.querySelector(".conversation-member-detail[data-status=cancelled]")&&!document.querySelector(".yc-member-stop")&&document.body.textContent.includes("CONTROL_PARENT_COMPLETE")', liveWindow), 'live terminal UI') check('UI: stopping a child updates the open member detail and completes the main reply') await read('document.querySelector(".conversation-member-detail__close").click();true', liveWindow) await until(() => read('!document.querySelector(".conversation-member-detail")', liveWindow), 'close member detail') check('UI: closing member detail leaves the completed parent conversation intact', await read('document.body.textContent.includes("CONTROL_PARENT_COMPLETE")', liveWindow)) liveWindow.destroy() phase = 'default-permission-approval' const approval = await start('agent-approval', 'default-sandbox') const pending = await until(async () => { const message = (await invoke('session', 'messages', approval.session.id)).at(-1) return message.interactions?.find(item => item.kind === 'approval' && item.status === 'pending') }, 'child write approval') check('APP: child write approval appears under the parent run with the child name', pending.runId === approval.run.runId && pending.title.includes('文件分析 A')) await invoke('chat', 'respond', approval.run.runId, pending.id, { kind: 'approval', optionId: 'reject-once' }) const approvedMessage = await finish(approval.session) check('APP: refusing child approval prevents the write and returns control', approvedMessage.status === 'done' && !await fs.stat(path.join(workspaceRoot, 'approval.txt')).then(() => true, () => false)) phase = 'cancel-pending-approval' const approvalCancel = await start('agent-approval-cancel', 'default-sandbox') await until(async () => (await invoke('session', 'messages', approvalCancel.session.id)).at(-1).interactions?.some(item => item.status === 'pending'), 'approval to cancel') await invoke('agents', 'update', { teamManagement: false }) const approvalCancelledMessage = await finish(approvalCancel.session) check('APP: disabling during child approval cancels the prompt without hanging the parent', approvalCancelledMessage.status === 'done' && approvalCancelledMessage.interactions.every(item => item.status !== 'pending') && approvalCancelledMessage.parts.some(part => part.type === 'subagent' && part.agent.status === 'cancelled')) phase = 'native-followup' await invoke('agents', 'update', { teamManagement: true }) const followup = await start('agent-followup') const followupMessage = await finish(followup.session) check('APP: a completed native child accepts follow-up instructions in its own context', followupMessage.status === 'done' && followupMessage.content.includes('FOLLOWUP_PARENT_COMPLETE') && requests.some(request => request.model === 'agent-followup' && request.worker && JSON.stringify(request.body.messages).includes('FOLLOWUP_CONTEXT') && JSON.stringify(request.body.messages).includes('CHILD_B_RESULT'))) phase = 'delete-native-history' const transcriptDirectory = path.join(directory, 'data', 'subagent-sessions', 'session-' + Buffer.from(normal.session.id).toString('base64url')) check('APP: native child transcripts belong to the parent session directory', await fs.stat(transcriptDirectory).then(info => info.isDirectory(), () => false)) await invoke('session', 'remove', normal.session.id) check('APP: deleting the parent removes its native child transcripts', !await fs.stat(transcriptDirectory).then(() => true, () => false)) check('UI: no renderer errors', rendererErrors.length === 0) phase = 'done' await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, requestCounts: [...models.keys()].map(model => ({ model, requests: requests.filter(row => row.model === model).length })), rendererErrors }, null, 2)) console.log('SUBAGENT_APP_PASS ' + directory) server.closeAllConnections(); server.close(); app.quit() }).catch(async error => { console.error(error) await direct?.close().catch(() => undefined) await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'FAIL', phase, error: String(error.stack || error), checks, requests }, null, 2)) server?.closeAllConnections(); server?.close(); app.exit(1) }) }