Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions src/cli/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -4669,7 +4679,6 @@ describe('fleet CLI runtime', () => {
}
return mount
})
let readCount = 0

const code = await runFleetCli([
'close-probe',
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/cli/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
225 changes: 102 additions & 123 deletions src/github/probe-closer.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -16,174 +15,159 @@ const githubWrite = (closes: Array<{ repo: string; number: number }> = []): Gith
closePullRequest: async (input) => { closes.push(input) },
})

const prPath = (number: number): string =>
`/github/repos/AgentWorkforce__pear/pulls/by-id/${number}.json`

const prMount = (
prNumber: number,
content: unknown,
reads: string[] = [],
): Pick<MountClient, 'readFile'> => ({
readFile: async (path) => {
reads.push(path)
if (path !== prPath(prNumber)) 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,
mount: prMount(123, { payload: { number: 123, ...openProbe } }, reads),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
})).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(123)])
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 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(124, { payload: {
number: 124,
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,
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(124)])
})

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(128, { payload: {
number: 128,
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,
mount,
})).rejects.toThrow(/missing \[factory-e2e\] probe marker/)
expect(calls).toHaveLength(1)
expect(reads).toEqual([prPath(128)])
})

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',
headRefName: 'ar-229-is-positive',
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',
prNumber: 279,
expectedIssueKey: 'AR-229',
requireTitleMarker: false,
githubWrite: githubWrite(closes),
runner,
mount: prMount(279, { number: 279, ...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(279)])
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(279, { payload: {
number: 279,
state: 'CLOSED',
headRefName: 'ar-229-is-positive',
title: 'Add isPositive util',
body: '',
} }, reads)

await expect(closeProbePr({
repo: 'AgentWorkforce/pear',
prNumber: 279,
expectedIssueKey: 'AR-229',
requireTitleMarker: false,
githubWrite: githubWrite(),
runner,
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(279)])
})

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(125, { payload: {
number: 125,
...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,
mount,
})).rejects.toThrow(/missing issue key AR-42/)
expect(calls).toHaveLength(1)
expect(reads).toEqual([prPath(125)])
})

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') }

Expand All @@ -192,49 +176,44 @@ describe('closeProbePr', () => {
prNumber: 126,
expectedIssueKey: 'AR-42',
githubWrite: write,
runner,
mount: prMount(126, { number: 126, ...openProbe }, reads),
})).rejects.toThrow(/workspace close failed/)
expect(calls.map((args) => args.slice(0, 3))).toEqual([
['pr', 'view', '126'],
])
expect(reads).toEqual([prPath(126)])
})

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'],
])
mount: prMount(127, { number: 127, ...openProbe }, reads),
})).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 127, state: 'CLOSED' })
expect(reads).toEqual([prPath(127)])
})

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(129),
mount: prMount(129, { number: 129, ...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(130),
})).rejects.toThrow(/mounted GitHub PR read path is unavailable/i)
})
})
Loading