History

maybe fix codex subturns

Julius Marminge committed 8 months ago

Commit ef843d41 parent 654ca0d4 files · +334 −16Browse files

Showing the changes to apps/server/src/codexAppServerManager.test.ts (1 of 4 files). Show all

Unified · Split

apps/server/src/codexAppServerManager.test.ts+187−0
@@ -37,6 +37,7 @@ function createSendTurnHarness() { · 36 unmodified lines
37 planType: null,37 planType: null,
38 sparkEnabled: true,38 sparkEnabled: true,
39 },39 },
40 collabReceiverTurns: new Map(),
40 };41 };
41 42
42 const requireSession = vi43 const requireSession = vi
@@ -75,6 +76,7 @@ function createThreadControlHarness() { · 32 unmodified lines
75 createdAt: "2026-02-10T00:00:00.000Z",76 createdAt: "2026-02-10T00:00:00.000Z",
76 updatedAt: "2026-02-10T00:00:00.000Z",77 updatedAt: "2026-02-10T00:00:00.000Z",
77 },78 },
79 collabReceiverTurns: new Map(),
78 };80 };
79 81
80 const requireSession = vi82 const requireSession = vi
@@ -117,6 +119,7 @@ function createPendingUserInputHarness() { · 36 unmodified lines
117 },119 },
118 ],120 ],
119 ]),121 ]),
122 collabReceiverTurns: new Map(),
120 };123 };
121 124
122 const requireSession = vi125 const requireSession = vi
@@ -135,6 +138,43 @@ function createPendingUserInputHarness() { · 12 unmodified lines
135 return { manager, context, requireSession, writeMessage, emitEvent };138 return { manager, context, requireSession, writeMessage, emitEvent };
136}139}
137 140
141function createCollabNotificationHarness() {
142 const manager = new CodexAppServerManager();
143 const context = {
144 session: {
145 provider: "codex",
146 status: "running",
147 threadId: asThreadId("thread_1"),
148 runtimeMode: "full-access",
149 model: "gpt-5.3-codex",
150 activeTurnId: "turn_parent",
151 resumeCursor: { threadId: "provider_parent" },
152 createdAt: "2026-02-10T00:00:00.000Z",
153 updatedAt: "2026-02-10T00:00:00.000Z",
154 },
155 account: {
156 type: "unknown",
157 planType: null,
158 sparkEnabled: true,
159 },
160 pending: new Map(),
161 pendingApprovals: new Map(),
162 pendingUserInputs: new Map(),
163 collabReceiverTurns: new Map<string, string>(),
164 nextRequestId: 1,
165 stopping: false,
166 };
167
168 const emitEvent = vi
169 .spyOn(manager as unknown as { emitEvent: (...args: unknown[]) => void }, "emitEvent")
170 .mockImplementation(() => {});
171 const updateSession = vi
172 .spyOn(manager as unknown as { updateSession: (...args: unknown[]) => void }, "updateSession")
173 .mockImplementation(() => {});
174
175 return { manager, context, emitEvent, updateSession };
176}
177
138describe("classifyCodexStderrLine", () => {178describe("classifyCodexStderrLine", () => {
139 it("ignores empty lines", () => {179 it("ignores empty lines", () => {
140 expect(classifyCodexStderrLine(" ")).toBeNull();180 expect(classifyCodexStderrLine(" ")).toBeNull();
@@ -721,6 +761,7 @@ describe("respondToUserInput", () => { · 580 unmodified lines
721 },761 },
722 pendingApprovals: new Map(),762 pendingApprovals: new Map(),
723 pendingUserInputs: new Map(),763 pendingUserInputs: new Map(),
764 collabReceiverTurns: new Map(),
724 };765 };
725 type ApprovalRequestContext = {766 type ApprovalRequestContext = {
726 session: typeof context.session;767 session: typeof context.session;
@@ -748,6 +789,152 @@ describe("respondToUserInput", () => { · 21 unmodified lines
748 });789 });
749});790});
750 791
792describe("collab child conversation routing", () => {
793 it("rewrites child notification turn ids onto the parent turn", () => {
794 const { manager, context, emitEvent } = createCollabNotificationHarness();
795
796 (
797 manager as unknown as {
798 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
799 }
800 ).handleServerNotification(context, {
801 method: "item/completed",
802 params: {
803 item: {
804 type: "collabAgentToolCall",
805 id: "call_collab_1",
806 receiverThreadIds: ["child_provider_1"],
807 },
808 threadId: "provider_parent",
809 turnId: "turn_parent",
810 },
811 });
812
813 (
814 manager as unknown as {
815 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
816 }
817 ).handleServerNotification(context, {
818 method: "item/agentMessage/delta",
819 params: {
820 threadId: "child_provider_1",
821 turnId: "turn_child_1",
822 itemId: "msg_child_1",
823 delta: "working",
824 },
825 });
826
827 expect(emitEvent).toHaveBeenLastCalledWith(
828 expect.objectContaining({
829 method: "item/agentMessage/delta",
830 turnId: "turn_parent",
831 itemId: "msg_child_1",
832 }),
833 );
834 });
835
836 it("suppresses child lifecycle notifications so they cannot replace the parent turn", () => {
837 const { manager, context, emitEvent, updateSession } = createCollabNotificationHarness();
838
839 (
840 manager as unknown as {
841 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
842 }
843 ).handleServerNotification(context, {
844 method: "item/completed",
845 params: {
846 item: {
847 type: "collabAgentToolCall",
848 id: "call_collab_1",
849 receiverThreadIds: ["child_provider_1"],
850 },
851 threadId: "provider_parent",
852 turnId: "turn_parent",
853 },
854 });
855 emitEvent.mockClear();
856 updateSession.mockClear();
857
858 (
859 manager as unknown as {
860 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
861 }
862 ).handleServerNotification(context, {
863 method: "turn/started",
864 params: {
865 threadId: "child_provider_1",
866 turn: { id: "turn_child_1" },
867 },
868 });
869
870 (
871 manager as unknown as {
872 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
873 }
874 ).handleServerNotification(context, {
875 method: "turn/completed",
876 params: {
877 threadId: "child_provider_1",
878 turn: { id: "turn_child_1", status: "completed" },
879 },
880 });
881
882 expect(emitEvent).not.toHaveBeenCalled();
883 expect(updateSession).not.toHaveBeenCalled();
884 });
885
886 it("rewrites child approval requests onto the parent turn", () => {
887 const { manager, context, emitEvent } = createCollabNotificationHarness();
888
889 (
890 manager as unknown as {
891 handleServerNotification: (context: unknown, notification: Record<string, unknown>) => void;
892 }
893 ).handleServerNotification(context, {
894 method: "item/completed",
895 params: {
896 item: {
897 type: "collabAgentToolCall",
898 id: "call_collab_1",
899 receiverThreadIds: ["child_provider_1"],
900 },
901 threadId: "provider_parent",
902 turnId: "turn_parent",
903 },
904 });
905 emitEvent.mockClear();
906
907 (
908 manager as unknown as {
909 handleServerRequest: (context: unknown, request: Record<string, unknown>) => void;
910 }
911 ).handleServerRequest(context, {
912 id: 42,
913 method: "item/commandExecution/requestApproval",
914 params: {
915 threadId: "child_provider_1",
916 turnId: "turn_child_1",
917 itemId: "call_child_1",
918 command: "bun install",
919 },
920 });
921
922 expect(Array.from(context.pendingApprovals.values())[0]).toEqual(
923 expect.objectContaining({
924 turnId: "turn_parent",
925 itemId: "call_child_1",
926 }),
927 );
928 expect(emitEvent).toHaveBeenCalledWith(
929 expect.objectContaining({
930 method: "item/commandExecution/requestApproval",
931 turnId: "turn_parent",
932 itemId: "call_child_1",
933 }),
934 );
935 });
936});
937
751describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume", () => {938describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume", () => {
752 it("keeps prior thread history when resuming with a changed runtime mode", async () => {939 it("keeps prior thread history when resuming with a changed runtime mode", async () => {
753 const workspaceDir = mkdtempSync(path.join(os.tmpdir(), "codex-live-resume-"));940 const workspaceDir = mkdtempSync(path.join(os.tmpdir(), "codex-live-resume-"));