Files
goodbuddy/tests/support/supervision-storage-fixture.ts
mesalogo d80d42b032 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.
2026-10-06 15:40:18 +08:00

111 lines
7.1 KiB
TypeScript

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) })