feat(storage): move desktop SQLite ownership into a storage utility process

Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.

- Remove the Main readWithFallback path and the supervision worker's
  separate business write connection; manifests are scanned read-only and
  committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
  unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
  ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
  storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
  directories and reclaim confirmed stale ones; stop writing duplicate
  parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
  acceptance fixtures, and maintenance/architecture documentation.

Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
This commit is contained in:
mesalogo
2026-10-06 15:40:18 +08:00
parent bc677a3718
commit d80d42b032
229 changed files with 13632 additions and 3800 deletions
+169 -2
View File
@@ -1,6 +1,9 @@
// @vitest-environment node
import { readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { join } from 'node:path'
import { dirname, join, resolve } from 'node:path'
import { ESLint } from 'eslint'
import { describe, expect, it } from 'vitest'
interface ThresholdRow {
@@ -21,7 +24,8 @@ interface EvaluateResult {
interface PerfThresholds {
requiredStatus: string
scenarios: Record<string, Record<string, { max?: number } | boolean>>
maxRendererCpuThrottle: number
scenarios: Record<string, Record<string, { max?: number; min?: number } | boolean>>
}
interface PerfCheckerModule {
@@ -32,6 +36,169 @@ interface PerfCheckerModule {
const require = createRequire(import.meta.url)
const checker = require('../build/check-perf-thresholds.cjs') as PerfCheckerModule
const thresholds = JSON.parse(readFileSync(checker.defaultThresholdsPath, 'utf8')) as PerfThresholds
const root = resolve(dirname(checker.defaultThresholdsPath), '..')
const committed = (path: string) => execFileSync('git', ['show', `HEAD:${path}`], { cwd: root, encoding: 'utf8' })
function assertThresholdRatchet(current: PerfThresholds, baseline: PerfThresholds) {
expect(current.requiredStatus).toBe(baseline.requiredStatus)
expect(current.maxRendererCpuThrottle).toBeLessThanOrEqual(baseline.maxRendererCpuThrottle)
for (const [id, limits] of Object.entries(baseline.scenarios)) {
const next = current.scenarios[id]
expect(next, `missing scenario ${id}`).toBeDefined()
if (!next) throw new Error(`Missing scenario ${id}`)
if (limits.optional !== true) expect(next.optional).not.toBe(true)
for (const [metric, bound] of Object.entries(limits)) {
if (metric === 'optional' || typeof bound === 'boolean') continue
const value = next[metric]
expect(value, `missing metric ${id}.${metric}`).toBeDefined()
if (typeof value !== 'object') throw new Error(`Invalid bound ${id}.${metric}`)
if (bound.max !== undefined) {
expect(Number.isFinite(value.max)).toBe(true)
expect(value.max).toBeLessThanOrEqual(bound.max)
}
if (bound.min !== undefined) {
expect(Number.isFinite(value.min)).toBe(true)
expect(value.min).toBeGreaterThanOrEqual(bound.min)
}
}
}
}
// These were execution owners when SA-07 replaced the import-only guard. Older
// commits have no context list; subsequent baselines use their committed list.
const initialContexts = [
'src/agent-daemon/agent-owned-acp-prompt.ts',
'src/agent-daemon/daemon.ts',
'src/agent-daemon/direct-linux-stdio-process-owner.ts',
'src/agent-daemon/index.ts',
'src/agent-daemon/runtime-composition.ts',
'src/main/desktop-storage-owner.ts',
'src/main/desktop-storage-files.ts',
'src/main/desktop-storage-runtime-operations.ts',
'src/main/desktop-storage-entry.ts',
'src/main/desktop-storage-runtime-host-fixture.ts',
'src/main/readonly-query-worker.ts',
'src/main/assistant-storage-worker.ts',
'src/main/agent/private-sqlite-database.ts'
]
describe('SA-07 architecture guards', () => {
const eslint = new ESLint({ cwd: root })
const lint = async (code: string, filePath = 'src/main/sa07-probe.ts') => {
const [result] = await eslint.lintText(code, { filePath: resolve(root, filePath) })
expect(result!.fatalErrorCount, code).toBe(0)
return result!.messages.filter(message => message.ruleId === 'architecture/sqlite-execution-context')
}
const boundaries = [
['node:sqlite', 'DatabaseSync'],
['../shared/node/private-sqlite-database', 'openPrivateSqliteDatabase'],
['../shared/node/private-sqlite-database', 'PreparedPrivateSqliteDatabaseFile'],
['./agent/private-sqlite-database', 'openPrivateSqliteDatabase'],
['./assistant/assistant-database', 'AssistantDatabase'],
['./knowledge/knowledge-database', 'KnowledgeDatabase'],
['./knowledge/external/external-knowledge-store', 'ExternalKnowledgeStore'],
['./conversation-attachment-storage', 'ConversationAttachmentStorage'],
['./agent/runtime-session-binding-store', 'SqliteRuntimeSessionBindingStore'],
['../agent-daemon/agent-model-gateway', 'AgentModelCallLedger'],
['../agent-daemon/event-journal', 'EventJournal'],
['../agent-daemon/runtime-owner-registry', 'RuntimeOwnerRegistry'],
['../agent-daemon/semantic-prompt-store', 'SemanticPromptStore'],
['../agent-daemon/index.js', 'AgentModelCallLedger'],
['./assistant/assistant-storage-upgrade', 'upgradeAssistantStorage'],
['./desktop-storage-owner', 'DesktopStorageOwner'],
['./desktop-storage-files', 'openDesktopStorageFiles'],
['./desktop-storage-runtime-operations', 'DesktopStorageRuntimeOwner']
]
it.each(boundaries)('restricts value imports from %s in Main, including indirect module access', async (source, name) => {
for (const code of [
`import { ${name} as value } from '${source}';`,
`import * as value from '${source}';`,
`import value from '${source}';`,
`const value = await import('${source}');`,
`const value = await import(\`${source}\`);`,
`const value = require('${source}');`,
`import value = require('${source}');`,
`export { ${name} as value } from '${source}';`,
`export * from '${source}';`,
`export * as value from '${source}';`
]) expect(await lint(code), code).toHaveLength(1)
for (const code of [
`import type { ${name} } from '${source}';`,
`import { type ${name} } from '${source}';`,
`import type * as value from '${source}';`,
`export type { ${name} } from '${source}';`,
`export { type ${name} } from '${source}';`,
`export type * from '${source}';`,
`type Value = import('${source}').${name};`
]) expect(await lint(code), code).toEqual([])
})
it('allows actual owner/worker/test contexts but not similarly named Main files', async () => {
const code = "import { AssistantDatabase } from './assistant/assistant-database'"
for (const file of [...initialContexts, 'src/main/assistant/assistant-storage-upgrade.test.ts', 'src/agent-daemon/daemon.ts']) {
expect(await lint(code, file), file).toEqual([])
}
for (const file of ['src/main/index.ts', 'src/main/fake-worker.ts', 'src/main/assistant-storage-startup.ts', 'src/renderer/src/probe.ts']) {
expect(await lint(code, file), file).toHaveLength(1)
}
expect(await lint("import { MemoryRuntimeSessionBindingStore } from './agent/runtime-session-binding-store'")).toEqual([])
})
it('removes the paged output sync-fs exception and globally ignores generated temp files', async () => {
const [result] = await eslint.lintText("import { readFileSync } from 'node:fs'", {
filePath: resolve(root, 'src/main/agent/paged-output-store.ts')
})
expect(result!.messages.some(message => message.ruleId === 'no-restricted-imports')).toBe(true)
expect(await eslint.isPathIgnored(resolve(root, 'temp/generated.test.ts'))).toBe(true)
const { default: config } = await import('../vitest.config')
expect(config.test?.exclude).toContain('temp/**')
expect(config.test?.projects).toEqual(expect.arrayContaining([
expect.objectContaining({ extends: true, test: expect.objectContaining({ name: 'unit' }) }),
expect.objectContaining({ extends: true, test: expect.objectContaining({ name: 'integration' }) })
]))
})
it('only shrinks allowlists and the App ceiling relative to local committed HEAD', () => {
const current = readFileSync(join(root, 'eslint.config.js'), 'utf8')
const baseline = committed('eslint.config.js')
const list = (source: string, name: string) => {
const match = source.match(new RegExp(`const ${name} = \\[([\\s\\S]*?)\\]`))
if (!match) return undefined
return [...match[1]!.matchAll(/'([^']+)'/g)].map(entry => entry[1]!)
}
for (const name of ['databaseSyncAllowlist', 'mainSyncFsAllowlist', 'rendererIpcSubscriptionAllowlist', 'sqliteExecutionContexts']) {
const before = list(baseline, name) ?? (name === 'sqliteExecutionContexts' ? initialContexts : undefined)
const after = list(current, name)
expect(before, name).toBeDefined()
expect(after, name).toBeDefined()
expect(after!.filter(file => !before!.includes(file)), `${name} additions`).toEqual([])
}
const ceiling = (source: string) => Number(source.match(/const appTsxMaxLines = (\d+)/)?.[1])
expect(ceiling(current)).toBeLessThanOrEqual(ceiling(baseline))
})
it('only tightens committed performance ceilings while allowing new checks', () => {
assertThresholdRatchet(thresholds, JSON.parse(committed('build/perf-thresholds.json')))
const baseline: PerfThresholds = { requiredStatus: 'passed', maxRendererCpuThrottle: 1, scenarios: { sample: { latency: { max: 10 } } } }
const tightened = structuredClone(baseline)
tightened.scenarios.sample!.latency = { max: 5 }
tightened.scenarios.newScenario = { latency: { max: 20 } }
assertThresholdRatchet(tightened, baseline)
for (const mutate of [
(value: PerfThresholds) => { value.scenarios.sample!.latency = { max: 11 } },
(value: PerfThresholds) => { delete value.scenarios.sample!.latency },
(value: PerfThresholds) => { delete value.scenarios.sample },
(value: PerfThresholds) => { value.scenarios.sample!.optional = true },
(value: PerfThresholds) => { value.maxRendererCpuThrottle = 4 }
]) {
const loosened = structuredClone(baseline)
mutate(loosened)
expect(() => assertThresholdRatchet(loosened, baseline)).toThrow()
}
})
})
// A report shaped like tests/support/app-perf-driver.mjs output, with values
// close to the 2026-10-02 non-throttled baseline.
+16 -5
View File
@@ -1,8 +1,7 @@
// @vitest-environment node
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
import { copyFile, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { copyFile, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { createServer } from 'vite'
import react from '@vitejs/plugin-react'
@@ -12,10 +11,14 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
import { SupervisionStoryStore } from '../src/main/assistant/supervision-stories'
import { SupervisionExperienceStore } from '../src/main/assistant/supervision-experiences'
it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
it(process.env.GOODBUDDY_STORY_GRAPH_FOCUS
? 'focuses the production 3D story camera through canvas and sidebar selection'
: process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
? 'renders the production supervision card at narrow widths in both themes'
: 'renders the production supervisor at desktop, narrow, and mobile widths', async () => {
const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-supervisor-'))
const temporaryRoot = resolve('temp/story-graph-focus')
await mkdir(temporaryRoot, { recursive: true })
const directory = await mkdtemp(join(temporaryRoot, 'supervisor-'))
// Optional local acceptance run against an online backup. Never initialize/migrate or run models.
let portable: string | undefined
if (process.env.GOODBUDDY_SUPERVISOR_BACKUP) {
@@ -48,6 +51,11 @@ it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
{
name: 'supervisor-fixture',
configureServer(server) {
if (process.env.GOODBUDDY_STORY_GRAPH_FOCUS) server.middlewares.use('/focus-export.json', async (_request, response) => {
const source = await readFile('docs/features/story-graph/3d-demo-data.js', 'utf8')
response.setHeader('Content-Type', 'application/json')
response.end(source.slice(source.indexOf('window.STORY_DATA = ') + 'window.STORY_DATA = '.length).trim().replace(/;$/, ''))
})
if (portable) server.middlewares.use('/portable-review.json', (_request, response) => {
response.setHeader('Content-Type', 'application/json')
response.end(portable)
@@ -73,6 +81,7 @@ it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
await server.listen()
const env: NodeJS.ProcessEnv = {
...process.env,
TEMP: directory, TMP: directory, TMPDIR: directory,
GOODBUDDY_SUPERVISOR_URL:
server.resolvedUrls!.local[0] + 'supervisor.html' + (process.env.GOODBUDDY_SUPERVISOR_SIDEBAR ? '?sidebar=1' : ''),
GOODBUDDY_SUPERVISOR_DIRECTORY: directory,
@@ -89,6 +98,7 @@ it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
driver
)
await copyFile(resolve('tests/support/supervisor-selection-driver.mjs'), join(directory, 'supervisor-selection-driver.mjs'))
await copyFile(resolve('tests/support/story-graph-focus-driver.mjs'), join(directory, 'story-graph-focus-driver.mjs'))
const child = spawn(createRequire(import.meta.url)('electron'), [driver], {
env,
stdio: ['ignore', 'pipe', 'pipe']
@@ -107,7 +117,8 @@ it(process.env.GOODBUDDY_SUPERVISOR_SIDEBAR
child.once('error', reject)
})
expect(code, output).toBe(0)
console.log(output)
if (process.env.GOODBUDDY_STORY_GRAPH_FOCUS) process.stdout.write(output)
else console.log(output)
} finally {
clearTimeout(timeout)
}
+289
View File
@@ -0,0 +1,289 @@
import { mkdirSync, appendFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { performance } from 'node:perf_hooks'
import { setTimeout as sleep } from 'node:timers/promises'
import process from 'node:process'
// Uses the existing full-App driver and production preload. No mock bridge.
const DRAIN_BOUND_MS = 180_000
// SubagentScheduler default concurrency; offered children above it queue by design.
const PRODUCT_SUBAGENT_CAP = 3
export async function runConcurrentAcceptance(h) {
const { app, win, js, waitFor, settle, clickSelector, typeText, measure, seed, report, writeReport, directory, artifacts, composer, sendButton, stopButton } = h
const mode = process.env.GB_PERF_MODE
if (!['C01', 'C02', 'C03', 'C04', 'C06', 'C07'].includes(mode)) throw new Error(`${mode}: fixture not implemented; no acceptance result`)
const pressure = mode === 'C02' || mode === 'C06'
const load = { projects: pressure ? 4 : 3, sessions: pressure ? 8 : 4, subtasks: pressure ? 16 : 8, fragmentIntervalMs: 16, toolUpdateIntervalMs: 1000, toolOutputBytes: 4096 }
report.acceptance = { mode, load, status: 'running', cycles: [], gaps: [
'Periodic 4 KiB tool updates are not yet implemented; offered text and delegated child requests are measured separately.',
'Storage queue wait/execute decomposition, cache/subscription counts and temporary-file accounting are not instrumented.',
'C05 storage fault injection remains outside this task.',
'C03 review publication requires an existing persisted review result; this run records whether one is available.'
] }
report.config.longConversationMessages = pressure ? 10_000 : 2_000
report.pending = 'concurrent-seed'
writeReport()
const seeded = await seed()
report.seed = { conversations: report.config.seedConversations, messagesPerConversation: report.config.seedMessagesPerConversation, longConversationMessages: report.config.longConversationMessages, seedMs: seeded.seedMs }
const projects = []
for (let index = 0; index < load.projects; index++) {
const rootPath = join(directory, `project-${index}`)
mkdirSync(rootPath, { recursive: true })
projects.push(await js(`window.goodbuddy.projects.create(${JSON.stringify({ name: `Concurrent ${index}`, description: 'Deterministic architecture acceptance', rootPath, runtimeSelection: { provider: 'model' } })})`))
}
win.webContents.reload()
await waitFor(`!!document.querySelector(${JSON.stringify(composer)})`, 60_000)
await sleep(1500)
await js(`(() => {
const state = window.__concurrent = { requests: {}, children: {}, peakChildren: 0, peakParents: 0, events: 0, errors: [] };
state.dispose = window.goodbuddy.agent.onEvent(event => {
state.events++;
const request = state.requests[event.requestId] ||= { events: 0, chars: 0, started: performance.now(), done: false };
request.events++;
if (event.type === 'text') request.chars += event.delta.length;
if (event.type === 'error') state.errors.push({ requestId: event.requestId, message: event.message });
if (event.type === 'done' || event.type === 'error') { request.done = true; request.settled = performance.now(); }
if (event.type === 'subagent') {
const id = event.childTaskId;
state.children[id] = { state: event.state, requestId: event.requestId };
state.peakChildren = Math.max(state.peakChildren, Object.values(state.children).filter(c => c.state === 'running').length);
}
state.peakParents = Math.max(state.peakParents, Object.values(state.requests).filter(r => !r.done).length);
});
return true;
})()`)
// insertText is dropped if focus has not settled yet; confirm it landed.
const enterPrompt = async prompt => {
for (let attempt = 0; attempt < 3; attempt++) {
await clickSelector(composer, { scroll: false })
await win.webContents.insertText(prompt)
const landed = await waitFor(`(document.querySelector(${JSON.stringify(composer)})?.value ?? '').includes(${JSON.stringify(prompt)})`, 2_000).then(() => true).catch(() => false)
if (landed) return
}
}
const measureAvailability = async (name, action) => {
const startedAt = performance.now()
let value
let error
try { value = await action() } catch (caught) { error = String(caught?.message ?? caught) }
return { name, startedAt, availableAt: error ? null : performance.now(), elapsedMs: error ? null : performance.now() - startedAt, error, value }
}
const runC03 = async () => {
const startedAt = performance.now()
await sleep(2_000)
const library = await js(`window.goodbuddy.knowledge.createLibrary(${JSON.stringify({ name: 'C03 deterministic library', description: 'isolated concurrent acceptance', storageMode: 'reference', graphEnabled: true, graphStrategy: 'rules' })})`)
const sourcePath = join(directory, 'c03-knowledge.txt')
writeFileSync(sourcePath, 'C03 deterministic knowledge source\n'.repeat(80))
const libraryId = String(library.id)
const importStarted = performance.now()
let importError
try {
await js(`window.goodbuddy.knowledge.importPaths(${JSON.stringify(libraryId)}, [${JSON.stringify(sourcePath)}], 'rules')`)
} catch (error) { importError = String(error?.message ?? error) }
const graphSource = await js('window.goodbuddy.supervision.overview({})')
const resultId = Array.isArray(graphSource) ? graphSource[0]?.id : undefined
const batches = []
for (let batch = 0; batch < 100; batch += 1) {
const conversationId = projects[batch % projects.length].id
const work = [
js(`window.goodbuddy.conversations.listSummaries()`),
js(`window.goodbuddy.knowledge.search([${JSON.stringify(libraryId)}], ${JSON.stringify(`c03-${batch}`)})`).catch(() => []),
js(`window.goodbuddy.supervision.graph(${JSON.stringify(resultId ? { resultId } : {})})`).catch(error => ({ error: String(error?.message ?? error) }))
]
batches.push({ batch, conversationId, startedAt: performance.now(), results: await Promise.all(work) })
}
report.acceptance.c03 = {
elapsedMs: performance.now() - startedAt,
reviewResultAvailable: Boolean(resultId),
reviewPublishBatches: 0,
knowledgeImport: { attempted: true, elapsedMs: performance.now() - importStarted, error: importError },
overlappingBatches: batches.length,
graphReads: batches.filter(item => !item.results[2]?.error).length,
historyReads: batches.length,
searchCalls: batches.length,
overlapWindowMs: batches.length ? performance.now() - batches[0].startedAt : 0,
status: resultId && !importError ? 'measured-with-gaps' : 'not-fulfilled'
}
writeReport()
}
const runC04 = async () => {
const startedAt = performance.now()
const faultConversation = await js(`window.goodbuddy.conversations.listSummaries().then(items => items[0]?.id)`)
if (!faultConversation) throw new Error('C04 requires a conversation')
await enterPrompt('gb-fault:hang isolate this request')
await waitFor(`!!document.querySelector(${JSON.stringify(sendButton)}) && !document.querySelector(${JSON.stringify(sendButton)}).disabled`, 15_000, 'C04 fault send')
await clickSelector(sendButton, { scroll: false })
await waitFor(`!!document.querySelector(${JSON.stringify(stopButton)})`, 15_000, 'C04 fault started')
await waitFor('Object.keys(window.__concurrent.requests).length > 0', 15_000, 'C04 runtime admitted')
const faultRequestId = await js('Object.keys(window.__concurrent.requests).at(-1)')
const faultStartedAt = performance.now()
await clickSelector('button.new-chat', { scroll: false })
await enterPrompt('gb-concurrent:parent:c04-health execute one healthy task')
await waitFor(`!!document.querySelector(${JSON.stringify(sendButton)}) && !document.querySelector(${JSON.stringify(sendButton)}).disabled`, 15_000, 'C04 peer send')
await clickSelector(sendButton, { scroll: false })
const peerStartedAt = performance.now()
await waitFor(`!!document.querySelector(${JSON.stringify(stopButton)})`, 15_000, 'C04 peer started')
await waitFor('Object.keys(window.__concurrent.requests).length >= 2', 15_000, 'C04 peer admitted')
const peerRequestId = await js('Object.keys(window.__concurrent.requests).at(-1)')
await js(`window.goodbuddy.agent.cancel(${JSON.stringify(faultRequestId)})`)
const cancelledAt = performance.now()
await waitFor(`window.__concurrent.requests[${JSON.stringify(faultRequestId)}]?.done === true`, 30_000, 'C04 fault cancelled')
await waitFor(`!document.querySelector(${JSON.stringify(stopButton)})`, 60_000, 'C04 peer complete')
const [status, summaries, state] = await Promise.all([
js('window.goodbuddy.agent.getStatus()'),
js('window.goodbuddy.conversations.listSummaries()'),
js('({ errors: window.__concurrent.errors, requests: window.__concurrent.requests })')
])
const faultState = { id: faultRequestId, request: state.requests[faultRequestId] }
const peerState = state.requests[peerRequestId]
report.acceptance.c04 = {
elapsedMs: performance.now() - startedAt,
faultStartedMs: faultStartedAt - startedAt,
cancellationObservedMs: cancelledAt - faultStartedAt,
healthyPeerCompletedMs: performance.now() - peerStartedAt,
faultRequestId,
peerRequestId,
peerRequestDone: peerState?.done === true,
peerErrorCount: state.errors.filter(error => error.requestId === peerRequestId).length,
healthyPeerUnaffected: peerState?.done === true && !state.errors.some(error => error.requestId === peerRequestId),
faultRequestCancelled: faultState.request?.done === true,
faultRequestSettled: faultState.request?.done === true,
queryResponsive: Array.isArray(summaries),
statusResponsive: status?.available === true,
cleanupCompleted: faultState.request?.done === true,
observedRequestCount: state.requests ? Object.keys(state.requests).length : 0,
status: faultState.request?.done === true &&
Array.isArray(summaries) &&
status?.available === true &&
state.requests && Object.keys(state.requests).length >= 2 && peerState?.done === true
? 'measured'
: 'not-fulfilled'
}
writeReport()
}
const runC07 = async () => {
const coldStartedAt = performance.now()
const cold = {
input: await measureAvailability('input', () => waitFor(`!!document.querySelector(${JSON.stringify(composer)})`)),
history: await measureAvailability('history', () => js('window.goodbuddy.conversations.listSummaries()')),
supervisor: await measureAvailability('supervisor-overview', () => js('window.goodbuddy.supervision.overview({})')),
graph: await measureAvailability('graph', () => js('window.goodbuddy.supervision.graph({})')),
knowledge: await measureAvailability('knowledge-snapshot', () => js('window.goodbuddy.knowledge.getSnapshot()'))
}
win.webContents.reload()
await waitFor(`!!document.querySelector(${JSON.stringify(composer)})`, 60_000, 'C07 warm composer')
const warm = {
input: await measureAvailability('input', () => waitFor(`!!document.querySelector(${JSON.stringify(composer)})`)),
history: await measureAvailability('history', () => js('window.goodbuddy.conversations.listSummaries()')),
supervisor: await measureAvailability('supervisor-overview', () => js('window.goodbuddy.supervision.overview({})')),
graph: await measureAvailability('graph', () => js('window.goodbuddy.supervision.graph({})')),
knowledge: await measureAvailability('knowledge-snapshot', () => js('window.goodbuddy.knowledge.getSnapshot()'))
}
report.acceptance.c07 = { cold: { elapsedMs: performance.now() - coldStartedAt, stages: cold }, warm: { stages: warm }, status: 'measured-with-gaps', sourceBody: 'not available without a persisted supervision source' }
writeReport()
}
if (mode === 'C03') await runC03()
if (mode === 'C04') await runC04()
if (mode === 'C07') await runC07()
if (['C03', 'C04', 'C07'].includes(mode)) {
report.acceptance.status = mode === 'C04'
? report.acceptance.c04?.status ?? 'not-fulfilled'
: 'not-fulfilled'
// C04 is a correctness scenario: cancellation settles and the healthy peer finishes.
report.status = mode === 'C04' && report.acceptance.c04?.healthyPeerUnaffected && report.acceptance.status === 'measured' ? 'passed' : report.acceptance.status
report.pending = null
await js('true')
await h.screenshot(`concurrent-${mode.toLowerCase()}-final`)
return
}
const selectProject = async index => {
await clickSelector('.project-switcher__trigger', { scroll: false })
await waitFor(`!![...document.querySelectorAll('[role=menuitemradio]')].find(e => e.textContent.includes('Concurrent ${index}'))`)
await js(`(() => { const e = [...document.querySelectorAll('[role=menuitemradio]')].find(e => e.textContent.includes('Concurrent ${index}')); e.dataset.perfTarget = 'true'; return true; })()`)
await clickSelector('[data-perf-target=true]')
await waitFor(`document.querySelector('.project-switcher__trigger')?.textContent.includes('Concurrent ${index}')`)
await settle()
}
const startCycle = async cycle => {
for (let index = 0; index < load.sessions; index++) {
await selectProject(index % projects.length)
await clickSelector('button.new-chat', { scroll: false })
await enterPrompt(`gb-concurrent:parent:c${cycle}-s${index} execute two delegated tasks`)
await waitFor(`!!document.querySelector(${JSON.stringify(sendButton)}) && !document.querySelector(${JSON.stringify(sendButton)}).disabled`)
await clickSelector(sendButton, { scroll: false })
await sleep(50)
}
}
const started = performance.now()
// C06: 5 minutes of repeated create/complete cycles; long enough to expose
// growth trends without a fixed 30-minute requirement.
const requiredMs = mode === 'C06' ? 300_000 : 150_000
report.acceptance.startedAt = new Date().toISOString()
report.acceptance.requiredMs = requiredMs
let cycle = 0
do {
await startCycle(cycle)
if (cycle === 0) {
report.pending = 'concurrent-warmup-30s'
writeReport()
await sleep(30_000)
}
const result = await measure(`${mode}-cycle-${cycle}`, 'Native typing while production parent and child requests execute', async () => {
const until = performance.now() + (mode === 'C06' ? 30_000 : 30_000)
let samples = 0
while (performance.now() < until) {
await clickSelector(composer, { scroll: false })
await typeText('abcdefghijklmnopqrstuvwxyz')
samples += 26
const state = await js(`(() => { const s = window.__concurrent; return { requests: s.requests, children: s.children, peakChildren: s.peakChildren, peakParents: s.peakParents, events: s.events, errors: s.errors }; })()`)
appendFileSync(join(artifacts, 'heartbeat.jsonl'), JSON.stringify({ at: new Date().toISOString(), elapsedMs: performance.now() - started, cycle, samples, state, pids: app.getAppMetrics() }) + '\n')
if (Object.values(state.requests).every(request => request.done) && performance.now() < until - 10_000) {
await startCycle(++cycle)
}
}
return { fields: { nativeKeySamplesOffered: samples } }
})
const drainStarted = performance.now()
// Children run behind the product subagent cap (3), so 16 delegated 12 s children
// need over a minute. The bound is identical for baseline and candidate; the
// measured drainMs is the throughput comparison.
const drained = await waitFor('Object.values(window.__concurrent.requests).every(r => r.done)', DRAIN_BOUND_MS, 'production request drain').then(() => true).catch(() => false)
const drainMs = performance.now() - drainStarted
const state = await js(`(() => { const { requests, children, peakChildren, peakParents, events, errors } = window.__concurrent; return { requests, children, peakChildren, peakParents, events, errors }; })()`)
const persisted = await js('window.goodbuddy.conversations.listSummaries().then(items => items.length)')
report.acceptance.cycles.push({ cycle, drained, drainMs, state, persistedSummaries: persisted, heapMB: await h.retainedHeapMB(), metrics: result.id })
report.acceptance.elapsedMs = performance.now() - started
writeReport()
cycle++
} while (performance.now() - started < requiredMs)
report.acceptance.fullDurationCompleted = performance.now() - started >= requiredMs
report.acceptance.elapsedMs = performance.now() - started
const state = await js('({peakChildren: window.__concurrent.peakChildren, peakParents: window.__concurrent.peakParents})')
report.acceptance.achieved = state
report.acceptance.budgets = report.scenarios.map(s => ({ id: s.id, input: s.latency?.count >= 100 && s.latency.p95 <= 16, frame: s.renderer.frames.p95 <= 17, longTasks: s.renderer.longTasks.count === 0, mainP99: s.main.eventLoopDelay.p99 <= 30, mainSingleBlock: 'not measured; monitor delay is not single-block duration' }))
// Release gate: correctness only. Budgets above are reported as target gaps;
// regressions are judged by alternating baseline/candidate runs, not here.
const cycles = report.acceptance.cycles
const errors = cycles.flatMap(item => item.state.errors ?? [])
report.acceptance.correctness = {
allCyclesDrained: cycles.length > 0 && cycles.every(item => item.drained),
errors: errors.length,
peakParents: state.peakParents, peakChildren: state.peakChildren,
childCap: Math.min(load.subtasks, PRODUCT_SUBAGENT_CAP),
drainMs: cycles.map(item => Math.round(item.drainMs)),
reachedLoad: state.peakParents >= load.sessions && state.peakChildren >= Math.min(load.subtasks, PRODUCT_SUBAGENT_CAP),
fullDuration: report.acceptance.fullDurationCompleted
}
const gate = report.acceptance.correctness
report.acceptance.status = gate.allCyclesDrained && gate.errors === 0 && gate.reachedLoad && gate.fullDuration ? 'correctness-passed' : 'not-fulfilled'
report.status = report.acceptance.status === 'correctness-passed' ? 'passed' : 'not-fulfilled'
report.pending = null
await js('window.__concurrent.dispose(); true')
await h.screenshot('concurrent-final')
}
+10
View File
@@ -264,6 +264,7 @@ async function measure(id, description, body) {
}
},
processes: {
pids: app.getAppMetrics().map(metric => ({ pid: metric.pid, type: metric.type, cpu: metric.cpu.percentCPUUsage, workingSetMB: metric.memory.workingSetSize / 1024 })),
cpu: Object.fromEntries(Object.entries(processes).map(([type, value]) => [type, value.cpu])),
workingSetMB: Object.fromEntries(Object.entries(processes).map(([type, value]) => [type, Math.round(value.workingSetMB)]))
},
@@ -511,6 +512,15 @@ try {
await applyCpuThrottle()
await sleep(1_500)
if (process.env.GB_PERF_MODE?.startsWith('C')) {
const { runConcurrentAcceptance } = await import('./app-perf-concurrent.mjs')
await runConcurrentAcceptance({ app, win, js, waitFor, settle, clickSelector, typeText, measure, seed, report, writeReport, screenshot, directory, artifacts, composer, sendButton, stopButton, retainedHeapMB })
clearTimeout(deadline)
writeReport()
app.exit(report.status === 'passed' ? 0 : 1)
return
}
const idle = await measure('idle', 'Fresh profile, no interaction for 3 s', async () => { await sleep(3_000) })
frameBaseline = idle.renderer.frames.p50 ?? 16.7
@@ -0,0 +1,35 @@
import type { AssistantDatabase } from '../../src/main/assistant/assistant-database'
import { createSupervisionDomainPorts } from '../../src/main/assistant/supervision-domain-ports'
import type { DesktopStorageClient } from '../../src/main/desktop-storage-client'
import { storageDataBytes } from '../../src/main/desktop-storage-contracts'
import type { ReadonlyQueryReader } from '../../src/main/readonly-query-reader'
import type { ReviewState } from '../../src/main/assistant/supervision-review-store'
/** Exercise the real caller facades and repository commits across an asynchronous data boundary. */
export function asyncSupervisionStorage(database: AssistantDatabase, before?: (domain: string, method: string, args: unknown[]) => Promise<void>, reader?: ReadonlyQueryReader) {
const call = async (domain: string, method: string, args: unknown[], options?: { signal?: AbortSignal }) => {
storageDataBytes(args)
const data = structuredClone(args)
if (before) await before(domain, method, data)
options?.signal?.throwIfAborted()
if (domain === 'assistant' && method === 'supervisionContext') {
if (reader) return reader.call('reviewContext', data, options?.signal)
const { scope } = data[0] as ReviewState['request']
return database.readSnapshot(() => ({ summary: database.reviewSummary(scope, 'supervisor'), background: database.reviewBackground(scope) }))
}
if (domain === 'assistant' && method === 'listSupervisionCandidates' && reader) return reader.call('reviewCandidates', data, options?.signal)
if (domain === 'review' && reader && (method === 'initialize' || method === 'resume')) {
const store = database.supervisionReviewStore()
const runId = String(data[0])
return method === 'initialize'
? store.initializeWithReader(runId, data[1] as ReviewState, reader.reviewManifest(runId), options?.signal)
: store.resumeWithReader(runId, reader.reviewManifest(runId), options?.signal)
}
const target = domain === 'assistant' ? database : domain === 'review' ? database.supervisionReviewStore(options?.signal)
: domain === 'stories' ? database.supervisionStories() : domain === 'experiences' ? database.supervisionExperiences()
: database.supervisionSuggestions()
const operation = (target as unknown as Record<string, (...input: unknown[]) => unknown>)[method]!
return structuredClone(await operation.apply(target, data))
}
return createSupervisionDomainPorts({ call: call as DesktopStorageClient['call'] })
}
+1 -1
View File
@@ -94,7 +94,7 @@ void app.whenReady().then(async () => {
const result = await supervisor.run(request, run.id)
return { status: result.status ?? 'completed', runId: result.runId }
} })
const config = heartbeat.create({ name: 'Isolated live test', scope: request.scope, timezone: 'UTC',
const config = await heartbeat.create({ name: 'Isolated live test', scope: request.scope, timezone: 'UTC',
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 }, now)
let tick = 0
const due = async () => {
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
import { z } from 'zod'
import { embeddingStorageProvider } from '../../src/main/knowledge/embedding-provider-key'
import { KnowledgeService } from '../../src/main/knowledge/knowledge-service'
import { TestKnowledgeService as KnowledgeService } from './knowledge-test-service'
import { knowledgeRetrievalTerms } from '../../src/main/knowledge/retrieval-text'
import { isPathInside } from '../../src/main/workspace-file-access'
import type { EmbeddingProvider } from '../../src/main/knowledge/types'
@@ -488,22 +488,22 @@ async function createSeededService(
embeddingProvider: provider
})
await service.initialize()
const library = service.createLibrary({
const library = (await service.createLibrary({
id: 'library-retrieval-eval',
name: 'Synthetic retrieval evaluation',
storageMode: 'reference',
graphEnabled: false
})
const source = service.database.upsertSource({
}))
const source = (await service.database.upsertSource({
id: 'source-retrieval-eval',
knowledgeBaseId: library.id,
type: 'file',
location: 'fixture://synthetic-bilingual-v1',
displayName: 'Synthetic fixture',
status: 'ready'
})
}))
for (const document of fixture.documents) {
service.database.upsertDocument(
await service.database.upsertDocument(
{
id: document.id,
knowledgeBaseId: library.id,
@@ -520,7 +520,7 @@ async function createSeededService(
role: 'standalone' as const
}))
)
service.database.replaceDocumentEmbeddings(
await service.database.replaceDocumentEmbeddings(
document.id,
embeddingStorageProvider(provider),
provider.model,
+29
View File
@@ -0,0 +1,29 @@
import { KnowledgeService, type KnowledgeServiceOptions } from '../../src/main/knowledge/knowledge-service'
import { KnowledgeDatabase } from '../../src/main/knowledge/knowledge-database'
import { createKnowledgeStoragePort } from '../../src/main/knowledge/knowledge-storage-port'
import { publishStorageDocument } from '../../src/main/desktop-storage-publication'
import { replaceDocumentGraph } from '../../src/main/desktop-storage-knowledge-operations'
import type { DesktopStorageClient } from '../../src/main/desktop-storage-client'
/** Tests own their connection; production composition injects the storage client. */
export class TestKnowledgeService extends KnowledgeService {
private readonly owner: KnowledgeDatabase
constructor(options: Omit<KnowledgeServiceOptions, 'database'> & { databasePath: string }) {
const owner = new KnowledgeDatabase(options.databasePath)
owner.initialize()
const call = async (domain: string, method: string, args: unknown[]) => {
if (method === 'publishDocument') return publishStorageDocument(owner, ...args as Parameters<typeof publishStorageDocument> extends [unknown, ...infer A] ? A : never)
if (method === 'replaceDocumentGraph') return replaceDocumentGraph(owner, ...args as Parameters<typeof replaceDocumentGraph> extends [unknown, ...infer A] ? A : never)
const target = domain === 'external' ? owner.externalStore : owner
return (target as unknown as Record<string, (...args: unknown[]) => unknown>)[method]!.apply(target, args)
}
const database = createKnowledgeStoragePort({ call: call as DesktopStorageClient['call'] })
super({ ...options, database })
this.owner = owner
}
override async dispose(): Promise<void> {
try { await super.dispose() } finally { this.owner.close() }
}
}
+79
View File
@@ -0,0 +1,79 @@
import { mkdir, mkdtemp, open, rm, stat, type FileHandle } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import type { PagedOutputBackingStore } from '../../src/main/agent/paged-output-store'
export const outputFixtureRoot = resolve('temp/goodbuddy-output-upgrade')
// Real asynchronous file IO for contract tests, not a production storage owner.
export class FileOutputBacking implements PagedOutputBackingStore {
private directory?: Promise<string>
private readonly entries = new Map<string, { owner: string; path: string; file?: FileHandle }>()
writes = 0
maximumWriteBytes = 0
async create(owner: string, handle: string): Promise<void> {
this.directory ??= mkdir(outputFixtureRoot, { recursive: true }).then(() =>
mkdtemp(join(outputFixtureRoot, 'backing-')))
const path = join(await this.directory, handle.replace(':', '-'))
const file = await open(path, 'wx+')
this.entries.set(handle, { owner, path, file })
}
private entry(owner: string, handle: string) {
const entry = this.entries.get(handle)
if (!entry || entry.owner !== owner) throw new Error('Unknown output owner')
return entry
}
async append(owner: string, handle: string, bytes: Uint8Array): Promise<void> {
const { file } = this.entry(owner, handle)
if (!file) throw new Error('Output already finished')
this.writes += 1
this.maximumWriteBytes = Math.max(this.maximumWriteBytes, bytes.byteLength)
for (let offset = 0; offset < bytes.byteLength;) {
const { bytesWritten } = await file.write(bytes, offset, bytes.byteLength - offset)
if (!bytesWritten) throw new Error('Output write made no progress')
offset += bytesWritten
}
}
async finish(owner: string, handle: string, totalBytes: number): Promise<void> {
const entry = this.entry(owner, handle)
await entry.file?.close()
entry.file = undefined
if ((await stat(entry.path)).size !== totalBytes) throw new Error('Output size mismatch')
}
async read(owner: string, handle: string, cursor: number, length: number): Promise<Uint8Array> {
const file = await open(this.entry(owner, handle).path, 'r')
try {
const bytes = Buffer.alloc(length)
let offset = 0
while (offset < length) {
const { bytesRead } = await file.read(bytes, offset, length - offset, cursor + offset)
if (!bytesRead) break
offset += bytesRead
}
return bytes.subarray(0, offset)
} finally {
await file.close()
}
}
async release(owner: string, handle: string): Promise<void> {
if (!this.entries.has(handle)) return
const entry = this.entry(owner, handle)
await entry.file?.close()
entry.file = undefined
await rm(entry.path, { force: true })
this.entries.delete(handle)
}
get count(): number { return this.entries.size }
get createdDirectory(): boolean { return this.directory !== undefined }
async dispose(): Promise<void> {
for (const [handle, entry] of this.entries) await this.release(entry.owner, handle)
if (this.directory) await rm(await this.directory, { recursive: true, force: true })
}
}
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import { join } from 'node:path'
import { writeFile } from 'node:fs/promises'
import { randomUUID, createHash } from 'node:crypto'
import { monitorEventLoopDelay, performance } from 'node:perf_hooks'
import { app } from 'electron'
import { DesktopStorageClient } from '../../src/main/desktop-storage-client'
import { supervisionGraphViewSchema } from '../../src/shared/supervision-contracts'
const root = process.env.GB_PORTABLE_UPGRADE_ROOT!
const work = join(root, 'work')
app.setPath('userData', join(root, 'electron'))
const report: Record<string, unknown> = { stage: 'startup', electron: process.versions.electron }
const options = { assistantPath: join(work, 'assistant.sqlite'), knowledgePath: join(work, 'knowledge.sqlite'),
defaultRootPath: work, userDataPath: work, entryPath: join(root, 'desktop-storage-entry.mjs'),
readerWorkerPath: join(root, 'readonly-query-worker.cjs'), upgradeWorkerPath: join(root, 'assistant-storage-worker.cjs') }
let storage: DesktopStorageClient | undefined
const digest = (value: unknown): string => createHash('sha256').update(JSON.stringify(value)).digest('hex')
async function stage(value: string): Promise<void> {
report.stage = value
await writeFile(join(root, 'fixture-summary.json'), JSON.stringify(report))
}
async function open(): Promise<DesktopStorageClient> {
const client = new DesktopStorageClient(options)
storage = client
await client.ready
return client
}
async function run(): Promise<void> {
await stage('startup')
await app.whenReady()
const started = performance.now()
let client = await open()
report.initializeMs = performance.now() - started
const projects = await client.call('assistant', 'listProjects', [])
assert.ok(projects.length > 0)
const original = await client.call('assistant', 'listSupervisionResults', [20])
assert.ok(original.length > 0, 'real supervision data required')
const graph = await client.call('assistant', 'getSupervisionGraph', [{ resultId: original[0]!.id }])
report.realRead = { projects: projects.length, results: original.length, graphSha256: digest(graph) }
await stage('reopen')
await client.close()
const reopenedAt = performance.now()
client = await open()
report.reopenMs = performance.now() - reopenedAt
assert.equal(digest(await client.call('assistant', 'getSupervisionGraph', [{ resultId: original[0]!.id }])), digest(graph))
if (process.env.GB_PORTABLE_UPGRADE_PHASE === 'exercise') {
await stage('task-event-dedupe')
const taskId = randomUUID()
await client.call('assistant', 'createTask', [{ id: taskId, title: 'Storage validation', instructions: 'Local deterministic validation', status: 'completed' }])
const event = { taskId, bindingId: randomUUID(), operationId: randomUUID(), semanticSequence: '1', eventIndex: 0,
kind: 'output', payload: { text: 'deterministic validation output' } }
assert.equal(await client.call('assistant', 'appendRemoteTaskEventOnce', [event]), true)
assert.equal(await client.call('assistant', 'appendRemoteTaskEventOnce', [event]), false)
await assert.rejects(client.call('assistant', 'appendRemoteTaskEventOnce', [{ ...event, payload: { text: 'conflict' } }]))
assert.equal(await client.call('assistant', 'getHighestCommittedRemoteTaskEventSequence', [event.bindingId, event.operationId]), '1')
await client.close()
client = await open()
assert.equal(await client.call('assistant', 'appendRemoteTaskEventOnce', [event]), false)
assert.equal(await client.call('assistant', 'getHighestCommittedRemoteTaskEventSequence', [event.bindingId, event.operationId]), '1')
report.dedupe = { insert: true, replay: true, conflict: true, reopenReplay: true }
await stage('overlapping-review-reads-publication')
// Fixed, bounded local workload; not the full C03/UI acceptance fixture.
const delay = monitorEventLoopDelay({ resolution: 1 })
const publicationMs: number[] = [], readMs: number[] = []
const at = '2026-10-05T00:00:00.000Z'
let highWater = 0
delay.enable()
const began = performance.now()
await Promise.all([
(async () => {
for (let batch = 0; batch < 100; batch++) {
const time = performance.now()
await client.call('assistant', 'saveSupervisionResult', [{
request: { trigger: 'manual', scope: { kind: 'global' }, timeRange: { from: at, to: at } },
evidence: Array.from({ length: 20 }, (_, index) => ({ id: `source-${index}`, sourceType: 'task' as const,
sourceId: taskId, title: 'Deterministic source', content: 'x'.repeat(2048), occurredAt: at })),
output: { summary: 'Deterministic review', changeDigest: '', openItems: [], entities: [], entityChanges: [], relations: [],
events: Array.from({ length: 20 }, (_, index) => ({ title: 'Deterministic event', description: '', occurredAt: at,
eventType: 'decision' as const, sourceReferenceIds: [`source-${index}`], entityIds: [] })) }
}])
publicationMs.push(performance.now() - time)
highWater = Math.max(highWater, client.pendingCount)
}
})(),
(async () => {
for (let index = 0; index < 100; index++) {
const time = performance.now()
await client.call('assistant', 'getSupervisionGraph', [{ resultId: original[index % original.length]!.id }])
await client.call('assistant', 'listSupervisionResults', [20])
readMs.push(performance.now() - time)
highWater = Math.max(highWater, client.pendingCount)
}
})()
])
const durationMs = performance.now() - began
delay.disable()
const stats = (values: number[]) => { const sorted = [...values].sort((a, b) => a - b); return {
samples: values.length, p50: sorted[Math.ceil(sorted.length * .5) - 1], p95: sorted[Math.ceil(sorted.length * .95) - 1], max: sorted.at(-1) } }
report.performance = { classification: 'single-run source-harness baseline, not C03 acceptance or before/after improvement',
durationMs, publications: stats(publicationMs), reviewReadPairs: stats(readMs), mainDelayP99Ms: delay.percentile(99) / 1e6,
mainDelayMaxMs: delay.max / 1e6, delaySamples: delay.count, pendingHighWaterObserved: highWater, pendingAfter: client.pendingCount,
batches: 100, sourcesPerBatch: 20, sourceBytes: 2048, mainRssBytes: process.memoryUsage().rss }
const published = await client.call('assistant', 'listSupervisionResults', [1])
const publishedGraph = supervisionGraphViewSchema.parse(await client.call('assistant', 'getSupervisionGraph', [{ resultId: published[0]!.id }]))
assert.equal(publishedGraph.events.length, 20)
assert.equal(publishedGraph.sources.length, 20)
const eventIds = new Set(publishedGraph.events.map(event => event.id))
const sourceIds = new Set(publishedGraph.sources.map(source => source.id))
assert.equal(publishedGraph.eventSources.length, 20)
assert.ok(publishedGraph.eventSources.every(link => eventIds.has(link.event_id) && sourceIds.has(link.source_id)))
await client.close()
client = await open()
assert.equal(digest(supervisionGraphViewSchema.parse(await client.call('assistant', 'getSupervisionGraph', [{ resultId: published[0]!.id }]))), digest(publishedGraph))
report.publicationReopen = true
}
await client.close()
storage = undefined
report.stage = 'passed'
}
void run().then(async () => {
await writeFile(join(root, 'fixture-summary.json'), JSON.stringify(report))
app.exit(0)
}, async () => {
// Never serialize an exception: SQLite/IPC errors may contain private content.
try { await storage?.close() } catch { /* Preserve the failing stage. */ }
await writeFile(join(root, 'fixture-summary.json'), JSON.stringify(report))
app.exit(1)
})
+335
View File
@@ -0,0 +1,335 @@
// Run with Node 24+: node tests/support/portable-storage-upgrade.mjs --launch
// --resume reuses an interrupted run's backup; --report prints aggregate evidence.
import assert from 'node:assert/strict'
import process from 'node:process'
import console from 'node:console'
import { Buffer } from 'node:buffer'
import { performance } from 'node:perf_hooks'
import { setTimeout, clearTimeout, setInterval, clearInterval } from 'node:timers'
import { createHash } from 'node:crypto'
import { createReadStream, openSync, closeSync } from 'node:fs'
import { cp, mkdir, readFile, readdir, rm, stat, unlink, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { DatabaseSync, backup } from 'node:sqlite'
import { spawn, execFileSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { cpus, totalmem, platform, release } from 'node:os'
import { build } from 'esbuild'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const source = join(root, 'dist/GoodBuddy-windows-x64/data')
const temporary = join(root, 'temp/goodbuddy-storage-upgrade')
const run = join(temporary, process.argv.includes('--fresh') ? 'fresh-run' : 'run')
const resume = process.argv.includes('--resume')
if (process.argv.includes('--report')) {
const result = JSON.parse(await readFile(join(temporary, 'summary.json'), 'utf8'))
const canonicalContentUnchanged = result.comparison && Object.values(result.comparison).every(value => value.changed.length === 0)
const databases = Object.fromEntries(Object.entries(result.metadata ?? {}).map(([name, metadata]) => [name, {
sourceBytes: result.sourceBefore[name]?.bytes, sourceWalBytes: result.sourceBefore[name + '-wal']?.bytes,
backupBytes: result.backup?.[name]?.file.bytes, workBytes: result.workFiles?.[name]?.bytes,
versionBefore: metadata.version, versionAfter: result.after?.[name]?.version,
freePagesBefore: metadata.freePages, freePagesAfter: result.after?.[name]?.freePages,
tables: Object.fromEntries(Object.entries(metadata.tables).map(([table, entry]) => [table, entry.count])),
snapshotBeforeMs: result.backup?.[name]?.snapshot.durationMs, snapshotAfterMs: result.after?.[name]?.durationMs,
comparison: result.comparison?.[name], integrity: result.finalIntegrity?.[name]
}]))
console.log(JSON.stringify({ startedAt: result.startedAt, environment: result.environment,
status: result.status, canonicalContentUnchanged, failure: result.failure, sourceUnchanged: result.sourceUnchanged, copiesRemoved: result.copiesRemoved,
durationMs: result.durationMs, databases, initialization: result.initialization, exercise: result.exercise,
attachments: result.attachments, notes: result.notesBefore, notesUnchanged: result.notesUnchanged }, null, 2))
process.exit(0)
}
if (process.argv.includes('--launch')) {
await mkdir(temporary, { recursive: true })
const log = openSync(join(temporary, 'execution.log'), 'w')
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '--supervise', ...process.argv.slice(2).filter(arg => arg !== '--launch')], { detached: true, stdio: ['ignore', log, log] })
closeSync(log)
child.unref()
console.log(`Launched verification supervisor ${child.pid}`)
process.exit(0)
}
if (process.argv.includes('--supervise')) {
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...process.argv.slice(2).filter(arg => arg !== '--supervise')], { stdio: 'inherit' })
const timer = setTimeout(() => {
console.log('Overall verification deadline exceeded')
if (process.platform === 'win32') execFileSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { timeout: 10_000, stdio: 'ignore' })
else child.kill('SIGKILL')
}, 900_000)
child.once('error', () => { clearTimeout(timer); process.exit(1) })
child.once('exit', code => { clearTimeout(timer); process.exit(code ?? 1) })
await new Promise(() => {})
}
if (process.argv.includes('--cleanup-interrupted')) {
console.log('Removing interrupted validation copies')
for (const name of ['assistant.sqlite', 'assistant.sqlite-wal', 'assistant.sqlite-shm']) {
try { await unlink(join(run, 'work', name)); console.log(`Removed ${name}`) }
catch (error) { console.log(`Cleanup ${name}: ${error.code}`) }
}
await rm(run, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 })
console.log('Interrupted validation copies removed')
process.exit(0)
}
const names = ['assistant.sqlite', 'knowledge.sqlite', 'conversation-attachments.sqlite', 'remote-runtime-bindings.sqlite']
const quote = value => `"${value.replaceAll('"', '""')}"`
const hash = value => createHash('sha256').update(value).digest('hex')
const canonical = value => JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? String(item) : item instanceof Uint8Array ? Buffer.from(item).toString('base64') : item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item)
async function fingerprint(path) {
const info = await stat(path).catch(error => { if (error.code !== 'ENOENT') throw error })
if (!info) return null
const digest = createHash('sha256')
for await (const chunk of createReadStream(path)) digest.update(chunk)
return { bytes: info.size, mtimeMs: info.mtimeMs, sha256: digest.digest('hex') }
}
async function sourceFingerprint() {
return Object.fromEntries(await Promise.all(names.flatMap(name => ['', '-wal', '-shm'].map(async suffix => [name + suffix, await fingerprint(join(source, name + suffix))]))))
}
async function contentFingerprint(directory) {
const entries = []
async function visit(path, relative) {
for (const entry of await readdir(path, { withFileTypes: true })) {
assert.ok(!entry.isSymbolicLink(), 'file-backed content must be isolated')
const name = join(relative, entry.name)
if (entry.isDirectory()) await visit(join(path, entry.name), name)
else {
const file = await fingerprint(join(path, entry.name))
entries.push({ name, bytes: file.bytes, sha256: file.sha256 })
}
}
}
await visit(directory, '')
return { files: entries.length, bytes: entries.reduce((sum, file) => sum + file.bytes, 0), sha256: hash(canonical(entries.sort((a, b) => a.name.localeCompare(b.name)))) }
}
function inspect(path) {
const db = new DatabaseSync(path, { readOnly: true })
try {
db.exec('BEGIN')
const tables = {}
for (const { name } of db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all()) {
const columns = db.prepare(`PRAGMA table_info(${quote(name)})`).all().map(column => column.name)
tables[name] = { count: db.prepare(`SELECT COUNT(*) AS n FROM ${quote(name)}`).get().n, columns }
}
return { version: db.prepare('PRAGMA user_version').get().user_version,
pageCount: db.prepare('PRAGMA page_count').get().page_count, pageSize: db.prepare('PRAGMA page_size').get().page_size,
freePages: db.prepare('PRAGMA freelist_count').get().freelist_count, tables }
} finally { db.close() }
}
// Hash every existing table/column, including IDs, order columns, provenance and references.
// Sorting row digests makes physical SQLite row order irrelevant without retaining bodies.
function snapshot(path, restoreSubagentPayload) {
const started = performance.now()
const blockDigests = new WeakMap()
const progressDigests = new WeakMap()
const db = new DatabaseSync(path, { readOnly: true })
try {
db.exec('BEGIN')
assert.equal(db.prepare('PRAGMA integrity_check').get().integrity_check, 'ok', 'integrity check failed')
const metadata = inspect(path)
const tables = {}
for (const [name, table] of Object.entries(metadata.tables)) {
console.log(`Hashing ${name}: ${table.count} rows`)
const rows = []
let previousTask, blocks = new Map()
const order = name === 'task_events' ? ' ORDER BY task_id, id' : ''
for (const row of db.prepare(`SELECT * FROM ${quote(name)}${order}`).iterate()) {
if (name === 'task_events' && row.kind === 'subagent') {
if (previousTask !== row.task_id) { blocks = new Map(); previousTask = row.task_id }
const restored = restoreSubagentPayload(JSON.parse(row.payload_json), blocks)
if (Array.isArray(restored?.progress)) {
// Production restoration shares unchanged immutable blocks. Hash their
// canonical content once, preserving the full ordered progress digest.
let progressDigest = progressDigests.get(restored.progress)
if (!progressDigest) {
progressDigest = hash(restored.progress.map(block => {
let digest = blockDigests.get(block)
if (!digest) { digest = hash(canonical(block)); blockDigests.set(block, digest) }
return digest
}).join('\n'))
progressDigests.set(restored.progress, progressDigest)
}
row.payload_json = canonical({ ...restored, progress: { count: restored.progress.length, sha256: progressDigest } })
} else row.payload_json = canonical(restored)
}
rows.push(hash(canonical(row)))
if (rows.length % 100_000 === 0) {
console.log(`Hashed ${name}: ${rows.length}/${table.count}`)
assert.ok(performance.now() - started < 600_000, 'snapshot deadline exceeded')
}
}
tables[name] = { count: table.count, sha256: hash(rows.sort().join('\n')) }
}
const violations = db.prepare('PRAGMA foreign_key_check').all()
return { ...metadata, tables, durationMs: performance.now() - started, integrity: 'ok', foreignKeyViolations: violations.length,
foreignKeyDigest: hash(canonical(violations)) }
} finally { db.close() }
}
if (process.argv.includes('--verify-source')) {
const result = JSON.parse(await readFile(join(temporary, 'summary.json'), 'utf8'))
assert.equal(canonical(await sourceFingerprint()), canonical(result.sourceBefore), 'original database files changed')
if (result.notesBefore) assert.deepEqual(await contentFingerprint(join(source, 'notes')), result.notesBefore, 'original notes changed')
assert.ok(Object.values(result.comparison).every(value => value.changed.length === 0))
console.log('Original DB/WAL/SHM fingerprints and notes still match; saved canonical comparisons have no changed tables')
process.exit(0)
}
const summary = { startedAt: new Date().toISOString(), externalModelCalls: 0,
environment: { platform: platform(), release: release(), cpu: cpus()[0]?.model, memoryBytes: totalmem(), node: process.version,
commit: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', timeout: 10_000 }).trim(), dirtyWorktree: true },
caveat: 'Per-database read-only SQLite online backup includes committed WAL. Sequential databases/files are not a cross-database transaction. Live application writes may change fingerprints; no source checkpoint or initialization is performed.' }
let before
let ownsRun = false
try {
console.log('Fingerprinting original database files')
await mkdir(temporary, { recursive: true })
before = await sourceFingerprint()
summary.sourceBefore = before
await writeFile(join(temporary, 'source-before.json'), JSON.stringify(before, null, 2))
console.log('Inspecting database metadata')
if (resume) summary.metadata = Object.fromEntries(names.map(name => [name, inspect(join(run, 'backup', name))]))
if (process.argv.includes('--inspect')) {
console.log(JSON.stringify(summary, null, 2))
} else {
// Never remove a previous run implicitly: it may belong to another validator.
if (!resume) await mkdir(run)
ownsRun = true
if (!resume) await mkdir(join(run, 'backup'))
if (resume) await rm(join(run, 'work'), { recursive: true, force: true })
await mkdir(join(run, 'work'))
for (const name of names) {
console.log(`${resume ? 'Reusing backup' : 'Backing up'} ${name}`)
if (!resume) {
// Capture main + WAL without letting SQLite touch the original SHM. Require
// stable source fingerprints across capture before using the online backup.
await mkdir(join(run, 'capture'), { recursive: true })
await cp(join(source, name), join(run, 'capture', name))
if (before[name + '-wal']) await cp(join(source, name + '-wal'), join(run, 'capture', name + '-wal'))
assert.deepEqual(await fingerprint(join(source, name)), before[name], 'source changed during capture')
assert.deepEqual(await fingerprint(join(source, name + '-wal')), before[name + '-wal'], 'WAL changed during capture')
const db = new DatabaseSync(join(run, 'capture', name), { readOnly: true })
try { await backup(db, join(run, 'backup', name)) } finally { db.close() }
}
await cp(join(run, 'backup', name), join(run, 'work', name))
}
summary.metadata = Object.fromEntries(names.map(name => [name, inspect(join(run, 'backup', name))]))
assert.ok(summary.metadata['assistant.sqlite'].tables.messages.count > 0, 'real messages required')
// Notes use relative file-backed content. No settings/credential files are copied.
if (await stat(join(source, 'notes')).catch(() => undefined)) {
summary.notesBefore = await contentFingerprint(join(source, 'notes'))
await cp(join(source, 'notes'), join(run, 'work/notes'), { recursive: true })
assert.deepEqual(await contentFingerprint(join(run, 'work/notes')), summary.notesBefore, 'note copy differs')
}
const assets = new DatabaseSync(join(run, 'backup/conversation-attachments.sqlite'), { readOnly: true })
summary.attachments = { referenced: 0, copied: 0, missing: 0, outsideRoot: 0, verifiedBytes: 0 }
const attachmentFiles = []
try {
for (const row of assets.prepare('SELECT path FROM attachments').iterate()) {
summary.attachments.referenced++
const relative = row.path.replaceAll('\\', '/')
const from = resolve(source, relative)
if (!from.startsWith(source + '/'.replace('/', process.platform === 'win32' ? '\\' : '/'))) { summary.attachments.outsideRoot++; continue }
const info = await stat(from).catch(() => undefined)
if (!info) { summary.attachments.missing++; continue }
await cp(from, join(run, 'work', relative), { recursive: true })
const directory = info.isDirectory()
const file = await (directory ? contentFingerprint(from) : fingerprint(from))
assert.equal((await (directory ? contentFingerprint(join(run, 'work', relative)) : fingerprint(join(run, 'work', relative)))).sha256, file.sha256, 'attachment copy differs')
attachmentFiles.push({ relative, file, directory })
summary.attachments.verifiedBytes += file.bytes
summary.attachments.copied++
}
} finally { assets.close() }
for (const [entry, output, format] of [
['src/main/desktop-storage-entry.ts', 'desktop-storage-entry.mjs', 'esm'],
['src/main/readonly-query-worker.ts', 'readonly-query-worker.cjs', 'cjs'],
['src/main/assistant-storage-worker.ts', 'assistant-storage-worker.cjs', 'cjs'],
['src/main/assistant/subagent-progress-storage.ts', 'canonical.mjs', 'esm'],
['tests/support/portable-storage-upgrade-fixture.ts', 'main.mjs', 'esm']
]) await build({ entryPoints: [join(root, entry)], outfile: join(run, output), bundle: true, platform: 'node', format,
external: ['electron', '@napi-rs/canvas'], logLevel: 'silent' })
const { restoreSubagentPayload } = await import(pathToFileURL(join(run, 'canonical.mjs')).href)
summary.backup = Object.fromEntries(await Promise.all(names.map(async name => [name, { file: await fingerprint(join(run, 'backup', name)), snapshot: snapshot(join(run, 'backup', name), restoreSubagentPayload) }])))
await writeFile(join(temporary, 'summary.json'), JSON.stringify(summary, null, 2))
const env = { ...process.env, GB_PORTABLE_UPGRADE_ROOT: run, TEMP: run, TMP: run, TMPDIR: run }
delete env.ELECTRON_RUN_AS_NODE
async function fixture(phase) {
console.log(`Production fixture: ${phase}`)
await rm(join(run, 'fixture-summary.json'), { force: true })
const child = spawn(createRequire(import.meta.url)('electron'), [join(run, 'main.mjs')], { env: { ...env, GB_PORTABLE_UPGRADE_PHASE: phase }, stdio: 'ignore' })
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
if (process.platform === 'win32') execFileSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { timeout: 10_000, stdio: 'ignore' })
else child.kill('SIGKILL')
}, 180_000)
const progress = setInterval(() => {
void readFile(join(run, 'fixture-summary.json'), 'utf8').then(text => console.log(`Fixture stage: ${JSON.parse(text).stage}`)).catch(() => {})
}, 10_000)
const code = await new Promise((resolveExit, reject) => { child.once('exit', resolveExit); child.once('error', reject) }).finally(() => { clearTimeout(timer); clearInterval(progress) })
const result = JSON.parse(await readFile(join(run, 'fixture-summary.json'), 'utf8'))
summary.fixture = result
assert.equal(timedOut, false, 'production fixture deadline exceeded')
assert.equal(code, 0, 'production fixture failed; see sanitized stage')
return result
}
summary.initialization = await fixture('initialize')
summary.after = Object.fromEntries(names.map(name => [name, snapshot(join(run, 'work', name), restoreSubagentPayload)]))
summary.comparison = {}
for (const name of names) {
const original = summary.backup[name].snapshot
const current = summary.after[name]
summary.comparison[name] = { unchanged: [], changed: [], added: Object.keys(current.tables).filter(table => !original.tables[table]) }
for (const [table, value] of Object.entries(original.tables)) {
summary.comparison[name][canonical(value) === canonical(current.tables[table]) ? 'unchanged' : 'changed'].push(table)
}
assert.equal((await fingerprint(join(run, 'backup', name))).sha256, summary.backup[name].file.sha256, 'backup changed')
assert.equal(current.foreignKeyDigest, original.foreignKeyDigest, 'foreign key violations changed')
assert.equal(summary.comparison[name].changed.length, 0, 'existing content changed; review startup recovery before accepting')
}
summary.exercise = await fixture('exercise')
summary.finalIntegrity = {}
for (const name of names) {
const db = new DatabaseSync(join(run, 'work', name), { readOnly: true })
try {
const integrity = db.prepare('PRAGMA integrity_check').get().integrity_check
const violations = db.prepare('PRAGMA foreign_key_check').all()
assert.equal(integrity, 'ok')
assert.equal(hash(canonical(violations)), summary.backup[name].snapshot.foreignKeyDigest)
summary.finalIntegrity[name] = { integrity, foreignKeyViolations: violations.length }
} finally { db.close() }
}
for (const { relative, file, directory } of attachmentFiles) {
const read = directory ? contentFingerprint : fingerprint
assert.deepEqual(await read(join(source, relative)), file, 'original attachment changed')
assert.equal((await read(join(run, 'work', relative)))?.sha256, file.sha256, 'work attachment changed')
}
summary.attachments.unchangedAfter = true
if (summary.notesBefore) {
assert.deepEqual(await contentFingerprint(join(source, 'notes')), summary.notesBefore, 'original notes changed')
assert.deepEqual(await contentFingerprint(join(run, 'work/notes')), summary.notesBefore, 'work notes changed')
summary.notesUnchanged = true
}
summary.workFiles = Object.fromEntries(await Promise.all(names.map(async name => [name, await fingerprint(join(run, 'work', name))])))
summary.status = 'passed'
}
} catch (error) {
summary.status = 'failed'
// Error messages, stacks and assertions can contain private row data or paths.
summary.failure = { name: error?.name, code: error?.code, stage: summary.fixture?.stage ?? (summary.backup ? 'validation' : 'backup/setup') }
process.exitCode = 1
} finally {
if (before) {
summary.sourceAfter = await sourceFingerprint()
summary.sourceUnchanged = canonical(before) === canonical(summary.sourceAfter)
if (!summary.sourceUnchanged) { summary.status = 'failed: source fingerprints changed'; process.exitCode = 1 }
}
if (!process.argv.includes('--inspect')) {
if (ownsRun) await rm(run, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 })
summary.copiesRemoved = ownsRun
summary.durationMs = Date.now() - Date.parse(summary.startedAt)
summary.reusedBackup = resume
await writeFile(join(temporary, 'summary.json'), JSON.stringify(summary, null, 2))
console.log(JSON.stringify({ status: summary.status, failure: summary.failure, sourceUnchanged: summary.sourceUnchanged, copiesRemoved: summary.copiesRemoved }))
}
}
+192
View File
@@ -0,0 +1,192 @@
import assert from 'node:assert/strict'
import console from 'node:console'
export async function validateStoryGraphFocus(win, js, wait, url) {
await win.loadURL(url + '?spiral=1&recap=1&focus=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('[...document.querySelectorAll("button")].some(b => b.textContent === "时间螺旋")')
await js('[...document.querySelectorAll("button")].find(b => b.textContent === "时间螺旋").click()')
await wait('window.focusFrames?.length > 0')
win.show(); win.focus(); win.webContents.focus()
await wait('document.hasFocus()')
const canvas = 'document.querySelector(".story-graph-3d__viewport canvas[tabindex]")'
const frame = () => js('window.focusFrames.at(-1)')
const sample = () => js(`new Promise(resolve => {
const start = performance.now(); const samples = [];
function tick(now) { samples.push({...window.focusFrames.at(-1)}); if (now-start < 650) requestAnimationFrame(tick); else resolve(samples); }
requestAnimationFrame(tick);
})`)
const changed = (a, b) => Math.hypot(...a.position.map((v, i) => v - b.position[i]))
const stable = samples => {
const tail = samples.filter(s => s.at === samples.at(-1).at)
assert(tail.length > 2, 'Animation finishes and stops requesting frames')
}
const pick = async name => {
await js('document.querySelector(".story-graph-3d__picker").click()')
await wait('!!document.querySelector(".story-graph-3d__menu [role=menuitem]")')
await js(`[...document.querySelectorAll('.story-graph-3d__menu [role=menuitem]')].find(b => b.textContent.includes(${JSON.stringify(name)})).click()`)
}
const clickTarget = async id => {
await js(`${canvas}.scrollIntoView()`)
await js('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
const at = await js(`(() => { const r=${canvas}.getBoundingClientRect(); const p=window.focusTargets.find(t=>t.id===${JSON.stringify(id)});
return {x:Math.round(r.left+(p.x+1)*r.width/2),y:Math.round(r.top+(1-p.y)*r.height/2)} })()`)
assert(await js(`document.elementFromPoint(${at.x},${at.y}) === ${canvas}`), 'Native target is on the visible canvas')
win.webContents.sendInputEvent({ type: 'mouseMove', ...at })
win.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...at })
win.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...at })
}
const list = async kind => {
await js(`(() => { const s=document.querySelector('.supervisor-workspace__category');
if(s) { s.value='${kind}'; s.dispatchEvent(new Event('change',{bubbles:true})); }
else document.querySelector('[id$="-list-tab-${kind}"]').click(); })()`)
}
const reports = []
const before = await frame()
await pick('Fixture project')
const flight = await sample()
assert(changed(before, flight.at(-1)) > 100, 'Hierarchy selection moves the actual Three camera')
assert(new Set(flight.map(s => s.at)).size > 4, 'Focus is animated over multiple rendered frames')
stable(flight)
for (let i = 1; i < flight.length; i++) {
const delta = Math.atan2(Math.sin(flight[i].yaw-flight[i-1].yaw), Math.cos(flight[i].yaw-flight[i-1].yaw))
assert(Math.abs(delta) < 0.8, 'No discontinuous camera turn')
}
reports.push({ hierarchyDistance: changed(before, flight.at(-1)), renderedFrames: new Set(flight.map(s => s.at)).size })
await js(`for(let i=0;i<55;i++) ${canvas}.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`)
const turned = await frame()
await pick('Fixture story')
const shortFlight = [turned, ...await sample()]
const rotation = shortFlight.slice(1).reduce((sum, f, i) => sum + Math.abs(Math.atan2(Math.sin(f.yaw-shortFlight[i].yaw), Math.cos(f.yaw-shortFlight[i].yaw))), 0)
assert(rotation <= Math.PI + 1e-6, 'Focus after a full manual revolution takes the shortest angular path')
reports.push({ shortestRotation: rotation })
await list('story')
await wait('!!document.querySelector(".supervisor-workspace__story-group button")')
await js('[...document.querySelectorAll(".supervisor-workspace__story-group button")].find(b => b.textContent.includes("Secondary story")).click()')
await wait('document.querySelector(".story-graph-3d__breadcrumb strong")?.textContent === "Secondary story"')
stable(await sample())
await js(`${canvas}.dispatchEvent(new KeyboardEvent('keydown',{key:'2',bubbles:true}))`)
await sample()
await clickTarget('feature')
await wait('document.querySelector(".story-graph-3d__breadcrumb strong")?.textContent === "Fixture story"')
stable(await sample())
assert(await js('[...document.querySelectorAll(".supervisor-workspace__story-group button")].find(b=>b.textContent.includes("Fixture story")).getAttribute("aria-pressed") === "true"'))
await list('experience')
await wait('!!document.querySelector("[id$=list-panel-experience] button")')
await js('document.querySelector("[id$=list-panel-experience] button").click()')
stable(await sample())
let target = await js('window.focusTargets.find(t => t.id === "focus-experience")')
assert(target && Math.abs(target.x) < 0.01 && Math.abs(target.y) < 0.01, 'External experience selection centers its actual diamond')
await js(`${canvas}.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`)
await clickTarget('focus-experience')
stable(await sample())
target = await js('window.focusTargets.find(t => t.id === "focus-experience")')
assert(Math.abs(target.x) < 0.01 && Math.abs(target.y) < 0.01, 'Native diamond pick refocuses the experience')
// Root has no within-project experience link. Selecting it must expose the project level.
await js('document.querySelector(".story-graph-3d__breadcrumb button").click()')
await sample()
await list('event')
await wait('!!document.querySelector(".supervisor-workspace__list-panel button")')
await js('document.querySelector(".supervisor-workspace__list-panel button").click()')
stable(await sample())
await list('experience')
await js('document.querySelector("[id$=list-panel-experience] button").click()')
stable(await sample())
assert.equal(await js('document.querySelector(".story-graph-3d__breadcrumb strong").textContent'), 'Fixture project')
for (const width of [1440, 390]) {
win.setContentSize(width, 1000)
await wait(`innerWidth === ${width}`)
await list('event')
await js('document.querySelectorAll(".supervisor-workspace__list-panel button")[1].click()')
stable(await sample())
target = await js('window.focusTargets.find(t => t.id === "event-1")')
assert(target && Math.abs(target.x) < 0.01 && Math.abs(target.y) < 0.01, 'External event centers at both viewport widths')
reports.push({ width, eventProjection: target })
await js('document.querySelector(".supervisor-workspace__list-panel button").click()')
await sample()
}
win.setContentSize(1440, 1000)
await wait('innerWidth === 1440')
await js('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))')
// A native canvas click must select the dot through its translucent stave and refocus it.
await js(`${canvas}.focus(); ${canvas}.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`)
const dot = await js(`(() => { const c=${canvas}; c.scrollIntoView(); const r=c.getBoundingClientRect();
const p=window.focusTargets.find(t=>t.id==='event-0'); return {x:Math.round(r.left+(p.x+1)*r.width/2),y:Math.round(r.top+(1-p.y)*r.height/2)} })()`)
const hitElement = await js(`document.elementFromPoint(${dot.x},${dot.y})?.outerHTML.slice(0,300)`)
assert(await js(`document.elementFromPoint(${dot.x},${dot.y}) === ${canvas}`), `Canvas hit point: ${JSON.stringify({ dot, hitElement })}`)
win.webContents.sendInputEvent({ type: 'mouseMove', ...dot })
win.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...dot })
win.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...dot })
await sample()
target = await js('window.focusTargets.find(t=>t.id==="event-0")')
assert(Math.abs(target.x) < 0.01 && Math.abs(target.y) < 0.01, `Native canvas dot click recenters the selected event: ${JSON.stringify({target, dot, hitElement, current:await frame()})}`)
assert(await js('document.querySelector(".supervisor-workspace__list-panel button").getAttribute("aria-pressed") === "true"'))
for (const input of ['keyboard', 'wheel', 'drag']) {
await pick('Secondary story')
await wait('window.focusFrames.at(-1).zoom > 0')
await js(`new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))`)
const pre = await frame()
if (input === 'keyboard') {
await js(`${canvas}.focus()`)
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Left' })
win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Left' })
} else {
const at = await js(`(() => { const c=${canvas}; c.scrollIntoView(); const r=c.getBoundingClientRect(); return {x:Math.round(r.left+r.width/2),y:Math.round(r.top+r.height/2)} })()`)
if (input === 'wheel') win.webContents.sendInputEvent({ type: 'mouseWheel', ...at, deltaY: 70, deltaX: 0 })
else {
win.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...at })
win.webContents.sendInputEvent({ type: 'mouseMove', button: 'left', modifiers: ['leftButtonDown'], x: at.x+30, y: at.y+10 })
win.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, x: at.x+30, y: at.y+10 })
}
}
await wait(`window.focusFrames.at(-1).at > ${pre.at}`)
const interrupted = await frame(), samples = await sample()
assert.deepEqual(samples.at(-1).position, interrupted.position, `${input} cancels the in-flight focus`)
assert.equal(samples.at(-1).zoom, interrupted.zoom)
reports.push({ interruption: input, position: interrupted.position, zoom: interrupted.zoom })
}
const manual = await frame()
for (let i = 0; i < 3; i++) {
await js('document.querySelector(".supervisor-workspace__review-context > .secondary-button").click()')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await sample()
assert.deepEqual((await frame()).position, manual.position, 'Equal-content refresh does not refocus')
}
win.webContents.debugger.attach('1.3')
await win.webContents.debugger.sendCommand('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] })
await wait('matchMedia("(prefers-reduced-motion: reduce)").matches')
await js('window.focusFrames = []')
await pick('Fixture story')
await sample()
const reduced = await js('window.focusFrames')
assert(new Set(reduced.map(s => JSON.stringify(s.position))).size <= 2, 'Reduced motion applies a single camera jump')
win.webContents.debugger.detach()
await pick('Secondary story')
await wait('window.focusRafs.size > 0')
await js('[...document.querySelectorAll(".supervisor-workspace__graph-mode button")].find(b=>b.getAttribute("aria-pressed")==="false").click()')
await wait('!document.querySelector(".story-graph-3d__gl")')
assert.equal(await js('window.focusRafs.size'), 0, 'Unmount cancels the owned camera RAF')
await win.loadURL(url + '?spiral=1&recap=1&focus=1&real-export=1')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await js('document.querySelector("#supervisor-tab-graph").click()')
await wait('[...document.querySelectorAll("button")].some(b => b.textContent === "时间螺旋")')
await js('[...document.querySelectorAll("button")].find(b => b.textContent === "时间螺旋").click()')
await wait('window.focusFrames?.length > 0')
await sample()
const realBefore = await frame()
await list('story')
await wait('!!document.querySelector(".supervisor-workspace__story-group button")')
await js('document.querySelector(".supervisor-workspace__story-group button").click()')
const realFlight = await sample()
assert(changed(realBefore, realFlight.at(-1)) > 5, 'Checked-in real export selection moves the production camera')
stable(realFlight)
await js(`${canvas}.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`)
const realManual = await frame()
await js('document.querySelector(".supervisor-workspace__review-context > .secondary-button").click()')
await wait('document.querySelector(".supervisor-workspace")?.getAttribute("aria-busy") === "false"')
await sample()
assert.deepEqual((await frame()).position, realManual.position)
const realExport = { ...await js('JSON.parse(document.documentElement.dataset.exportCounts)'), cameraDistance: changed(realBefore, realFlight.at(-1)) }
console.log(JSON.stringify({ storyGraphFocus: 'passed', reports, refreshes: 4, reducedMotionFrames: reduced.length, canvasClick: true, unmountPendingRafs: 0, realExport, modelCalls: 0 }))
}
@@ -0,0 +1,110 @@
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { app } from 'electron'
import { DesktopStorageClient } from '../../src/main/desktop-storage-client'
import { createSupervisionDomainPorts } from '../../src/main/assistant/supervision-domain-ports'
import { createProductionSupervisorService, createProductionSuggestionPhraser } from '../../src/main/assistant/supervision-production'
import { SupervisionModelPool } from '../../src/main/assistant/supervision-model-pool'
import { HeartbeatService } from '../../src/main/assistant/heartbeat-service'
import { deriveSuggestions } from '../../src/main/assistant/supervision-suggester'
import type { AgentRuntime } from '../../src/main/agent/runtime'
import type { SupervisionEvidence, SupervisionRunRequest } from '../../src/shared/supervision-contracts'
const root = process.env.GB_SUPERVISION_STORAGE_ROOT!
app.setPath('userData', join(root, 'electron'))
async function run(): Promise<void> {
await app.whenReady()
const storage = new DesktopStorageClient({ assistantPath: join(root, 'assistant.sqlite'), knowledgePath: join(root, 'knowledge.sqlite'),
defaultRootPath: root, userDataPath: root, entryPath: join(root, 'desktop-storage-entry.mjs'),
readerWorkerPath: join(root, 'readonly-query-worker.cjs'), upgradeWorkerPath: join(root, 'assistant-storage-worker.cjs') })
const pool = new SupervisionModelPool()
try {
await storage.ready
const ports = createSupervisionDomainPorts(storage)
const project = (await storage.call('assistant', 'listProjects', []))[0]!
const now = Date.now()
const seed = async (content: string) => storage.call('assistant', 'saveLocalConversations', [[{
header: { id: randomUUID(), projectId: project.id, title: 'Review fixture', updatedAt: now },
messages: [{ id: randomUUID(), role: 'user', content, state: 'complete', createdAt: now }]
}]])
await seed('A'.repeat(1500))
const request: SupervisionRunRequest = { trigger: 'heartbeat', scope: { kind: 'global' },
timeRange: { from: new Date(now - 1000).toISOString(), to: new Date(now + 10000).toISOString() } }
const phases: string[] = []
let block = false
let entered!: () => void
const modelEntered = new Promise<void>(resolve => { entered = resolve })
const runtime: AgentRuntime = { runtimeId: 'model', capability: 'chat', requiresToolApproval: false, supportsToolExecution: false,
getStatus: async () => ({ id: 'model', label: 'Fixture', available: true, detail: 'Fixture ready', supportsToolExecution: false }),
dispose: async () => {},
run: async function* (input, signal) {
if (block) {
entered()
await new Promise<void>(resolve => signal.addEventListener('abort', () => resolve(), { once: true }))
yield { type: 'error', requestId: input.requestId, status: 'cancelled', message: 'Cancelled fixture' }
return
}
let output: unknown
const empty = { summary: 'Review', changeDigest: '', openItems: ['Choose the next milestone'], events: [], entities: [], entityChanges: [], relations: [] }
if (input.prompt.includes('You organise the GoodBuddy')) {
phases.push('stories')
output = { stories: [{ key: 'new_1', level: 'feature', name: 'Delivery' }],
assignments: [{ event: 'e_1', story: 'new_1' }, { event: 'e_2', story: 'new_1' }] }
} else if (input.prompt.includes('You distil reusable experience')) {
phases.push('experiences')
output = { experiences: [{ key: 'new_1', statement: 'Check the assumption before publishing', formed: ['e_1'] }] }
} else if (input.prompt.includes('You write short, concrete suggestions')) {
phases.push('suggestions')
const candidates = JSON.parse(input.prompt.split('CANDIDATES:\n\n')[1]!.split('\n\nReturn only JSON.')[0]!)
output = { suggestions: candidates }
} else if (input.prompt.includes('These inputs are navigation summaries')) {
phases.push('navigation')
output = empty
} else {
phases.push('leaf')
const evidence = JSON.parse(input.prompt.split('BOUNDED EVIDENCE:\n\n')[1]!.split('\n\nReturn only JSON.')[0]!) as SupervisionEvidence[]
output = { ...empty, events: [{ title: 'Decide delivery approach', description: 'Validate the assumption first', eventType: 'decision',
occurredAt: new Date(now).toISOString(), entityIds: [], sourceReferenceIds: [evidence[0]!.id] }] }
}
yield { type: 'text', requestId: input.requestId, delta: JSON.stringify(output) }
yield { type: 'model-usage', requestId: input.requestId, callId: 'call', runtime: 'model', provider: 'fixture', model: 'fixture',
inputTokens: 4, outputTokens: 2, cacheReadTokens: 0, cacheWriteTokens: 0 }
yield { type: 'done', requestId: input.requestId }
} }
const settings = async () => ({ supervisionReview: { pageSize: 10, batchCharacters: 1000, batchMessages: 10,
executionSeconds: 300, experienceMinEvents: 2 } })
const supervisor = createProductionSupervisorService(ports.supervision, settings, async () => runtime, pool)
const phrase = createProductionSuggestionPhraser(ports.supervision, settings, async () => runtime, pool)
const heartbeat = new HeartbeatService(ports.heartbeat, {
review: async ({ run }) => {
const result = await supervisor.run(request, run.id)
return { status: result.status ?? 'completed', runId: result.runId }
},
suggest: async ({ run, supervisionRunId }) => deriveSuggestions(ports.suggestions, phrase, { heartbeatRunId: run.id, supervisionRunId })
})
const config = await heartbeat.create({ name: 'Fixture', scope: request.scope, timezone: 'UTC', recurrence: { type: 'daily', localTime: '09:00' },
enabled: true, lookbackHours: 24, retentionDays: 30 })
const first = await heartbeat.runNow({ id: config.id, idempotencyKey: 'first' })
assert.equal(first.status, 'completed')
assert.deepEqual(phases, ['leaf', 'leaf', 'navigation', 'stories', 'experiences', 'suggestions'])
assert.equal((await ports.stories.list(request.scope)).length, 1)
assert.equal((await ports.experiences.list()).length, 1)
assert.ok(await ports.suggestions.countForHeartbeat(first.id) > 0)
assert.equal((await storage.call('assistant', 'getTokenUsageSummary', [])).systemTotals?.callCount, 6)
assert.equal((await heartbeat.runNow({ id: config.id, idempotencyKey: 'quiet' })).status, 'no_change')
assert.equal(phases.length, 6)
await seed('A new decision')
block = true
const pending = supervisor.run(request)
await modelEntered
const runId = supervisor.execution().runId!
await supervisor.cancel(runId)
assert.equal((await pending).status, 'cancelled')
assert.equal(supervisor.execution().active, false)
assert.equal((await storage.call('assistant', 'listSupervisionActivity', [])).find(row => row.id === runId)!.status, 'cancelled')
assert.equal(storage.pendingCount, 0)
console.log('supervision-storage: passed')
} finally { pool.dispose(); await storage.close() }
}
void run().then(() => app.exit(0), error => { console.error(error); app.exit(1) })
@@ -6,6 +6,7 @@ import process from 'node:process'
import console from 'node:console'
import { setTimeout } from 'node:timers/promises'
import { validateSupervisorSelection } from './supervisor-selection-driver.mjs'
import { validateStoryGraphFocus } from './story-graph-focus-driver.mjs'
const directory = process.env.GOODBUDDY_SUPERVISOR_DIRECTORY
const artifacts = process.env.GOODBUDDY_SUPERVISOR_ARTIFACTS || directory
@@ -64,6 +65,11 @@ app
await settle()
}
const reports = []
if (process.env.GOODBUDDY_STORY_GRAPH_FOCUS) {
await validateStoryGraphFocus(win, js, wait, process.env.GOODBUDDY_SUPERVISOR_URL)
assert.deepEqual(errors, [])
win.destroy(); app.quit(); return
}
if (process.env.GOODBUDDY_SUPERVISOR_WINDOWING) {
await validateSupervisorSelection(win, js, wait, process.env.GOODBUDDY_SUPERVISOR_URL)
assert.deepEqual(errors, [])
+61 -1
View File
@@ -14,6 +14,18 @@ import { installBundledUiFonts } from '../../src/renderer/src/fonts'
installBundledUiFonts()
const params = new URLSearchParams(location.search)
if (params.has('focus')) {
const request = window.requestAnimationFrame.bind(window), cancel = window.cancelAnimationFrame.bind(window)
const pending = new Set<number>()
Reflect.set(window, 'focusRafs', pending)
window.requestAnimationFrame = callback => {
const tracked = new Error().stack?.includes('StoryGraph3D.tsx')
const id = request(now => { pending.delete(id); callback(now) })
if (tracked) pending.add(id)
return id
}
window.cancelAnimationFrame = id => { pending.delete(id); cancel(id) }
}
if (params.has('spiral')) {
const THREE = await import('three')
const add = THREE.Group.prototype.add
@@ -60,6 +72,30 @@ if (params.has('spiral')) {
}
THREE.Scene.prototype.onAfterRender = function (_renderer, scene, camera) {
document.documentElement.dataset.camera = JSON.stringify(camera.position.toArray())
if (params.has('focus')) {
const frames = Reflect.get(window, 'focusFrames') ?? []
const direction = camera.getWorldDirection(new THREE.Vector3())
const snapshot = { at: performance.now(), position: camera.position.toArray(), yaw: Math.atan2(direction.x, -direction.z), zoom: (camera as import('three').PerspectiveCamera).zoom }
frames.push(snapshot)
if (frames.length > 240) frames.shift()
Reflect.set(window, 'focusFrames', frames)
const targets: unknown[] = []
scene.traverse(object => {
if (object.userData.node && !object.userData.cluster) {
const positions = (object as import('three').Mesh).geometry.getAttribute('position')
const mid = Math.floor(positions.count / 4) * 2
const p = new THREE.Vector3().fromBufferAttribute(positions, mid)
.lerp(new THREE.Vector3().fromBufferAttribute(positions, mid + 1), 0.37).project(camera)
targets.push({ id: object.userData.node.id, kind: 'story', x: p.x, y: p.y })
}
if (!object.userData.cluster && !object.userData.link) return
const p = object.position.clone().project(camera)
targets.push({ id: object.userData.link?.id ?? object.userData.cluster.events[0].id,
kind: object.userData.link ? 'experience' : 'event', x: p.x, y: p.y })
})
Reflect.set(window, 'focusTargets', targets)
return
}
const point = new THREE.Vector3(), bounds = { left: 1, right: -1, top: -1, bottom: 1 }
scene.traverse(object => {
const positions = (object as import('three').Mesh).geometry?.getAttribute('position')
@@ -221,7 +257,10 @@ Object.defineProperty(window, 'goodbuddy', {
events: [...graph.events, { id: 'outside-review', title: 'Outside review', occurred_at: '2026-08-01T00:00:00Z' }].filter((_event, index) => index % 2 === storyIndex).map(event => ({
id: event.id, title: event.title, startedAt: event.occurred_at, endedAt: event.occurred_at, projectId: 'p', primary: true, userSet: false
}))
})), experiences: [], unassigned: 0, canUndo: false }) } : {}),
})), experiences: params.has('focus') ? [{ id: 'focus-experience', statement: 'Fixture experience', conditions: '', boundaries: '', userEdited: false,
events: [0, 1].map(index => ({ id: `event-${index}`, role: index ? 'applied' : 'formed', note: '', title: graph.events[index]!.title,
projectId: 'p', at: graph.events[index]!.occurred_at, storyId: index ? 'secondary' : 'feature', storyName: index ? 'Secondary story' : 'Fixture story' }))
}] : [], unassigned: 0, canUndo: false }) } : {}),
continueContext: async () => ({ prompt: '模拟讨论上下文:本周已核对交付清单,负责人已确认。\n\n原始依据:模拟会议记录。外部评审时间仍待确认,下一步需要核对验收条件。' }),
continue: async () => { throw new Error('Preview fixture must not send a message') },
execution: async () => ({ active: params.has('activity'), ...(params.has('activity') ? { runId: 'activity-1' } : {}) }),
@@ -307,6 +346,27 @@ Object.defineProperty(window, 'goodbuddy', {
}
})
let portableProjects: AssistantProject[] = []
if (params.has('real-export')) {
// Use the checked-in read-only export. Its proposed feature grouping is not user-confirmed.
const data: { range: string[]; projects: Array<{ name: string; features: Array<{ id: string; name: string; events: Array<{ id: string; at: string; title: string; text: string }> }> }> } = await (await fetch('/focus-export.json')).json()
const savedEvents = new Map<string, SupervisionGraphView['events'][number]>()
const exportedStories = data.projects.flatMap((project, index) => project.features.map(feature => ({
id: feature.id, projectId: `export-${index}`, projectName: project.name, parentId: null, level: 'feature' as const, name: feature.name,
description: '', state: 'active' as const, stateEventId: null, userEdited: false, startedAt: null, endedAt: null,
events: feature.events.filter(event => {
if (savedEvents.has(event.id)) return false
savedEvents.set(event.id, { id: event.id, title: event.title || event.id, description: event.text, occurred_at: event.at, project_id: `export-${index}` })
return true
}).map(event => ({ id: event.id, title: event.title || event.id, startedAt: event.at, endedAt: event.at, projectId: `export-${index}`, primary: true, userSet: false }))
})))
const api = window.goodbuddy.supervision, overview = api.overview
Object.assign(api, {
overview: async () => (await overview()).map(result => ({ ...result, timeRange: { from: `${data.range[0]}T00:00:00Z`, to: `${data.range[1]}T00:00:00Z` } })),
graph: async () => ({ ...graph, events: [...savedEvents.values()], attention: [], eventEntities: [], eventSources: [] }),
stories: async () => ({ stories: exportedStories, experiences: [], unassigned: 0, canUndo: false })
})
document.documentElement.dataset.exportCounts = JSON.stringify({ projects: data.projects.length, stories: exportedStories.length, events: savedEvents.size })
}
if (params.has('portable')) {
const data = await (await fetch('/portable-review.json')).json()
portableProjects = data.projects