cc-haha/desktop/scripts/build-sidecars.test.ts
程序员阿江(Relakkes) 53a15098c0 perf(desktop): add SQLite-backed local data index #1016
Project sessions, activity, scheduled runs, traces, and searchable content into rebuildable managed databases while preserving canonical-file fallbacks. Add diagnostics, recovery, parity coverage, and performance acceptance checks.
2026-07-16 04:16:02 +08:00

623 lines
20 KiB
TypeScript

// @vitest-environment node
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path, { join as joinPath } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
reserveLocalPort,
resolveHostTriple,
resolveSidecarExecutable,
} from '../electron/services/sidecarManager'
function readBuildScript() {
return readFileSync(path.resolve(import.meta.dirname, 'build-sidecars.ts'), 'utf8')
}
function readCliLauncher() {
return readFileSync(
path.resolve(import.meta.dirname, '../../bin/claude-haha'),
'utf8',
)
}
function readJson(pathname: string): { scripts?: Record<string, string> } {
return JSON.parse(readFileSync(pathname, 'utf8')) as {
scripts?: Record<string, string>
}
}
function extractWindowsX64BunTarget(source: string) {
const match = source.match(/case 'x86_64-pc-windows-msvc':[\s\S]*?return '([^']+)'/)
return match?.[1] ?? null
}
type SidecarProcess = {
child: ChildProcessWithoutNullStreams
exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>
logs: () => string
}
function controlledSidecarEnvironment(
homeDir: string,
configDir: string,
localAccessToken: string,
): NodeJS.ProcessEnv {
const inheritedKeys = [
'PATH',
'TMPDIR',
'TEMP',
'TMP',
'SystemRoot',
'WINDIR',
'ComSpec',
'PATHEXT',
'LD_LIBRARY_PATH',
'DYLD_LIBRARY_PATH',
] as const
const env: NodeJS.ProcessEnv = {}
for (const key of inheritedKeys) {
if (process.env[key] !== undefined) env[key] = process.env[key]
}
return {
...env,
HOME: homeDir,
CLAUDE_CONFIG_DIR: configDir,
CC_HAHA_LOCAL_ACCESS_TOKEN: localAccessToken,
NODE_ENV: 'test',
NO_PROXY: '127.0.0.1,localhost,::1',
no_proxy: '127.0.0.1,localhost,::1',
}
}
function startCompiledSidecar(options: {
executable: string
repoRoot: string
port: number
env: NodeJS.ProcessEnv
}): SidecarProcess {
const child = spawn(options.executable, [
'server',
'--app-root',
options.repoRoot,
'--host',
'127.0.0.1',
'--port',
String(options.port),
], {
cwd: options.repoRoot,
env: options.env,
stdio: ['ignore', 'pipe', 'pipe'],
})
let output = ''
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
child.stdout.on('data', chunk => {
output += String(chunk)
})
child.stderr.on('data', chunk => {
output += String(chunk)
})
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolveExit, rejectExit) => {
child.once('error', rejectExit)
child.once('exit', (code, signal) => resolveExit({ code, signal }))
},
)
return { child, exited, logs: () => output }
}
async function waitForCompiledSidecar(options: {
baseUrl: string
expectedSessionId: string
deadlineMs: number
exited: SidecarProcess['exited']
logs: () => string
localAccessToken: string
onAuthProbe?: (proof: {
missingTokenStatus: number
wrongTokenStatus: number
correctTokenStatus: number
}) => void
}): Promise<void> {
const deadline = Date.now() + options.deadlineMs
let authProbeComplete = false
let lastFailure = 'no response received'
while (Date.now() < deadline) {
const exited = await Promise.race([
options.exited.then(result => ({ exited: true as const, result })),
new Promise<{ exited: false }>(resolveWait => {
setTimeout(() => resolveWait({ exited: false }), 0)
}),
])
if (exited.exited) {
throw new Error(
`compiled sidecar exited before readiness (${JSON.stringify(exited.result)}):\n${options.logs()}`,
)
}
try {
const authorizedHeaders = {
Authorization: `Bearer ${options.localAccessToken}`,
}
const health = await fetchBeforeCompiledSidecarDeadline(
`${options.baseUrl}/health`,
{ headers: authorizedHeaders },
deadline,
)
if (!health.ok) throw new Error(`health returned ${health.status}`)
let pendingAuthProof: {
missingTokenStatus: number
wrongTokenStatus: number
} | undefined
if (!authProbeComplete) {
const [missingTokenResponse, wrongTokenResponse] = await Promise.all([
fetchBeforeCompiledSidecarDeadline(
`${options.baseUrl}/api/sessions?limit=1&offset=0`,
{},
deadline,
),
fetchBeforeCompiledSidecarDeadline(
`${options.baseUrl}/api/sessions?limit=1&offset=0`,
{ headers: { Authorization: 'Bearer wrong-local-access-token' } },
deadline,
),
])
pendingAuthProof = {
missingTokenStatus: missingTokenResponse.status,
wrongTokenStatus: wrongTokenResponse.status,
}
}
const sessionsResponse = await fetchBeforeCompiledSidecarDeadline(
`${options.baseUrl}/api/sessions?limit=400&offset=0`,
{ headers: authorizedHeaders },
deadline,
)
if (!sessionsResponse.ok) {
throw new Error(`sessions returned ${sessionsResponse.status}`)
}
if (pendingAuthProof) {
options.onAuthProbe?.({
...pendingAuthProof,
correctTokenStatus: sessionsResponse.status,
})
authProbeComplete = true
}
const body = await sessionsResponse.json() as {
sessions?: Array<{ id?: string }>
total?: number
index?: { mode?: string; state?: string; indexed?: number }
}
if (
body.index?.mode === 'on' &&
body.index.state === 'ready' &&
body.index.indexed === 1 &&
body.total === 1 &&
body.sessions?.some(session => session.id === options.expectedSessionId)
) {
return
}
lastFailure = `sessions not ready: ${JSON.stringify(body)}`
} catch (error) {
// Startup and backfill are asynchronous; keep polling until the deadline.
lastFailure = error instanceof Error ? error.message : String(error)
}
const remainingMs = deadline - Date.now()
if (remainingMs > 0) {
await new Promise(resolveWait => setTimeout(resolveWait, Math.min(25, remainingMs)))
}
}
throw new Error(
`compiled sidecar did not become ready (${lastFailure}):\n${options.logs()}`,
)
}
async function fetchBeforeCompiledSidecarDeadline(
input: RequestInfo | URL,
init: RequestInit,
deadline: number,
): Promise<Response> {
const remainingMs = deadline - Date.now()
if (remainingMs <= 0) {
throw new Error('compiled sidecar request deadline exceeded')
}
const controller = new AbortController()
let responseAfterHeaders: Response | undefined
let activeReader: ReadableStreamDefaultReader<Uint8Array> | undefined
const operation = (async (): Promise<Response> => {
const response = await fetch(input, { ...init, signal: controller.signal })
responseAfterHeaders = response
if (controller.signal.aborted) {
try {
const cancellation = response.body?.cancel(controller.signal.reason)
if (cancellation) void Promise.resolve(cancellation).catch(() => {})
} catch {
// A fetch that ignored abort may also return a non-cancellable body.
}
throw controller.signal.reason instanceof Error
? controller.signal.reason
: new Error('compiled sidecar request deadline exceeded')
}
if (!response.body) return response
const reader = response.body.getReader()
activeReader = reader
const chunks: Uint8Array[] = []
let byteLength = 0
try {
while (true) {
const result = await reader.read()
if (result.done) break
chunks.push(result.value)
byteLength += result.value.byteLength
}
} finally {
if (activeReader === reader) activeReader = undefined
try {
reader.releaseLock()
} catch {
// A timed-out read may still be pending on a non-cooperative stream.
}
}
const body = new Uint8Array(byteLength)
let offset = 0
for (const chunk of chunks) {
body.set(chunk, offset)
offset += chunk.byteLength
}
return new Response(body.byteLength > 0 ? body.buffer : null, {
status: response.status,
statusText: response.statusText,
headers: new Headers(response.headers),
})
})()
return new Promise<Response>((resolve, reject) => {
let settled = false
const cancelBodyBestEffort = (reason: Error): void => {
try {
const cancellation = activeReader
? activeReader.cancel(reason)
: responseAfterHeaders?.body?.cancel(reason)
if (cancellation) void Promise.resolve(cancellation).catch(() => {})
} catch {
// Cancellation is best-effort; the deadline must not wait for it.
}
}
const timeout = setTimeout(() => {
if (settled) return
settled = true
clearTimeout(timeout)
const error = new Error('compiled sidecar request deadline exceeded')
controller.abort(error)
cancelBodyBestEffort(error)
reject(error)
}, remainingMs)
operation.then(
response => {
if (settled) {
try {
const cancellation = response.body?.cancel()
if (cancellation) void Promise.resolve(cancellation).catch(() => {})
} catch {
// Consume a late response without extending the caller deadline.
}
return
}
settled = true
clearTimeout(timeout)
resolve(response)
},
error => {
if (settled) return
settled = true
clearTimeout(timeout)
reject(error)
},
)
})
}
async function terminateCompiledSidecar(processHandle: SidecarProcess): Promise<void> {
if (processHandle.child.exitCode !== null || processHandle.child.signalCode !== null) {
await processHandle.exited
return
}
processHandle.child.kill('SIGTERM')
const graceful = await Promise.race([
processHandle.exited.then(() => true),
new Promise<boolean>(resolveWait => setTimeout(() => resolveWait(false), 10_000)),
])
if (graceful) return
processHandle.child.kill('SIGKILL')
await processHandle.exited
}
describe('build-sidecars Windows x64 target mapping', () => {
it('uses the baseline Bun runtime so older CPUs do not crash with Illegal Instruction', () => {
expect(extractWindowsX64BunTarget(readBuildScript())).toBe('bun-windows-x64-baseline')
})
it('compiles the sidecar with the transcript classifier feature', () => {
expect(readBuildScript()).toContain("features: ['TRANSCRIPT_CLASSIFIER']")
})
it('starts the development CLI with the transcript classifier feature', () => {
expect(readCliLauncher()).toContain('--feature=TRANSCRIPT_CLASSIFIER')
})
it('wires the opt-in compiled sidecar smoke into the native gate', () => {
const desktopPackage = readJson(path.resolve(import.meta.dirname, '../package.json'))
const rootPackage = readJson(path.resolve(import.meta.dirname, '../../package.json'))
expect(desktopPackage.scripts?.['test:compiled-sidecar-smoke']).toContain(
'CC_HAHA_RUN_COMPILED_SIDECAR_SMOKE=1',
)
expect(rootPackage.scripts?.['check:native']).toContain(
'test:compiled-sidecar-smoke',
)
})
it('keeps the request deadline active until a delayed response body is consumable', async () => {
const originalFetch = globalThis.fetch
let bodyClosed = false
let bodyTimer: ReturnType<typeof setTimeout> | undefined
globalThis.fetch = (async () => new Response(new ReadableStream<Uint8Array>({
start(controller) {
bodyTimer = setTimeout(() => {
controller.enqueue(new TextEncoder().encode('{"ready":true}'))
bodyClosed = true
controller.close()
}, 20)
},
}), {
status: 200,
headers: { 'content-type': 'application/json' },
})) as typeof fetch
try {
const response = await fetchBeforeCompiledSidecarDeadline(
'http://127.0.0.1:1/delayed-body',
{},
Date.now() + 250,
)
expect(bodyClosed).toBe(true)
await expect(response.json()).resolves.toEqual({ ready: true })
} finally {
if (bodyTimer) clearTimeout(bodyTimer)
globalThis.fetch = originalFetch
}
})
it('rejects at the deadline and consumes non-cooperative late fetch settlements', async () => {
const originalFetch = globalThis.fetch
try {
for (const outcome of ['resolve', 'reject'] as const) {
let lateBodyCancelCount = 0
let resolveFetch!: (response: Response) => void
let rejectFetch!: (error: Error) => void
globalThis.fetch = (() => new Promise<Response>((resolve, reject) => {
resolveFetch = resolve
rejectFetch = reject
})) as typeof fetch
const request = fetchBeforeCompiledSidecarDeadline(
`http://127.0.0.1:1/late-${outcome}`,
{},
Date.now() + 10,
)
const lateTimer = setTimeout(() => {
if (outcome === 'resolve') {
resolveFetch(new Response(new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode('{"late":'))
},
cancel() {
lateBodyCancelCount += 1
},
}), { status: 200 }))
} else {
rejectFetch(new Error('late fetch rejection'))
}
}, 40)
await expect(request).rejects.toThrow('compiled sidecar request deadline exceeded')
await new Promise(resolveWait => setTimeout(resolveWait, 50))
clearTimeout(lateTimer)
if (outcome === 'resolve') expect(lateBodyCancelCount).toBe(1)
}
} finally {
globalThis.fetch = originalFetch
}
})
it('bounds a sidecar response with headers but a never-ending body and still cleans up', async () => {
const originalFetch = globalThis.fetch
const originalEnvironment = {
HOME: process.env.HOME,
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
CC_HAHA_LOCAL_INDEX: process.env.CC_HAHA_LOCAL_INDEX,
CC_HAHA_LOCAL_ACCESS_TOKEN: process.env.CC_HAHA_LOCAL_ACCESS_TOKEN,
}
const rootDir = await mkdtemp(joinPath(tmpdir(), 'cc-haha-hung-sidecar-smoke-'))
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
stdio: ['pipe', 'pipe', 'pipe'],
})
const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolveExit, rejectExit) => {
child.once('error', rejectExit)
child.once('exit', (code, signal) => resolveExit({ code, signal }))
},
)
const processHandle: SidecarProcess = {
child,
exited,
logs: () => '',
}
let bodyCancelCount = 0
let rejectCancellation: ((error: Error) => void) | undefined
globalThis.fetch = (async () => new Response(new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode('{"partial":'))
},
cancel() {
bodyCancelCount += 1
return new Promise<void>((_resolve, reject) => {
rejectCancellation = reject
})
},
}), {
status: 200,
headers: { 'content-type': 'application/json' },
})) as typeof fetch
const startedAt = performance.now()
let guardTimer: ReturnType<typeof setTimeout> | undefined
try {
await expect(Promise.race([
waitForCompiledSidecar({
baseUrl: 'http://127.0.0.1:1',
expectedSessionId: 'never-ready',
deadlineMs: 25,
exited,
logs: processHandle.logs,
localAccessToken: 'hung-sidecar-local-access-token',
}),
new Promise<never>((_resolve, reject) => {
guardTimer = setTimeout(
() => reject(new Error('never-ending response body guard fired')),
250,
)
}),
])).rejects.toThrow('compiled sidecar did not become ready')
} finally {
if (guardTimer) clearTimeout(guardTimer)
globalThis.fetch = originalFetch
try {
await terminateCompiledSidecar(processHandle)
} finally {
await rm(rootDir, { recursive: true, force: true })
}
}
expect(performance.now() - startedAt).toBeLessThan(150)
expect(bodyCancelCount).toBeGreaterThan(0)
rejectCancellation?.(new Error('late body cancellation rejection'))
await new Promise(resolveWait => setTimeout(resolveWait, 0))
expect(child.exitCode !== null || child.signalCode !== null).toBe(true)
expect(await stat(rootDir).then(() => true, () => false)).toBe(false)
expect(process.env.HOME).toBe(originalEnvironment.HOME)
expect(process.env.CLAUDE_CONFIG_DIR).toBe(originalEnvironment.CLAUDE_CONFIG_DIR)
expect(process.env.CC_HAHA_LOCAL_INDEX).toBe(originalEnvironment.CC_HAHA_LOCAL_INDEX)
expect(process.env.CC_HAHA_LOCAL_ACCESS_TOKEN).toBe(
originalEnvironment.CC_HAHA_LOCAL_ACCESS_TOKEN,
)
})
})
const compiledSidecarSmokeEnabled =
process.env.CC_HAHA_RUN_COMPILED_SIDECAR_SMOKE === '1'
describe.skipIf(!compiledSidecarSmokeEnabled)('compiled sidecar local-index smoke', () => {
it('uses SQLite by default, serves one indexed session, and reopens the database', async () => {
const repoRoot = path.resolve(import.meta.dirname, '../..')
const desktopRoot = path.resolve(import.meta.dirname, '..')
const executable = resolveSidecarExecutable(
desktopRoot,
resolveHostTriple(),
)
await stat(executable)
const rootDir = await mkdtemp(joinPath(tmpdir(), 'cc-haha-compiled-sidecar-smoke-'))
const homeDir = joinPath(rootDir, 'home')
const configDir = joinPath(homeDir, '.claude')
const projectDir = joinPath(configDir, 'projects', '-tmp-compiled-sidecar-smoke')
const sessionId = 'compiled-sidecar-smoke-session'
const localAccessToken = 'compiled-sidecar-smoke-local-access-token'
const authenticationProofs: Array<{
missingTokenStatus: number
wrongTokenStatus: number
correctTokenStatus: number
}> = []
const databasePath = joinPath(
configDir,
'cc-haha',
'db',
'index-v1.sqlite',
)
let activeProcess: SidecarProcess | undefined
const startAndVerify = async (): Promise<void> => {
const port = await reserveLocalPort('127.0.0.1')
activeProcess = startCompiledSidecar({
executable,
repoRoot,
port,
env: controlledSidecarEnvironment(homeDir, configDir, localAccessToken),
})
try {
await waitForCompiledSidecar({
baseUrl: `http://127.0.0.1:${port}`,
expectedSessionId: sessionId,
deadlineMs: 30_000,
exited: activeProcess.exited,
logs: activeProcess.logs,
localAccessToken,
onAuthProbe: proof => authenticationProofs.push(proof),
})
expect((await stat(databasePath)).isFile()).toBe(true)
} finally {
await terminateCompiledSidecar(activeProcess)
activeProcess = undefined
}
}
try {
await mkdir(projectDir, { recursive: true })
await writeFile(
joinPath(projectDir, `${sessionId}.jsonl`),
`${JSON.stringify({
parentUuid: null,
isSidechain: false,
type: 'user',
message: {
role: 'user',
content: 'Compiled sidecar smoke prompt',
},
uuid: 'compiled-sidecar-smoke-user-entry',
timestamp: '2026-07-15T00:00:00.000Z',
})}\n`,
'utf8',
)
await startAndVerify()
await startAndVerify()
expect(authenticationProofs).toEqual([
{
missingTokenStatus: 403,
wrongTokenStatus: 403,
correctTokenStatus: 200,
},
{
missingTokenStatus: 403,
wrongTokenStatus: 403,
correctTokenStatus: 200,
},
])
} finally {
try {
if (activeProcess) await terminateCompiledSidecar(activeProcess)
} finally {
await rm(rootDir, { recursive: true, force: true })
}
}
expect(await stat(rootDir).then(() => true, () => false)).toBe(false)
}, 90_000)
})