From b0afe4b7ff7c394be3667e8ff0e53b3869b0b864 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Thu, 27 Aug 2026 14:41:43 -0700 Subject: [PATCH 1/4] [SDK/Factories] Add Factory Pagination And Completion Options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 +++++ nodejs/docs/factories.md | 29 +++++++++++++++++--- nodejs/src/extension.ts | 2 ++ nodejs/src/factory.ts | 38 ++++++++++++++++++++++++++ nodejs/src/index.ts | 2 ++ nodejs/src/session.ts | 12 ++++++++- nodejs/test/factory.test.ts | 54 +++++++++++++++++++++++++++++++++---- 7 files changed, 133 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d695ed6fb6..345308849c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] +### Feature: Node Agent Factories pagination and run notifications + +The experimental Node.js Agent Factories convenience API now supports paginated run history. Existing `session.factory.listRuns()` calls still return the runs array, while calls with `afterSeq`, `beforeSeq`, or `limit` return the full page with cursor and truncation metadata. + +Factory `run` and `resume` options now accept `notifyOnComplete` and `logPhaseNames`. The SDK forwards these options to the Copilot CLI for new and resumed runs. + ### Feature: rotating session-scoped GitHub credentials All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback. diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md index 903a3de8b1..99dedad3dc 100644 --- a/nodejs/docs/factories.md +++ b/nodejs/docs/factories.md @@ -139,6 +139,8 @@ Run by registered name or handle: const run = await session.factory.run("review-changed", { args: { files: ["src/a.ts"] }, limits: { maxAiCredits: 3 }, + notifyOnComplete: true, + logPhaseNames: true, }); if (run.status === "completed") { @@ -153,7 +155,12 @@ The name overload is: ```ts session.factory.run( name: string, - options?: { args?: JsonValue; limits?: FactoryLimits }, + options?: { + args?: JsonValue; + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` @@ -162,6 +169,8 @@ Resume by run ID without resending the name or arguments: ```ts const run = await session.factory.resume(runId, { limits: { maxAiCredits: 6 }, + notifyOnComplete: true, + logPhaseNames: true, }); ``` @@ -170,10 +179,16 @@ The signature is: ```ts session.factory.resume( runId: string, - options?: { limits?: FactoryLimits }, + options?: { + limits?: FactoryLimits; + notifyOnComplete?: boolean; + logPhaseNames?: boolean; + }, ): Promise; ``` +Set `notifyOnComplete` to notify the originating session when the factory completes. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. + Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`. An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice. @@ -219,8 +234,13 @@ The calling session can inspect its own factory runs: ```ts const runs = await session.factory.listRuns(); +const runsPage = await session.factory.listRuns({ + afterSeq, + beforeSeq, + limit, +}); const detail = await session.factory.getRunDetail(runId); -const page = await session.factory.getRunProgress(runId, { +const progressPage = await session.factory.getRunProgress(runId, { phaseId, afterSeq, beforeSeq, @@ -228,7 +248,8 @@ const page = await session.factory.getRunProgress(runId, { }); ``` -- `listRuns()` returns the newest default page of this session's durable factory runs. +- `listRuns()` returns only the runs array from the newest default page of this session's durable factory runs. This overload preserves the original convenience API. +- `listRuns({ afterSeq, beforeSeq, limit })` returns the full page. Its `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` fields let callers continue paging without raw RPC calls. - `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page. - `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail. diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index d756308734..ac0ccdb7ce 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -85,6 +85,8 @@ export { type FactoryRunResult, type FactoryRunStatus, type FactoryRunSummary, + type FactoryListRunsOptions, + type FactoryRunsPage, type FactoryRunDetail, type FactoryProgressPage, type FactoryProgressLine, diff --git a/nodejs/src/factory.ts b/nodejs/src/factory.ts index 8a6c787471..6212f462b4 100644 --- a/nodejs/src/factory.ts +++ b/nodejs/src/factory.ts @@ -4,6 +4,8 @@ import type { FactoryGetRunProgressRequest, + FactoryListRunsRequest, + FactoryListRunsResult, FactoryProgressPage, FactoryRunDetail, FactoryRunResult, @@ -26,6 +28,22 @@ export type { FactoryRunSummary, } from "./generated/rpc.js"; +/** + * Options for paging durable factory runs. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryListRunsOptions = FactoryListRunsRequest; + +/** + * A page of durable factory runs and its paging metadata. + * + * @experimental Part of the experimental Agent Factories surface and may + * change or be removed in future SDK or CLI releases. + */ +export type FactoryRunsPage = FactoryListRunsResult; + /** * Run statuses a factory run can no longer move away from. * @@ -242,6 +260,10 @@ export interface RunOptions { args?: TArgs; /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; /** * Prior run whose persisted identity, arguments, journal, and accounting should be resumed. * @@ -259,6 +281,10 @@ export interface RunOptions { export interface ResumeOptions { /** Optional per-invocation resource ceiling overrides. */ limits?: FactoryLimits; + /** Whether to notify the originating session when the factory completes. */ + notifyOnComplete?: boolean; + /** Whether to emit factory phase names to the session transcript. */ + logPhaseNames?: boolean; } /** @@ -328,8 +354,20 @@ export interface SessionFactoryApi { waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise; /** * List the newest default page of this session's durable factory runs. + * + * This backwards-compatible overload returns only the runs array. Pass + * paging options to receive the full page, including its cursors and + * truncation metadata. */ listRuns(): Promise; + /** + * Page this session's durable factory runs. + * + * `afterSeq` and `beforeSeq` are exclusive cursors. The result includes + * `oldestSeq`, `newestSeq`, `hasMoreNewer`, and `omittedOlder` so callers + * can continue paging without using the raw RPC client. + */ + listRuns(options: FactoryListRunsOptions): Promise; /** Read durable phases, direct agents, and the latest progress tail for a run. */ getRunDetail(runId: string): Promise; /** Page durable progress forward, backward, or from the latest tail. */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 9d55ab1d10..1f4359ff8c 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -209,6 +209,8 @@ export type { FactoryRunResult, FactoryRunStatus, FactoryRunSummary, + FactoryListRunsOptions, + FactoryRunsPage, FactoryRunDetail, FactoryProgressPage, FactoryProgressLine, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 65ff00921c..eb4c6b7561 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -71,6 +71,7 @@ import { FactoryResumeError, isFactoryRunTerminal, type FactoryResumeErrorCode, + type FactoryListRunsOptions, type FactoryRunResult, type FactoryAgentOptions, type RunOptions, @@ -462,6 +463,8 @@ export class CopilotSession { if (options?.resumeFromRunId !== undefined) { return this.factory.resume(options.resumeFromRunId, { limits: options.limits, + notifyOnComplete: options.notifyOnComplete, + logPhaseNames: options.logPhaseNames, }); } const envelope = await this.rpc.factory.run({ @@ -469,6 +472,8 @@ export class CopilotSession { args: options?.args === undefined ? {} : options.args, options: { limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }, }); @@ -481,6 +486,8 @@ export class CopilotSession { response = await this.rpc.factory.resume({ runId, limits: options?.limits, + notifyOnComplete: options?.notifyOnComplete, + logPhaseNames: options?.logPhaseNames, }); } catch (error) { if ( @@ -499,7 +506,10 @@ export class CopilotSession { }) as SessionFactoryApi["resume"], getRun: async (runId) => this.rpc.factory.getRun({ runId }), waitForRun: (runId, options) => this.waitForFactoryRun(runId, options?.signal), - listRuns: async () => (await this.rpc.factory.listRuns({})).runs, + listRuns: (async (options?: FactoryListRunsOptions) => { + const page = await this.rpc.factory.listRuns(options ?? {}); + return options === undefined ? page.runs : page; + }) as SessionFactoryApi["listRuns"], getRunDetail: (runId) => this.rpc.factory.getRunDetail({ runId }), getRunProgress: (runId, options = {}) => this.rpc.factory.getRunProgress({ runId, ...options }), diff --git a/nodejs/test/factory.test.ts b/nodejs/test/factory.test.ts index dcf434616a..c9b8f65074 100644 --- a/nodejs/test/factory.test.ts +++ b/nodejs/test/factory.test.ts @@ -435,6 +435,7 @@ describe("factories", () => { const guide = readFileSync(new URL("../docs/factories.md", import.meta.url), "utf8"); const publicApi = readFileSync(new URL("../src/factory.ts", import.meta.url), "utf8"); const listRunsPagingWording = "newest default page of this session's durable factory runs"; + const listRunsMetadata = ["oldestSeq", "newestSeq", "hasMoreNewer", "omittedOlder"]; const resumeCodes = [ "not_found", "non_resumable", @@ -458,6 +459,9 @@ describe("factories", () => { for (const document of [normalizedGuide, normalizedPublicApi]) { expect(document).toContain(listRunsPagingWording); + for (const field of listRunsMetadata) { + expect(document).toContain(field); + } } expect(normalizedGuide).toContain( @@ -1500,14 +1504,31 @@ describe("factories", () => { revision: 4, }; const detail = { ...summary, phases: [], agents: [], progress }; + const runsPage = { + runs: [summary], + oldestSeq: 11, + newestSeq: 12, + hasMoreNewer: true, + omittedOlder: 10, + }; const sendRequest = vi.fn(async (method: string) => { - if (method === "session.factory.listRuns") return { runs: [summary] }; + if (method === "session.factory.listRuns") return runsPage; if (method === "session.factory.getRunDetail") return detail; return progress; }); const session = new CopilotSession("session-observe", { sendRequest } as never); await expect(session.factory.listRuns()).resolves.toEqual([summary]); + const listedPage = await session.factory.listRuns({ + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(listedPage).toEqual(runsPage); + expect(listedPage.oldestSeq).toBe(11); + expect(listedPage.newestSeq).toBe(12); + expect(listedPage.hasMoreNewer).toBe(true); + expect(listedPage.omittedOlder).toBe(10); await expect(session.factory.getRunDetail("run-observe")).resolves.toEqual(detail); await expect( session.factory.getRunProgress("run-observe", { @@ -1519,11 +1540,17 @@ describe("factories", () => { expect(sendRequest).toHaveBeenNthCalledWith(1, "session.factory.listRuns", { sessionId: session.sessionId, }); - expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.getRunDetail", { + expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.listRuns", { + sessionId: session.sessionId, + afterSeq: 10, + beforeSeq: 20, + limit: 50, + }); + expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunDetail", { sessionId: session.sessionId, runId: "run-observe", }); - expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.getRunProgress", { + expect(sendRequest).toHaveBeenNthCalledWith(4, "session.factory.getRunProgress", { sessionId: session.sessionId, runId: "run-observe", phaseId: "p0", @@ -2132,6 +2159,8 @@ describe("factories", () => { await expect( session.factory.resume("run-prior", { limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }) ).resolves.toMatchObject({ status: "completed", @@ -2141,13 +2170,20 @@ describe("factories", () => { session.factory.run("by-name", { args: { value: 1 }, limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, resumeFromRunId: "run-prior", }) ).resolves.toMatchObject({ status: "completed", result: { name: "stored-name", persistedArgs: true }, }); - await expect(session.factory.run(factory)).resolves.toMatchObject({ + await expect( + session.factory.run(factory, { + notifyOnComplete: true, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ status: "completed", result: { name: "friendly-run" }, }); @@ -2155,17 +2191,25 @@ describe("factories", () => { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: true, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(2, "session.factory.resume", { sessionId: session.sessionId, runId: "run-prior", limits: { maxTotalSubagents: 7 }, + notifyOnComplete: false, + logPhaseNames: true, }); expect(sendRequest).toHaveBeenNthCalledWith(3, "session.factory.run", { sessionId: session.sessionId, name: "friendly-run", args: {}, - options: { limits: undefined }, + options: { + limits: undefined, + notifyOnComplete: true, + logPhaseNames: false, + }, }); }); From d3541d0da9ab2bc1e5c3b9de3b36efff42137db8 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Thu, 27 Aug 2026 15:11:12 -0700 Subject: [PATCH 2/4] Add factory convenience E2E coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/test/e2e/factory.e2e.test.ts | 102 +++++++++++++++++- .../test/e2e/fixtures/factory-extension.mjs | 16 +++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index cddd8e47b0..269cf59aca 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -148,6 +148,101 @@ it.skipIf(isInProcessTransport)( } ); +it.skipIf(isInProcessTransport)( + "forwards factory runtime controls across the SDK process boundary", + async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const suppressed = await session.factory.run("phased", { + notifyOnComplete: false, + logPhaseNames: false, + }); + + expect(suppressed).toMatchObject({ + status: "completed", + result: "finished", + }); + const progress = await session.factory.getRunProgress(suppressed.runId); + expect(progress.records).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "phase", text: "Collect" }), + expect.objectContaining({ kind: "log", text: "Collected" }), + expect.objectContaining({ kind: "phase", text: "Summarize" }), + expect.objectContaining({ kind: "log", text: "Summarized" }), + ]) + ); + + const events = await session.getEvents(); + expect( + events.some( + (event) => + event.type === "system.notification" && + event.data.kind.type === "factory_completed" && + event.data.kind.runId === suppressed.runId + ) + ).toBe(false); + expect( + events.filter( + (event) => event.type === "session.info" && event.data.infoType === "factory_phase" + ) + ).toEqual([]); + } +); + +it.skipIf(isInProcessTransport)("pages factory runs and returns cursor metadata", async () => { + if (!factoryTestContext) { + throw new Error("Factory E2E requires the stdio transport"); + } + const { workDir } = factoryTestContext; + await using session = await setupFactoryExtension(workDir); + + const first = await session.factory.run("argument-echo", { + args: { ordinal: 1 }, + notifyOnComplete: false, + }); + const second = await session.factory.run("argument-echo", { + args: { ordinal: 2 }, + notifyOnComplete: false, + }); + const third = await session.factory.run("argument-echo", { + args: { ordinal: 3 }, + notifyOnComplete: false, + }); + + const newest = await session.factory.listRuns({ limit: 1 }); + expect(newest).toMatchObject({ + runs: [expect.objectContaining({ runId: third.runId })], + hasMoreNewer: false, + omittedOlder: 2, + }); + expect(newest.oldestSeq).toBe(newest.newestSeq); + expect(newest.oldestSeq).not.toBeNull(); + + const older = await session.factory.listRuns({ + beforeSeq: newest.oldestSeq!, + limit: 1, + }); + expect(older).toMatchObject({ + runs: [expect.objectContaining({ runId: second.runId })], + hasMoreNewer: true, + omittedOlder: 1, + }); + + const oldest = await session.factory.listRuns({ + beforeSeq: older.oldestSeq!, + limit: 1, + }); + expect(oldest).toMatchObject({ + runs: [expect.objectContaining({ runId: first.runId })], + hasMoreNewer: true, + omittedOlder: 0, + }); +}); + it.skipIf(isInProcessTransport)( "runs a factory when its session denies every permission request", async () => { @@ -180,7 +275,12 @@ it.skipIf(isInProcessTransport)( status: "error", }); - await expect(session.factory.resume(failedRun.runId)).resolves.toMatchObject({ + await expect( + session.factory.resume(failedRun.runId, { + notifyOnComplete: false, + logPhaseNames: false, + }) + ).resolves.toMatchObject({ status: "completed", result: "resumed", }); diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs index 45227a1bea..77ec308651 100644 --- a/nodejs/test/e2e/fixtures/factory-extension.mjs +++ b/nodejs/test/e2e/fixtures/factory-extension.mjs @@ -40,6 +40,21 @@ const arrayResult = defineFactory({ run: async () => [1, "two", false], }); +const phased = defineFactory({ + meta: { + name: "phased", + description: "Record named phases and ordinary progress.", + phases: [{ title: "Collect" }, { title: "Summarize" }], + }, + run: async ({ phase, log }) => { + phase("Collect"); + log("Collected"); + phase("Summarize"); + log("Summarized"); + return "finished"; + }, +}); + const forwardsSubagentOptions = defineFactory({ meta: { name: "forwards-subagent-options", @@ -141,6 +156,7 @@ session = await joinSession({ factories: [ argumentEcho, arrayResult, + phased, forwardsSubagentOptions, startsFromContextSession, startsFromModuleSession, From 1d92358a3b29b274c2a7502819435dce32e75052 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Thu, 27 Aug 2026 15:12:24 -0700 Subject: [PATCH 3/4] Clarify factory completion notification guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/docs/factories.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/docs/factories.md b/nodejs/docs/factories.md index 99dedad3dc..23b9d0fed3 100644 --- a/nodejs/docs/factories.md +++ b/nodejs/docs/factories.md @@ -187,7 +187,7 @@ session.factory.resume( ): Promise; ``` -Set `notifyOnComplete` to notify the originating session when the factory completes. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. +Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs. Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`. From 894a4f826bdc80542d0860abdcf5d138d44015b0 Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Thu, 27 Aug 2026 15:18:50 -0700 Subject: [PATCH 4/4] Isolate factory E2E completion notifications Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- nodejs/test/e2e/factory.e2e.test.ts | 26 +++++++++++++------ .../test/e2e/fixtures/factory-extension.mjs | 1 + 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index 269cf59aca..669a1f9c64 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -84,6 +84,7 @@ it.skipIf(isInProcessTransport)( const result = await session.factory.run("argument-echo", { args: { source: "sdk-e2e", count: 11 }, + notifyOnComplete: false, }); expect(result).toMatchObject({ @@ -140,7 +141,7 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const run = await session.factory.run("argument-echo"); + const run = await session.factory.run("argument-echo", { notifyOnComplete: false }); const error = await session.factory.resume(run.runId).catch((caught: unknown) => caught); expect(error).toBeInstanceOf(FactoryResumeError); @@ -253,7 +254,9 @@ it.skipIf(isInProcessTransport)( const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); await using session = await setupFactoryExtension(workDir, denyPermissions); - await expect(session.factory.run("argument-echo")).resolves.toMatchObject({ + await expect( + session.factory.run("argument-echo", { notifyOnComplete: false }) + ).resolves.toMatchObject({ status: "completed", }); expect(denyPermissions).not.toHaveBeenCalled(); @@ -270,7 +273,7 @@ it.skipIf(isInProcessTransport)( const denyPermissions = vi.fn(() => ({ kind: "reject" as const })); await using session = await setupFactoryExtension(workDir, denyPermissions); - const failedRun = await session.factory.run("fails-once"); + const failedRun = await session.factory.run("fails-once", { notifyOnComplete: false }); expect(failedRun).toMatchObject({ status: "error", }); @@ -297,7 +300,9 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("starts-from-context-session"); + const result = await session.factory.run("starts-from-context-session", { + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", @@ -316,7 +321,9 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("starts-from-module-session"); + const result = await session.factory.run("starts-from-module-session", { + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", @@ -336,7 +343,7 @@ it.skipIf(isInProcessTransport)( const extensionDir = join(workDir, ".github", "extensions", "factory-smoke"); await using session = await setupFactoryExtension(workDir); - const parked = session.factory.run("parked"); + const parked = session.factory.run("parked", { notifyOnComplete: false }); await retry( "wait for the parked factory to enter its body", async () => { @@ -382,7 +389,7 @@ it.skipIf(isInProcessTransport)( const { workDir } = factoryTestContext; await using session = await setupFactoryExtension(workDir); - const result = await session.factory.run("array-result"); + const result = await session.factory.run("array-result", { notifyOnComplete: false }); expect(result).toMatchObject({ status: "completed", @@ -401,7 +408,10 @@ it.skipIf(isInProcessTransport)( await using session = await setupFactoryExtension(workDir); const args = [1, "two", false]; - const result = await session.factory.run("argument-echo", { args }); + const result = await session.factory.run("argument-echo", { + args, + notifyOnComplete: false, + }); expect(result).toMatchObject({ status: "completed", diff --git a/nodejs/test/e2e/fixtures/factory-extension.mjs b/nodejs/test/e2e/fixtures/factory-extension.mjs index 77ec308651..fb344b4863 100644 --- a/nodejs/test/e2e/fixtures/factory-extension.mjs +++ b/nodejs/test/e2e/fixtures/factory-extension.mjs @@ -169,6 +169,7 @@ void waitForMarker("start-b", 30_000) .then(async () => { const result = await session.factory.run("argument-echo", { args: { source: "module-watcher" }, + notifyOnComplete: false, }); writeFileSync(marker("b-result"), JSON.stringify({ status: "success", result })); })