mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-06 06:41:56 +00:00
fix(pi): resume archived sessions safely (#1308)
* fix(pi): resume archived sessions safely * fix(pi): harden native resume startup * fix(pi): harden resume termination evidence * fix(runner): persist resume process evidence * fix(runner): track resume process generations * fix(runner): verify full session tree shutdown * fix(pi): block pre-mapping resume dedup
This commit is contained in:
@@ -7,12 +7,14 @@ describe('parseRemoteAgentCommandOptions', () => {
|
||||
expect(parseRemoteAgentCommandOptions([
|
||||
'--started-by', 'runner',
|
||||
'--hapi-starting-mode', 'remote',
|
||||
'--existing-session-id', 'hapi-session-1',
|
||||
'--permission-mode', 'yolo',
|
||||
'--resume', 'session-1',
|
||||
'--model', 'model-a'
|
||||
], GEMINI_PERMISSION_MODES)).toEqual({
|
||||
startedBy: 'runner',
|
||||
startingMode: 'remote',
|
||||
existingSessionId: 'hapi-session-1',
|
||||
permissionMode: 'yolo',
|
||||
resumeSessionId: 'session-1',
|
||||
model: 'model-a'
|
||||
@@ -67,6 +69,7 @@ describe('parseRemoteAgentCommandOptions', () => {
|
||||
expect(() => parseRemoteAgentCommandOptions(['--resume'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --resume value')
|
||||
expect(() => parseRemoteAgentCommandOptions(['--model'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model value')
|
||||
expect(() => parseRemoteAgentCommandOptions(['--model-reasoning-effort'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model-reasoning-effort value')
|
||||
expect(() => parseRemoteAgentCommandOptions(['--existing-session-id'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --existing-session-id value')
|
||||
})
|
||||
|
||||
it('accepts OpenCode-native -s / --session as resume aliases', () => {
|
||||
@@ -190,6 +193,7 @@ describe('parseRemoteAgentCommandOptions — pi flavor', () => {
|
||||
'--hapi-starting-mode', 'remote',
|
||||
'--model', 'claude-sonnet-4-5',
|
||||
'--session-id', 'pi-sess-full',
|
||||
'--existing-session-id', 'hapi-session-pi-full',
|
||||
],
|
||||
ALLOWED
|
||||
)
|
||||
@@ -198,6 +202,7 @@ describe('parseRemoteAgentCommandOptions — pi flavor', () => {
|
||||
startingMode: 'remote',
|
||||
model: 'claude-sonnet-4-5',
|
||||
resumeSessionId: 'pi-sess-full',
|
||||
existingSessionId: 'hapi-session-pi-full',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type RemoteAgentCommandOptions<TPermissionMode extends PermissionMode> =
|
||||
effort?: string
|
||||
modelReasoningEffort?: string
|
||||
resumeSessionId?: string
|
||||
existingSessionId?: string
|
||||
}
|
||||
|
||||
export function parseRemoteAgentCommandOptions<TPermissionMode extends PermissionMode>(
|
||||
@@ -28,6 +29,12 @@ export function parseRemoteAgentCommandOptions<TPermissionMode extends Permissio
|
||||
} else {
|
||||
throw new Error('Invalid --hapi-starting-mode (expected local or remote)')
|
||||
}
|
||||
} else if (arg === '--existing-session-id') {
|
||||
const sessionId = args[++i]
|
||||
if (!sessionId || sessionId.startsWith('-')) {
|
||||
throw new Error('Missing --existing-session-id value')
|
||||
}
|
||||
options.existingSessionId = sessionId
|
||||
} else if (arg === '--permission-mode') {
|
||||
const mode = args[++i]
|
||||
if (!mode || !(allowedPermissionModes as readonly string[]).includes(mode)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ const {
|
||||
checkIfRunnerRunningAndCleanupStaleStateMock: vi.fn(),
|
||||
listRunnerSessionsMock: vi.fn(async () => []),
|
||||
stopRunnerMock: vi.fn(async () => {}),
|
||||
stopRunnerSessionMock: vi.fn(async () => true),
|
||||
stopRunnerSessionMock: vi.fn(async (_sessionId: string): Promise<'stopped' | 'already_gone' | 'still_alive'> => 'stopped'),
|
||||
spawnHappyCLIMock: vi.fn(() => ({ unref: vi.fn() })),
|
||||
startRunnerMock: vi.fn(async () => {}),
|
||||
getLatestRunnerLogMock: vi.fn(async () => null),
|
||||
@@ -127,3 +127,25 @@ describe('runnerCommand start', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('runnerCommand stop-session', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['stopped', 'Session stopped'],
|
||||
['already_gone', 'Session was already stopped'],
|
||||
['still_alive', 'Failed to stop session'],
|
||||
] as const)('renders the %s runner result', async (status, expected) => {
|
||||
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
stopRunnerSessionMock.mockResolvedValueOnce(status)
|
||||
try {
|
||||
await runnerCommand.run(createContext(['stop-session', 'session-1']))
|
||||
expect(stopRunnerSessionMock).toHaveBeenCalledWith('session-1')
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(expected)
|
||||
} finally {
|
||||
consoleLogSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,8 +113,14 @@ export const runnerCommand: CommandDefinition = {
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await stopRunnerSession(sessionId)
|
||||
console.log(success ? 'Session stopped' : 'Failed to stop session')
|
||||
const status = await stopRunnerSession(sessionId)
|
||||
if (status === 'stopped') {
|
||||
console.log('Session stopped')
|
||||
} else if (status === 'already_gone') {
|
||||
console.log('Session was already stopped')
|
||||
} else {
|
||||
console.log('Failed to stop session')
|
||||
}
|
||||
} catch {
|
||||
console.log('No runner running')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user