From bbc8a1277dee470c9044940f361d1bef0a0eb96a Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 26 Aug 2026 21:14:21 +0200 Subject: [PATCH 1/4] fix(github): close probe PRs without local gh reads Session-Id: 01a03f5e-1101-7122-b7d0-e104ef9468c7 --- src/github/probe-closer.test.ts | 211 +++++++++++++------------------ src/github/probe-closer.ts | 85 ++++++++----- src/orchestrator/factory.test.ts | 34 ++--- src/orchestrator/factory.ts | 17 ++- src/ports/mount.ts | 1 + 5 files changed, 167 insertions(+), 181 deletions(-) diff --git a/src/github/probe-closer.test.ts b/src/github/probe-closer.test.ts index 476c0e04..f383101c 100644 --- a/src/github/probe-closer.test.ts +++ b/src/github/probe-closer.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from 'vitest' import { closeProbePr } from './probe-closer' -import type { GhRunner } from './merge-gate' -import type { GithubConnectionWrite } from '../ports' +import type { GithubConnectionWrite, MountClient } from '../ports' const openProbe = { state: 'OPEN', @@ -16,84 +15,79 @@ const githubWrite = (closes: Array<{ repo: string; number: number }> = []): Gith closePullRequest: async (input) => { closes.push(input) }, }) +const prPath = '/github/repos/AgentWorkforce__pear/pulls/by-id/123.json' + +const prMount = ( + content: unknown, + reads: string[] = [], +): Pick => ({ + readFile: async (path) => { + reads.push(path) + if (path !== prPath) throw new Error(`unexpected mounted PR path ${path}`) + return { content } + }, +}) + describe('closeProbePr', () => { - it('guards, closes, and confirms CLOSED via read-back', async () => { - const calls: string[][] = [] + it('guards from the mounted PR and closes through the confirmed App write path', async () => { + const reads: string[] = [] const closes: Array<{ repo: string; number: number }> = [] - const runner: GhRunner = async (args) => { - calls.push(args) - if (args[0] === 'pr' && args[1] === 'view') { - return { stdout: JSON.stringify(calls.length === 1 ? openProbe : { ...openProbe, state: 'CLOSED' }) } - } - throw new Error(`unexpected gh args ${args.join(' ')}`) - } await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 123, expectedIssueKey: 'AR-42', githubWrite: githubWrite(closes), - runner, + path: prPath, + mount: prMount({ payload: openProbe }, reads), })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 123, state: 'CLOSED' }) - expect(calls.map((args) => args.slice(0, 3))).toEqual([ - ['pr', 'view', '123'], - ['pr', 'view', '123'], - ]) + expect(reads).toEqual([prPath]) expect(closes).toEqual([{ repo: 'AgentWorkforce/pear', number: 123 }]) }) it('refuses a non-probe PR before closing', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - return { - stdout: JSON.stringify({ - state: 'OPEN', - headRefName: 'feature/real-fix', - title: 'Fix a real production issue', - body: 'No synthetic marker here; mentions AR-42 only as context.', - }), - } - } + const reads: string[] = [] + const mount = prMount({ payload: { + state: 'OPEN', + headRefName: 'feature/real-fix', + title: 'Fix a real production issue', + body: 'No synthetic marker here; mentions AR-42 only as context.', + } }, reads) await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 124, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - runner, + path: prPath, + mount, })).rejects.toThrow(/missing \[factory-e2e\] probe marker/) - expect(calls).toHaveLength(1) - expect(calls[0]?.slice(0, 3)).toEqual(['pr', 'view', '124']) + expect(reads).toEqual([prPath]) }) it('requires the factory-e2e marker as a title prefix, not only body or branch text', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - return { - stdout: JSON.stringify({ - state: 'OPEN', - headRefName: 'factory-e2e/ar-42-probe', - title: 'AR-42 probe without title marker', - body: '[factory-e2e] Closes AR-42', - }), - } - } + const reads: string[] = [] + const mount = prMount({ payload: { + state: 'OPEN', + headRefName: 'factory-e2e/ar-42-probe', + title: 'AR-42 probe without title marker', + body: '[factory-e2e] Closes AR-42', + } }, reads) await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 128, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - runner, + path: prPath, + mount, })).rejects.toThrow(/missing \[factory-e2e\] probe marker/) - expect(calls).toHaveLength(1) + expect(reads).toEqual([prPath]) }) it('allows issue-gated callers to close markerless branch-convention PRs', async () => { - const calls: string[][] = [] + const reads: string[] = [] const closes: Array<{ repo: string; number: number }> = [] const markerlessProbe = { state: 'OPEN', @@ -101,13 +95,6 @@ describe('closeProbePr', () => { title: 'Add isPositive util', body: '', } - const runner: GhRunner = async (args) => { - calls.push(args) - if (args[0] === 'pr' && args[1] === 'view') { - return { stdout: JSON.stringify(calls.length === 1 ? markerlessProbe : { ...markerlessProbe, state: 'CLOSED' }) } - } - throw new Error(`unexpected gh args ${args.join(' ')}`) - } await expect(closeProbePr({ repo: 'AgentWorkforce/pear', @@ -115,28 +102,21 @@ describe('closeProbePr', () => { expectedIssueKey: 'AR-229', requireTitleMarker: false, githubWrite: githubWrite(closes), - runner, + path: prPath, + mount: prMount(markerlessProbe, reads), })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 279, state: 'CLOSED' }) - expect(calls.map((args) => args.slice(0, 3))).toEqual([ - ['pr', 'view', '279'], - ['pr', 'view', '279'], - ]) + expect(reads).toEqual([prPath]) expect(closes).toEqual([{ repo: 'AgentWorkforce/pear', number: 279 }]) }) it('treats an already-closed probe PR as idempotent success', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - return { - stdout: JSON.stringify({ - state: 'CLOSED', - headRefName: 'ar-229-is-positive', - title: 'Add isPositive util', - body: '', - }), - } - } + const reads: string[] = [] + const mount = prMount({ payload: { + state: 'CLOSED', + headRefName: 'ar-229-is-positive', + title: 'Add isPositive util', + body: '', + } }, reads) await expect(closeProbePr({ repo: 'AgentWorkforce/pear', @@ -144,46 +124,34 @@ describe('closeProbePr', () => { expectedIssueKey: 'AR-229', requireTitleMarker: false, githubWrite: githubWrite(), - runner, + path: prPath, + mount, })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 279, state: 'CLOSED' }) - expect(calls.map((args) => args.slice(0, 3))).toEqual([ - ['pr', 'view', '279'], - ]) + expect(reads).toEqual([prPath]) }) it('refuses a probe that is not tied to the expected issue key before closing', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - return { - stdout: JSON.stringify({ - ...openProbe, - body: 'Closes AR-99', - headRefName: 'factory-e2e/ar-99-probe', - title: '[factory-e2e] AR-99 probe', - }), - } - } + const reads: string[] = [] + const mount = prMount({ payload: { + ...openProbe, + body: 'Closes AR-99', + headRefName: 'factory-e2e/ar-99-probe', + title: '[factory-e2e] AR-99 probe', + } }, reads) await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 125, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - runner, + path: prPath, + mount, })).rejects.toThrow(/missing issue key AR-42/) - expect(calls).toHaveLength(1) + expect(reads).toEqual([prPath]) }) it('fails closed when workspace close errors and does not claim success', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - if (args[0] === 'pr' && args[1] === 'view') { - return { stdout: JSON.stringify(openProbe) } - } - throw new Error(`unexpected gh args ${args.join(' ')}`) - } + const reads: string[] = [] const write = githubWrite() write.closePullRequest = async () => { throw new Error('workspace close failed') } @@ -192,49 +160,46 @@ describe('closeProbePr', () => { prNumber: 126, expectedIssueKey: 'AR-42', githubWrite: write, - runner, + path: prPath, + mount: prMount(openProbe, reads), })).rejects.toThrow(/workspace close failed/) - expect(calls.map((args) => args.slice(0, 3))).toEqual([ - ['pr', 'view', '126'], - ]) + expect(reads).toEqual([prPath]) }) - it('fails closed when read-back is not CLOSED', async () => { - const calls: string[][] = [] - const runner: GhRunner = async (args) => { - calls.push(args) - if (args[0] === 'pr' && args[1] === 'view') { - return { stdout: JSON.stringify(openProbe) } - } - return { stdout: '' } - } + it('does not require an unauthenticated read-back after the App close is confirmed', async () => { + const reads: string[] = [] await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 127, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - runner, - })).rejects.toThrow(/live state is OPEN/) - expect(calls.map((args) => args.slice(0, 3))).toEqual([ - ['pr', 'view', '127'], - ['pr', 'view', '127'], - ]) + path: prPath, + mount: prMount(openProbe, reads), + })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 127, state: 'CLOSED' }) + expect(reads).toEqual([prPath]) }) - it('reports a clear connection error without invoking gh when GitHub writes are unavailable', async () => { - let runnerCalled = false - const runner: GhRunner = async () => { - runnerCalled = true - throw new Error('gh must not be invoked') - } + it('reports a clear connection error without reading the mount when GitHub writes are unavailable', async () => { + const reads: string[] = [] await expect(closeProbePr({ repo: 'AgentWorkforce/pear', prNumber: 129, expectedIssueKey: 'AR-42', - runner, + path: prPath, + mount: prMount(openProbe, reads), })).rejects.toThrow('GitHub write path not available on this mount — connect GitHub to your workspace') - expect(runnerCalled).toBe(false) + expect(reads).toEqual([]) + }) + + it('fails loudly when the mounted PR read capability is unavailable', async () => { + await expect(closeProbePr({ + repo: 'AgentWorkforce/pear', + prNumber: 130, + expectedIssueKey: 'AR-42', + githubWrite: githubWrite(), + path: prPath, + })).rejects.toThrow(/mounted GitHub PR read path is unavailable/i) }) }) diff --git a/src/github/probe-closer.ts b/src/github/probe-closer.ts index 30b0e231..11bb1d46 100644 --- a/src/github/probe-closer.ts +++ b/src/github/probe-closer.ts @@ -1,6 +1,6 @@ import { containsIssueKey } from '../issue-key-match' -import type { GithubConnectionWrite } from '../ports' -import { defaultGhRunner, type GhRunner } from './merge-gate' +import type { GithubConnectionWrite, MountClient } from '../ports' +import { wrappedPayload } from '../writeback/shared' const FACTORY_E2E_MARKER = '[factory-e2e]' @@ -10,7 +10,9 @@ export interface CloseProbePrInput { expectedIssueKey: string requireTitleMarker?: boolean githubWrite?: GithubConnectionWrite - runner?: GhRunner + /** Exact mounted PR metadata path returned by discovery. */ + path?: string + mount?: Pick } export interface CloseProbePrResult { @@ -24,40 +26,50 @@ export async function closeProbePr(input: CloseProbePrInput): Promise> => { - // TODO(issue-52): replace this transitional gh read with the mounted PR meta - // once every supported adapter shape exposes the probe guard fields. - const result = await run([ - 'pr', - 'view', - String(input.prNumber), - '--repo', - input.repo, - '--json', - 'state,headRefName,body,title', - ]) - if (!result.stdout.trim()) { - throw new Error(`Unable to guard probe PR #${input.prNumber}: gh returned empty output`) +const viewPr = async ( + mount: Pick, + input: CloseProbePrInput, +): Promise> => { + const path = input.path ?? pullByIdPath(input.repo, input.prNumber) + let content: unknown + try { + content = (await mount.readFile(path)).content + } catch (error) { + throw new Error( + `Unable to guard probe PR ${input.repo}#${input.prNumber} from mounted metadata at ${path}: ` + + `${error instanceof Error ? error.message : String(error)}`, + ) + } + const payload = wrappedPayload(content) + const explicitNumber = numberValue(payload.number) + if (explicitNumber !== undefined && explicitNumber !== input.prNumber) { + throw new Error( + `Unable to guard probe PR ${input.repo}#${input.prNumber}: mounted record at ${path} identifies PR #${explicitNumber}`, + ) + } + const head = recordValue(payload.head) + return { + state: stringValue(payload.state), + headRefName: stringValue(payload.headRefName) ?? stringValue(payload.head_ref) ?? stringValue(head.ref), + body: stringValue(payload.body), + title: stringValue(payload.title), } - return parseGhJson(result.stdout) } const assertClosableProbe = (live: Record, input: CloseProbePrInput): 'OPEN' | 'CLOSED' => { @@ -87,9 +99,18 @@ const normalizeState = (state?: string): string | undefined => state?.toUpperCas const stringValue = (value: unknown): string | undefined => typeof value === 'string' ? value : undefined -const parseGhJson = (stdout: string): Record => { - const parsed = JSON.parse(stdout) as unknown - return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) - ? parsed as Record - : {} +const pullByIdPath = (repo: string, number: number): string => { + const [owner, name, ...extra] = repo.split('/') + if (!owner || !name || extra.length > 0 || !Number.isSafeInteger(number) || number <= 0) { + throw new Error(`Invalid GitHub pull request identity ${repo}#${number}`) + } + return `/github/repos/${encodeURIComponent(owner)}__${encodeURIComponent(name)}/pulls/by-id/${number}.json` } + +const numberValue = (value: unknown): number | undefined => + typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined + +const recordValue = (value: unknown): Record => + value !== null && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : {} diff --git a/src/orchestrator/factory.test.ts b/src/orchestrator/factory.test.ts index 813b133f..abdf1e1a 100644 --- a/src/orchestrator/factory.test.ts +++ b/src/orchestrator/factory.test.ts @@ -9,7 +9,6 @@ import { AppGithubWriteback, FactoryConfigSchema, checkFactoryLoopLiveness, - closeProbePr, createFactory, createRelayflowPolicyRegistry, isDispatchableIssue, @@ -21837,46 +21836,35 @@ describe('FactoryLoop', () => { }) it('treats already-closed mount-resolved probe PRs as completed instead of re-wedging', async () => { + const prPath = '/github/repos/AgentWorkforce__pear/pulls/by-id/860.json' + let closeCalls = 0 const mount = new FakeMountClient({ [issuePath(360)]: issueFile(360), - '/github/repos/AgentWorkforce__pear/pulls/by-id/860.json': prFile(860, { + [prPath]: prFile(860, { title: 'Add already closed probe work', body: '', head_ref: 'ar-360-closed-work', state: 'CLOSED', }), + }, { + publishPullRequest: async () => { throw new Error('unexpected publish') }, + closePullRequest: async () => { + closeCalls += 1 + throw new Error('already-closed PR must not be closed again') + }, }) const fleet = new FakeFleetClient() - const closeViewCalls: string[][] = [] const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage(), - probeCloser: (input) => closeProbePr({ - ...input, - githubWrite: { - publishPullRequest: async () => { throw new Error('unexpected publish') }, - closePullRequest: async () => { throw new Error('already-closed PR must not be closed again') }, - }, - runner: async (args) => { - closeViewCalls.push(args) - return { - stdout: JSON.stringify({ - state: 'CLOSED', - title: 'Add already closed probe work', - body: '', - headRefName: 'ar-360-closed-work', - }), - } - }, - }), }) await factory.dispatch(await factory.triageIssue(parseLinearIssue(issuePath(360), issueFile(360)))) await factory.runLoop({ maxIterations: 1 }) - expect(closeViewCalls).toHaveLength(1) - expect(closeViewCalls[0]).toContain('view') + expect(mount.reads.filter((path) => path === prPath)).toHaveLength(2) + expect(closeCalls).toBe(0) expect(fleet.releases.map((release) => release.reason)).toEqual(['issue-done', 'issue-done']) expect(factory.status().inFlight).toEqual([]) expect(factory.status().counters.done).toBe(1) diff --git a/src/orchestrator/factory.ts b/src/orchestrator/factory.ts index 06498764..3e45e789 100644 --- a/src/orchestrator/factory.ts +++ b/src/orchestrator/factory.ts @@ -783,6 +783,7 @@ export class FactoryLoop implements Factory { readonly #mergeGate: GithubMergeGatePort readonly #verificationGate?: VerificationGate readonly #probeCloser: ProbeCloser + readonly #defaultProbeCloser: boolean readonly #probePrResolver: ProbePrResolver readonly #customProbePrResolver: boolean readonly #hasProbePrGhRunner: boolean @@ -1255,6 +1256,7 @@ export class FactoryLoop implements Factory { maxTeardownTimeoutMs: config.verification.maxTeardownTimeoutMs, }) : undefined) + this.#defaultProbeCloser = !ports.probeCloser this.#probeCloser = ports.probeCloser ?? closeProbePr this.#customProbePrResolver = Boolean(ports.probePrResolver) this.#hasProbePrGhRunner = Boolean(ports.probePrGhRunner) @@ -19669,13 +19671,22 @@ export class FactoryLoop implements Factory { return } - await this.#probeCloser({ + const closeInput = { repo: probe.repo, prNumber: probe.prNumber, expectedIssueKey: issue.key, requireTitleMarker: false, - ...(this.#mount.githubWrite ? { githubWrite: this.#mount.githubWrite } : {}), - }) + } + if (this.#defaultProbeCloser) { + await closeProbePr({ + ...closeInput, + ...(probe.path ? { path: probe.path } : {}), + mount: this.#mount, + ...(this.#mount.githubWrite ? { githubWrite: this.#mount.githubWrite } : {}), + }) + } else { + await this.#probeCloser(closeInput) + } this.#increment('mergeGateSyntheticClosed') } diff --git a/src/ports/mount.ts b/src/ports/mount.ts index 2028d821..34505b74 100644 --- a/src/ports/mount.ts +++ b/src/ports/mount.ts @@ -140,6 +140,7 @@ export interface GithubConnectionWrite { /** Authenticated issue read through the same connected GitHub App, when supported. */ getIssue?(repo: string, number: number): Promise publishPullRequest(input: GithubPublishPullRequestInput): Promise + /** Resolves only after the provider-backed close operation is acknowledged. */ closePullRequest(input: { repo: string; number: number }): Promise /** App-authored issue comment through the workspace GitHub connection. */ postIssueComment?(input: { From ff51e00ab0c4da1b44e55a7ced1095902f143830 Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 26 Aug 2026 21:45:46 +0200 Subject: [PATCH 2/4] test(github): bind probe fixtures to requested PR Session-Id: 01a03f5e-1101-7122-b7d0-e104ef9468c7 --- src/github/probe-closer.test.ts | 56 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/github/probe-closer.test.ts b/src/github/probe-closer.test.ts index f383101c..656f09c2 100644 --- a/src/github/probe-closer.test.ts +++ b/src/github/probe-closer.test.ts @@ -15,15 +15,17 @@ const githubWrite = (closes: Array<{ repo: string; number: number }> = []): Gith closePullRequest: async (input) => { closes.push(input) }, }) -const prPath = '/github/repos/AgentWorkforce__pear/pulls/by-id/123.json' +const prPath = (number: number): string => + `/github/repos/AgentWorkforce__pear/pulls/by-id/${number}.json` const prMount = ( + prNumber: number, content: unknown, reads: string[] = [], ): Pick => ({ readFile: async (path) => { reads.push(path) - if (path !== prPath) throw new Error(`unexpected mounted PR path ${path}`) + if (path !== prPath(prNumber)) throw new Error(`unexpected mounted PR path ${path}`) return { content } }, }) @@ -38,17 +40,17 @@ describe('closeProbePr', () => { prNumber: 123, expectedIssueKey: 'AR-42', githubWrite: githubWrite(closes), - path: prPath, - mount: prMount({ payload: openProbe }, reads), + mount: prMount(123, { payload: { number: 123, ...openProbe } }, reads), })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 123, state: 'CLOSED' }) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(123)]) expect(closes).toEqual([{ repo: 'AgentWorkforce/pear', number: 123 }]) }) it('refuses a non-probe PR before closing', async () => { const reads: string[] = [] - const mount = prMount({ payload: { + const mount = prMount(124, { payload: { + number: 124, state: 'OPEN', headRefName: 'feature/real-fix', title: 'Fix a real production issue', @@ -60,15 +62,15 @@ describe('closeProbePr', () => { prNumber: 124, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - path: prPath, mount, })).rejects.toThrow(/missing \[factory-e2e\] probe marker/) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(124)]) }) it('requires the factory-e2e marker as a title prefix, not only body or branch text', async () => { const reads: string[] = [] - const mount = prMount({ payload: { + const mount = prMount(128, { payload: { + number: 128, state: 'OPEN', headRefName: 'factory-e2e/ar-42-probe', title: 'AR-42 probe without title marker', @@ -80,10 +82,9 @@ describe('closeProbePr', () => { prNumber: 128, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - path: prPath, mount, })).rejects.toThrow(/missing \[factory-e2e\] probe marker/) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(128)]) }) it('allows issue-gated callers to close markerless branch-convention PRs', async () => { @@ -102,16 +103,16 @@ describe('closeProbePr', () => { expectedIssueKey: 'AR-229', requireTitleMarker: false, githubWrite: githubWrite(closes), - path: prPath, - mount: prMount(markerlessProbe, reads), + mount: prMount(279, { number: 279, ...markerlessProbe }, reads), })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 279, state: 'CLOSED' }) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(279)]) expect(closes).toEqual([{ repo: 'AgentWorkforce/pear', number: 279 }]) }) it('treats an already-closed probe PR as idempotent success', async () => { const reads: string[] = [] - const mount = prMount({ payload: { + const mount = prMount(279, { payload: { + number: 279, state: 'CLOSED', headRefName: 'ar-229-is-positive', title: 'Add isPositive util', @@ -124,15 +125,15 @@ describe('closeProbePr', () => { expectedIssueKey: 'AR-229', requireTitleMarker: false, githubWrite: githubWrite(), - path: prPath, mount, })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 279, state: 'CLOSED' }) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(279)]) }) it('refuses a probe that is not tied to the expected issue key before closing', async () => { const reads: string[] = [] - const mount = prMount({ payload: { + const mount = prMount(125, { payload: { + number: 125, ...openProbe, body: 'Closes AR-99', headRefName: 'factory-e2e/ar-99-probe', @@ -144,10 +145,9 @@ describe('closeProbePr', () => { prNumber: 125, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - path: prPath, mount, })).rejects.toThrow(/missing issue key AR-42/) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(125)]) }) it('fails closed when workspace close errors and does not claim success', async () => { @@ -160,10 +160,9 @@ describe('closeProbePr', () => { prNumber: 126, expectedIssueKey: 'AR-42', githubWrite: write, - path: prPath, - mount: prMount(openProbe, reads), + mount: prMount(126, { number: 126, ...openProbe }, reads), })).rejects.toThrow(/workspace close failed/) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(126)]) }) it('does not require an unauthenticated read-back after the App close is confirmed', async () => { @@ -174,10 +173,9 @@ describe('closeProbePr', () => { prNumber: 127, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - path: prPath, - mount: prMount(openProbe, reads), + mount: prMount(127, { number: 127, ...openProbe }, reads), })).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 127, state: 'CLOSED' }) - expect(reads).toEqual([prPath]) + expect(reads).toEqual([prPath(127)]) }) it('reports a clear connection error without reading the mount when GitHub writes are unavailable', async () => { @@ -187,8 +185,8 @@ describe('closeProbePr', () => { repo: 'AgentWorkforce/pear', prNumber: 129, expectedIssueKey: 'AR-42', - path: prPath, - mount: prMount(openProbe, reads), + path: prPath(129), + mount: prMount(129, { number: 129, ...openProbe }, reads), })).rejects.toThrow('GitHub write path not available on this mount — connect GitHub to your workspace') expect(reads).toEqual([]) }) @@ -199,7 +197,7 @@ describe('closeProbePr', () => { prNumber: 130, expectedIssueKey: 'AR-42', githubWrite: githubWrite(), - path: prPath, + path: prPath(130), })).rejects.toThrow(/mounted GitHub PR read path is unavailable/i) }) }) From a4fb478574b2c928fb8ff73849c2d33ff3276a99 Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 26 Aug 2026 21:51:45 +0200 Subject: [PATCH 3/4] fix(cli): pass mounted PR reader to close-probe Session-Id: 01a03f5e-1101-7122-b7d0-e104ef9468c7 --- src/cli/fleet.test.ts | 21 +++++++++++---------- src/cli/fleet.ts | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cli/fleet.test.ts b/src/cli/fleet.test.ts index 3aaf6e17..25d600ac 100644 --- a/src/cli/fleet.test.ts +++ b/src/cli/fleet.test.ts @@ -4637,7 +4637,17 @@ describe('fleet CLI runtime', () => { const closes: Array<{ repo: string; number: number }> = [] const integrations = fakeIntegrationConnections(async () => ({ ready: true, state: 'ready' })) const cloudMountFromConfig = vi.fn(async (opts) => { - const mount = mountWithIntegrationConnections({}, integrations) + const mount = mountWithIntegrationConnections({ + '/github/repos/AgentWorkforce__pear/pulls/by-id/42.json': { + payload: { + number: 42, + state: 'OPEN', + headRefName: 'factory-e2e/ar-77-probe', + title: '[factory-e2e] AR-77 probe', + body: 'Closes AR-77', + }, + }, + }, integrations) mount.githubWrite = { publishPullRequest: async () => { throw new Error('unexpected publish') }, closePullRequest: async (input) => { @@ -4669,7 +4679,6 @@ describe('fleet CLI runtime', () => { } return mount }) - let readCount = 0 const code = await runFleetCli([ 'close-probe', @@ -4683,14 +4692,6 @@ describe('fleet CLI runtime', () => { stderr: errors, resolveWorkspace: async () => ({ workspaceId: 'rw_test' }), cloudMountFromConfig, - probePrGhRunner: async () => ({ - stdout: JSON.stringify({ - state: readCount++ === 0 ? 'OPEN' : 'CLOSED', - headRefName: 'factory-e2e/ar-77-probe', - title: '[factory-e2e] AR-77 probe', - body: 'Closes AR-77', - }), - }), }) expect(code, errors.text()).toBe(0) diff --git a/src/cli/fleet.ts b/src/cli/fleet.ts index 3e4e65c8..e7f53588 100644 --- a/src/cli/fleet.ts +++ b/src/cli/fleet.ts @@ -407,6 +407,7 @@ export async function runFleetCli(argv: string[], deps: FleetCliDeps = {}): Prom prNumber: command.prNumber, expectedIssueKey: command.issue, ...(githubWrite ? { githubWrite } : {}), + ...(mount ? { mount } : {}), ...(deps.probePrGhRunner ? { runner: deps.probePrGhRunner } : {}), }) writeJson(out, result) From 3ae01db0aecd45eb35a9471562b0a7db22038e6e Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 26 Aug 2026 22:04:25 +0200 Subject: [PATCH 4/4] test(github): reject mismatched mounted probe PR Session-Id: 01a03f5e-1101-7122-b7d0-e104ef9468c7 --- src/github/probe-closer.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/github/probe-closer.test.ts b/src/github/probe-closer.test.ts index 656f09c2..6e84e449 100644 --- a/src/github/probe-closer.test.ts +++ b/src/github/probe-closer.test.ts @@ -47,6 +47,22 @@ describe('closeProbePr', () => { expect(closes).toEqual([{ repo: 'AgentWorkforce/pear', number: 123 }]) }) + it('refuses a mounted record for a different PR before closing', async () => { + const reads: string[] = [] + const closes: Array<{ repo: string; number: number }> = [] + + await expect(closeProbePr({ + repo: 'AgentWorkforce/pear', + prNumber: 131, + expectedIssueKey: 'AR-42', + githubWrite: githubWrite(closes), + mount: prMount(131, { payload: { number: 999, ...openProbe } }, reads), + })).rejects.toThrow(/mounted record.*identifies PR #999/i) + + expect(reads).toEqual([prPath(131)]) + expect(closes).toEqual([]) + }) + it('refuses a non-probe PR before closing', async () => { const reads: string[] = [] const mount = prMount(124, { payload: {