mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* 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
35 lines
1.3 KiB
JavaScript
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)
|
|
}
|
|
}
|
|
)
|