mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix: reset Muse hook state and strengthen regression tests (#33008)
* fix: reset Muse hook state and strengthen boundary tests * test: require live permission, replay, and recency controls --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -91,19 +91,24 @@ it('removes old generated trees and retains recent, future, malformed and mutabl
|
||||
|
||||
it('does not follow cache, label, entry or shared-format symlinks', async () => {
|
||||
const options = await fixture()
|
||||
const outside = await stamped(join(options.root, 'outside', `${commit}-format-4`), 4)
|
||||
const outsideCache = join(options.root, 'outside-cache')
|
||||
const outsideLabel = join(options.root, 'outside-label')
|
||||
const outsideEntry = join(options.root, 'outside-entry', `${commit}-format-4`)
|
||||
const outsideFormat = join(options.root, 'outside-format')
|
||||
const sentinels = [
|
||||
await stamped(join(outsideCache, 'v-old', `${commit}-format-4`), 4),
|
||||
await stamped(join(outsideLabel, `${commit}-format-4`), 4),
|
||||
await stamped(outsideEntry, 4),
|
||||
await stamped(join(outsideFormat, commit), CHECKOUT_FORMAT)
|
||||
]
|
||||
const second = join(options.root, 'linked-worktree')
|
||||
await mkdir(join(second, 'tests', 'e2e'), { recursive: true })
|
||||
await symlink(
|
||||
join(outside, '..'),
|
||||
join(second, 'tests', 'e2e', '.cross-version-checkouts'),
|
||||
'junction'
|
||||
)
|
||||
await symlink(outsideCache, join(second, 'tests', 'e2e', '.cross-version-checkouts'), 'junction')
|
||||
options.targets.worktrees.push(second)
|
||||
await symlink(join(outside, '..'), join(options.cache, 'linked-label'), 'junction')
|
||||
await symlink(outsideLabel, join(options.cache, 'linked-label'), 'junction')
|
||||
await mkdir(join(options.cache, 'linked-entry'))
|
||||
await symlink(outside, join(options.cache, 'linked-entry', `${commit}-format-4`), 'junction')
|
||||
await symlink(join(outside, '..'), join(options.shared, 'format-4'), 'junction')
|
||||
await symlink(outsideEntry, join(options.cache, 'linked-entry', `${commit}-format-4`), 'junction')
|
||||
await symlink(outsideFormat, join(options.shared, `format-${CHECKOUT_FORMAT}`), 'junction')
|
||||
const result = await reclaimReleaseTestCaches({
|
||||
...options,
|
||||
apply: true,
|
||||
@@ -111,7 +116,11 @@ it('does not follow cache, label, entry or shared-format symlinks', async () =>
|
||||
minAgeDays: 0
|
||||
})
|
||||
expect(result.entries).toBe(0)
|
||||
expect(await readFile(join(outside, 'source.ts'), 'utf8')).toContain('historical')
|
||||
for (const sentinel of sentinels) {
|
||||
expect(await readFile(join(sentinel, 'source.ts'), 'utf8')).toBe(
|
||||
'export const historical = true\n'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('skips publication locks and a shared source copy lease', async () => {
|
||||
|
||||
@@ -182,6 +182,14 @@ it('does not sweep while cache initialization holds the maintenance lock', async
|
||||
|
||||
it('tracks a real Vite server until shutdown has completed', async () => {
|
||||
const options = await fixture()
|
||||
let unblockClose
|
||||
let enteredClose
|
||||
const closeBlocked = new Promise((resolve) => {
|
||||
unblockClose = resolve
|
||||
})
|
||||
const closeEntered = new Promise((resolve) => {
|
||||
enteredClose = resolve
|
||||
})
|
||||
const server = await createServer({
|
||||
configFile: false,
|
||||
root: options.worktree,
|
||||
@@ -189,13 +197,30 @@ it('tracks a real Vite server until shutdown has completed', async () => {
|
||||
logLevel: 'silent',
|
||||
server: { middlewareMode: true, watch: null, hmr: false },
|
||||
optimizeDeps: { noDiscovery: true, include: [] },
|
||||
plugins: [createViteCacheRetentionPlugin(options.cacheRoot)]
|
||||
plugins: [
|
||||
createViteCacheRetentionPlugin(options.cacheRoot),
|
||||
{
|
||||
name: 'hold-vite-close',
|
||||
async closeBundle() {
|
||||
enteredClose()
|
||||
await closeBlocked
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
try {
|
||||
await age(options.cacheRoot)
|
||||
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
|
||||
} finally {
|
||||
await server.close()
|
||||
const closing = server.close()
|
||||
try {
|
||||
await closeEntered
|
||||
await age(options.cacheRoot)
|
||||
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
|
||||
} finally {
|
||||
unblockClose()
|
||||
await closing
|
||||
}
|
||||
}
|
||||
expect(await readdir(join(options.cacheRoot, VITE_CACHE_USAGE_DIRECTORY))).toEqual([])
|
||||
expect(await reclaimViteCaches({ ...options, apply: true })).toMatchObject({ entries: 0 })
|
||||
|
||||
@@ -56,6 +56,7 @@ it('does not interpret piped input as approval', async () => {
|
||||
const reclaim = vi.fn().mockResolvedValue({ completed: true })
|
||||
await runReclaimCommand([], { ...streams, reclaim })
|
||||
expect(reclaim).toHaveBeenCalledTimes(1)
|
||||
expect(reclaim.mock.calls[0][0].apply).toBe(false)
|
||||
expect(streams.text()).not.toContain('[y/N]')
|
||||
})
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ it('leaves historical caches out of automatic invocations', async () => {
|
||||
excludeWorktree: '/dev-worktree'
|
||||
}
|
||||
await reclaimWorktreeSpace(options)
|
||||
expect(options.execute).toHaveBeenCalledTimes(3)
|
||||
expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain(
|
||||
'reclaim-release-test-caches'
|
||||
)
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('AgentHookServer listener replay', () => {
|
||||
})
|
||||
}
|
||||
|
||||
it('re-attaching a retired pane while idle re-opens it for a much later first turn', async () => {
|
||||
it('re-attaching an idle retired pane accepts completion and a later turn', async () => {
|
||||
const server = new AgentHookServer()
|
||||
await server.start({ env: 'production' })
|
||||
try {
|
||||
@@ -707,6 +707,13 @@ describe('AgentHookServer closed-tab suppression bound', () => {
|
||||
internals.markTabClosedForAgentStatus(`closed-tab-${i}`)
|
||||
}
|
||||
|
||||
const refreshedTab = `closed-tab-${total - CLOSED_AGENT_STATUS_TAB_IDS_MAX}`
|
||||
const nextOldestTab = `closed-tab-${total - CLOSED_AGENT_STATUS_TAB_IDS_MAX + 1}`
|
||||
internals.markTabClosedForAgentStatus(refreshedTab)
|
||||
internals.markTabClosedForAgentStatus('closed-tab-next')
|
||||
|
||||
expect(internals.closedAgentStatusTabIds.has(refreshedTab)).toBe(true)
|
||||
expect(internals.closedAgentStatusTabIds.has(nextOldestTab)).toBe(false)
|
||||
expect(internals.closedAgentStatusTabIds.size).toBe(CLOSED_AGENT_STATUS_TAB_IDS_MAX)
|
||||
expect(internals.closedAgentStatusTabIds.has('closed-tab-0')).toBe(false)
|
||||
expect(internals.closedAgentStatusTabIds.has(`closed-tab-${total - 1}`)).toBe(true)
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isAgentHookEndpointFileName, parseAgentHookEndpointFile } from './agent-hook-endpoint-file'
|
||||
import { parseAgentHookEndpointFile } from './agent-hook-endpoint-file'
|
||||
import { getEndpointFileName, writeEndpointFile } from './agent-hook-listener/endpoint-publication'
|
||||
|
||||
const ENDPOINT_LINES = [
|
||||
'ORCA_AGENT_HOOK_PORT=12345',
|
||||
'ORCA_AGENT_HOOK_TOKEN=token-123',
|
||||
'ORCA_AGENT_HOOK_ENV=production',
|
||||
'ORCA_AGENT_HOOK_VERSION=1'
|
||||
]
|
||||
const REQUIRED_FIELDS = [
|
||||
'ORCA_AGENT_HOOK_PORT',
|
||||
'ORCA_AGENT_HOOK_TOKEN',
|
||||
'ORCA_AGENT_HOOK_ENV',
|
||||
'ORCA_AGENT_HOOK_VERSION'
|
||||
]
|
||||
const ENDPOINT = { port: '12345', token: 'token-123', env: 'production', version: '1' }
|
||||
|
||||
describe('agent hook endpoint files', () => {
|
||||
it('recognizes POSIX and Windows endpoint file names', () => {
|
||||
expect(isAgentHookEndpointFileName('endpoint.env')).toBe(true)
|
||||
expect(isAgentHookEndpointFileName('endpoint.cmd')).toBe(true)
|
||||
expect(isAgentHookEndpointFileName('endpoint.ps1')).toBe(false)
|
||||
})
|
||||
|
||||
it('parses POSIX endpoint.env contents', () => {
|
||||
expect(
|
||||
parseAgentHookEndpointFile(
|
||||
@@ -44,22 +56,39 @@ describe('agent hook endpoint files', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves equals signs in endpoint values', () => {
|
||||
expect(
|
||||
parseAgentHookEndpointFile(
|
||||
[
|
||||
'ORCA_AGENT_HOOK_PORT=12345',
|
||||
'ORCA_AGENT_HOOK_TOKEN=token=with=equals',
|
||||
'ORCA_AGENT_HOOK_ENV=production',
|
||||
'ORCA_AGENT_HOOK_VERSION=1'
|
||||
].join('\n')
|
||||
).token
|
||||
).toBe('token=with=equals')
|
||||
})
|
||||
|
||||
it('throws when required endpoint fields are missing', () => {
|
||||
expect(() => parseAgentHookEndpointFile('ORCA_AGENT_HOOK_PORT=12345')).toThrow(
|
||||
it.each(REQUIRED_FIELDS)('rejects an independently missing %s', (field) => {
|
||||
expect(parseAgentHookEndpointFile(ENDPOINT_LINES.join('\n'))).toEqual(ENDPOINT)
|
||||
const missing = ENDPOINT_LINES.filter((line) => !line.startsWith(`${field}=`)).join('\n')
|
||||
expect(() => parseAgentHookEndpointFile(missing)).toThrow(
|
||||
'Agent hook endpoint file is missing required fields'
|
||||
)
|
||||
})
|
||||
|
||||
it.each(REQUIRED_FIELDS)('rejects an independently empty %s', (field) => {
|
||||
expect(parseAgentHookEndpointFile(ENDPOINT_LINES.join('\n'))).toEqual(ENDPOINT)
|
||||
const empty = ENDPOINT_LINES.map((line) =>
|
||||
line.startsWith(`${field}=`) ? `${field}=` : line
|
||||
).join('\n')
|
||||
expect(() => parseAgentHookEndpointFile(empty)).toThrow(
|
||||
'Agent hook endpoint file is missing required fields'
|
||||
)
|
||||
})
|
||||
|
||||
it('parses the endpoint bytes published by the platform writer', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-hook-endpoint-parser-'))
|
||||
try {
|
||||
const path = join(directory, getEndpointFileName())
|
||||
expect(
|
||||
writeEndpointFile(directory, path, {
|
||||
port: 12345,
|
||||
token: 'token-123',
|
||||
env: 'production',
|
||||
version: '1'
|
||||
})
|
||||
).toBe(true)
|
||||
expect(parseAgentHookEndpointFile(readFileSync(path, 'utf8'))).toEqual(ENDPOINT)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { mkdtempSync, readdirSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
|
||||
import type * as NodeFs from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const fsMocks = vi.hoisted(() => ({
|
||||
enabled: false,
|
||||
closeSync: vi.fn(),
|
||||
readSync: vi.fn(),
|
||||
statSync: vi.fn(),
|
||||
unlinkSync: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
opendirSync: () => ({
|
||||
closeSync: fsMocks.closeSync,
|
||||
readSync: fsMocks.readSync
|
||||
}),
|
||||
statSync: fsMocks.statSync,
|
||||
unlinkSync: fsMocks.unlinkSync
|
||||
}))
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NodeFs>()
|
||||
return {
|
||||
...actual,
|
||||
opendirSync: (path: string, options: { bufferSize: number }) =>
|
||||
fsMocks.enabled
|
||||
? { closeSync: fsMocks.closeSync, readSync: fsMocks.readSync }
|
||||
: actual.opendirSync(path, options),
|
||||
statSync: (path: string) => (fsMocks.enabled ? fsMocks.statSync(path) : actual.statSync(path)),
|
||||
unlinkSync: (path: string) =>
|
||||
fsMocks.enabled ? fsMocks.unlinkSync(path) : actual.unlinkSync(path)
|
||||
}
|
||||
})
|
||||
|
||||
import {
|
||||
AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES,
|
||||
@@ -23,11 +33,13 @@ import {
|
||||
|
||||
describe('agent hook endpoint temp cleanup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetAllMocks()
|
||||
fsMocks.enabled = false
|
||||
fsMocks.statSync.mockReturnValue({ mtimeMs: 0 })
|
||||
})
|
||||
|
||||
it('stops an unbounded directory source at the scan cap', () => {
|
||||
fsMocks.enabled = true
|
||||
let entry = 0
|
||||
fsMocks.readSync.mockImplementation(() => ({
|
||||
name: `.endpoint-${(entry += 1)}.tmp`
|
||||
@@ -40,19 +52,47 @@ describe('agent hook endpoint temp cleanup', () => {
|
||||
expect(fsMocks.closeSync).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('only removes stale endpoint temp files', () => {
|
||||
fsMocks.readSync
|
||||
.mockReturnValueOnce({ name: '.endpoint-stale.tmp' })
|
||||
.mockReturnValueOnce({ name: '.endpoint-fresh.tmp' })
|
||||
.mockReturnValueOnce({ name: 'endpoint.env' })
|
||||
.mockReturnValueOnce(null)
|
||||
fsMocks.statSync
|
||||
.mockReturnValueOnce({ mtimeMs: 0 })
|
||||
.mockReturnValueOnce({ mtimeMs: 9 * 60 * 1000 })
|
||||
|
||||
it('counts skipped names against the scan cap', () => {
|
||||
fsMocks.enabled = true
|
||||
let scanned = 0
|
||||
fsMocks.readSync.mockImplementation(() =>
|
||||
++scanned <= AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES + 2
|
||||
? { name: `unrelated-${scanned}` }
|
||||
: null
|
||||
)
|
||||
sweepStaleAgentHookEndpointTemps('/endpoint', 10 * 60 * 1000)
|
||||
expect(fsMocks.readSync).toHaveBeenCalledTimes(AGENT_HOOK_ENDPOINT_SWEEP_MAX_ENTRIES)
|
||||
expect(fsMocks.statSync).not.toHaveBeenCalled()
|
||||
expect(fsMocks.unlinkSync).not.toHaveBeenCalled()
|
||||
expect(fsMocks.closeSync).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
expect(fsMocks.unlinkSync).toHaveBeenCalledOnce()
|
||||
expect(fsMocks.unlinkSync).toHaveBeenCalledWith(expect.stringContaining('.endpoint-stale.tmp'))
|
||||
it('removes only matching temp files strictly older than five minutes', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-hook-endpoint-sweep-'))
|
||||
try {
|
||||
const now = 1_000_000
|
||||
for (const [name, age] of [
|
||||
['.endpoint-stale.tmp', 300_001],
|
||||
['.endpoint-boundary.tmp', 300_000],
|
||||
['.endpoint-fresh.tmp', 299_999],
|
||||
['.endpoint-wrong-extension.txt', 900_000],
|
||||
['wrong-prefix.tmp', 900_000],
|
||||
['endpoint.env', 900_000]
|
||||
] as const) {
|
||||
const path = join(directory, name)
|
||||
writeFileSync(path, 'endpoint fixture')
|
||||
utimesSync(path, (now - age) / 1000, (now - age) / 1000)
|
||||
}
|
||||
sweepStaleAgentHookEndpointTemps(directory, now)
|
||||
expect(readdirSync(directory).sort()).toEqual([
|
||||
'.endpoint-boundary.tmp',
|
||||
'.endpoint-fresh.tmp',
|
||||
'.endpoint-wrong-extension.txt',
|
||||
'endpoint.env',
|
||||
'wrong-prefix.tmp'
|
||||
])
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { normalizeHookPayload } from './agent-hook-listener'
|
||||
import type { AgentHookSource } from './agent-hook-relay'
|
||||
import {
|
||||
clearAllListenerCaches,
|
||||
clearPaneCacheState,
|
||||
@@ -10,54 +11,53 @@ import {
|
||||
} from './agent-hook-listener/listener-state'
|
||||
import { warnOnHookEnvOrVersionMismatch } from './agent-hook-listener/listener-limits'
|
||||
import { resolveHookSource } from './agent-hook-listener/source-routing'
|
||||
import { bindOpenCodeSession } from './agent-hook-listener/opencode-session-registry'
|
||||
import { makePaneKey } from './stable-pane-id'
|
||||
|
||||
const PANE = makePaneKey('tab-hooks', '11111111-1111-4111-8112-111111111111')
|
||||
const MOVED_PANE = makePaneKey('tab-hooks', '22222222-2222-4222-8222-222222222222')
|
||||
const ROUTES = {
|
||||
'/hook/claude': 'claude',
|
||||
'/hook/codex': 'codex',
|
||||
'/hook/gemini': 'gemini',
|
||||
'/hook/antigravity': 'antigravity',
|
||||
'/hook/amp': 'amp',
|
||||
'/hook/opencode': 'opencode',
|
||||
'/hook/mimo-code': 'mimo-code',
|
||||
'/hook/cursor': 'cursor',
|
||||
'/hook/pi': 'pi',
|
||||
'/hook/omp': 'omp',
|
||||
'/hook/prime-agent': 'prime-agent',
|
||||
'/hook/droid': 'droid',
|
||||
'/hook/command-code': 'command-code',
|
||||
'/hook/grok': 'grok',
|
||||
'/hook/copilot': 'copilot',
|
||||
'/hook/hermes': 'hermes',
|
||||
'/hook/devin': 'devin',
|
||||
'/hook/kimi': 'kimi',
|
||||
'/hook/jcode': 'jcode'
|
||||
} as const
|
||||
function normalizeProviderState(
|
||||
source: (typeof ROUTES)[keyof typeof ROUTES],
|
||||
source: AgentHookSource,
|
||||
eventName: string,
|
||||
payload: Record<string, unknown> = {}
|
||||
) {
|
||||
const routedSource = resolveHookSource(`/hook/${source}`)
|
||||
if (routedSource === null) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
normalizeHookPayload(
|
||||
createHookListenerState(),
|
||||
source,
|
||||
routedSource,
|
||||
{ paneKey: PANE, hook_event_name: eventName, payload },
|
||||
'production'
|
||||
)?.payload ?? null
|
||||
)
|
||||
}
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
describe('agent hook extraction boundaries', () => {
|
||||
it('routes exactly the complete provider vocabulary', () => {
|
||||
for (const [pathname, source] of Object.entries(ROUTES)) {
|
||||
expect(resolveHookSource(pathname)).toBe(source)
|
||||
expect(resolveHookSource(`${pathname}/`)).toBeNull()
|
||||
expect(resolveHookSource(`${pathname}?v=1`)).toBeNull()
|
||||
expect(resolveHookSource(pathname.toUpperCase())).toBeNull()
|
||||
}
|
||||
it.each([
|
||||
['qoder', 'UserPromptSubmit'],
|
||||
['qoder-cn', 'UserPromptSubmit'],
|
||||
['qwen-code', 'UserPromptSubmit'],
|
||||
['codebuddy', 'UserPromptSubmit'],
|
||||
['opencode2', 'SessionBusy'],
|
||||
['muse', 'UserPromptSubmit'],
|
||||
['zcode', 'UserPromptSubmit'],
|
||||
['dsh', 'UserPromptSubmit'],
|
||||
['kiro', 'userPromptSubmit']
|
||||
] as const)('normalizes %s hook traffic through its routed source', (source, eventName) => {
|
||||
expect(normalizeProviderState(source, eventName, { prompt: 'routed prompt' })).toMatchObject({
|
||||
state: 'working',
|
||||
agentType: source,
|
||||
prompt: 'routed prompt'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects unknown and malformed hook paths', () => {
|
||||
expect(resolveHookSource('/hook/unknown')).toBeNull()
|
||||
expect(resolveHookSource('/hook/devin/')).toBeNull()
|
||||
expect(resolveHookSource('/HOOK/DEVIN')).toBeNull()
|
||||
})
|
||||
|
||||
it('preserves thin provider lifecycle branches and source attribution', () => {
|
||||
@@ -86,9 +86,17 @@ describe('agent hook extraction boundaries', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('warns before tab rejection and caps version and environment warning keys independently', () => {
|
||||
it('warns before tab rejection and bounds distinct version and environment warnings', () => {
|
||||
const orderState = createHookListenerState()
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
expect(
|
||||
normalizeHookPayload(
|
||||
createHookListenerState(),
|
||||
'devin',
|
||||
{ paneKey: PANE, payload: { hook_event_name: 'UserPromptSubmit' } },
|
||||
'production'
|
||||
)?.payload
|
||||
).toMatchObject({ state: 'working', agentType: 'devin' })
|
||||
const rejected = normalizeHookPayload(
|
||||
orderState,
|
||||
'devin',
|
||||
@@ -96,13 +104,25 @@ describe('agent hook extraction boundaries', () => {
|
||||
paneKey: PANE,
|
||||
tabId: 'wrong-tab',
|
||||
version: 'outdated',
|
||||
payload: { hook_event_name: 'UnknownEvent' }
|
||||
payload: { hook_event_name: 'UserPromptSubmit' }
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(rejected).toBeNull()
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
|
||||
warn.mockClear()
|
||||
const repeatedState = createHookListenerState()
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
warnOnHookEnvOrVersionMismatch(repeatedState, {
|
||||
version: 'repeated-version',
|
||||
env: 'repeated-environment',
|
||||
expectedEnv: 'production'
|
||||
})
|
||||
}
|
||||
expect(warn).toHaveBeenCalledTimes(2)
|
||||
|
||||
warn.mockClear()
|
||||
const cappedState = createHookListenerState()
|
||||
for (let index = 0; index < 40; index++) {
|
||||
warnOnHookEnvOrVersionMismatch(cappedState, {
|
||||
@@ -113,10 +133,10 @@ describe('agent hook extraction boundaries', () => {
|
||||
}
|
||||
expect(cappedState.warnedVersions.size).toBe(32)
|
||||
expect(cappedState.warnedEnvs.size).toBe(32)
|
||||
warn.mockRestore()
|
||||
expect(warn).toHaveBeenCalledTimes(64)
|
||||
})
|
||||
|
||||
it('moves every pane-owned collection and preserves NUL suffixes', () => {
|
||||
it('moves selected pane-owned collections and preserves NUL suffixes', () => {
|
||||
const state = createHookListenerState()
|
||||
const scoped = `${PANE}\0child`
|
||||
const movedScoped = `${MOVED_PANE}\0child`
|
||||
@@ -169,22 +189,59 @@ describe('agent hook extraction boundaries', () => {
|
||||
expect(cache.get(movedScoped)).toBe('scoped')
|
||||
expect(cache.get(sibling)).toBe('sibling')
|
||||
expect(cache.has(PANE)).toBe(false)
|
||||
expect(cache.has(scoped)).toBe(false)
|
||||
}
|
||||
expect(state.lastStatusByPaneKey.get(MOVED_PANE)?.payload.prompt).toBe('exact')
|
||||
expect(state.lastStatusByPaneKey.get(movedScoped)?.payload.prompt).toBe('scoped')
|
||||
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
|
||||
expect(state.lastStatusByPaneKey.has(PANE)).toBe(false)
|
||||
expect(state.lastStatusByPaneKey.has(scoped)).toBe(false)
|
||||
for (const set of paneSets) {
|
||||
expect(set.has(MOVED_PANE)).toBe(true)
|
||||
expect(set.has(movedScoped)).toBe(true)
|
||||
expect(set.has(sibling)).toBe(true)
|
||||
expect(set.has(PANE)).toBe(false)
|
||||
expect(set.has(scoped)).toBe(false)
|
||||
}
|
||||
|
||||
movePaneCacheState(state, MOVED_PANE, MOVED_PANE)
|
||||
expect((state.lastPromptByPaneKey as Map<string, unknown>).get(MOVED_PANE)).toBe('exact')
|
||||
})
|
||||
|
||||
it('preserves bound OpenCode session ownership and launch identity after a pane move', () => {
|
||||
const state = createHookListenerState()
|
||||
normalizeHookPayload(
|
||||
state,
|
||||
'devin',
|
||||
{
|
||||
paneKey: PANE,
|
||||
launchToken: 'moved-launch',
|
||||
payload: { hook_event_name: 'UserPromptSubmit' }
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(
|
||||
bindOpenCodeSession(state, 'moved-session', { paneKey: PANE, boundAt: 1, basis: 'argv' })
|
||||
).toBe(true)
|
||||
movePaneCacheState(state, PANE, MOVED_PANE)
|
||||
const event = normalizeHookPayload(
|
||||
state,
|
||||
'opencode',
|
||||
{
|
||||
paneKey: PANE,
|
||||
opencodeSharedServer: 1,
|
||||
payload: { hook_event_name: 'SessionBusy', sessionID: 'moved-session' }
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(event).toMatchObject({
|
||||
paneKey: MOVED_PANE,
|
||||
launchToken: 'moved-launch',
|
||||
payload: { state: 'working', agentType: 'opencode' }
|
||||
})
|
||||
expect(state.lastLaunchTokenByPaneKey.has(PANE)).toBe(false)
|
||||
})
|
||||
|
||||
it('clears exact and NUL-scoped cache keys but not sibling prefixes', () => {
|
||||
const state = createHookListenerState()
|
||||
const scoped = `${PANE}\0thread`
|
||||
@@ -230,6 +287,7 @@ describe('agent hook extraction boundaries', () => {
|
||||
expect(state.lastStatusByPaneKey.has(scoped)).toBe(false)
|
||||
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
|
||||
expect(state.ampCompletedCacheKeys.has(scoped)).toBe(false)
|
||||
expect(state.ampCompletedCacheKeys.has(PANE)).toBe(false)
|
||||
expect(state.ampCompletedCacheKeys.has(sibling)).toBe(true)
|
||||
expect(state.claudeLeadStateByPaneKey.has(PANE)).toBe(false)
|
||||
expect(state.codexLeadStateByPaneKey.has(PANE)).toBe(false)
|
||||
@@ -267,6 +325,28 @@ describe('agent hook extraction boundaries', () => {
|
||||
state.claudeRunningNonAgentTaskPaneKeys.add(PANE)
|
||||
state.codexLeadStateByPaneKey.set(PANE, { state: 'working', stateStartedAt: 1 })
|
||||
state.grokActiveTurnByPaneKey.set(PANE, { promptId: 'prompt-1' })
|
||||
const notification = {
|
||||
paneKey: PANE,
|
||||
payload: { hook_event_name: 'Notification', notification_type: 'permission_prompt' }
|
||||
}
|
||||
normalizeHookPayload(
|
||||
state,
|
||||
'muse',
|
||||
{
|
||||
paneKey: PANE,
|
||||
payload: {
|
||||
hook_event_name: 'PermissionRequest',
|
||||
tool_name: 'stale-tool',
|
||||
tool_input: { command: 'old command' }
|
||||
}
|
||||
},
|
||||
'production'
|
||||
)
|
||||
expect(normalizeHookPayload(state, 'muse', notification, 'production')?.payload).toMatchObject({
|
||||
state: 'waiting',
|
||||
agentType: 'muse',
|
||||
toolName: 'stale-tool'
|
||||
})
|
||||
|
||||
clearAllListenerCaches(state)
|
||||
|
||||
@@ -276,5 +356,8 @@ describe('agent hook extraction boundaries', () => {
|
||||
expect(state.claudeRunningNonAgentTaskPaneKeys.size).toBe(0)
|
||||
expect(state.codexLeadStateByPaneKey.size).toBe(0)
|
||||
expect(state.grokActiveTurnByPaneKey.size).toBe(0)
|
||||
const afterReset = normalizeHookPayload(state, 'muse', notification, 'production')
|
||||
expect(afterReset?.payload).toMatchObject({ state: 'waiting', agentType: 'muse' })
|
||||
expect(afterReset?.payload.toolName).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -313,6 +313,7 @@ export function clearAllListenerCaches(state: HookListenerState): void {
|
||||
state.codexLeadStateByPaneKey.clear()
|
||||
state.grokActiveTurnByPaneKey.clear()
|
||||
state.grokMainAgentStatusByPaneKey.clear()
|
||||
state.musePaneStateByPaneKey.clear()
|
||||
state.opencodeSessionPaneBySessionId.clear()
|
||||
state.lastLaunchTokenByPaneKey.clear()
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('annotation viewport bridge live target', () => {
|
||||
guest.install(true)
|
||||
guest.flushFrames()
|
||||
|
||||
expect(guest.lastReport()?.target).toBeUndefined()
|
||||
expect(guest.lastReport()).toEqual({ scrollX: 0, scrollY: 0, viewportWidth: 1000 })
|
||||
})
|
||||
|
||||
it('re-reports on resize and when the element changes size', async () => {
|
||||
@@ -279,7 +279,7 @@ describe('annotation viewport bridge live target', () => {
|
||||
guest.target.removeAttribute(BROWSER_GRAB_SELECTED_ATTRIBUTE)
|
||||
guest.window.dispatchEvent(new guest.window.Event('scroll'))
|
||||
guest.flushFrames()
|
||||
expect(guest.lastReport()?.target).toBeUndefined()
|
||||
expect(guest.lastReport()).toEqual({ scrollX: 0, scrollY: 0, viewportWidth: 1000 })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -590,6 +590,13 @@ describe('publishAgentHookEnvelope redelivery', () => {
|
||||
publishAgentHookEnvelope(dispatcher, envelope)
|
||||
}
|
||||
|
||||
const refreshed = makeEnvelope({})
|
||||
refreshed.paneKey = 'tab-1:4f1b0f4e-0000-4000-8000-000000000001'
|
||||
publishAgentHookEnvelope(dispatcher, refreshed)
|
||||
const newest = makeEnvelope({})
|
||||
newest.paneKey = 'tab-1:4f1b0f4e-0000-4000-8000-000000000065'
|
||||
publishAgentHookEnvelope(dispatcher, newest)
|
||||
|
||||
primary.blocked = false
|
||||
primary.drain()
|
||||
vi.advanceTimersByTime(1_000)
|
||||
@@ -598,6 +605,9 @@ describe('publishAgentHookEnvelope redelivery', () => {
|
||||
expect(redelivered).toHaveLength(64)
|
||||
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000064')
|
||||
expect(redelivered).not.toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000000')
|
||||
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000001')
|
||||
expect(redelivered).not.toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000002')
|
||||
expect(redelivered).toContain('tab-1:4f1b0f4e-0000-4000-8000-000000000065')
|
||||
} finally {
|
||||
dispatcher.dispose()
|
||||
}
|
||||
|
||||
@@ -150,7 +150,11 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
|
||||
connectionId: 'conn-test',
|
||||
mainAgent: { state: 'done', outcome: 'cancellation' }
|
||||
})
|
||||
const replayOutcomes: (string | undefined)[] = []
|
||||
orcaServer.setListener((event) => replayOutcomes.push(event.payload.mainAgent?.outcome))
|
||||
replayOutcomes.length = 0
|
||||
expect(hookServer.replayCachedPayloadsForPanes()).toBe(1)
|
||||
await expect.poll(() => replayOutcomes).toEqual(['cancellation'])
|
||||
await expect
|
||||
.poll(() => orcaServer.getStatusSnapshotForPane(paneKey)[0]?.mainAgent?.outcome)
|
||||
.toBe('cancellation')
|
||||
@@ -341,6 +345,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
|
||||
tool_input: { command: 'pnpm test' }
|
||||
})
|
||||
).resolves.toMatchObject({ status: 204 })
|
||||
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
|
||||
await expect(
|
||||
postClaude({
|
||||
hook_event_name: 'PreToolUse',
|
||||
@@ -394,6 +399,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'false' }
|
||||
})
|
||||
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
|
||||
await postClaude({ hook_event_name: 'TeammateIdle', teammate_name: 'reviewer' })
|
||||
|
||||
const start = Date.now()
|
||||
@@ -444,6 +450,7 @@ describe('Integration: relay hook server → mux → AgentHookServer.ingestRemot
|
||||
tool_input: { command: 'rm -rf /tmp/orca-2824-permission-target' }
|
||||
})
|
||||
).resolves.toMatchObject({ status: 204 })
|
||||
await expect.poll(() => orcaServer.getStatusSnapshot()[0]?.state).toBe('waiting')
|
||||
await expect(
|
||||
postClaude({
|
||||
hook_event_name: 'PostToolUse',
|
||||
|
||||
Reference in New Issue
Block a user