mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-16 20:40:01 +00:00
feat: add hapi resume command (#647)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { runCodex } from './runCodex'
|
||||
|
||||
const mockCodexSession = vi.hoisted(() => ({
|
||||
setPermissionMode: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
setModelReasoningEffort: vi.fn(),
|
||||
setCollaborationMode: vi.fn(),
|
||||
stopKeepAlive: vi.fn()
|
||||
}))
|
||||
|
||||
const harness = vi.hoisted(() => ({
|
||||
bootstrapArgs: [] as Array<Record<string, unknown>>,
|
||||
loopArgs: [] as Array<Record<string, unknown>>,
|
||||
session: {
|
||||
onUserMessage: vi.fn(),
|
||||
onCancelQueuedMessage: vi.fn(),
|
||||
rpcHandlerManager: {
|
||||
registerHandler: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/agent/sessionFactory', () => ({
|
||||
bootstrapSession: vi.fn(async (options: Record<string, unknown>) => {
|
||||
harness.bootstrapArgs.push(options)
|
||||
return {
|
||||
api: {},
|
||||
session: harness.session
|
||||
}
|
||||
}),
|
||||
bootstrapExistingSession: vi.fn(async (options: Record<string, unknown>) => {
|
||||
harness.bootstrapArgs.push(options)
|
||||
return {
|
||||
api: {},
|
||||
session: harness.session
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('./loop', () => ({
|
||||
loop: vi.fn(async (options: Record<string, unknown>) => {
|
||||
harness.loopArgs.push(options)
|
||||
const onSessionReady = options.onSessionReady as ((session: unknown) => void) | undefined
|
||||
onSessionReady?.(mockCodexSession)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/claude/registerKillSessionHandler', () => ({
|
||||
registerKillSessionHandler: vi.fn()
|
||||
}))
|
||||
|
||||
const lifecycleMock = vi.hoisted(() => ({
|
||||
registerProcessHandlers: vi.fn(),
|
||||
cleanupAndExit: vi.fn(async () => {}),
|
||||
markCrash: vi.fn(),
|
||||
setExitCode: vi.fn(),
|
||||
setArchiveReason: vi.fn(),
|
||||
setSessionEndReason: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/agent/runnerLifecycle', () => ({
|
||||
createModeChangeHandler: vi.fn(() => vi.fn()),
|
||||
createRunnerLifecycle: vi.fn(() => lifecycleMock),
|
||||
setControlledByUser: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/agent/localHandoff', () => ({
|
||||
registerLocalHandoffHandler: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/ui/logger', () => ({
|
||||
logger: {
|
||||
debug: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/attachmentFormatter', () => ({
|
||||
formatMessageWithAttachments: vi.fn((text: string) => text)
|
||||
}))
|
||||
|
||||
vi.mock('@/modules/common/slashCommands', () => ({
|
||||
listSlashCommands: vi.fn(async () => [])
|
||||
}))
|
||||
|
||||
vi.mock('./utils/slashCommands', () => ({
|
||||
resolveCodexSlashCommand: vi.fn(() => ({
|
||||
kind: 'passthrough'
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('./codexSpecialCommands', () => ({
|
||||
parseCodexSpecialCommand: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
vi.mock('./utils/codexCliOverrides', () => ({
|
||||
parseCodexCliOverrides: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
import { runCodex as runCodexImpl } from './runCodex'
|
||||
|
||||
describe('runCodex', () => {
|
||||
beforeEach(() => {
|
||||
harness.bootstrapArgs.length = 0
|
||||
harness.loopArgs.length = 0
|
||||
harness.session.onUserMessage.mockReset()
|
||||
harness.session.onCancelQueuedMessage.mockReset()
|
||||
harness.session.rpcHandlerManager.registerHandler.mockReset()
|
||||
mockCodexSession.setPermissionMode.mockReset()
|
||||
mockCodexSession.setModel.mockReset()
|
||||
mockCodexSession.setModelReasoningEffort.mockReset()
|
||||
mockCodexSession.setCollaborationMode.mockReset()
|
||||
lifecycleMock.registerProcessHandlers.mockClear()
|
||||
lifecycleMock.cleanupAndExit.mockClear()
|
||||
lifecycleMock.markCrash.mockClear()
|
||||
lifecycleMock.setExitCode.mockClear()
|
||||
lifecycleMock.setArchiveReason.mockClear()
|
||||
lifecycleMock.setSessionEndReason.mockClear()
|
||||
})
|
||||
|
||||
it('uses the requested collaboration mode when resuming locally', async () => {
|
||||
const options = {
|
||||
existingSessionId: 'hapi-session-1',
|
||||
workingDirectory: '/tmp/project',
|
||||
resumeSessionId: 'codex-thread-1',
|
||||
collaborationMode: 'plan'
|
||||
} as Parameters<typeof runCodex>[0] & { collaborationMode: 'plan' }
|
||||
|
||||
await runCodexImpl(options)
|
||||
|
||||
expect(harness.bootstrapArgs[0]).toEqual(expect.objectContaining({
|
||||
sessionId: 'hapi-session-1',
|
||||
workingDirectory: '/tmp/project'
|
||||
}))
|
||||
expect(harness.loopArgs[0]).toEqual(expect.objectContaining({
|
||||
resumeSessionId: 'codex-thread-1',
|
||||
collaborationMode: 'plan'
|
||||
}))
|
||||
expect(mockCodexSession.setCollaborationMode).toHaveBeenLastCalledWith('plan')
|
||||
})
|
||||
})
|
||||
+24
-11
@@ -7,7 +7,8 @@ import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler'
|
||||
import type { AgentState } from '@/api/types';
|
||||
import type { CodexSession } from './session';
|
||||
import { parseCodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import { bootstrapSession } from '@/agent/sessionFactory';
|
||||
import { bootstrapExistingSession, bootstrapSession } from '@/agent/sessionFactory';
|
||||
import { registerLocalHandoffHandler } from '@/agent/localHandoff';
|
||||
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
|
||||
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
|
||||
import { CodexCollaborationModeSchema, PermissionModeSchema } from '@hapi/protocol/schemas';
|
||||
@@ -29,8 +30,11 @@ export async function runCodex(opts: {
|
||||
resumeSessionId?: string;
|
||||
model?: string;
|
||||
modelReasoningEffort?: ReasoningEffort;
|
||||
collaborationMode?: EnhancedMode['collaborationMode'];
|
||||
existingSessionId?: string;
|
||||
workingDirectory?: string;
|
||||
}): Promise<void> {
|
||||
const workingDirectory = getInvokedCwd();
|
||||
const workingDirectory = opts.workingDirectory ?? getInvokedCwd();
|
||||
const startedBy = opts.startedBy ?? 'terminal';
|
||||
|
||||
logger.debug(`[codex] Starting with options: startedBy=${startedBy}`);
|
||||
@@ -38,14 +42,22 @@ export async function runCodex(opts: {
|
||||
let state: AgentState = {
|
||||
controlledByUser: false
|
||||
};
|
||||
const { api, session } = await bootstrapSession({
|
||||
flavor: 'codex',
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
agentState: state,
|
||||
model: opts.model,
|
||||
modelReasoningEffort: opts.modelReasoningEffort
|
||||
});
|
||||
const bootstrap = opts.existingSessionId
|
||||
? await bootstrapExistingSession({
|
||||
sessionId: opts.existingSessionId,
|
||||
flavor: 'codex',
|
||||
startedBy,
|
||||
workingDirectory
|
||||
})
|
||||
: await bootstrapSession({
|
||||
flavor: 'codex',
|
||||
startedBy,
|
||||
workingDirectory,
|
||||
agentState: state,
|
||||
model: opts.model,
|
||||
modelReasoningEffort: opts.modelReasoningEffort
|
||||
});
|
||||
const { api, session } = bootstrap;
|
||||
|
||||
const startingMode: 'local' | 'remote' = startedBy === 'runner' ? 'remote' : 'local';
|
||||
|
||||
@@ -64,7 +76,7 @@ export async function runCodex(opts: {
|
||||
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
|
||||
let currentModel = opts.model;
|
||||
let currentModelReasoningEffort: ReasoningEffort | undefined = opts.modelReasoningEffort;
|
||||
let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default';
|
||||
let currentCollaborationMode: EnhancedMode['collaborationMode'] = opts.collaborationMode ?? 'default';
|
||||
|
||||
const lifecycle = createRunnerLifecycle({
|
||||
session,
|
||||
@@ -74,6 +86,7 @@ export async function runCodex(opts: {
|
||||
|
||||
lifecycle.registerProcessHandlers();
|
||||
registerKillSessionHandler(session.rpcHandlerManager, lifecycle.cleanupAndExit);
|
||||
registerLocalHandoffHandler(session.rpcHandlerManager, lifecycle);
|
||||
|
||||
const applyCurrentConfigToSession = (options?: { syncModel?: boolean }) => {
|
||||
const sessionInstance = sessionWrapperRef.current;
|
||||
|
||||
Reference in New Issue
Block a user