Files
orca/config/scripts/mobile-launch-contract-workflow.test.mjs
T
Brennan Benson abc8386e14 fix(mobile): name a create's launch so a lost reply cannot build two workspaces (#21137)
* fix(mobile): name a create's launch so a lost reply cannot build two workspaces

`agent.launch` admits a caller-supplied `operationId` through a durable ledger, so
exactly one execution happens and every replay returns the recorded answer. No client
sent one, so the machinery was inert and the original defect was still live: mobile
retries a lost create by design, and a retried launch built a second agent in a second
workspace.

Mobile now mints an operation id per create candidate and sends it whenever the host
advertises `agent.launch.replay.v1`.

The invariant is one operation per candidate. `computeAgentLaunchFingerprint` folds
`target` whole, so the workspace name is inside the fingerprint; carrying one id across
a name-collision bump would meet its own row under a differing fingerprint and refuse
`agent_session_operation_conflict`, failing the create outright on the second candidate.
The id is therefore minted beside `clientMutationId` at the top of each loop iteration
and reused verbatim by every retry arm inside that candidate — never re-minted, since a
new id is a new operation.

Admission runs ahead of every effect, so `_invalid` / `_expired` / `_capacity` prove
nothing launched: those re-send the same candidate unnamed rather than let bookkeeping
fail a create the host would have performed. `_unknown` is the one refusal that is not
safe to re-send, and it surfaces.

Also corrects a false comment: the legacy path caches the whole launch under
`clientMutationId`, so inside its 60s window a replay adds neither a workspace nor a
surface, and outside it adds both — not "a second surface, never a second workspace".

* fix(mobile): preserve launch identity on refusals

* fix(mobile): use launch receipts to authorize replay

* test: move mobile launch replay coverage outside node project

* fix(mobile): enforce replay-safe launch delivery at the host

* test: run mobile launch contracts in mobile checks

* test: cover mobile launch contract workflow dependencies
2026-09-17 10:06:11 -07:00

35 lines
1.3 KiB
JavaScript

import { existsSync, readFileSync } from 'node:fs'
import { dirname, matchesGlob, relative, resolve, sep } from 'node:path'
import { expect, it } from 'vitest'
import ts from 'typescript-api'
import { parse } from 'yaml'
const projectDir = resolve(import.meta.dirname, '../..')
const workflow = parse(readFileSync(resolve(projectDir, '.github/workflows/mobile.yml'), 'utf8'))
it.each(['agent-launch-mobile-replay', 'mobile-agent-launch-architecture'])(
'runs Mobile Checks when a direct root dependency of %s changes',
(name) => {
const suite = resolve(projectDir, `mobile/src/tasks/${name}.test.ts`)
// Parse source only: the root test project must never load the mobile dependency graph.
const imports = ts
.preProcessFile(readFileSync(suite, 'utf8'), true)
.importedFiles.filter((entry) => entry.fileName.startsWith('.'))
.map((entry) =>
relative(projectDir, resolve(dirname(suite), `${entry.fileName}.ts`))
.split(sep)
.join('/')
)
.filter((file) => file.startsWith('src/'))
expect(imports).not.toEqual([])
for (const file of imports) {
expect(existsSync(resolve(projectDir, file)), file).toBe(true)
expect(
workflow.on.pull_request.paths.some((pattern) => matchesGlob(file, pattern)),
file
).toBe(true)
}
}
)