| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { | 3 | import { |
| | 4 | CheckpointId, |
| 4 | EnvironmentId, | 5 | EnvironmentId, |
| 5 | NodeId, | 6 | NodeId, |
| 6 | ProviderInstanceId, | 7 | ProviderInstanceId, |
| ... | @@ -30,8 +31,10 @@ import * as Sink from "effect/Sink"; | ... | @@ -30,8 +31,10 @@ import * as Sink from "effect/Sink"; |
| 30 | import * as Stream from "effect/Stream"; | 31 | import * as Stream from "effect/Stream"; |
| 31 | import * as TestClock from "effect/testing/TestClock"; | 32 | import * as TestClock from "effect/testing/TestClock"; |
| 32 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; | 33 | import { ChildProcess, ChildProcessSpawner } from "effect/process"; |
| | 34 | import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; |
| 33 | | 35 | |
| 34 | import * as ServerConfig from "../../config.ts"; | 36 | import * as ServerConfig from "../../config.ts"; |
| | 37 | import * as ProviderContinuationRequests from "../ProviderContinuationRequests.ts"; |
| 35 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; | 38 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 36 | import * as IdAllocator from "../IdAllocator.ts"; | 39 | import * as IdAllocator from "../IdAllocator.ts"; |
| 37 | import { | 40 | import { |
| ... | @@ -40,7 +43,12 @@ import { | ... | @@ -40,7 +43,12 @@ import { |
| 40 | type ProviderAdapterV2SessionRuntime, | 43 | type ProviderAdapterV2SessionRuntime, |
| 41 | } from "../ProviderAdapter.ts"; | 44 | } from "../ProviderAdapter.ts"; |
| 42 | import { handoffBudget } from "../ContextHandoffBudget.ts"; | 45 | import { handoffBudget } from "../ContextHandoffBudget.ts"; |
| 43 | import { makePiAdapterV2, PI_PROVIDER } from "./PiAdapterV2.ts"; | 46 | import { |
| | 47 | makePiAdapterV2, |
| | 48 | PiAdapterV2Driver, |
| | 49 | PI_PROVIDER, |
| | 50 | type PiAdapterV2Options, |
| | 51 | } from "./PiAdapterV2.ts"; |
| 44 | import { makePiRpcConnection, type PiRpcRecord } from "./PiRpc.ts"; | 52 | import { makePiRpcConnection, type PiRpcRecord } from "./PiRpc.ts"; |
| 45 | | 53 | |
| 46 | const layerServerConfig = ServerConfig.layerTest(process.cwd(), { | 54 | const layerServerConfig = ServerConfig.layerTest(process.cwd(), { |
| ... | @@ -88,7 +96,9 @@ interface FakePi { | ... | @@ -88,7 +96,9 @@ interface FakePi { |
| 88 | readonly resolveDeferredState: (data: unknown) => Effect.Effect<void>; | 96 | readonly resolveDeferredState: (data: unknown) => Effect.Effect<void>; |
| 89 | /** Reject the next `get_state` request. */ | 97 | /** Reject the next `get_state` request. */ |
| 90 | readonly failNextState: () => void; | 98 | readonly failNextState: () => void; |
| 91 | readonly deferNextLifecycle: (type: "switch_session" | "new_session") => void; | 99 | readonly deferNextLifecycle: (type: "switch_session" | "new_session" | "fork") => void; |
| | 100 | /** Hold a model-select extension hook until its UI request is answered. */ |
| | 101 | readonly deferNextModelSelection: () => void; |
| 92 | readonly queueModels: (models: ReadonlyArray<unknown>) => void; | 102 | readonly queueModels: (models: ReadonlyArray<unknown>) => void; |
| 93 | readonly vetoNextNewSession: () => void; | 103 | readonly vetoNextNewSession: () => void; |
| 94 | /** Every request received by the fake process. */ | 104 | /** Every request received by the fake process. */ |
| ... | @@ -291,6 +301,9 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () { | ... | @@ -291,6 +301,9 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () { |
| 291 | deferNextLifecycle: (type) => { | 301 | deferNextLifecycle: (type) => { |
| 292 | deferredLifecycle = type; | 302 | deferredLifecycle = type; |
| 293 | }, | 303 | }, |
| | 304 | deferNextModelSelection: () => { |
| | 305 | deferredLifecycle = "set_model"; |
| | 306 | }, |
| 294 | queueModels: (value) => { | 307 | queueModels: (value) => { |
| 295 | models = value; | 308 | models = value; |
| 296 | }, | 309 | }, |
| ... | @@ -310,22 +323,44 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () { | ... | @@ -310,22 +323,44 @@ const makeFakePi: Effect.Effect<FakePi> = Effect.gen(function* () { |
| 310 | } satisfies FakePi; | 323 | } satisfies FakePi; |
| 311 | }); | 324 | }); |
| 312 | | 325 | |
| 313 | const makeAdapter = Effect.fnUntraced(function* (fake: FakePi, launchArgs = "", forkFake?: FakePi) { | 326 | const makeAdapter = Effect.fnUntraced(function* ( |
| | 327 | fake: FakePi, |
| | 328 | launchArgs = "", |
| | 329 | forkFake?: FakePi, |
| | 330 | continuationRequests?: PiAdapterV2Options["continuationRequests"], |
| | 331 | ) { |
| 314 | const idAllocator = yield* IdAllocator.IdAllocatorV2; | 332 | const idAllocator = yield* IdAllocator.IdAllocatorV2; |
| 315 | const serverConfig = yield* ServerConfig.ServerConfig; | 333 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 316 | const fileSystem = yield* FileSystem.FileSystem; | 334 | const fileSystem = yield* FileSystem.FileSystem; |
| | 335 | const spawner = |
| | 336 | forkFake === undefined |
| | 337 | ? fake.spawner |
| | 338 | : ChildProcessSpawner.make((command) => |
| | 339 | ChildProcess.isStandardCommand(command) && command.args.includes("--fork") |
| | 340 | ? forkFake.spawner.spawn(command) |
| | 341 | : fake.spawner.spawn(command), |
| | 342 | ); |
| | 343 | if (continuationRequests !== undefined) { |
| | 344 | return yield* PiAdapterV2Driver.create({ |
| | 345 | instanceId: PI_INSTANCE_ID, |
| | 346 | displayName: undefined, |
| | 347 | enabled: true, |
| | 348 | environment: [], |
| | 349 | config: { enabled: true, binaryPath: "pi", launchArgs, customModels: [] }, |
| | 350 | }).pipe( |
| | 351 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), |
| | 352 | Effect.provideService(HostProcessEnvironment, {}), |
| | 353 | Effect.provideService(ProviderContinuationRequests.ProviderContinuationRequests, { |
| | 354 | ...continuationRequests, |
| | 355 | take: Effect.never, |
| | 356 | }), |
| | 357 | ); |
| | 358 | } |
| 317 | return makePiAdapterV2({ | 359 | return makePiAdapterV2({ |
| 318 | instanceId: PI_INSTANCE_ID, | 360 | instanceId: PI_INSTANCE_ID, |
| 319 | settings: { enabled: true, binaryPath: "pi", launchArgs, customModels: [] }, | 361 | settings: { enabled: true, binaryPath: "pi", launchArgs, customModels: [] }, |
| 320 | environment: {}, | 362 | environment: {}, |
| 321 | spawner: | 363 | spawner, |
| 322 | forkFake === undefined | | |
| 323 | ? fake.spawner | | |
| 324 | : ChildProcessSpawner.make((command) => | | |
| 325 | ChildProcess.isStandardCommand(command) && command.args.includes("--fork") | | |
| 326 | ? forkFake.spawner.spawn(command) | | |
| 327 | : fake.spawner.spawn(command), | | |
| 328 | ), | | |
| 329 | fileSystem, | 364 | fileSystem, |
| 330 | idAllocator, | 365 | idAllocator, |
| 331 | serverConfig, | 366 | serverConfig, |
| ... | @@ -338,8 +373,9 @@ const openRuntime = Effect.fnUntraced(function* ( | ... | @@ -338,8 +373,9 @@ const openRuntime = Effect.fnUntraced(function* ( |
| 338 | threadId = THREAD_ID, | 373 | threadId = THREAD_ID, |
| 339 | providerSessionId = SESSION_ID, | 374 | providerSessionId = SESSION_ID, |
| 340 | forkFake?: FakePi, | 375 | forkFake?: FakePi, |
| | 376 | continuationRequests?: PiAdapterV2Options["continuationRequests"], |
| 341 | ) { | 377 | ) { |
| 342 | const adapter = yield* makeAdapter(fake, "", forkFake); | 378 | const adapter = yield* makeAdapter(fake, "", forkFake, continuationRequests); |
| 343 | const runtime = yield* adapter.openSession({ | 379 | const runtime = yield* adapter.openSession({ |
| 344 | threadId, | 380 | threadId, |
| 345 | providerSessionId, | 381 | providerSessionId, |
| ... | @@ -397,6 +433,7 @@ const startTurn = Effect.fnUntraced(function* ( | ... | @@ -397,6 +433,7 @@ const startTurn = Effect.fnUntraced(function* ( |
| 397 | selection?: ModelSelection, | 433 | selection?: ModelSelection, |
| 398 | runOrdinal = 1, | 434 | runOrdinal = 1, |
| 399 | threadId = THREAD_ID, | 435 | threadId = THREAD_ID, |
| | 436 | continuation = false, |
| 400 | ) { | 437 | ) { |
| 401 | const appThread = yield* makeAppThread(model, threadId); | 438 | const appThread = yield* makeAppThread(model, threadId); |
| 402 | const runId = RunId.make(`run:${threadId}:${runOrdinal}`); | 439 | const runId = RunId.make(`run:${threadId}:${runOrdinal}`); |
| ... | @@ -413,8 +450,8 @@ const startTurn = Effect.fnUntraced(function* ( | ... | @@ -413,8 +450,8 @@ const startTurn = Effect.fnUntraced(function* ( |
| 413 | messageId: `message:${threadId}:${runOrdinal}` as never, | 450 | messageId: `message:${threadId}:${runOrdinal}` as never, |
| 414 | text, | 451 | text, |
| 415 | attachments, | 452 | attachments, |
| 416 | createdBy: "user", | 453 | createdBy: continuation ? "agent" : "user", |
| 417 | creationSource: "web", | 454 | creationSource: continuation ? "provider" : "web", |
| 418 | }, | 455 | }, |
| 419 | modelSelection: selection ?? modelSelection(model), | 456 | modelSelection: selection ?? modelSelection(model), |
| 420 | runtimePolicy, | 457 | runtimePolicy, |
| ... | @@ -461,6 +498,786 @@ const expectModelFailure = (errorMessage: string) => | ... | @@ -461,6 +498,786 @@ const expectModelFailure = (errorMessage: string) => |
| 461 | }).pipe(Effect.scoped, Effect.provide(layerTest)); | 498 | }).pipe(Effect.scoped, Effect.provide(layerTest)); |
| 462 | | 499 | |
| 463 | describe("PiAdapterV2", () => { | 500 | describe("PiAdapterV2", () => { |
| | 501 | it.effect.each([false, true])( |
| | 502 | "serializes rollback fork hooks with native wake ownership, wake=%s", |
| | 503 | (nativeWake) => |
| | 504 | Effect.gen(function* () { |
| | 505 | const fake = yield* makeFakePi; |
| | 506 | const offers = |
| | 507 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 508 | const { runtime, takeEvent } = yield* openRuntime( |
| | 509 | fake, |
| | 510 | "default", |
| | 511 | THREAD_ID, |
| | 512 | SESSION_ID, |
| | 513 | undefined, |
| | 514 | { |
| | 515 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 516 | }, |
| | 517 | ); |
| | 518 | const providerThread = yield* runtime.ensureThread({ |
| | 519 | threadId: THREAD_ID, |
| | 520 | modelSelection: modelSelection("default"), |
| | 521 | runtimePolicy, |
| | 522 | }); |
| | 523 | fake.deferNextLifecycle("fork"); |
| | 524 | const rollingBack = yield* runtime |
| | 525 | .rollbackThread({ |
| | 526 | providerThread, |
| | 527 | target: { |
| | 528 | type: "thread_start", |
| | 529 | checkpointId: CheckpointId.make("checkpoint-start"), |
| | 530 | appRunOrdinal: 0, |
| | 531 | }, |
| | 532 | providerThreadTurns: [ |
| | 533 | { |
| | 534 | id: ProviderTurnId.make("previous-turn"), |
| | 535 | providerThreadId: providerThread.id, |
| | 536 | nodeId: NodeId.make("previous-node"), |
| | 537 | runAttemptId: null, |
| | 538 | nativeTurnRef: { |
| | 539 | driver: PI_PROVIDER, |
| | 540 | nativeId: "user-before-wake", |
| | 541 | strength: "strong", |
| | 542 | }, |
| | 543 | ordinal: 1, |
| | 544 | status: "completed", |
| | 545 | startedAt: providerThread.createdAt, |
| | 546 | completedAt: providerThread.createdAt, |
| | 547 | }, |
| | 548 | ], |
| | 549 | }) |
| | 550 | .pipe(Effect.result, Effect.forkScoped); |
| | 551 | const fork = yield* fake.takeRequest("fork"); |
| | 552 | const overlappingTurn = yield* startTurn(runtime, providerThread).pipe(Effect.result); |
| | 553 | assert.equal(overlappingTurn._tag, "Failure"); |
| | 554 | const overlappingRegistration = yield* runtime |
| | 555 | .ensureThread({ |
| | 556 | threadId: THREAD_ID, |
| | 557 | modelSelection: modelSelection("default"), |
| | 558 | runtimePolicy, |
| | 559 | }) |
| | 560 | .pipe(Effect.result); |
| | 561 | assert.equal(overlappingRegistration._tag, "Failure"); |
| | 562 | yield* fake.emit({ |
| | 563 | type: "extension_ui_request", |
| | 564 | id: "fork-hook-dialog", |
| | 565 | method: "confirm", |
| | 566 | title: "Restore this branch?", |
| | 567 | }); |
| | 568 | const forkDialog = yield* takeEvent((event) => event.type === "runtime_request.updated"); |
| | 569 | assert.isTrue(forkDialog.type === "runtime_request.updated"); |
| | 570 | if (forkDialog.type !== "runtime_request.updated") return; |
| | 571 | assert.isNull(forkDialog.runtimeRequest.providerTurnId); |
| | 572 | yield* runtime.respondToRuntimeRequest({ |
| | 573 | requestId: forkDialog.runtimeRequest.id, |
| | 574 | decision: "accept", |
| | 575 | }); |
| | 576 | yield* fake.takeRequest("extension_ui_response"); |
| | 577 | if (nativeWake) { |
| | 578 | yield* fake.emit({ type: "agent_start" }); |
| | 579 | yield* fake.emit({ type: "message_start", message: { role: "assistant" } }); |
| | 580 | yield* fake.emit({ |
| | 581 | type: "message_update", |
| | 582 | assistantMessageEvent: { |
| | 583 | type: "text_end", |
| | 584 | contentIndex: 0, |
| | 585 | content: "Discarded branch wake output", |
| | 586 | }, |
| | 587 | }); |
| | 588 | yield* fake.emit({ type: "agent_settled" }); |
| | 589 | const retired = yield* takeEvent( |
| | 590 | (event) => |
| | 591 | event.type === "provider_session.updated" && |
| | 592 | event.providerSession.status === "stopped", |
| | 593 | ).pipe(Effect.as(true), Effect.raceFirst(Queue.take(offers).pipe(Effect.as(false)))); |
| | 594 | assert.isTrue(retired, "Pi offered native work while rollback was changing its binding"); |
| | 595 | const result = yield* Fiber.join(rollingBack); |
| | 596 | assert.equal(result._tag, "Failure"); |
| | 597 | assert.isFalse(yield* runtime.hasPendingBackgroundWork!); |
| | 598 | const staleTurn = yield* startTurn( |
| | 599 | runtime, |
| | 600 | providerThread, |
| | 601 | "default", |
| | 602 | [], |
| | 603 | "Synthetic wake", |
| | 604 | undefined, |
| | 605 | 1, |
| | 606 | THREAD_ID, |
| | 607 | true, |
| | 608 | ).pipe(Effect.result); |
| | 609 | assert.equal(staleTurn._tag, "Failure"); |
| | 610 | assert.isFalse(fake.allRequests().some((request) => request["type"] === "prompt")); |
| | 611 | return; |
| | 612 | } |
| | 613 | fake.queueState({ sessionFile: "/fake/rolled-back-session.jsonl" }); |
| | 614 | yield* fake.emit({ |
| | 615 | type: "response", |
| | 616 | id: fork["id"], |
| | 617 | command: "fork", |
| | 618 | success: true, |
| | 619 | data: { cancelled: false }, |
| | 620 | }); |
| | 621 | const result = yield* Fiber.join(rollingBack); |
| | 622 | assert.equal(result._tag, "Success"); |
| | 623 | if (result._tag !== "Success") return; |
| | 624 | assert.equal( |
| | 625 | result.success.providerThread.nativeThreadRef?.nativeId, |
| | 626 | "/fake/rolled-back-session.jsonl", |
| | 627 | ); |
| | 628 | yield* startTurn(runtime, result.success.providerThread); |
| | 629 | const prompt = yield* fake.takeRequest("prompt"); |
| | 630 | assert.equal(prompt["message"], "Hello pi"); |
| | 631 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| | 632 | yield* fake.emit({ type: "agent_start" }); |
| | 633 | yield* fake.emit({ type: "agent_settled" }); |
| | 634 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 635 | assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed"); |
| | 636 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 637 | ); |
| | 638 | |
| | 639 | it.effect.each([false, true])( |
| | 640 | "retains native wake ownership after a rejected joining prompt, compaction aborted=%s", |
| | 641 | (aborted) => |
| | 642 | Effect.gen(function* () { |
| | 643 | const fake = yield* makeFakePi; |
| | 644 | const offers = |
| | 645 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 646 | const { runtime, takeEvent } = yield* openRuntime( |
| | 647 | fake, |
| | 648 | "default", |
| | 649 | THREAD_ID, |
| | 650 | SESSION_ID, |
| | 651 | undefined, |
| | 652 | { |
| | 653 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 654 | }, |
| | 655 | ); |
| | 656 | const providerThread = yield* runtime.ensureThread({ |
| | 657 | threadId: THREAD_ID, |
| | 658 | modelSelection: modelSelection("default"), |
| | 659 | runtimePolicy, |
| | 660 | }); |
| | 661 | yield* fake.takeRequest("get_state"); |
| | 662 | yield* fake.emit({ type: "agent_start" }); |
| | 663 | yield* Queue.take(offers); |
| | 664 | yield* fake.emit({ type: "agent_settled" }); |
| | 665 | yield* fake.emit({ type: "compaction_start", reason: "manual" }); |
| | 666 | yield* startTurn(runtime, providerThread); |
| | 667 | yield* fake.takeRequest("prompt"); |
| | 668 | const rejection = "Cannot submit a prompt while compaction is in progress."; |
| | 669 | yield* fake.emit({ type: "response", command: "prompt", success: false, error: rejection }); |
| | 670 | yield* fake.emit({ |
| | 671 | type: "extension_ui_request", |
| | 672 | id: "native-compaction-dialog", |
| | 673 | method: "confirm", |
| | 674 | title: "Continue native work?", |
| | 675 | }); |
| | 676 | const dialog = yield* takeEvent((event) => event.type === "runtime_request.updated"); |
| | 677 | assert.isTrue(dialog.type === "runtime_request.updated"); |
| | 678 | if (dialog.type !== "runtime_request.updated") return; |
| | 679 | assert.isNotNull(dialog.runtimeRequest.providerTurnId); |
| | 680 | yield* runtime.respondToRuntimeRequest({ |
| | 681 | requestId: dialog.runtimeRequest.id, |
| | 682 | decision: "accept", |
| | 683 | }); |
| | 684 | yield* fake.takeRequest("extension_ui_response"); |
| | 685 | if (aborted) { |
| | 686 | yield* fake.emit({ type: "compaction_end", result: null, aborted: true }); |
| | 687 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 688 | assert.isTrue( |
| | 689 | terminal.type === "turn.terminal" && |
| | 690 | terminal.status === "failed" && |
| | 691 | terminal.failure.message === rejection, |
| | 692 | ); |
| | 693 | return; |
| | 694 | } |
| | 695 | fake.queueState({ isStreaming: true }); |
| | 696 | yield* fake.emit({ |
| | 697 | type: "compaction_end", |
| | 698 | result: { summary: "Native compaction finished" }, |
| | 699 | willRetry: true, |
| | 700 | }); |
| | 701 | yield* fake.emit({ type: "agent_start" }); |
| | 702 | yield* fake.emit({ type: "message_start", message: { role: "assistant" } }); |
| | 703 | yield* fake.emit({ |
| | 704 | type: "message_update", |
| | 705 | assistantMessageEvent: { |
| | 706 | type: "text_end", |
| | 707 | contentIndex: 0, |
| | 708 | content: "Native work completed", |
| | 709 | }, |
| | 710 | }); |
| | 711 | const message = yield* takeEvent((event) => event.type === "message.updated"); |
| | 712 | assert.isTrue(message.type === "message.updated" && message.message.runId !== null); |
| | 713 | yield* fake.takeRequest("get_state"); |
| | 714 | yield* fake.emit({ type: "agent_settled" }); |
| | 715 | const terminal = yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 716 | assert.isTrue( |
| | 717 | terminal.type === "turn.terminal" && |
| | 718 | terminal.status === "failed" && |
| | 719 | terminal.failure.message === rejection, |
| | 720 | ); |
| | 721 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 722 | ); |
| | 723 | |
| | 724 | it.effect("rejects rollback while native wake output is awaiting ownership", () => |
| | 725 | Effect.gen(function* () { |
| | 726 | const fake = yield* makeFakePi; |
| | 727 | const offers = |
| | 728 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 729 | const { runtime, takeEvent } = yield* openRuntime( |
| | 730 | fake, |
| | 731 | "default", |
| | 732 | THREAD_ID, |
| | 733 | SESSION_ID, |
| | 734 | undefined, |
| | 735 | { |
| | 736 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 737 | }, |
| | 738 | ); |
| | 739 | const providerThread = yield* runtime.ensureThread({ |
| | 740 | threadId: THREAD_ID, |
| | 741 | modelSelection: modelSelection("default"), |
| | 742 | runtimePolicy, |
| | 743 | }); |
| | 744 | yield* fake.emit({ type: "agent_start" }); |
| | 745 | yield* Queue.take(offers); |
| | 746 | yield* fake.emit({ type: "message_start", message: { role: "assistant" } }); |
| | 747 | yield* fake.emit({ |
| | 748 | type: "message_update", |
| | 749 | assistantMessageEvent: { |
| | 750 | type: "text_end", |
| | 751 | contentIndex: 0, |
| | 752 | content: "Current branch wake output", |
| | 753 | }, |
| | 754 | }); |
| | 755 | yield* fake.emit({ type: "agent_settled" }); |
| | 756 | yield* takeEvent( |
| | 757 | (event) => |
| | 758 | event.type === "provider_session.updated" && event.providerSession.status === "ready", |
| | 759 | ); |
| | 760 | const rolledBack = yield* runtime |
| | 761 | .rollbackThread({ |
| | 762 | providerThread, |
| | 763 | target: { |
| | 764 | type: "thread_start", |
| | 765 | checkpointId: CheckpointId.make("checkpoint-start"), |
| | 766 | appRunOrdinal: 0, |
| | 767 | }, |
| | 768 | providerThreadTurns: [ |
| | 769 | { |
| | 770 | id: ProviderTurnId.make("previous-turn"), |
| | 771 | providerThreadId: providerThread.id, |
| | 772 | nodeId: NodeId.make("previous-node"), |
| | 773 | runAttemptId: null, |
| | 774 | nativeTurnRef: { |
| | 775 | driver: PI_PROVIDER, |
| | 776 | nativeId: "user-before-wake", |
| | 777 | strength: "strong", |
| | 778 | }, |
| | 779 | ordinal: 1, |
| | 780 | status: "completed", |
| | 781 | startedAt: providerThread.createdAt, |
| | 782 | completedAt: providerThread.createdAt, |
| | 783 | }, |
| | 784 | ], |
| | 785 | }) |
| | 786 | .pipe(Effect.result); |
| | 787 | assert.equal( |
| | 788 | rolledBack._tag, |
| | 789 | "Failure", |
| | 790 | "Rollback changed the native session while its wake was still buffered", |
| | 791 | ); |
| | 792 | assert.isFalse(fake.allRequests().some((request) => request["type"] === "fork")); |
| | 793 | assert.isTrue(yield* runtime.hasPendingBackgroundWork!); |
| | 794 | yield* startTurn( |
| | 795 | runtime, |
| | 796 | providerThread, |
| | 797 | "default", |
| | 798 | [], |
| | 799 | "Synthetic wake", |
| | 800 | undefined, |
| | 801 | 1, |
| | 802 | THREAD_ID, |
| | 803 | true, |
| | 804 | ); |
| | 805 | const message = yield* takeEvent((event) => event.type === "message.updated"); |
| | 806 | assert.equal( |
| | 807 | message.type === "message.updated" && message.message.text, |
| | 808 | "Current branch wake output", |
| | 809 | ); |
| | 810 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 811 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 812 | ); |
| | 813 | |
| | 814 | it.effect("answers model-selection dialogs before a user turn claims a native wake", () => |
| | 815 | Effect.gen(function* () { |
| | 816 | const fake = yield* makeFakePi; |
| | 817 | const offers = |
| | 818 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 819 | const { runtime, takeEvent } = yield* openRuntime( |
| | 820 | fake, |
| | 821 | "default", |
| | 822 | THREAD_ID, |
| | 823 | SESSION_ID, |
| | 824 | undefined, |
| | 825 | { offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid) }, |
| | 826 | ); |
| | 827 | const providerThread = yield* runtime.ensureThread({ |
| | 828 | threadId: THREAD_ID, |
| | 829 | modelSelection: modelSelection("default"), |
| | 830 | runtimePolicy, |
| | 831 | }); |
| | 832 | yield* fake.emit({ type: "agent_start" }); |
| | 833 | yield* Queue.take(offers); |
| | 834 | const requests: Array<Extract<ProviderAdapterV2Event, { type: "runtime_request.updated" }>> = |
| | 835 | []; |
| | 836 | fake.deferNextModelSelection(); |
| | 837 | const starting = yield* startTurn( |
| | 838 | runtime, |
| | 839 | providerThread, |
| | 840 | "default", |
| | 841 | [], |
| | 842 | "Continue the review", |
| | 843 | modelSelection("xai/grok-4.6"), |
| | 844 | ).pipe(Effect.forkScoped); |
| | 845 | const modelRequest = yield* fake.takeRequest("set_model"); |
| | 846 | yield* fake.emit({ |
| | 847 | type: "extension_ui_request", |
| | 848 | id: "model-selection-dialog", |
| | 849 | method: "confirm", |
| | 850 | title: "Use this model?", |
| | 851 | }); |
| | 852 | yield* fake.emit({ type: "agent_settled" }); |
| | 853 | yield* takeEvent((event) => { |
| | 854 | if (event.type === "runtime_request.updated") requests.push(event); |
| | 855 | return ( |
| | 856 | event.type === "provider_session.updated" && event.providerSession.status === "ready" |
| | 857 | ); |
| | 858 | }); |
| | 859 | const dialog = requests[0]?.runtimeRequest; |
| | 860 | assert.isDefined(dialog, "Pi buffered the dialog needed to finish model selection"); |
| | 861 | assert.equal(requests[0]?.threadId, THREAD_ID); |
| | 862 | assert.isNull(dialog!.providerTurnId); |
| | 863 | yield* runtime.respondToRuntimeRequest({ requestId: dialog!.id, decision: "accept" }); |
| | 864 | const response = yield* fake.takeRequest("extension_ui_response"); |
| | 865 | assert.equal(response["id"], "model-selection-dialog"); |
| | 866 | assert.isTrue(response["confirmed"]); |
| | 867 | yield* fake.emit({ |
| | 868 | type: "response", |
| | 869 | id: modelRequest["id"], |
| | 870 | command: "set_model", |
| | 871 | success: true, |
| | 872 | data: { provider: "xai", id: "grok-4.6" }, |
| | 873 | }); |
| | 874 | yield* Fiber.join(starting); |
| | 875 | const prompt = yield* fake.takeRequest("prompt"); |
| | 876 | assert.equal(prompt["message"], "Continue the review"); |
| | 877 | assert.equal(prompt["streamingBehavior"], "steer"); |
| | 878 | const resolved = yield* takeEvent( |
| | 879 | (event) => |
| | 880 | event.type === "runtime_request.updated" && event.runtimeRequest.id === dialog!.id, |
| | 881 | ); |
| | 882 | assert.isTrue( |
| | 883 | resolved.type === "runtime_request.updated" && |
| | 884 | resolved.runtimeRequest.status === "resolved", |
| | 885 | ); |
| | 886 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 887 | ); |
| | 888 | |
| | 889 | it.effect("adopts a completed native wake without sending a synthetic prompt", () => |
| | 890 | Effect.gen(function* () { |
| | 891 | const fake = yield* makeFakePi; |
| | 892 | const offers = |
| | 893 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 894 | const { runtime, takeEvent } = yield* openRuntime( |
| | 895 | fake, |
| | 896 | "default", |
| | 897 | THREAD_ID, |
| | 898 | SESSION_ID, |
| | 899 | undefined, |
| | 900 | { |
| | 901 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 902 | }, |
| | 903 | ); |
| | 904 | const providerThread = yield* runtime.ensureThread({ |
| | 905 | threadId: THREAD_ID, |
| | 906 | modelSelection: modelSelection("default"), |
| | 907 | runtimePolicy, |
| | 908 | }); |
| | 909 | yield* fake.emit({ type: "agent_start" }); |
| | 910 | const offer = yield* Queue.take(offers).pipe( |
| | 911 | Effect.raceFirst( |
| | 912 | takeEvent( |
| | 913 | (event) => |
| | 914 | event.type === "provider_session.updated" && event.providerSession.status === "error", |
| | 915 | ).pipe( |
| | 916 | Effect.andThen(Effect.sync(() => assert.fail("Pi killed a routable native wake"))), |
| | 917 | ), |
| | 918 | ), |
| | 919 | ); |
| | 920 | assert.equal(offer.threadId, THREAD_ID); |
| | 921 | assert.equal(offer.providerThreadId, providerThread.id); |
| | 922 | assert.equal(offer.delivery, "adapter_buffered"); |
| | 923 | assert.isTrue(yield* runtime.hasPendingBackgroundWork!); |
| | 924 | let dispatchCommitted = false; |
| | 925 | yield* offer.dispatchIfCurrent!( |
| | 926 | Effect.sync(() => { |
| | 927 | dispatchCommitted = true; |
| | 928 | }), |
| | 929 | ); |
| | 930 | assert.isTrue(dispatchCommitted); |
| | 931 | assert.isTrue(yield* runtime.hasPendingBackgroundWork!); |
| | 932 | yield* fake.emit({ type: "message_start", message: { role: "assistant" } }); |
| | 933 | yield* fake.emit({ |
| | 934 | type: "message_update", |
| | 935 | assistantMessageEvent: { |
| | 936 | type: "text_delta", |
| | 937 | contentIndex: 0, |
| | 938 | delta: "Background review finished", |
| | 939 | }, |
| | 940 | }); |
| | 941 | yield* fake.emit({ type: "message_end", message: { role: "assistant" } }); |
| | 942 | yield* fake.emit({ type: "agent_settled" }); |
| | 943 | yield* takeEvent( |
| | 944 | (event) => |
| | 945 | event.type === "provider_session.updated" && event.providerSession.status === "ready", |
| | 946 | ); |
| | 947 | assert.isTrue(yield* runtime.hasPendingBackgroundWork!); |
| | 948 | yield* startTurn( |
| | 949 | runtime, |
| | 950 | providerThread, |
| | 951 | "default", |
| | 952 | [], |
| | 953 | "Synthetic wake", |
| | 954 | undefined, |
| | 955 | 1, |
| | 956 | THREAD_ID, |
| | 957 | true, |
| | 958 | ); |
| | 959 | const message = yield* takeEvent((event) => event.type === "message.updated"); |
| | 960 | assert.equal( |
| | 961 | message.type === "message.updated" && message.message.text, |
| | 962 | "Background review finished", |
| | 963 | ); |
| | 964 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 965 | assert.equal(fake.allRequests().filter((request) => request["type"] === "prompt").length, 0); |
| | 966 | assert.isFalse(yield* runtime.hasPendingBackgroundWork!); |
| | 967 | let dispatched = false; |
| | 968 | yield* offer.dispatchIfCurrent!( |
| | 969 | Effect.sync(() => { |
| | 970 | dispatched = true; |
| | 971 | }), |
| | 972 | ); |
| | 973 | assert.isFalse(dispatched); |
| | 974 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 975 | ); |
| | 976 | |
| | 977 | it.effect.each([false, true])( |
| | 978 | "a user prompt takes the wake without premature settlement, native wake settled=%s", |
| | 979 | (settled) => |
| | 980 | Effect.gen(function* () { |
| | 981 | const fake = yield* makeFakePi; |
| | 982 | const offers = |
| | 983 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 984 | const { runtime, takeEvent } = yield* openRuntime( |
| | 985 | fake, |
| | 986 | "default", |
| | 987 | THREAD_ID, |
| | 988 | SESSION_ID, |
| | 989 | undefined, |
| | 990 | { |
| | 991 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 992 | }, |
| | 993 | ); |
| | 994 | const providerThread = yield* runtime.ensureThread({ |
| | 995 | threadId: THREAD_ID, |
| | 996 | modelSelection: modelSelection("default"), |
| | 997 | runtimePolicy, |
| | 998 | }); |
| | 999 | yield* fake.emit({ type: "agent_start" }); |
| | 1000 | const offer = yield* Queue.take(offers); |
| | 1001 | if (settled) { |
| | 1002 | yield* fake.emit({ type: "agent_settled" }); |
| | 1003 | yield* takeEvent( |
| | 1004 | (event) => |
| | 1005 | event.type === "provider_session.updated" && event.providerSession.status === "ready", |
| | 1006 | ); |
| | 1007 | } |
| | 1008 | yield* startTurn(runtime, providerThread); |
| | 1009 | const prompt = yield* fake.takeRequest("prompt"); |
| | 1010 | assert.equal(prompt["message"], "Hello pi"); |
| | 1011 | assert.equal(prompt["streamingBehavior"], "steer"); |
| | 1012 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| | 1013 | yield* fake.emit({ type: "agent_start" }); |
| | 1014 | yield* fake.emit({ |
| | 1015 | type: "extension_ui_request", |
| | 1016 | id: "owned-dialog", |
| | 1017 | method: "confirm", |
| | 1018 | title: "Continue?", |
| | 1019 | }); |
| | 1020 | const dialog = yield* takeEvent((event) => event.type === "runtime_request.updated"); |
| | 1021 | assert.isTrue(dialog.type === "runtime_request.updated"); |
| | 1022 | if (dialog.type !== "runtime_request.updated") return; |
| | 1023 | assert.isNotNull(dialog.runtimeRequest.providerTurnId); |
| | 1024 | yield* offer.clearIfCurrent!(); |
| | 1025 | yield* runtime.respondToRuntimeRequest({ |
| | 1026 | requestId: dialog.runtimeRequest.id, |
| | 1027 | decision: "accept", |
| | 1028 | }); |
| | 1029 | const response = yield* fake.takeRequest("extension_ui_response"); |
| | 1030 | assert.isTrue(response["confirmed"]); |
| | 1031 | yield* fake.emit({ type: "message_start", message: { role: "assistant" } }); |
| | 1032 | yield* fake.emit({ |
| | 1033 | type: "message_update", |
| | 1034 | assistantMessageEvent: { |
| | 1035 | type: "text_end", |
| | 1036 | contentIndex: 0, |
| | 1037 | content: "User prompt completed", |
| | 1038 | }, |
| | 1039 | }); |
| | 1040 | yield* fake.emit({ type: "agent_settled" }); |
| | 1041 | const message = yield* takeEvent( |
| | 1042 | (event) => |
| | 1043 | event.type === "message.updated" && event.message.text === "User prompt completed", |
| | 1044 | ); |
| | 1045 | assert.isTrue(message.type === "message.updated" && message.message.runId !== null); |
| | 1046 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 1047 | let dispatched = false; |
| | 1048 | yield* offer.dispatchIfCurrent!( |
| | 1049 | Effect.sync(() => { |
| | 1050 | dispatched = true; |
| | 1051 | }), |
| | 1052 | ); |
| | 1053 | assert.isFalse(dispatched); |
| | 1054 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 1055 | ); |
| | 1056 | |
| | 1057 | it.effect("defers a wake's late settlement until the joined user prompt is acknowledged", () => |
| | 1058 | Effect.gen(function* () { |
| | 1059 | const fake = yield* makeFakePi; |
| | 1060 | const offers = |
| | 1061 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 1062 | const { runtime, takeEvent } = yield* openRuntime( |
| | 1063 | fake, |
| | 1064 | "default", |
| | 1065 | THREAD_ID, |
| | 1066 | SESSION_ID, |
| | 1067 | undefined, |
| | 1068 | { |
| | 1069 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 1070 | }, |
| | 1071 | ); |
| | 1072 | const providerThread = yield* runtime.ensureThread({ |
| | 1073 | threadId: THREAD_ID, |
| | 1074 | modelSelection: modelSelection("default"), |
| | 1075 | runtimePolicy, |
| | 1076 | }); |
| | 1077 | yield* fake.emit({ type: "agent_start" }); |
| | 1078 | yield* Queue.take(offers); |
| | 1079 | yield* startTurn(runtime, providerThread); |
| | 1080 | yield* fake.takeRequest("prompt"); |
| | 1081 | yield* fake.emit({ type: "agent_settled" }); |
| | 1082 | yield* fake.emit({ |
| | 1083 | type: "extension_ui_request", |
| | 1084 | method: "notify", |
| | 1085 | message: "Ordering barrier", |
| | 1086 | }); |
| | 1087 | yield* takeEvent( |
| | 1088 | (event) => |
| | 1089 | event.type === "turn_item.updated" && |
| | 1090 | event.turnItem.type === "dynamic_tool" && |
| | 1091 | event.turnItem.toolName === "notify", |
| | 1092 | ); |
| | 1093 | assert.equal(runtime.providerSession.status, "running"); |
| | 1094 | fake.queueState({ isStreaming: true }); |
| | 1095 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| | 1096 | yield* fake.takeRequest("get_state"); |
| | 1097 | yield* fake.emit({ type: "agent_start" }); |
| | 1098 | yield* fake.emit({ type: "agent_settled" }); |
| | 1099 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 1100 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 1101 | ); |
| | 1102 | |
| | 1103 | it.effect("settles a command-only user prompt after taking a completed wake", () => |
| | 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" }); |
| | 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 | yield* fake.emit({ type: "response", command: "prompt", success: true }); |
| | 1133 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 1134 | yield* startTurn( |
| | 1135 | runtime, |
| | 1136 | providerThread, |
| | 1137 | "default", |
| | 1138 | [], |
| | 1139 | "Stale continuation", |
| | 1140 | undefined, |
| | 1141 | 2, |
| | 1142 | THREAD_ID, |
| | 1143 | true, |
| | 1144 | ); |
| | 1145 | yield* takeEvent((event) => event.type === "turn.terminal"); |
| | 1146 | assert.equal(fake.allRequests().filter((request) => request["type"] === "prompt").length, 1); |
| | 1147 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 1148 | ); |
| | 1149 | |
| | 1150 | it.effect("keeps a pending wake on its thread and lets Stop retire its process", () => |
| | 1151 | Effect.gen(function* () { |
| | 1152 | const fake = yield* makeFakePi; |
| | 1153 | const offers = |
| | 1154 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 1155 | const { runtime, takeEvent } = yield* openRuntime( |
| | 1156 | fake, |
| | 1157 | "default", |
| | 1158 | THREAD_ID, |
| | 1159 | SESSION_ID, |
| | 1160 | undefined, |
| | 1161 | { |
| | 1162 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 1163 | }, |
| | 1164 | ); |
| | 1165 | const providerThread = yield* runtime.ensureThread({ |
| | 1166 | threadId: THREAD_ID, |
| | 1167 | modelSelection: modelSelection("default"), |
| | 1168 | runtimePolicy, |
| | 1169 | }); |
| | 1170 | yield* fake.emit({ type: "agent_start" }); |
| | 1171 | const offer = yield* Queue.take(offers); |
| | 1172 | assert.isTrue(yield* runtime.hasPendingBackgroundWorkForThread!(providerThread)); |
| | 1173 | const otherThread = { |
| | 1174 | ...providerThread, |
| | 1175 | appThreadId: ThreadId.make("other-thread"), |
| | 1176 | id: ProviderThreadId.make("other-provider-thread"), |
| | 1177 | }; |
| | 1178 | assert.isFalse(yield* runtime.hasPendingBackgroundWorkForThread!(otherThread)); |
| | 1179 | const failure = yield* runtime |
| | 1180 | .resumeThread({ providerThread: otherThread }) |
| | 1181 | .pipe(Effect.flip); |
| | 1182 | assert.equal(failure._tag, "ProviderAdapterResumeThreadError"); |
| | 1183 | yield* runtime.interruptTurn({ |
| | 1184 | providerThread, |
| | 1185 | providerTurnId: ProviderTurnId.make("settled-turn"), |
| | 1186 | requestRuntimeRestart: true, |
| | 1187 | }); |
| | 1188 | yield* takeEvent( |
| | 1189 | (event) => |
| | 1190 | event.type === "provider_session.updated" && event.providerSession.status === "stopped", |
| | 1191 | ); |
| | 1192 | assert.isFalse(yield* runtime.hasPendingBackgroundWork!); |
| | 1193 | let dispatched = false; |
| | 1194 | yield* offer.dispatchIfCurrent!( |
| | 1195 | Effect.sync(() => { |
| | 1196 | dispatched = true; |
| | 1197 | }), |
| | 1198 | ); |
| | 1199 | assert.isFalse(dispatched); |
| | 1200 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 1201 | ); |
| | 1202 | |
| | 1203 | it.effect.each(["archive", "stop", "transport death"] as const)( |
| | 1204 | "retiring a wake by %s cancels visible dialogs and invalidates its offer", |
| | 1205 | (retirement) => |
| | 1206 | Effect.gen(function* () { |
| | 1207 | const fake = yield* makeFakePi; |
| | 1208 | const offers = |
| | 1209 | yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>(); |
| | 1210 | const { runtime, takeEvent } = yield* openRuntime( |
| | 1211 | fake, |
| | 1212 | "default", |
| | 1213 | THREAD_ID, |
| | 1214 | SESSION_ID, |
| | 1215 | undefined, |
| | 1216 | { |
| | 1217 | offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid), |
| | 1218 | }, |
| | 1219 | ); |
| | 1220 | const providerThread = yield* runtime.ensureThread({ |
| | 1221 | threadId: THREAD_ID, |
| | 1222 | modelSelection: modelSelection("default"), |
| | 1223 | runtimePolicy, |
| | 1224 | }); |
| | 1225 | yield* fake.emit({ type: "agent_start" }); |
| | 1226 | const offer = yield* Queue.take(offers); |
| | 1227 | yield* fake.emit({ |
| | 1228 | type: "extension_ui_request", |
| | 1229 | id: "wake-dialog", |
| | 1230 | method: "confirm", |
| | 1231 | title: "Continue background work?", |
| | 1232 | }); |
| | 1233 | const dialog = yield* takeEvent((event) => event.type === "runtime_request.updated"); |
| | 1234 | assert.isTrue(dialog.type === "runtime_request.updated"); |
| | 1235 | if (dialog.type !== "runtime_request.updated") return; |
| | 1236 | assert.isNull(dialog.runtimeRequest.providerTurnId); |
| | 1237 | if (retirement === "archive") yield* offer.clearIfCurrent!(); |
| | 1238 | else if (retirement === "stop") { |
| | 1239 | yield* runtime.interruptTurn({ |
| | 1240 | providerThread, |
| | 1241 | providerTurnId: ProviderTurnId.make("settled-turn"), |
| | 1242 | requestRuntimeRestart: true, |
| | 1243 | }); |
| | 1244 | } else yield* fake.closeStdout; |
| | 1245 | let requestCancelled = false; |
| | 1246 | let nodeCancelled = false; |
| | 1247 | let itemCancelled = false; |
| | 1248 | yield* takeEvent((event) => { |
| | 1249 | if ( |
| | 1250 | event.type === "runtime_request.updated" && |
| | 1251 | event.runtimeRequest.id === dialog.runtimeRequest.id |
| | 1252 | ) |
| | 1253 | requestCancelled = event.runtimeRequest.status === "cancelled"; |
| | 1254 | if (event.type === "node.updated" && event.node.id === dialog.runtimeRequest.nodeId) |
| | 1255 | nodeCancelled = event.node.status === "cancelled"; |
| | 1256 | if ( |
| | 1257 | event.type === "turn_item.updated" && |
| | 1258 | event.turnItem.nodeId === dialog.runtimeRequest.nodeId |
| | 1259 | ) |
| | 1260 | itemCancelled = event.turnItem.status === "cancelled"; |
| | 1261 | return ( |
| | 1262 | event.type === "provider_session.updated" && |
| | 1263 | event.providerSession.status === |
| | 1264 | (retirement === "transport death" ? "error" : "stopped") |
| | 1265 | ); |
| | 1266 | }); |
| | 1267 | assert.isTrue(requestCancelled, "Retired wake left a pending runtime request"); |
| | 1268 | assert.isTrue(nodeCancelled); |
| | 1269 | assert.isTrue(itemCancelled); |
| | 1270 | assert.isFalse(yield* runtime.hasPendingBackgroundWork!); |
| | 1271 | let dispatched = false; |
| | 1272 | yield* offer.dispatchIfCurrent!( |
| | 1273 | Effect.sync(() => { |
| | 1274 | dispatched = true; |
| | 1275 | }), |
| | 1276 | ); |
| | 1277 | assert.isFalse(dispatched); |
| | 1278 | }).pipe(Effect.scoped, Effect.provide(layerTest)), |
| | 1279 | ); |
| | 1280 | |
| 464 | it.effect("stops provider-initiated work that has no T3 turn owner", () => | 1281 | it.effect("stops provider-initiated work that has no T3 turn owner", () => |
| 465 | Effect.gen(function* () { | 1282 | Effect.gen(function* () { |
| 466 | const fake = yield* makeFakePi; | 1283 | const fake = yield* makeFakePi; |