test(codex): express patch approval through provider actions

Keep protocol framing in the fake app-server and precompute the approval title. Streamed completion already requires an item ID; expose that narrowing so completion cleanup stays inline without repeating its guard.
This commit is contained in:
paseo-bot[bot]
2026-11-02 18:15:20 +00:00
parent 83071e1be8
commit 588c16af15
3 changed files with 40 additions and 29 deletions
@@ -236,21 +236,13 @@ describe("Codex file-change approvals", () => {
["file-alpha", "/workspace/project/alpha.txt", "@@ -1 +1 @@\n-blue\n+green\n"],
["file-beta", "/workspace/project/beta.txt", "@@ -1 +1 @@\n-small\n+large\n"],
]) {
appServer.child.stdout.write(
`${JSON.stringify({
method: "item/started",
params: {
threadId: "thread-1",
turnId: "native-A",
item: {
type: "fileChange",
id,
status: "inProgress",
changes: [{ path: filePath, kind: { type: "update", move_path: null }, diff }],
},
},
})}\n`,
);
appServer.startsFileChange({
threadId: "thread-1",
turnId: "native-A",
callId: id,
path: filePath,
diff,
});
}
const permission = waitForNextPermission(session);
appServer.requestFileChangeApproval({
@@ -6781,10 +6781,6 @@ export class CodexAppServerAgentSession implements AgentSession {
return !this.closed && this.activeForegroundTurnId === turnId;
}
private forgetPendingToolCallDetail(itemId: string | null | undefined): void {
if (itemId) this.pendingToolCallDetails.delete(itemId);
}
private async handleItemCompletedNotification(
parsed: Extract<ParsedCodexNotification, { kind: "item_completed" }>,
): Promise<void> {
@@ -6796,7 +6792,9 @@ export class CodexAppServerAgentSession implements AgentSession {
if (shouldIgnoreMirroredLifecycleItem(parsed.source, parsed.item)) {
return;
}
this.forgetPendingToolCallDetail(parsed.item.id);
if (parsed.item.id) {
this.pendingToolCallDetails.delete(parsed.item.id);
}
this.receiveAsyncQuestion(parsed.threadId, parsed.item);
if (this.isUserMessageItem(parsed.item)) {
await this.handleUserMessageItem(parsed);
@@ -6844,10 +6842,8 @@ export class CodexAppServerAgentSession implements AgentSession {
if (timelineItem.type === "assistant_message") {
this.pendingAssistantMessageBoundary = true;
}
if (itemId) {
this.emittedItemCompletedIds.add(itemId);
this.emittedItemStartedIds.delete(itemId);
}
this.emittedItemCompletedIds.add(itemId);
this.emittedItemStartedIds.delete(itemId);
await this.replayPendingSubAgentNotifications(registeredChildThreadIds);
return;
}
@@ -6883,7 +6879,7 @@ export class CodexAppServerAgentSession implements AgentSession {
private consumeStreamedTextCompletion(
timelineItem: AgentTimelineItem,
itemId: string | null | undefined,
): boolean {
): itemId is string {
if (!itemId) {
return false;
}
@@ -7280,15 +7276,16 @@ export class CodexAppServerAgentSession implements AgentSession {
input: { reason: parsed.reason ?? null },
output: null,
};
const title =
detail.type === "edit" && detail.filePath
? `Apply file changes: ${detail.filePath}`
: "Apply file changes";
const request: AgentPermissionRequest = {
id: requestId,
provider: CODEX_PROVIDER,
name: "CodexFileChange",
kind: "tool",
title:
detail.type === "edit" && detail.filePath
? `Apply file changes: ${detail.filePath}`
: "Apply file changes",
title,
description: parsed.reason ?? undefined,
detail,
metadata: {
@@ -43,6 +43,13 @@ interface FakeTerminalInput {
processId: string;
text: string;
}
interface FakeFileChange {
threadId: string;
turnId: string;
callId: string;
path: string;
diff: string;
}
interface FakeLegacyPatch {
threadId: string;
callId: string;
@@ -88,6 +95,7 @@ export interface FakeCodexAppServer {
completesSubAgentActivity(params: FakeSubAgentActivity): void;
completesCompaction(params: { threadId: string; itemId: string }): void;
runsLegacyCommand(params: FakeLegacyCommand): void;
startsFileChange(params: FakeFileChange): void;
appliesLegacyPatch(params: FakeLegacyPatch): void;
completesCommand(params: FakeLegacyCommand): void;
completesSilentCommand(params: FakeSilentCommand): void;
@@ -467,6 +475,20 @@ export function createFakeCodexAppServer(
success: true,
});
},
startsFileChange(params) {
writeNotification("item/started", {
threadId: params.threadId,
turnId: params.turnId,
item: {
type: "fileChange",
id: params.callId,
status: "inProgress",
changes: [
{ path: params.path, kind: { type: "update", move_path: null }, diff: params.diff },
],
},
});
},
appliesLegacyPatch(params) {
const changes = [
{