Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content. Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation. Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit. Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
175 lines
11 KiB
TypeScript
175 lines
11 KiB
TypeScript
// @vitest-environment node
|
|
import { createHash, randomUUID } from 'node:crypto'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { DatabaseSync } from 'node:sqlite'
|
|
import { build } from 'esbuild'
|
|
import { afterAll, afterEach, beforeAll, expect, it, vi } from 'vitest'
|
|
import { AssistantDatabase } from './assistant-database'
|
|
import { createProductionSupervisorService } from './supervision-production'
|
|
import { SupervisionModelPool } from './supervision-model-pool'
|
|
import type { AgentRuntime } from '../agent/runtime'
|
|
import { supervisionReviewSettingsSchema } from '../../shared/supervision-review-contracts'
|
|
import type { ReviewState } from './supervision-review-store'
|
|
import { ReadonlyQueryReader } from '../readonly-query-reader'
|
|
|
|
let directory: string, workerPath: string
|
|
const cleanup: Array<() => Promise<void>> = []
|
|
beforeAll(async () => {
|
|
directory = await mkdtemp(join(tmpdir(), 'supervision-worker-'))
|
|
workerPath = join(directory, 'readonly-query-worker.cjs')
|
|
await build({ entryPoints: [resolve('src/main/readonly-query-worker.ts')], outfile: workerPath,
|
|
bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
|
|
}, 60_000)
|
|
afterEach(async () => { for (const close of cleanup.splice(0)) await close() })
|
|
afterAll(async () => { await rm(directory, { recursive: true, force: true, maxRetries: 10 }) })
|
|
|
|
async function fixture() {
|
|
const root = await mkdtemp(join(directory, 'data-'))
|
|
const path = join(root, 'assistant.sqlite')
|
|
const db = new AssistantDatabase(path)
|
|
db.initialize(root)
|
|
db.enableReadonlyWorker(workerPath)
|
|
const sql = new DatabaseSync(path)
|
|
const project = db.listProjects()[0]!
|
|
const other = db.createProject({ name: 'Other', description: '', rootPath: root })
|
|
const now = Date.parse('2026-10-01T12:00:00Z')
|
|
const conversations = [project, other].map(p => ({ id: randomUUID(), projectId: p.id, title: p.name, updatedAt: now,
|
|
messages: Array.from({ length: 211 }, (_, index) => ({ id: randomUUID(), role: 'user' as const,
|
|
state: 'complete' as const, content: index === 210 ? '' : `Source ${index} \u{1f600}`, createdAt: now })) }))
|
|
db.replaceConversations(conversations)
|
|
sql.prepare('UPDATE messages SET metadata_json = ? WHERE id = ?').run(JSON.stringify({ sourceReferences: [
|
|
{ documentId: 'document', documentName: 'Reference', snippet: 'Knowledge \u{1f600}', chunkId: 'chunk' }
|
|
] }), conversations[0]!.messages[0]!.id)
|
|
db.createTask({ id: randomUUID(), conversationId: conversations[0]!.id, projectId: project.id,
|
|
title: 'Visible task', instructions: 'Test', origin: 'user', visible: true })
|
|
const state: ReviewState = { request: { trigger: 'manual', scope: { kind: 'global' },
|
|
timeRange: { from: '2026-01-01T00:00:00.000Z', to: '2027-01-01T00:00:00.000Z' } },
|
|
config: { ...supervisionReviewSettingsSchema.parse({}), version: 1, timeoutSeconds: 30, concurrency: 1 }, phase: 'collecting' }
|
|
cleanup.push(async () => { sql.close(); db.close(); await rm(root, { recursive: true, force: true, maxRetries: 10 }) })
|
|
return { root, path, db, sql, state, project, conversations }
|
|
}
|
|
|
|
it('preserves every source, revision, Unicode length and checkpoint for global, project and re-analysis scopes', async () => {
|
|
const f = await fixture()
|
|
const rows = f.sql.prepare('SELECT * FROM supervision_review_current WHERE length(body) > 0').all()
|
|
expect(new Set(rows.map(row => row.type))).toEqual(new Set(['conversation', 'task', 'knowledge']))
|
|
const hash = (value: unknown) => createHash('sha256').update(String(value)).digest('hex')
|
|
const source = rows.find(row => row.type === 'conversation')!
|
|
f.sql.prepare('INSERT INTO review_checkpoints VALUES (?, ?, ?, ?, ?, ?)').run('supervisor', 'timeline', String(source.source), hash(source.context), 3, Array.from(String(source.body)).length)
|
|
for (const scope of [{ kind: 'global' as const }, { kind: 'projects' as const, projectIds: [f.project.id] }]) {
|
|
for (const reanalyze of [false, true]) {
|
|
const state = { ...f.state, request: { ...f.state.request, scope, reanalyze } }
|
|
const id = f.db.startSupervisionRun(state.request)
|
|
await f.db.initializeSupervisionReview(id, state, new AbortController().signal)
|
|
const expected = rows.filter(row => scope.kind === 'global' || row.project_id === f.project.id).map(row => ({
|
|
source: row.source, revision: hash(row.context), project_id: row.project_id, conversation_id: row.conversation_id,
|
|
sequence: row.sequence, length: Array.from(String(row.body)).length,
|
|
initial_offset: !reanalyze && row.source === source.source ? 3 : 0,
|
|
processed_offset: !reanalyze && row.source === source.source ? 3 : 0
|
|
})).sort((a, b) => String(a.source).localeCompare(String(b.source)))
|
|
const actual = f.sql.prepare(`SELECT source, revision, project_id, conversation_id, sequence, length, initial_offset, processed_offset
|
|
FROM supervision_review_sources WHERE run_id = ?`).all(id).sort((a, b) => String(a.source).localeCompare(String(b.source)))
|
|
expect(actual).toEqual(expected)
|
|
expect(f.db.supervisionReviewStore().load(id).initializing).toBeUndefined()
|
|
}
|
|
}
|
|
expect(await f.db.supervisionContext(f.state.request)).toEqual({ summary: f.db.reviewSummary(f.state.request.scope, 'supervisor'), background: f.db.reviewBackground(f.state.request.scope) })
|
|
expect(await f.db.supervisionCandidates(f.state.request)).toEqual(f.db.listSupervisionCandidates(f.state.request))
|
|
})
|
|
|
|
it('rebuilds a partially written manifest after restart and validates completed legacy manifests on resume', async () => {
|
|
const f = await fixture()
|
|
const id = f.db.startSupervisionRun(f.state.request)
|
|
f.sql.exec(`CREATE TRIGGER fail_manifest BEFORE INSERT ON supervision_review_sources
|
|
WHEN (SELECT COUNT(*) FROM supervision_review_sources) >= 200 BEGIN SELECT RAISE(ABORT, 'interrupted initialization'); END`)
|
|
await expect(f.db.initializeSupervisionReview(id, f.state, new AbortController().signal)).rejects.toThrow('interrupted initialization')
|
|
expect(f.db.supervisionReviewStore().progress(id).sources).toBe(200)
|
|
expect(() => f.db.supervisionReviewStore().assertComplete(id)).toThrow('incomplete source manifest')
|
|
f.sql.exec('DROP TRIGGER fail_manifest')
|
|
f.db.close()
|
|
f.db.initialize(f.root)
|
|
await f.db.resumeSupervisionReview(id, new AbortController().signal)
|
|
expect(f.db.supervisionReviewStore().progress(id).sources).toBe(422)
|
|
expect(f.db.supervisionReviewStore().load(id).initializing).toBeUndefined()
|
|
// Existing saved runs have no initialization flag and retain their frozen versions.
|
|
f.sql.prepare('UPDATE messages SET content = ? WHERE id = ?').run('Changed', f.conversations[0]!.messages[0]!.id)
|
|
await expect(f.db.resumeSupervisionReview(id, new AbortController().signal)).rejects.toThrow('source changed')
|
|
expect(f.db.supervisionReviewStore().load(id).restartRequired).toBe(true)
|
|
})
|
|
|
|
it.each(['paused', 'cancelled'] as const)('accepts %s during production initialization, holds the slot, and never calls the model', async status => {
|
|
const f = await fixture()
|
|
const pool = new SupervisionModelPool()
|
|
cleanup.push(async () => pool.dispose())
|
|
const run = vi.fn(async function* () { throw new Error('Model must not start'); yield { type: 'done' } })
|
|
const service = createProductionSupervisorService(f.db, async () => ({}), async () => ({ run, dispose: async () => {} }) as unknown as AgentRuntime, pool)
|
|
const initialize = f.db.initializeSupervisionReview.bind(f.db)
|
|
let cancellation: Promise<void> | undefined
|
|
vi.spyOn(f.db, 'initializeSupervisionReview').mockImplementation((id, state, signal) => {
|
|
const pending = initialize(id, state, signal)
|
|
if (status === 'paused') service.pause(id)
|
|
else cancellation = service.cancel(id)
|
|
expect(service.execution()).toMatchObject({ active: true, runId: id, stopping: status })
|
|
return pending
|
|
})
|
|
const result = await service.run(f.state.request)
|
|
await cancellation
|
|
expect(result.status).toBe(status)
|
|
expect(run).not.toHaveBeenCalled()
|
|
expect(service.execution().active).toBe(false)
|
|
expect(f.db.supervisionReviewStore().load(result.runId!).initializing).toBe(true)
|
|
expect(f.sql.prepare('SELECT COUNT(*) AS n FROM review_checkpoints').get()!.n).toBe(0)
|
|
if (status === 'cancelled') await expect(service.resume(result.runId!)).rejects.toThrow('SUPERVISION_REVIEW_CANCELLED')
|
|
else {
|
|
await f.db.resumeSupervisionReview(result.runId!, new AbortController().signal)
|
|
expect(f.db.supervisionReviewStore().progress(result.runId!).sources).toBe(422)
|
|
}
|
|
})
|
|
|
|
it('fails explicitly when the worker entry is missing instead of running the scan on Main', async () => {
|
|
const f = await fixture()
|
|
f.db.enableReadonlyWorker(join(directory, 'missing-worker.cjs'))
|
|
const id = f.db.startSupervisionRun(f.state.request)
|
|
await expect(f.db.initializeSupervisionReview(id, f.state, new AbortController().signal)).rejects.toThrow()
|
|
expect(f.db.supervisionReviewStore().load(id).initializing).toBe(true)
|
|
expect(f.db.supervisionReviewStore().progress(id).sources).toBe(0)
|
|
})
|
|
|
|
it('publishes full worker-selected coverage atomically and skips it on the next production review', async () => {
|
|
const f = await fixture()
|
|
const pool = new SupervisionModelPool()
|
|
cleanup.push(async () => pool.dispose())
|
|
let calls = 0
|
|
const runtime = { async *run() {
|
|
calls++
|
|
yield { type: 'text', delta: JSON.stringify({ summary: 'Local stub', changeDigest: '', openItems: [], events: [], entities: [], entityChanges: [], relations: [] }) }
|
|
yield { type: 'done' }
|
|
}, async dispose() {}, async releaseConversation() {} } as unknown as AgentRuntime
|
|
const service = createProductionSupervisorService(f.db, async () => ({}), async () => runtime, pool)
|
|
const result = await service.run(f.state.request)
|
|
expect(result).toMatchObject({ status: 'completed', coverage: { sources: 422, remainingSources: 0, complete: true } })
|
|
expect(f.sql.prepare('SELECT COUNT(*) AS n FROM review_checkpoints').get()!.n).toBe(422)
|
|
expect(f.db.listSupervisionResults()).toHaveLength(1)
|
|
const before = calls
|
|
await expect(service.run(f.state.request)).resolves.toMatchObject({ status: 'no_change', coverage: { sources: 0 } })
|
|
expect(calls).toBe(before)
|
|
})
|
|
|
|
it.each(['crash', 'close'] as const)('settles writer %s before releasing pending work and can rebuild after reopening', async action => {
|
|
const f = await fixture()
|
|
const id = f.db.startSupervisionRun(f.state.request)
|
|
f.db.supervisionReviewStore().prepareInitialization(id, f.state)
|
|
const reader = new ReadonlyQueryReader('supervision', f.path, workerPath)
|
|
const pending = reader.call('initialize', [id, f.state])
|
|
const failure = expect(pending).rejects.toThrow()
|
|
if (action === 'close') reader.close()
|
|
else await reader.terminateWorkerForTest()
|
|
await failure
|
|
expect(reader.pendingCount).toBe(0)
|
|
reader.close()
|
|
await f.db.resumeSupervisionReview(id, new AbortController().signal)
|
|
expect(f.db.supervisionReviewStore().progress(id).sources).toBe(422)
|
|
})
|