mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* test(opencode): specify fresh-session clear * feat(opencode): open a fresh session on clear * fix(opencode): release clear latch on cancel * fix(opencode): retry transient clear handoffs * fix(opencode): confirm clear archive delivery * fix(web): preserve superseded session access * fix(clear): invalidate transferred schedules * fix(runner): restore live spawn dedupe * fix(clear): preserve latched scheduled prompts * fix(runner): quarantine unverified children * fix(clear): retain handoff retry ownership * fix(runner): release recovered spawn dedupe * fix(clear): retain archive retry ownership * fix(clear): settle rejected immediate prompts * fix(clear): block reopening replaced sources * fix(clear): settle prompts when clear is cancelled * fix(clear): make fresh-session handoff durable * fix(clear): finalize only after native cleanup * fix(clear): abort failed native handoffs * fix(clear): gate recovery on cleanup proof * fix(clear): retry metadata persistence failures * fix(clear): preserve handoff ownership through teardown * fix(clear): abort incomplete cleanup reservations * fix(clear): require explicit exit before abort * fix(clear): verify owner exit before recovery * fix(clear): guard recovery handoff races * fix(clear): serialize cleanup callbacks * fix(clear): make callback retries idempotent * fix(clear): bind callbacks to reservations * fix(clear): recover pending spawns * fix(clear): deduplicate held prompts * fix(clear): validate redirect ownership * fix(clear): replay prompts in FIFO order * fix(clear): gate replacement delivery
189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { configuration } from '@/configuration'
|
|
|
|
const axiosPostMock = vi.hoisted(() => vi.fn())
|
|
const ioMock = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('axios', () => ({
|
|
default: {
|
|
post: axiosPostMock
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/api/auth', () => ({
|
|
getAuthToken: () => 'cli-token'
|
|
}))
|
|
|
|
vi.mock('socket.io-client', () => ({
|
|
io: ioMock
|
|
}))
|
|
|
|
vi.mock('@/api/rpc/RpcHandlerManager', () => ({
|
|
RpcHandlerManager: class {
|
|
onSocketConnect(): void { }
|
|
onSocketDisconnect(): void { }
|
|
registerHandler(): void { }
|
|
handleRequest(): Promise<string> {
|
|
return Promise.resolve('{}')
|
|
}
|
|
}
|
|
}))
|
|
|
|
vi.mock('../modules/common/registerCommonHandlers', () => ({
|
|
registerCommonHandlers: () => { }
|
|
}))
|
|
|
|
vi.mock('@/terminal/TerminalManager', () => ({
|
|
TerminalManager: class {
|
|
closeAll(): void { }
|
|
}
|
|
}))
|
|
|
|
import { ApiClient } from './api'
|
|
import { ApiSessionClient } from './apiSession'
|
|
|
|
describe('API extra headers integration', () => {
|
|
const now = 1_710_000_000_000
|
|
|
|
beforeEach(() => {
|
|
configuration._setApiUrl('https://hapi.example.com')
|
|
configuration._setExtraHeaders({})
|
|
axiosPostMock.mockReset()
|
|
ioMock.mockReset()
|
|
})
|
|
|
|
it('adds extra headers to REST requests', async () => {
|
|
configuration._setExtraHeaders({
|
|
Cookie: 'CF_Authorization=token'
|
|
})
|
|
|
|
axiosPostMock.mockResolvedValue({
|
|
data: {
|
|
session: {
|
|
id: 'session-1',
|
|
namespace: 'default',
|
|
seq: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
active: true,
|
|
activeAt: now,
|
|
metadata: {
|
|
path: '/tmp/project',
|
|
host: 'test-host'
|
|
},
|
|
metadataVersion: 0,
|
|
agentState: null,
|
|
agentStateVersion: 0,
|
|
thinking: false,
|
|
thinkingAt: now,
|
|
todos: [],
|
|
model: null,
|
|
modelReasoningEffort: null,
|
|
effort: null,
|
|
permissionMode: undefined,
|
|
collaborationMode: undefined
|
|
}
|
|
}
|
|
})
|
|
|
|
const client = await ApiClient.create()
|
|
await client.getOrCreateSession({
|
|
tag: 'test',
|
|
metadata: {
|
|
path: '/tmp/project',
|
|
host: 'test-host'
|
|
},
|
|
state: null
|
|
})
|
|
|
|
expect(axiosPostMock).toHaveBeenCalledOnce()
|
|
expect(axiosPostMock.mock.calls[0]?.[2]).toMatchObject({
|
|
headers: {
|
|
Cookie: 'CF_Authorization=token',
|
|
Authorization: 'Bearer cli-token',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('uses the CLI REST bridge to request a fresh OpenCode session after source cleanup', async () => {
|
|
axiosPostMock.mockResolvedValue({ data: { ok: true, sessionId: 'fresh-session' } })
|
|
|
|
const client = await ApiClient.create()
|
|
await expect(client.clearOpenCodeSession('source-session')).resolves.toBe('fresh-session')
|
|
|
|
expect(axiosPostMock).toHaveBeenCalledWith(
|
|
'https://hapi.example.com/cli/sessions/source-session/clear-opencode',
|
|
{},
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({ Authorization: 'Bearer cli-token' })
|
|
})
|
|
)
|
|
})
|
|
|
|
it('uses the CLI REST bridge to reserve before source cleanup', async () => {
|
|
axiosPostMock.mockResolvedValue({ data: { ok: true, sessionId: 'fresh-session' } })
|
|
const client = await ApiClient.create()
|
|
await expect(client.reserveOpenCodeClearSession('source-session')).resolves.toBe('fresh-session')
|
|
expect(axiosPostMock).toHaveBeenCalledWith(
|
|
'https://hapi.example.com/cli/sessions/source-session/clear-opencode/reserve', {},
|
|
expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer cli-token' }) })
|
|
)
|
|
})
|
|
|
|
it.each(['confirmOpenCodeClearCleanup', 'abortOpenCodeClearSession'] as const)('sends reservation identity with %s', async (method) => {
|
|
axiosPostMock.mockResolvedValue({ data: { ok: true, sessionId: 'fresh-session' } })
|
|
const client = await ApiClient.create()
|
|
await expect(client[method]('source-session', 'fresh-session')).resolves.toBe('fresh-session')
|
|
expect(axiosPostMock).toHaveBeenCalledWith(
|
|
expect.stringContaining('/clear-opencode/'),
|
|
{ replacementSessionId: 'fresh-session' },
|
|
expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer cli-token' }) })
|
|
)
|
|
})
|
|
|
|
it('adds extra headers to socket transport options', () => {
|
|
configuration._setExtraHeaders({
|
|
Cookie: 'CF_Authorization=token'
|
|
})
|
|
|
|
const fakeSocket = {
|
|
on: vi.fn(),
|
|
connect: vi.fn(),
|
|
emit: vi.fn(),
|
|
volatile: { emit: vi.fn() }
|
|
}
|
|
ioMock.mockReturnValue(fakeSocket)
|
|
|
|
new ApiSessionClient('cli-token', {
|
|
id: 'session-1',
|
|
namespace: 'default',
|
|
seq: 1,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
active: true,
|
|
activeAt: now,
|
|
metadata: null,
|
|
metadataVersion: 0,
|
|
agentState: null,
|
|
agentStateVersion: 0,
|
|
thinking: false,
|
|
thinkingAt: now,
|
|
todos: [],
|
|
model: null,
|
|
modelReasoningEffort: null,
|
|
effort: null,
|
|
serviceTier: null,
|
|
permissionMode: undefined,
|
|
collaborationMode: undefined
|
|
})
|
|
|
|
expect(ioMock).toHaveBeenCalledOnce()
|
|
expect(ioMock.mock.calls[0]?.[1]).toMatchObject({
|
|
extraHeaders: {
|
|
Cookie: 'CF_Authorization=token'
|
|
}
|
|
})
|
|
})
|
|
})
|