| ... | ... | @@ -1045,97 +1045,112 @@ describe("PiAdapterV2", () => { |
| 1045 | 1045 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 1046 | 1046 | ); |
| 1047 | 1047 | |
| 1048 | | it.effect("defers a wake's late settlement until the joined user prompt is acknowledged", () => |
| 1049 | | Effect.gen(function* () { |
| 1050 | | const fake = yield* makeFakePi; |
| 1051 | | const offers = |
| 1052 | | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| 1053 | | const { runtime, takeEvent } = yield* openRuntime( |
| 1054 | | fake, |
| 1055 | | "default", |
| 1056 | | THREAD_ID, |
| 1057 | | SESSION_ID, |
| 1058 | | undefined, |
| 1059 | | { |
| 1060 | | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| 1061 | | }, |
| 1062 | | ); |
| 1063 | | const providerThread = yield* runtime.ensureThread({ |
| 1064 | | threadId: THREAD_ID, |
| 1065 | | modelSelection: modelSelection("default"), |
| 1066 | | runtimePolicy, |
| 1067 | | }); |
| 1068 | | yield* fake.emit({ type: "agent_start" }); |
| 1069 | | yield* Queue.take(offers); |
| 1070 | | yield* startTurn(runtime, providerThread); |
| 1071 | | yield* fake.takeRequest("prompt"); |
| 1072 | | yield* fake.emit({ type: "agent_settled" }); |
| 1073 | | yield* fake.emit({ |
| 1074 | | type: "extension_ui_request", |
| 1075 | | method: "notify", |
| 1076 | | message: "Ordering barrier", |
| 1077 | | }); |
| 1078 | | yield* takeEvent( |
| 1079 | | (event) => |
| 1080 | | event.type === "turn_item.updated" && |
| 1081 | | event.turnItem.type === "dynamic_tool" && |
| 1082 | | event.turnItem.toolName === "notify", |
| 1083 | | ); |
| 1084 | | assert.equal(runtime.providerSession.status, "running"); |
| 1085 | | fake.queueState({ isStreaming: true }); |
| 1086 | | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| 1087 | | yield* fake.takeRequest("get_state"); |
| 1088 | | yield* fake.emit({ type: "agent_start" }); |
| 1089 | | yield* fake.emit({ type: "agent_settled" }); |
| 1090 | | yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1091 | | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 1048 | it.effect.each([false, true])( |
| 1049 | "defers a wake's late settlement until the joined user prompt is acknowledged, aborted=%s", |
| 1050 | (aborted) => |
| 1051 | Effect.gen(function* () { |
| 1052 | const fake = yield* makeFakePi; |
| 1053 | const offers = |
| 1054 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| 1055 | const { runtime, takeEvent } = yield* openRuntime( |
| 1056 | fake, |
| 1057 | "default", |
| 1058 | THREAD_ID, |
| 1059 | SESSION_ID, |
| 1060 | undefined, |
| 1061 | { |
| 1062 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| 1063 | }, |
| 1064 | ); |
| 1065 | const providerThread = yield* runtime.ensureThread({ |
| 1066 | threadId: THREAD_ID, |
| 1067 | modelSelection: modelSelection("default"), |
| 1068 | runtimePolicy, |
| 1069 | }); |
| 1070 | yield* fake.emit({ type: "agent_start" }); |
| 1071 | yield* Queue.take(offers); |
| 1072 | yield* startTurn(runtime, providerThread); |
| 1073 | yield* fake.takeRequest("prompt"); |
| 1074 | yield* fake.emit({ type: "agent_settled", aborted }); |
| 1075 | yield* fake.emit({ |
| 1076 | type: "extension_ui_request", |
| 1077 | method: "notify", |
| 1078 | message: "Ordering barrier", |
| 1079 | }); |
| 1080 | yield* takeEvent( |
| 1081 | (event) => |
| 1082 | event.type === "turn_item.updated" && |
| 1083 | event.turnItem.type === "dynamic_tool" && |
| 1084 | event.turnItem.toolName === "notify", |
| 1085 | ); |
| 1086 | assert.equal(runtime.providerSession.status, "running"); |
| 1087 | fake.queueState({ isStreaming: true }); |
| 1088 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| 1089 | yield* fake.takeRequest("get_state"); |
| 1090 | yield* fake.emit({ type: "agent_start" }); |
| 1091 | yield* fake.emit({ type: "agent_settled" }); |
| 1092 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1093 | assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed"); |
| 1094 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 1092 | 1095 | ); |
| 1093 | 1096 | |
| 1094 | | it.effect("settles a command-only user prompt after taking a completed wake", () => |
| 1095 | | Effect.gen(function* () { |
| 1096 | | const fake = yield* makeFakePi; |
| 1097 | | const offers = |
| 1098 | | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| 1099 | | const { runtime, takeEvent } = yield* openRuntime( |
| 1100 | | fake, |
| 1101 | | "default", |
| 1102 | | THREAD_ID, |
| 1103 | | SESSION_ID, |
| 1104 | | undefined, |
| 1105 | | { |
| 1106 | | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| 1107 | | }, |
| 1108 | | ); |
| 1109 | | const providerThread = yield* runtime.ensureThread({ |
| 1110 | | threadId: THREAD_ID, |
| 1111 | | modelSelection: modelSelection("default"), |
| 1112 | | runtimePolicy, |
| 1113 | | }); |
| 1114 | | yield* fake.emit({ type: "agent_start" }); |
| 1115 | | yield* Queue.take(offers); |
| 1116 | | yield* fake.emit({ type: "agent_settled" }); |
| 1117 | | yield* takeEvent( |
| 1118 | | (event) => |
| 1119 | | event.type === "provider_session.updated" && event.providerSession.status === "ready", |
| 1120 | | ); |
| 1121 | | yield* startTurn(runtime, providerThread, "default", [], "/hello"); |
| 1122 | | yield* fake.takeRequest("prompt"); |
| 1123 | | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| 1124 | | yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1125 | | yield* startTurn( |
| 1126 | | runtime, |
| 1127 | | providerThread, |
| 1128 | | "default", |
| 1129 | | [], |
| 1130 | | "Stale continuation", |
| 1131 | | undefined, |
| 1132 | | 2, |
| 1133 | | THREAD_ID, |
| 1134 | | true, |
| 1135 | | ); |
| 1136 | | yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1137 | | assert.equal(fake.allRequests().filter((request) => request["type"] === "prompt").length, 1); |
| 1138 | | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 1097 | it.effect.each([ |
| 1098 | { aborted: false, resumes: false, expected: "completed" }, |
| 1099 | { aborted: true, resumes: false, expected: "interrupted" }, |
| 1100 | { aborted: true, resumes: true, expected: "completed" }, |
| 1101 | ])( |
| 1102 | "settles a completed wake, aborted=$aborted, resumes=$resumes", |
| 1103 | ({ aborted, resumes, expected }) => |
| 1104 | Effect.gen(function* () { |
| 1105 | const fake = yield* makeFakePi; |
| 1106 | const offers = |
| 1107 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| 1108 | const { runtime, takeEvent } = yield* openRuntime( |
| 1109 | fake, |
| 1110 | "default", |
| 1111 | THREAD_ID, |
| 1112 | SESSION_ID, |
| 1113 | undefined, |
| 1114 | { |
| 1115 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| 1116 | }, |
| 1117 | ); |
| 1118 | const providerThread = yield* runtime.ensureThread({ |
| 1119 | threadId: THREAD_ID, |
| 1120 | modelSelection: modelSelection("default"), |
| 1121 | runtimePolicy, |
| 1122 | }); |
| 1123 | yield* fake.emit({ type: "agent_start" }); |
| 1124 | yield* Queue.take(offers); |
| 1125 | yield* fake.emit({ type: "agent_settled", aborted }); |
| 1126 | yield* takeEvent( |
| 1127 | (event) => |
| 1128 | event.type === "provider_session.updated" && event.providerSession.status === "ready", |
| 1129 | ); |
| 1130 | yield* startTurn(runtime, providerThread, "default", [], "/hello"); |
| 1131 | yield* fake.takeRequest("prompt"); |
| 1132 | if (resumes) yield* fake.emit({ type: "agent_start" }); |
| 1133 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| 1134 | if (resumes) yield* fake.emit({ type: "agent_settled", aborted: false }); |
| 1135 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1136 | assert.isTrue(terminal.type === "turn.terminal" && terminal.status === expected); |
| 1137 | yield* startTurn( |
| 1138 | runtime, |
| 1139 | providerThread, |
| 1140 | "default", |
| 1141 | [], |
| 1142 | "Stale continuation", |
| 1143 | undefined, |
| 1144 | 2, |
| 1145 | THREAD_ID, |
| 1146 | true, |
| 1147 | ); |
| 1148 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| 1149 | assert.equal( |
| 1150 | fake.allRequests().filter((request) => request["type"] === "prompt").length, |
| 1151 | 1, |
| 1152 | ); |
| 1153 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 1139 | 1154 | ); |
| 1140 | 1155 | |
| 1141 | 1156 | it.effect("keeps a pending wake on its thread and lets Stop retire its process", () => |
| ... | ... | @@ -3508,6 +3523,79 @@ describe("PiAdapterV2", () => { |
| 3508 | 3523 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 3509 | 3524 | ); |
| 3510 | 3525 | |
| 3526 | it.effect.each([undefined, false, true])( |
| 3527 | "preserves the native settlement outcome, aborted=%s", |
| 3528 | (aborted) => |
| 3529 | Effect.gen(function* () { |
| 3530 | const fake = yield* makeFakePi; |
| 3531 | const { runtime, takeEvent } = yield* openRuntime(fake); |
| 3532 | const providerThread = yield* runtime.ensureThread({ |
| 3533 | threadId: THREAD_ID, |
| 3534 | modelSelection: modelSelection("default"), |
| 3535 | runtimePolicy, |
| 3536 | }); |
| 3537 | yield* startTurn(runtime, providerThread); |
| 3538 | yield* fake.takeRequest("prompt"); |
| 3539 | yield* fake.emit({ type: "agent_start" }); |
| 3540 | yield* fake.emit({ |
| 3541 | type: "agent_settled", |
| 3542 | ...(aborted === undefined ? {} : { aborted }), |
| 3543 | }); |
| 3544 | const completed = yield* takeEvent( |
| 3545 | (event) => |
| 3546 | event.type === "provider_turn.updated" && event.providerTurn.status !== "running", |
| 3547 | ); |
| 3548 | const status = aborted === true ? "interrupted" : "completed"; |
| 3549 | assert.isTrue( |
| 3550 | completed.type === "provider_turn.updated" && completed.providerTurn.status === status, |
| 3551 | ); |
| 3552 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| 3553 | assert.isTrue( |
| 3554 | terminal.type === "turn.terminal" && |
| 3555 | terminal.status === status && |
| 3556 | terminal.failure === null, |
| 3557 | ); |
| 3558 | assert.equal(runtime.providerSession.status, "ready"); |
| 3559 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 3560 | ); |
| 3561 | |
| 3562 | it.effect("stops retry progress when Pi aborts the native run", () => |
| 3563 | Effect.gen(function* () { |
| 3564 | const fake = yield* makeFakePi; |
| 3565 | const { runtime, takeEvent } = yield* openRuntime(fake); |
| 3566 | const providerThread = yield* runtime.ensureThread({ |
| 3567 | threadId: THREAD_ID, |
| 3568 | modelSelection: modelSelection("default"), |
| 3569 | runtimePolicy, |
| 3570 | }); |
| 3571 | yield* startTurn(runtime, providerThread); |
| 3572 | yield* fake.takeRequest("prompt"); |
| 3573 | yield* fake.emit({ type: "agent_start" }); |
| 3574 | yield* fake.emit({ |
| 3575 | type: "auto_retry_start", |
| 3576 | attempt: 2, |
| 3577 | maxAttempts: 5, |
| 3578 | delayMs: 6_000, |
| 3579 | errorMessage: "temporary network failure", |
| 3580 | }); |
| 3581 | const retrying = yield* takeEvent( |
| 3582 | (event) => event.type === "turn_item.updated" && event.turnItem.type === "error", |
| 3583 | ); |
| 3584 | yield* fake.emit({ type: "agent_settled", aborted: true }); |
| 3585 | const stopped = yield* takeEvent( |
| 3586 | (event) => event.type === "turn_item.updated" && event.turnItem.type === "error", |
| 3587 | ); |
| 3588 | assert.isTrue( |
| 3589 | retrying.type === "turn_item.updated" && |
| 3590 | stopped.type === "turn_item.updated" && |
| 3591 | stopped.turnItem.id === retrying.turnItem.id && |
| 3592 | stopped.turnItem.status === "interrupted", |
| 3593 | ); |
| 3594 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| 3595 | assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted"); |
| 3596 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| 3597 | ); |
| 3598 | |
| 3511 | 3599 | it.effect("stops active retry progress when the turn is interrupted", () => |
| 3512 | 3600 | Effect.gen(function* () { |
| 3513 | 3601 | const fake = yield* makeFakePi; |