Files
goodbuddy/tests/supervisor-layout.electron.test.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

130 lines
5.8 KiB
TypeScript

// @vitest-environment node
import { spawn } from 'node:child_process'
import { createRequire } from 'node:module'
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'
import { expect, it } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
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_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 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) {
const connection = new DatabaseSync(process.env.GOODBUDDY_SUPERVISOR_BACKUP, { readOnly: true })
try {
const database = new AssistantDatabase(process.env.GOODBUDDY_SUPERVISOR_BACKUP)
Reflect.set(database, 'database', connection)
const results = database.listSupervisionResults()
portable = JSON.stringify({ results,
projects: connection.prepare('SELECT id, name FROM projects').all(),
graphs: Object.fromEntries(results.map(result => [result.id, database.getSupervisionGraph({ resultId: result.id })])),
stories: Object.fromEntries(results.map(result => [JSON.stringify(result.scope), {
stories: new SupervisionStoryStore(connection).list(result.scope),
experiences: new SupervisionExperienceStore(connection).list(result.scope.kind === 'projects' ? result.scope.projectIds : undefined),
unassigned: new SupervisionStoryStore(connection).unassignedCount(result.scope), canUndo: false
}]))
})
} finally { connection.close() }
}
const css = await readFile(
'src/renderer/src/supervisor-workspace.css',
'utf8'
)
const server = await createServer({
configFile: false,
root: resolve('.'),
cacheDir: join(directory, 'vite'),
plugins: [
react(),
{
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)
})
server.middlewares.use(
'/supervisor.html',
async (_request, response) => {
response.setHeader('Content-Type', 'text/html; charset=utf-8')
response.end(
await server.transformIndexHtml(
'/supervisor.html',
'<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SIMULATED Supervisor validation</title><body><div id="root"></div><script type="module" src="/tests/support/supervisor-layout-regression.tsx"></script></body></html>'
)
)
}
)
}
}
],
server: { host: '127.0.0.1', port: 0, watch: null }
})
try {
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,
GOODBUDDY_SUPERVISOR_TOKENS: JSON.stringify([
...new Set(
[...css.matchAll(/var\((--[\w-]+)\)/g)].map((match) => match[1])
)
])
}
delete env.ELECTRON_RUN_AS_NODE
const driver = join(directory, 'driver.mjs')
await copyFile(
resolve('tests/support/supervisor-layout-driver.mjs'),
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']
})
let output = ''
child.stdout.on('data', (data) => {
output += data
})
child.stderr.on('data', (data) => {
output += data
})
const timeout = setTimeout(() => child.kill(), 90000)
try {
const code = await new Promise((resolve, reject) => {
child.once('exit', resolve)
child.once('error', reject)
})
expect(code, output).toBe(0)
if (process.env.GOODBUDDY_STORY_GRAPH_FOCUS) process.stdout.write(output)
else console.log(output)
} finally {
clearTimeout(timeout)
}
} finally {
await server.close()
await rm(directory, { recursive: true, force: true })
}
}, 120000)