Files
hapi/cli/src/api/api.extraHeaders.test.ts
T
Junmo KimandGitHub f44c9ff3e6 feat(opencode): open a fresh session on clear (#1300)
* 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
2026-08-03 18:06:39 +08:00

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'
}
})
})
})