Files
goodbuddy/tests/support/incremental-review-live.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

139 lines
8.6 KiB
TypeScript

import { app, safeStorage } from 'electron'
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { RuntimeSettingsStore } from '../../src/main/runtime-settings-store'
import { createDefaultModelRuntime } from '../../src/main/agent/create-runtime'
import { AssistantDatabase } from '../../src/main/assistant/assistant-database'
import { SupervisorService } from '../../src/main/assistant/supervisor-service'
import { HeartbeatService } from '../../src/main/assistant/heartbeat-service'
import type { SupervisionRunRequest } from '../../src/shared/supervision-contracts'
const directory = process.env.GB_REVIEW_LIVE_DIRECTORY!
const settingsPath = process.env.GB_REVIEW_LIVE_SETTINGS!
const profileDirectory = join(directory, 'profile')
mkdirSync(profileDirectory, { recursive: true })
app.setPath('userData', profileDirectory)
if (process.platform === 'win32') {
const localState = JSON.parse(readFileSync(join(dirname(settingsPath), 'Local State'), 'utf8'))
writeFileSync(join(profileDirectory, 'Local State'), JSON.stringify({ os_crypt: localState.os_crypt }))
}
void app.whenReady().then(async () => {
const original = readFileSync(settingsPath)
const snapshot = join(directory, 'settings.snapshot.json')
writeFileSync(snapshot, original)
const store = new RuntimeSettingsStore(snapshot, {
isAvailable: () => safeStorage.isEncryptionAvailable(),
encrypt: () => { throw new Error('Probe must not write credentials') },
decrypt: (value) => safeStorage.decryptString(value)
}, {})
const settings = await store.getResolvedSettings().finally(() => unlinkSync(snapshot))
assert(settings.modelName && settings.modelProtocol !== 'openai-images-generations', 'Default profile is not a text model')
assert(settings.modelAuthentication === 'none' || settings.apiKey, 'Default profile credential is unavailable')
settings.workspacePath = directory
settings.modelProfiles = settings.modelProfiles.map((profile) => ({ ...profile, maximumOutputTokens: 2500 }))
const observations: Array<{ status?: number; tools: number; stage: string }> = []
let stage = ''
const originalFetch = globalThis.fetch
globalThis.fetch = async (input, init) => {
assert(observations.length < 3, 'Three-request live budget exhausted')
const body = JSON.parse(String(init?.body))
assert.equal(body.tools?.length ?? 0, 0)
assert(!/"(?:input_image|image_url|input_file)"/.test(JSON.stringify(body)), 'Unexpected attachment')
const record = { stage, tools: 0, status: undefined as number | undefined }
observations.push(record)
const response = await originalFetch(input, init)
record.status = response.status
return response
}
const runtime = createDefaultModelRuntime(directory, settings)
const db = new AssistantDatabase(join(directory, 'assistant.sqlite'))
db.initialize(directory)
const sql = new DatabaseSync(join(directory, 'assistant.sqlite'))
const call = async (prompt: string) => {
const conversationId = `review-probe:${randomUUID()}`
let text = ''
try {
for await (const event of runtime.run({ requestId: randomUUID(), conversationId, prompt },
AbortSignal.timeout(90000), async () => 'deny')) {
if (event.type === 'text') text += event.delta
if (event.type === 'error') throw new Error(event.message)
assert(event.type !== 'tool', 'Unexpected tool event')
}
return text
} finally { await runtime.releaseConversation?.(conversationId) }
}
try {
const now = new Date()
const messageId = randomUUID()
db.replaceConversations([{ id: randomUUID(), projectId: db.listProjects()[0]!.id, title: 'Atlas release', updatedAt: now.getTime(),
messages: [{ id: messageId, role: 'user', state: 'complete', createdAt: now.getTime() - 1000,
content: 'The Atlas release uses SQLite. The release date is October 4. Track Atlas as one release entity.' }] }])
const request: SupervisionRunRequest = { trigger: 'heartbeat', scope: { kind: 'global' }, timeRange: {
from: new Date(now.getTime() - 3600_000).toISOString(), to: new Date(now.getTime() + 3600_000).toISOString()
} }
const supervisor = new SupervisorService({
collect: async () => { throw new Error('Expected automatic collector') },
incremental: async (input) => db.collectIncrementalReview(input, 'supervisor')
}, { summarize: async (input) => {
stage = 'supervisor'
return call([input.systemInstruction, 'OUTPUT CONTRACT:', input.outputContract,
'REVIEW TIME RANGE:', JSON.stringify(input.request.timeRange), 'KNOWN ENTITIES:', JSON.stringify(input.candidates),
'PREVIOUS SUMMARY (background only):', input.previousSummary ?? '', 'BOUNDED EVIDENCE:', JSON.stringify(input.evidence), 'Return only JSON.'].join('\n\n'))
} }, {
scope: (scope) => db.resolveReviewScope(scope), summary: (input) => db.reviewSummary(input.scope, 'supervisor'),
start: (input, heartbeatRunId) => db.startSupervisionRun(input, heartbeatRunId),
fail: (id, error) => db.failSupervisionRun(id, error), noChange: (id) => db.noChangeSupervisionRun(id),
candidates: async (input) => db.listSupervisionCandidates(input), save: async (result) => db.saveSupervisionResult(result)
})
// The heartbeat reads no sources itself; it only triggers the shared review.
const heartbeat = new HeartbeatService(db, { review: async ({ run }) => {
const result = await supervisor.run(request, run.id)
return { status: result.status ?? 'completed', runId: result.runId }
} })
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 () => {
sql.prepare('UPDATE heartbeat_configs SET next_run_at = ? WHERE id = ?').run(new Date(now.getTime() - 10000 + tick++).toISOString(), config.id)
await heartbeat.processDue(new Date())
}
await due()
assert.equal(observations.length, 1, 'A heartbeat must make exactly one review request')
assert.equal(db.listSupervisionActivity()[0]!.status, 'completed')
const first = db.listSupervisionCandidates(request)
assert(first.length > 0, 'Model did not create an entity')
const firstGraph = db.getSupervisionGraph()
await due()
assert.equal(observations.length, 1, 'Unchanged automatic run made a request')
assert.equal(db.listSupervisionActivity()[0]!.status, 'no_change')
assert.deepEqual(db.getSupervisionGraph(), firstGraph)
sql.prepare('UPDATE messages SET content = ? WHERE id = ?').run(
'The same Atlas release has moved from October 4 to October 6. SQLite remains the storage choice. Update the existing Atlas release entity.', messageId)
const changed = await supervisor.run(request)
assert.equal(observations.length, 2)
const reused = changed.output.entities.filter((entity) => entity.persistedId && first.some((candidate) => candidate.id === entity.persistedId))
assert(reused.length > 0, 'Changed review did not reuse a known entity ID')
assert.equal((await supervisor.run(request)).status, 'no_change')
assert.equal(observations.length, 2)
assert.deepEqual(sql.prepare('PRAGMA foreign_key_check').all(), [])
assert(readFileSync(settingsPath).equals(original), 'Source settings changed')
console.log(JSON.stringify({ model: settings.modelName, protocol: settings.modelProtocol, actualCalls: observations,
unchangedAutomaticCalls: 0, unchangedSupervisorCalls: 0, originalSettingsUnchanged: true,
firstEntityCount: first.length, reusedEntityCount: reused.length, changedEvidenceCount: changed.evidence.length,
changedSummary: changed.output.summary, changedEntities: changed.output.entities.map(({ label, description }) => ({ label, description })),
resultCount: db.listSupervisionResults().length, integrity: sql.prepare('PRAGMA integrity_check').get() }))
} catch (error) {
const message = error instanceof Error ? error.message : 'Live validation failed'
console.log(JSON.stringify({ failed: settings.apiKey ? message.replaceAll(settings.apiKey, '[redacted]') : message,
model: settings.modelName, actualCalls: observations }))
process.exitCode = 1
} finally {
sql.close(); db.close(); await runtime.dispose?.(); globalThis.fetch = originalFetch
}
app.exit(process.exitCode ? 1 : 0)
}).catch(() => { console.log('Live preflight failed: encrypted default text profile could not be loaded'); app.exit(1) })