mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-28 02:34:20 -05:00
* diagnostics(issue-1830): trace interruption causality * test(issue-1830): lock interruption ownership matrix * fix(codex): preserve stream deadline contract * fix(diagnostics): harden interruption trace lifecycle Refs #1830 * fix(diagnostics): harden interruption trace settlement Refs #1830 * fix(diagnostics): preserve interruption causality * fix(diagnostics): address interruption trace review * fix(diagnostics): preserve tracing observer contracts * fix(diagnostics): preserve interruption trace contracts * test(permissions): cover interactive hook interrupts
184 lines
6.6 KiB
TypeScript
184 lines
6.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
import {
|
|
acquireSharedMutationLock,
|
|
releaseSharedMutationLock,
|
|
} from './test/sharedMutationLock.js'
|
|
import { QueryEngine } from './QueryEngine.js'
|
|
import {
|
|
__getInterruptionTraceSnapshotForTests,
|
|
__resetInterruptionTraceForTests,
|
|
__waitForInterruptionTraceFlushForTests,
|
|
registerInterruptionController,
|
|
} from './utils/interruptionTrace.js'
|
|
|
|
const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE
|
|
|
|
beforeEach(async () => {
|
|
await acquireSharedMutationLock('QueryEngine.interruptionTrace.test.ts')
|
|
})
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await __waitForInterruptionTraceFlushForTests()
|
|
__resetInterruptionTraceForTests()
|
|
if (originalTrace === undefined) delete process.env.OPENCLAUDE_INTERRUPT_TRACE
|
|
else process.env.OPENCLAUDE_INTERRUPT_TRACE = originalTrace
|
|
} finally {
|
|
releaseSharedMutationLock()
|
|
}
|
|
})
|
|
|
|
describe('QueryEngine interruption tracing', () => {
|
|
test('does not record lifecycle entries while tracing is disabled', async () => {
|
|
delete process.env.OPENCLAUDE_INTERRUPT_TRACE
|
|
const engine = Object.create(QueryEngine.prototype) as QueryEngine
|
|
const controller = new AbortController()
|
|
;(engine as unknown as { abortController: AbortController }).abortController =
|
|
controller
|
|
;(engine as unknown as {
|
|
submitMessageImpl(): AsyncGenerator<never, void, unknown>
|
|
}).submitMessageImpl = async function* () {}
|
|
|
|
for await (const _message of engine.submitMessage('hello')) {
|
|
// The stub deliberately yields nothing.
|
|
}
|
|
|
|
expect(__getInterruptionTraceSnapshotForTests()).toEqual([])
|
|
})
|
|
|
|
test('records a programmatic query-root interruption before aborting', () => {
|
|
process.env.OPENCLAUDE_INTERRUPT_TRACE = '1'
|
|
const controller = new AbortController()
|
|
const engine = Object.create(QueryEngine.prototype) as QueryEngine
|
|
;(engine as unknown as {
|
|
abortController: AbortController
|
|
}).abortController = controller
|
|
|
|
engine.interrupt('sdk_interrupt')
|
|
|
|
const requested = __getInterruptionTraceSnapshotForTests().find(
|
|
entry => entry.event === 'abort.requested',
|
|
)
|
|
expect(controller.signal.aborted).toBe(true)
|
|
expect(requested).toMatchObject({
|
|
source: 'sdk_interrupt',
|
|
subsystem: 'query_engine',
|
|
controllerRole: 'query-root',
|
|
})
|
|
})
|
|
|
|
test('records start and terminal lifecycle for successful SDK turns', async () => {
|
|
process.env.OPENCLAUDE_INTERRUPT_TRACE = '1'
|
|
const engine = Object.create(QueryEngine.prototype) as QueryEngine
|
|
const controller = new AbortController()
|
|
;(engine as unknown as { abortController: AbortController }).abortController =
|
|
controller
|
|
;(engine as unknown as {
|
|
submitMessageImpl(): AsyncGenerator<never, void, unknown>
|
|
}).submitMessageImpl = async function* () {}
|
|
|
|
for await (const _message of engine.submitMessage('hello')) {
|
|
// The stub deliberately yields nothing.
|
|
}
|
|
|
|
const trace = __getInterruptionTraceSnapshotForTests()
|
|
const started = trace.find(entry => entry.event === 'query.started')
|
|
const terminal = trace.find(entry => entry.event === 'query.terminal')
|
|
expect(started).toMatchObject({
|
|
subsystem: 'query_engine',
|
|
querySource: 'sdk',
|
|
controllerRole: 'query-root',
|
|
})
|
|
expect(terminal).toMatchObject({
|
|
subsystem: 'query_engine',
|
|
queryId: started?.queryId,
|
|
outcome: 'completed',
|
|
})
|
|
expect(typeof started?.eventId).toBe('string')
|
|
expect(typeof terminal?.causalEventId).toBe('string')
|
|
expect(terminal!.causalEventId).toBe(started!.eventId)
|
|
})
|
|
|
|
test('records aborted and failed SDK turn terminals', async () => {
|
|
process.env.OPENCLAUDE_INTERRUPT_TRACE = '1'
|
|
|
|
for (const scenario of ['aborted', 'failed'] as const) {
|
|
__resetInterruptionTraceForTests()
|
|
const engine = Object.create(QueryEngine.prototype) as QueryEngine
|
|
const controller = new AbortController()
|
|
;(engine as unknown as { abortController: AbortController }).abortController =
|
|
controller
|
|
;(engine as unknown as {
|
|
submitMessageImpl(): AsyncGenerator<never, void, unknown>
|
|
}).submitMessageImpl = async function* () {
|
|
if (scenario === 'aborted') {
|
|
controller.abort('interrupt')
|
|
return
|
|
}
|
|
throw new Error('turn failed')
|
|
}
|
|
|
|
const drain = async () => {
|
|
for await (const _message of engine.submitMessage('hello')) {
|
|
// The stub deliberately yields nothing.
|
|
}
|
|
}
|
|
if (scenario === 'failed') await expect(drain()).rejects.toThrow('turn failed')
|
|
else await drain()
|
|
|
|
const trace = __getInterruptionTraceSnapshotForTests()
|
|
const started = trace.find(entry => entry.event === 'query.started')
|
|
const terminal = trace.find(entry => entry.event === 'query.terminal')
|
|
expect(terminal?.outcome).toBe(scenario)
|
|
expect(typeof started?.eventId).toBe('string')
|
|
expect(typeof terminal?.eventId).toBe('string')
|
|
if (scenario === 'aborted') {
|
|
const observed = trace.find(
|
|
entry => entry.event === 'signal.observed',
|
|
)
|
|
expect(typeof observed?.eventId).toBe('string')
|
|
expect(terminal?.causalEventId).toBe(observed!.eventId)
|
|
} else {
|
|
expect(terminal?.causalEventId).toBe(started!.eventId)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('registers the query root when tracing is enabled at the turn boundary', async () => {
|
|
delete process.env.OPENCLAUDE_INTERRUPT_TRACE
|
|
const engine = Object.create(QueryEngine.prototype) as QueryEngine
|
|
const controller = new AbortController()
|
|
;(engine as unknown as { abortController: AbortController }).abortController =
|
|
controller
|
|
registerInterruptionController(controller, {
|
|
subsystem: 'query_engine',
|
|
controllerRole: 'query-root',
|
|
})
|
|
process.env.OPENCLAUDE_INTERRUPT_TRACE = '1'
|
|
;(engine as unknown as {
|
|
submitMessageImpl(): AsyncGenerator<never, void, unknown>
|
|
}).submitMessageImpl = async function* () {
|
|
controller.abort()
|
|
}
|
|
|
|
for await (const _message of engine.submitMessage('hello')) {
|
|
// The stub deliberately yields nothing.
|
|
}
|
|
|
|
const trace = __getInterruptionTraceSnapshotForTests()
|
|
const registered = trace.find(
|
|
entry =>
|
|
entry.event === 'controller.registered' &&
|
|
entry.controllerRole === 'query-root',
|
|
)
|
|
const observed = trace.find(entry => entry.event === 'signal.observed')
|
|
const terminal = trace.find(entry => entry.event === 'query.terminal')
|
|
expect(registered).toBeDefined()
|
|
expect(typeof observed?.eventId).toBe('string')
|
|
expect(terminal).toMatchObject({
|
|
outcome: 'aborted',
|
|
causalEventId: observed!.eventId,
|
|
})
|
|
})
|
|
})
|