feat: add hapi resume command (#647)

This commit is contained in:
leko
2026-05-20 06:18:42 +08:00
committed by GitHub
parent 79d919675e
commit 197f327590
29 changed files with 1777 additions and 93 deletions
+193
View File
@@ -864,6 +864,199 @@ describe('session model', () => {
}
})
it('resolves a local resume target for a Codex session', () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const session = engine.getOrCreateSession(
'local-resume-codex',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'codex',
codexSessionId: 'codex-thread-1'
},
{ controlledByUser: false },
'default',
'gpt-5.4',
undefined,
'xhigh'
)
const result = engine.resolveLocalResumeTarget(session.id, 'default')
expect(result).toEqual({
type: 'success',
target: {
sessionId: session.id,
flavor: 'codex',
directory: '/tmp/project',
machineId: 'machine-1',
host: 'localhost',
active: session.active,
thinking: session.thinking,
controlledByUser: false,
agentSessionId: 'codex-thread-1',
model: 'gpt-5.4',
effort: null,
modelReasoningEffort: 'xhigh',
permissionMode: undefined,
collaborationMode: undefined
}
})
} finally {
engine.stop()
}
})
it('recovers a Claude local resume target from stored messages', () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const session = engine.getOrCreateSession(
'local-resume-claude-from-message',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'claude'
},
null,
'default'
)
store.messages.addMessage(session.id, {
role: 'agent',
content: {
type: 'output',
data: {
sessionId: '22222222-2222-4222-8222-222222222222'
}
}
})
const result = engine.resolveLocalResumeTarget(session.id, 'default')
expect(result.type).toBe('success')
if (result.type === 'success') {
expect(result.target.flavor).toBe('claude')
expect(result.target.agentSessionId).toBe('22222222-2222-4222-8222-222222222222')
}
} finally {
engine.stop()
}
})
it('returns resume_unavailable when the local resume target lacks an agent session id', () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const session = engine.getOrCreateSession(
'local-resume-no-agent-id',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'codex'
},
null,
'default'
)
expect(engine.resolveLocalResumeTarget(session.id, 'default')).toEqual({
type: 'error',
message: 'Resume session ID unavailable',
code: 'resume_unavailable'
})
} finally {
engine.stop()
}
})
it('local handoff succeeds immediately for inactive sessions', async () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const session = engine.getOrCreateSession(
'local-handoff-inactive',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'codex',
codexSessionId: 'codex-thread-1'
},
{ controlledByUser: false },
'default'
)
engine.handleSessionEnd({ sid: session.id, time: Date.now() })
await expect(engine.handoffSessionToLocal(session.id, 'default')).resolves.toEqual({
type: 'success'
})
} finally {
engine.stop()
}
})
it('local handoff rejects sessions already controlled by a local terminal', async () => {
const store = new Store(':memory:')
const engine = new SyncEngine(
store,
{} as never,
new RpcRegistry(),
{ broadcast() {} } as never
)
try {
const session = engine.getOrCreateSession(
'local-handoff-already-local',
{
path: '/tmp/project',
host: 'localhost',
machineId: 'machine-1',
flavor: 'codex',
codexSessionId: 'codex-thread-1'
},
{ controlledByUser: true },
'default'
)
engine.handleSessionAlive({ sid: session.id, time: Date.now(), mode: 'local' })
await expect(engine.handoffSessionToLocal(session.id, 'default')).resolves.toEqual({
type: 'error',
message: 'Session is already controlled by a local terminal',
code: 'already_local'
})
} finally {
engine.stop()
}
})
describe('session dedup by agent session ID', () => {
it('merges duplicate when codexSessionId collides', async () => {
const store = new Store(':memory:')