Skip to content
Merged
25 changes: 23 additions & 2 deletions src/cli/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ describe('fleet CLI runtime', () => {

it('configures guarded GitHub writeback when close-probe creates a cloud mount', async () => {
const output = buffer()
const errors = buffer()
const closes: Array<{ repo: string; number: number }> = []
const cloudMountFromConfig = vi.fn(async (opts) => {
const mount = new FakeMountClient()
Expand All @@ -1503,6 +1504,26 @@ describe('fleet CLI runtime', () => {
const path = `/github/repos/${input.repo}/pulls/${input.number}/close.json`
const allowed = await opts?.isAllowedDraft?.(path, {}, { guarded: true })
if (!allowed) throw new Error('GitHub close draft rejected by mount predicate')
expect(await opts?.isAllowedDraft?.(
`/github/repos/${input.repo}/refs/factory.json`,
{ ref: 'refs/heads/factory/77', sha: 'abc123' },
{ guarded: true },
)).toBe(true)
expect(await opts?.isAllowedDraft?.(
`/github/repos/${input.repo}/refs/refs%2Fheads%2Ffactory%2F77.json`,
{ ref: 'refs/heads/factory/77', sha: 'abc123' },
{ guarded: true },
)).toBe(true)
expect(await opts?.isAllowedDraft?.(
`/github/repos/${input.repo}/refs/refs%2Fheads%2Fmain.json`,
{ ref: 'refs/heads/main', sha: 'abc123' },
{ guarded: true },
)).toBe(false)
expect(await opts?.isAllowedDraft?.(
`/github/repos/${input.repo}/refs/arbitrary.json`,
{ ref: 'refs/heads/factory/77', sha: 'abc123' },
{ guarded: true },
)).toBe(false)
closes.push(input)
},
}
Expand All @@ -1519,7 +1540,7 @@ describe('fleet CLI runtime', () => {
'AR-77',
], {
stdout: output,
stderr: buffer(),
stderr: errors,
resolveWorkspace: async () => ({ workspaceId: 'rw_test' }),
cloudMountFromConfig,
probePrGhRunner: async () => ({
Expand All @@ -1532,7 +1553,7 @@ describe('fleet CLI runtime', () => {
}),
})

expect(code).toBe(0)
expect(code, errors.text()).toBe(0)
expect(cloudMountFromConfig).toHaveBeenCalledWith(expect.objectContaining({
workspaceId: 'rw_test',
isAllowedDraft: expect.any(Function),
Expand Down
2 changes: 1 addition & 1 deletion src/cli/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,7 @@ async function isAllowedFactoryDraft(
}

const isFactoryGithubWritebackPath = (path: string): boolean =>
/^\/github\/repos\/[^/]+\/[^/]+\/(?:pull-requests\/factory-[^/]+\.json|refs\/refs%2Fheads%2F[^/]+\.json|pulls\/[1-9]\d*\/close\.json)$/iu.test(path)
/^\/github\/repos\/[^/]+\/[^/]+\/(?:pull-requests\/factory-[^/]+\.json|refs\/(?:factory\.json|refs%2Fheads%2Ffactory%2F[^/]+\.json)|pulls\/[1-9]\d*\/close\.json)$/iu.test(path)

const isAllowedFactoryGithubDraft = (
path: string,
Expand Down
7 changes: 7 additions & 0 deletions src/issue-key-match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ describe('issue key matching', () => {
expect(containsExplicitIssueReference('Closes AR-229', 'AR-229')).toBe(true)
expect(containsExplicitIssueReference('This merely mentions ar-229 in passing.', 'AR-229')).toBe(false)
})

it('does not treat numeric test counts as GitHub issue references', () => {
expect(containsExplicitIssueReference('tsc, eslint, and 52 tests all pass.', '52')).toBe(false)
expect(containsExplicitIssueReference('Fixes #52', '52')).toBe(true)
expect(containsExplicitIssueReference('Issue: 52', '52')).toBe(true)
expect(containsExplicitIssueReference('https://github.com/AgentWorkforce/hoopsheet/issues/52', '52')).toBe(true)
})
})
16 changes: 15 additions & 1 deletion src/issue-key-match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,21 @@ export const containsIssueKey = (value: string, issueKey: string): boolean => {

export const containsExplicitIssueReference = (value: string, issueKey: string): boolean => {
const parts = ISSUE_KEY_PARTS.exec(issueKey)
if (!parts) return containsIssueKey(value, issueKey)
if (!parts) {
if (/^\d+$/u.test(issueKey)) {
const number = escapeRegex(issueKey)
return new RegExp(
[
`#${number}(?!\\d)`,
`https?://github\\.com/[^\\s/]+/[^\\s/]+/issues/${number}(?!\\d)`,
`(?:^|\\n)\\s*(?:github\\s+)?issue\\s*:?[ \\t]*#?${number}(?!\\d)`,
`(?:^|\\n)\\s*(?:closes|fixes|resolves)\\s*:?[ \\t]*#?${number}(?!\\d)`,
].join('|'),
'i',
).test(value)
}
return containsIssueKey(value, issueKey)
}

const prefix = escapeRegex(parts[1] ?? '')
const number = escapeRegex(parts[2] ?? '')
Expand Down
46 changes: 46 additions & 0 deletions src/mount/relayfile-cloud-mount-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type RelayfileCloudMountClientConfig,
type RelayFileClientLike,
} from './relayfile-cloud-mount-client'
import { RelayfileGithubConnectionWrite } from './relayfile-github-connection-write'

const storedAuth = (overrides: Partial<StoredAuth> = {}): StoredAuth => ({
apiUrl: 'https://cloud.example',
Expand Down Expand Up @@ -1066,6 +1067,51 @@ describe('RelayfileCloudMountClient', () => {

await expect(mount.confirmWrite('/linear/issues/new.json', { timeoutMs: 5 }))
.rejects.toThrow(/Field "id" is read-only/)
await expect(mount.getConfirmedWriteFailureReason('/linear/issues/new.json'))
.resolves.toBe('Field "id" is read-only and cannot be written')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

it('feeds an existing-reference provider failure into the GitHub update-ref fallback', async () => {
const fake = new FakeRelayFileClient()
fake.ops.set('op-1', {
opId: 'op-1',
status: 'failed',
attemptCount: 1,
lastError: 'GitHub writeback failed with status 422: Reference already exists',
})
fake.ops.set('op-2', {
opId: 'op-2',
status: 'succeeded',
attemptCount: 1,
providerResult: { status: 200, externalId: 'factory/test-existing-ref' },
})
fake.ops.set('op-3', {
opId: 'op-3',
status: 'succeeded',
attemptCount: 1,
providerResult: { status: 201, externalId: '66' },
})
const mount = new RelayfileCloudMountClient({
workspaceId: 'rw_test',
client: fake,
isAllowedDraft: () => true,
})
const writer = new RelayfileGithubConnectionWrite({ mount })

await expect(writer.publishPullRequest({
repo: 'AgentWorkforce/factory',
headRef: 'factory/test-existing-ref',
headSha: '1234567890abcdef1234567890abcdef12345678',
baseRef: 'main',
title: 'Title',
body: 'Body',
})).resolves.toMatchObject({ number: 66 })

expect(fake.writeFileCalls.map((call) => call.path)).toEqual([
'/github/repos/AgentWorkforce/factory/refs/factory.json',
'/github/repos/AgentWorkforce/factory/refs/refs%2Fheads%2Ffactory%2Ftest-existing-ref.json',
expect.stringMatching(/\/github\/repos\/AgentWorkforce\/factory\/pull-requests\/factory-/u),
])
})

it('delegates subscribe through the workspace-scoped event client', () => {
Expand Down
15 changes: 14 additions & 1 deletion src/mount/relayfile-cloud-mount-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class RelayfileCloudMountClient implements MountClient {
readonly #isAllowedDelete?: (path: string, currentContent: unknown) => boolean | Promise<boolean>
readonly #lastOpByPath = new Map<string, string>()
readonly #confirmedExternalIdByPath = new Map<string, string>()
readonly #confirmedFailureReasonByPath = new Map<string, string>()

constructor(config: RelayfileCloudMountClientConfig = {}) {
if (!config.client) {
Expand Down Expand Up @@ -301,6 +302,7 @@ export class RelayfileCloudMountClient implements MountClient {

const serialized = serializeContent(content)
this.#confirmedExternalIdByPath.delete(path)
this.#confirmedFailureReasonByPath.delete(path)

const writeAtCurrentRevision = async (): Promise<WriteQueuedResponse> => {
let baseRevision = '0'
Expand Down Expand Up @@ -329,6 +331,7 @@ export class RelayfileCloudMountClient implements MountClient {

async deleteFile(path: string): Promise<void> {
this.#confirmedExternalIdByPath.delete(path)
this.#confirmedFailureReasonByPath.delete(path)
const current = await this.#client.readFile(this.workspaceId, path)
const currentContent = parseRemoteContent(current)
if (isProviderPath(path)) {
Expand Down Expand Up @@ -440,7 +443,13 @@ export class RelayfileCloudMountClient implements MountClient {
const deadline = Date.now() + (opts.timeoutMs ?? 90_000)
for (;;) {
const operation = await this.#client.getOp(this.workspaceId, opId)
const status = mapOperationStatus(operation)
let status: 'acked' | 'pending' | 'failed'
try {
status = mapOperationStatus(operation)
} catch (error) {
this.#confirmedFailureReasonByPath.set(path, providerResultError(operation))
throw error
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (status !== 'pending') {
const providerId = [
operation.providerResult?.externalId,
Expand All @@ -457,6 +466,10 @@ export class RelayfileCloudMountClient implements MountClient {
}
}

async getConfirmedWriteFailureReason(path: string): Promise<string | undefined> {
return this.#confirmedFailureReasonByPath.get(path)
}

async getConfirmedWriteExternalId(path: string): Promise<string | undefined> {
return this.#confirmedExternalIdByPath.get(path)
}
Expand Down
Loading