mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-04 00:01:41 +00:00
Add Codex clear and compact slash commands (#541)
* Add Codex clear and compact slash commands * Stabilize queued thinking event test * Interrupt active Codex turn before slash commands
This commit is contained in:
@@ -9,6 +9,9 @@ const harness = vi.hoisted(() => ({
|
||||
startThreadIds: [] as string[],
|
||||
resumeThreadIds: [] as string[],
|
||||
startTurnThreadIds: [] as string[],
|
||||
interruptedTurns: [] as Array<{ threadId: string; turnId: string }>,
|
||||
compactThreadIds: [] as string[],
|
||||
suppressTurnCompletion: false,
|
||||
remainingThreadSystemErrors: 0
|
||||
}));
|
||||
|
||||
@@ -62,6 +65,10 @@ vi.mock('./codexAppServerClient', () => {
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
if (harness.suppressTurnCompletion) {
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
const completed = { status: 'Completed', turn: { id: turnId } };
|
||||
harness.notifications.push({ method: 'turn/completed', params: completed });
|
||||
this.notificationHandler?.('turn/completed', completed);
|
||||
@@ -69,7 +76,16 @@ vi.mock('./codexAppServerClient', () => {
|
||||
return { turn: { id: turnId } };
|
||||
}
|
||||
|
||||
async interruptTurn(): Promise<Record<string, never>> {
|
||||
async interruptTurn(params?: { threadId?: string; turnId?: string }): Promise<Record<string, never>> {
|
||||
harness.interruptedTurns.push({
|
||||
threadId: params?.threadId ?? 'thread-unknown',
|
||||
turnId: params?.turnId ?? 'turn-unknown'
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
async compactThread(params?: { threadId?: string }): Promise<Record<string, never>> {
|
||||
harness.compactThreadIds.push(params?.threadId ?? 'thread-unknown');
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -117,6 +133,7 @@ function createSessionStub(messages = ['hello from launcher test']) {
|
||||
const codexMessages: unknown[] = [];
|
||||
const thinkingChanges: boolean[] = [];
|
||||
const foundSessionIds: string[] = [];
|
||||
const resetThreadCalls: string[] = [];
|
||||
let currentModel: string | null | undefined;
|
||||
let agentState: FakeAgentState = {
|
||||
requests: {},
|
||||
@@ -168,6 +185,10 @@ function createSessionStub(messages = ['hello from launcher test']) {
|
||||
session.sessionId = id;
|
||||
foundSessionIds.push(id);
|
||||
},
|
||||
resetCodexThread() {
|
||||
resetThreadCalls.push(session.sessionId ?? 'none');
|
||||
session.sessionId = null;
|
||||
},
|
||||
sendAgentMessage(message: unknown) {
|
||||
client.sendAgentMessage(message);
|
||||
},
|
||||
@@ -185,6 +206,7 @@ function createSessionStub(messages = ['hello from launcher test']) {
|
||||
codexMessages,
|
||||
thinkingChanges,
|
||||
foundSessionIds,
|
||||
resetThreadCalls,
|
||||
rpcHandlers,
|
||||
getModel: () => currentModel,
|
||||
getAgentState: () => agentState
|
||||
@@ -199,6 +221,9 @@ describe('codexRemoteLauncher', () => {
|
||||
harness.startThreadIds = [];
|
||||
harness.resumeThreadIds = [];
|
||||
harness.startTurnThreadIds = [];
|
||||
harness.interruptedTurns = [];
|
||||
harness.compactThreadIds = [];
|
||||
harness.suppressTurnCompletion = false;
|
||||
harness.remainingThreadSystemErrors = 0;
|
||||
});
|
||||
|
||||
@@ -260,4 +285,105 @@ describe('codexRemoteLauncher', () => {
|
||||
expect(session.sessionId).toBe('thread-2');
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('clears codex thread state without starting a turn', async () => {
|
||||
const { session, sessionEvents, resetThreadCalls } = createSessionStub(['/clear', 'next message']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(resetThreadCalls).toEqual(['none']);
|
||||
expect(harness.startThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1']);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Context was reset'
|
||||
});
|
||||
expect(session.sessionId).toBe('thread-1');
|
||||
});
|
||||
|
||||
it('interrupts an in-flight turn before clearing codex thread state', async () => {
|
||||
harness.suppressTurnCompletion = true;
|
||||
const { session, sessionEvents, resetThreadCalls } = createSessionStub(['first message', '/clear']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.interruptedTurns).toEqual([{ threadId: 'thread-1', turnId: 'turn-1' }]);
|
||||
expect(resetThreadCalls).toEqual(['thread-1']);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Context was reset'
|
||||
});
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('compacts the current thread without starting a turn', async () => {
|
||||
const { session, sessionEvents } = createSessionStub(['first message', '/compact']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.compactThreadIds).toEqual(['thread-1']);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Compaction started'
|
||||
});
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Compaction completed'
|
||||
});
|
||||
});
|
||||
|
||||
it('interrupts an in-flight turn before compacting the current thread', async () => {
|
||||
harness.suppressTurnCompletion = true;
|
||||
const { session, sessionEvents } = createSessionStub(['first message', '/compact']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.startTurnThreadIds).toEqual(['thread-1']);
|
||||
expect(harness.interruptedTurns).toEqual([{ threadId: 'thread-1', turnId: 'turn-1' }]);
|
||||
expect(harness.compactThreadIds).toEqual(['thread-1']);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Compaction completed'
|
||||
});
|
||||
expect(session.thinking).toBe(false);
|
||||
});
|
||||
|
||||
it('reports nothing to compact when no codex thread exists', async () => {
|
||||
const { session, sessionEvents } = createSessionStub(['/compact']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual([]);
|
||||
expect(harness.startTurnThreadIds).toEqual([]);
|
||||
expect(harness.compactThreadIds).toEqual([]);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: 'Nothing to compact'
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects argument-bearing codex slash commands without starting a turn', async () => {
|
||||
const { session, sessionEvents } = createSessionStub(['/compact now']);
|
||||
|
||||
const exitReason = await codexRemoteLauncher(session as never);
|
||||
|
||||
expect(exitReason).toBe('exit');
|
||||
expect(harness.startThreadIds).toEqual([]);
|
||||
expect(harness.startTurnThreadIds).toEqual([]);
|
||||
expect(harness.compactThreadIds).toEqual([]);
|
||||
expect(sessionEvents).toContainEqual({
|
||||
type: 'message',
|
||||
message: '/compact does not accept arguments'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user