diff --git a/.github/workflows/adhoc-mac-build.yml b/.github/workflows/adhoc-mac-build.yml index 53501add289..4f667bf2778 100644 --- a/.github/workflows/adhoc-mac-build.yml +++ b/.github/workflows/adhoc-mac-build.yml @@ -235,11 +235,14 @@ jobs: fi done echo "head_sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT" - # Why the main repo's tags: package.json on a branch is as stale as the - # main it forked from, and stable patches never merge back into it. - published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \ - --repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \ - --json tagName --jq '.[].tagName' || true)" + # Why git tags, not GitHub releases: unpublishing a buggy cut deletes the + # GitHub release and leaves the tag, which still owns that number. + # Releases-only let adhoc sit on a number already taken, so the updater + # would not install it. Empty on failure — the script then falls back + # to package.json. + published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \ + "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \ + --jq '.[].ref | sub("^refs/tags/"; "")' || true)" ORCA_PUBLISHED_VERSIONS="$published" ORCA_ADHOC_LABEL="${LABEL:-$REF}" \ node config/scripts/adhoc-build-version.mjs \ >"$RUNNER_TEMP/adhoc-identity.txt" diff --git a/.github/workflows/daily-mac-build.yml b/.github/workflows/daily-mac-build.yml index a758d8db3a1..41b87526fea 100644 --- a/.github/workflows/daily-mac-build.yml +++ b/.github/workflows/daily-mac-build.yml @@ -90,7 +90,7 @@ jobs: uses: actions/checkout@v6 with: ref: main - # Version helpers only read HEAD; published versions come from the release API. + # Version helpers only read HEAD; published versions come from git tags. fetch-depth: 1 # Why: this job only reads stablyai/orca and never pushes; every write # goes to the daily repo through a minted App token passed by env. @@ -209,17 +209,21 @@ jobs: # number free", where a stranded draft still holds one. names="$(gh release list --repo "$DAILY_REPO" --limit 200 --json name \ --jq '.[].name // empty')" - # Why the main repo's tags decide the base version rather than - # package.json: main's version only moves on `release:` commits, and - # stable patches are cut from release branches that never merge back, so - # package.json can sit several patches behind what users are running. A + # Why git tags, not GitHub releases: unpublishing a buggy cut deletes the + # GitHub release and leaves the tag. That dragged hourlies backwards so + # electron-updater stopped offering them; dailies would do the same. A # separate token because GH_TOKEN above is the App's, scoped to the # daily repo. Empty on failure — the script then falls back to # package.json, which is stale but never wrong enough to fail a build. - published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \ - --repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \ - --json tagName --jq '.[].tagName' || true)" - echo "Highest published tag seen: $(head -1 <<<"$published")" + main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \ + "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \ + --jq '.[].ref | sub("^refs/tags/"; "")' || true)" + # Already-shipped channel tags are a second floor so unpublishing a + # buggy main release cannot drag this series below a daily already out. + channel_tags="$(gh release list --repo "$DAILY_REPO" --limit 200 --json tagName \ + --jq '.[].tagName' || true)" + published="$main_tags"$'\n'"$channel_tags" + echo "Published version sources: $(grep -c . <<<"$main_tags" || true) main tags, $(grep -c . <<<"$channel_tags" || true) channel tags" ORCA_PUBLISHED_VERSIONS="$published" ORCA_DAILY_RELEASE_NAMES="$names" \ node config/scripts/daily-build-version.mjs \ >"$RUNNER_TEMP/daily-identity.txt" diff --git a/.github/workflows/git-command-termination-runtime.yml b/.github/workflows/git-command-termination-runtime.yml new file mode 100644 index 00000000000..1602bae956a --- /dev/null +++ b/.github/workflows/git-command-termination-runtime.yml @@ -0,0 +1,22 @@ +name: Git command termination runtime +on: + pull_request: + paths: + - 'src/main/git/command-runner/spawned-command-tree-kill*' + - '.github/workflows/git-command-termination-runtime.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + windows-exit: + runs-on: windows-latest + timeout-minutes: 20 + env: + ORCA_BACKGROUND_LAUNCH: '1' + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: ./.github/actions/install-node-dependencies + - name: Verify exited native child does not trigger taskkill + run: node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/git/command-runner/spawned-command-tree-kill.test.ts diff --git a/.github/workflows/hourly-mac-build.yml b/.github/workflows/hourly-mac-build.yml index b4bdd803a6a..1aed485a666 100644 --- a/.github/workflows/hourly-mac-build.yml +++ b/.github/workflows/hourly-mac-build.yml @@ -137,7 +137,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.head_sha }} - # Version helpers only read HEAD; published versions come from the release API. + # Version helpers only read HEAD; published versions come from git tags. fetch-depth: 1 # Why: this job only reads stablyai/orca and never pushes; every write # goes to the hourly repo through a minted App token passed by env. @@ -213,17 +213,25 @@ jobs: # number free", where a stranded draft still holds one. names="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json name \ --jq '.[].name // empty')" - # Why the main repo's tags decide the base version rather than - # package.json: main's version only moves on `release:` commits, and - # stable patches are cut from release branches that never merge back, so - # package.json can sit several patches behind what users are running. A - # separate token because GH_TOKEN above is the App's, scoped to the - # hourly repo. Empty on failure — the script then falls back to - # package.json, which is stale but never wrong enough to fail a build. - published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \ - --repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \ - --json tagName --jq '.[].tagName' || true)" - echo "Highest published tag seen: $(head -1 <<<"$published")" + # Why git tags, not GitHub releases: unpublishing a buggy cut deletes the + # GitHub release and leaves the tag. On 2026-09-14 we deleted v1.4.202's + # release for a bug; hourlies had already climbed to 1.4.203, then + # `gh release list` fell back to v1.4.201 and the next hourlies shipped + # as 1.4.202-hourly — which electron-updater will not install over + # 1.4.203-hourly or over the still-tagged 1.4.202. A separate token + # because GH_TOKEN above is the App's, scoped to the hourly repo. Empty + # on failure — the script then falls back to package.json, which is + # stale but never wrong enough to fail a build. + main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \ + "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \ + --jq '.[].ref | sub("^refs/tags/"; "")' || true)" + # Already-shipped channel tags are a second floor: even if main's tag + # list is empty this run, a 1.4.203-hourly already out must not be + # followed by a 1.4.202-hourly. + channel_tags="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json tagName \ + --jq '.[].tagName' || true)" + published="$main_tags"$'\n'"$channel_tags" + echo "Published version sources: $(grep -c . <<<"$main_tags" || true) main tags, $(grep -c . <<<"$channel_tags" || true) channel tags" ORCA_PUBLISHED_VERSIONS="$published" ORCA_HOURLY_RELEASE_NAMES="$names" \ node config/scripts/hourly-build-version.mjs \ >"$RUNNER_TEMP/hourly-identity.txt" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 398bd61bd04..7418e8f80aa 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -170,6 +170,9 @@ jobs: - name: Check reliability gate manifest run: pnpm run check:reliability-gates + - name: Enforce dead design-system classes + run: pnpm run check:dead-classes + - name: Check VM runtime rollback compatibility env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -326,40 +329,59 @@ jobs: # Why: the 2.25.5 lane is a source build of a pinned tarball, so it produced the # same binary on every PR for minutes of runner time. The key carries the version # because that is the only input; the sha256 assertion below still guards the - # tarball on the miss path that actually builds. + # tarball on the miss path that actually builds. Only this PR's own later pushes + # can restore it — GitHub scopes a cache written from a pull_request run to that + # ref — so a first push always takes the build path below. - name: Cache baseline Git build uses: actions/cache@v5 with: path: ~/.cache/orca-git-compat/git-2.25.5 key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5 + # Why its own step: this is `make -j$(nproc)` on every core, and the lanes below + # spend their wall clock waiting on container starts, not on Git. Sharing a runner + # with the build stretched one ~1.5s boundary case past Vitest's 30s timeout, so + # the build has to finish before anything timed starts. + - name: Build the baseline Git binary + run: | + archive="$RUNNER_TEMP/git-2.25.5.tar.gz" + source="$HOME/.cache/orca-git-compat/git-2.25.5" + if [ -x "$source/git" ]; then + exit 0 + fi + curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" + echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \ + | sha256sum --check + mkdir -p "$source" + tar -xzf "$archive" -C "$source" --strip-components=1 + make -C "$source" -j"$(nproc)" \ + NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git + # Why: the linked binaries are what the next run needs; the objects that + # produced them are most of the tree and would bloat the cache entry. + find "$source" -name '*.o' -delete + - name: Verify Git binary compatibility matrix run: | + specs=( + "alpine/git:edge-2.38.1|2.38.1" + "alpine/git:v2.49.1|2.49.1" + ) + # Why pull up front: a lane's first `docker run` otherwise pulls its image + # while the sibling lane is mid-test, and that stall is charged to the test. + for spec in "${specs[@]}"; do + docker pull --quiet "${spec%%|*}" + done + pids=() ( - archive="$RUNNER_TEMP/git-2.25.5.tar.gz" - source="$HOME/.cache/orca-git-compat/git-2.25.5" - if [ ! -x "$source/git" ]; then - curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" - echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \ - | sha256sum --check - mkdir -p "$source" - tar -xzf "$archive" -C "$source" --strip-components=1 - make -C "$source" -j"$(nproc)" \ - NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git - # Why: the linked binaries are what the next run needs; the objects that - # produced them are most of the tree and would bloat the cache entry. - find "$source" -name '*.o' -delete - fi - ORCA_GIT_COMPAT_BINARY="$source/git" ORCA_GIT_COMPAT_VERSION="2.25.5" \ + ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \ + ORCA_GIT_COMPAT_VERSION="2.25.5" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts ) & pids+=("$!") - for spec in \ - "alpine/git:edge-2.38.1|2.38.1" \ - "alpine/git:v2.49.1|2.49.1"; do + for spec in "${specs[@]}"; do ( image="${spec%%|*}" version="${spec#*|}" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index b21feae3230..490eda88c33 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -56,6 +56,7 @@ jobs: --exclude=src/main/daemon/node-pty-fd-leak.test.ts \ --exclude=src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \ --exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \ + --exclude=src/main/pty/omp-shell-wrapper-alias-safety.test.ts \ --exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \ --exclude=src/main/shell-startup-feature-channel.test.ts \ --exclude=src/main/terminal-history-fish-session.node-pty.test.ts \ diff --git a/AGENTS.md b/AGENTS.md index 74c049a49fd..0c11d13a9ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Design System -All UI work — layout, color, typography, spacing, component selection, UX behavior — must follow [`docs/STYLEGUIDE.md`](./docs/STYLEGUIDE.md). Use the tokens defined in `src/renderer/src/assets/main.css` (the canonical source) and the shadcn primitives in `src/renderer/src/components/ui/`. Don't invent new color values, font sizes, or shadow tiers when a documented one already covers the role. When STYLEGUIDE.md is silent, follow the resolution order in its final section. +All UI work — layout, color, typography, spacing, component selection, UX behavior — must follow [`docs/STYLEGUIDE.md`](./docs/STYLEGUIDE.md). Most of it is linted: `pnpm run check:code-quality:changed` fails on new restyles of a `components/ui/` primitive, raw palette colors, and computed `className` strings; `pnpm lint` fails on any class Tailwind cannot generate. See the Enforcement section of the style guide before suppressing either. Use the tokens defined in `src/renderer/src/assets/main.css` (the canonical source) and the shadcn primitives in `src/renderer/src/components/ui/`. Don't invent new color values, font sizes, or shadow tiers when a documented one already covers the role. When STYLEGUIDE.md is silent, follow the resolution order in its final section. ## Electron UI Validation @@ -46,6 +46,7 @@ Avoid type assertions except `as const`. Unavoidable casts need a line-specific - **Typecheck**: `pnpm tc` (or `tc:node` / `tc:cli` / `tc:web`) - **Test**: `pnpm test [path/to/file.test.ts]` - **Lint**: `oxlint`, or `pnpm run check:code-quality:changed` for changed files (full `pnpm lint` is slow); format with `pnpm format` +- **Design system**: `pnpm run lint:design-system` for the full renderer report (not a gate); the changed-lines gate above is what CI enforces # Considerations diff --git a/cloud/apps/relay/src/database-postgres-timeout.test.ts b/cloud/apps/relay/src/database-postgres-timeout.test.ts index f678fecc4bb..df300383c1b 100644 --- a/cloud/apps/relay/src/database-postgres-timeout.test.ts +++ b/cloud/apps/relay/src/database-postgres-timeout.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { POSTGRES_STATEMENT_STATS_MIGRATION } from './postgres-statement-stats.js' const fakes = vi.hoisted(() => ({ configs: [] as Array>, @@ -119,11 +120,16 @@ describe('PostgreSQL relay deadlines', () => { }) expect(ddl.length).toBeGreaterThan(0) + expect(ddl).toContain(POSTGRES_STATEMENT_STATS_MIGRATION) // Statements can open with a leading `--` rationale comment. const body = (statement: string): string => statement.replace(/^(?:\s*--[^\n]*\n)*\s*/, '') expect( - ddl.every((statement) => /^(?:CREATE|ALTER TABLE)\b/i.test(body(statement))) + ddl.every( + (statement) => + statement === POSTGRES_STATEMENT_STATS_MIGRATION || + /^(?:CREATE|ALTER TABLE)\b/i.test(body(statement)) + ) ).toBe(true) // The backfill is DML, so it stays on the deadline-bearing serving pool. expect(ddl.some((statement) => statement.includes('INSERT INTO'))).toBe(false) diff --git a/cloud/apps/relay/src/database.ts b/cloud/apps/relay/src/database.ts index 41ead67ea60..84069646b33 100644 --- a/cloud/apps/relay/src/database.ts +++ b/cloud/apps/relay/src/database.ts @@ -10,6 +10,8 @@ import { type PostgresPoolPressureCounts } from './postgres-pool-pressure.js' import { applyPostgresSchema } from './postgres-schema-startup.js' +import { POSTGRES_STATEMENT_STATS_MIGRATION } from './postgres-statement-stats.js' +import { reportPostgresQueryFailure } from './postgres-query-failure.js' import { CellInventoryHoldSamples, emptyCellInventoryHoldCounts, @@ -619,6 +621,7 @@ CREATE INDEX IF NOT EXISTS relay_audit_events_at ON relay_audit_events(at); // auto-named; the replacement is named, so both statements are no-ops on a // database the current schema created and neither can drop the other. export const POSTGRES_SCHEMA_MIGRATIONS = [ + POSTGRES_STATEMENT_STATS_MIGRATION, `ALTER TABLE relay_region_decisions ADD COLUMN IF NOT EXISTS last_considered_at BIGINT NOT NULL DEFAULT 0`, `ALTER TABLE relay_region_decisions ADD COLUMN IF NOT EXISTS cohort_bucket BIGINT NOT NULL DEFAULT 0`, `ALTER TABLE relay_region_rehome_attempts @@ -908,12 +911,25 @@ class PostgresDatabase implements RelayDatabase { } async query(sql: string, params: unknown[] = []): Promise { - const client = await this.pressure.connect() + const startedAt = performance.now() + let phase: 'acquire' | 'execute' = 'acquire' + let client: pg.PoolClient | undefined try { + client = await this.pressure.connect() + phase = 'execute' const result = await client.query(postgresSql(sql), params) return returnsRows(sql) ? (result.rows as SqlRow[]) : [{ changes: result.rowCount ?? 0 }] + } catch (error) { + reportPostgresQueryFailure({ + error, + phase, + sql, + elapsedMs: performance.now() - startedAt, + pool: this.pool + }) + throw error } finally { - client.release() + client?.release() } } diff --git a/cloud/apps/relay/src/postgres-query-failure-postgres.test.ts b/cloud/apps/relay/src/postgres-query-failure-postgres.test.ts new file mode 100644 index 00000000000..df082de289b --- /dev/null +++ b/cloud/apps/relay/src/postgres-query-failure-postgres.test.ts @@ -0,0 +1,70 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { openRelayDatabase, type RelayDatabase } from './database.js' + +const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL +const describePostgres = databaseUrl ? describe : describe.skip + +describePostgres('real PostgreSQL query failure phases', () => { + let database: RelayDatabase + + beforeAll(async () => { + database = await openRelayDatabase({ + databaseUrl, + dataDir: '', + poolMax: 1, + statementTimeoutMs: 50 + }) + }) + afterAll(async () => { + await database.close() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('distinguishes a server statement timeout and leaves the pool usable', async () => { + const log = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + await expect(database.query('SELECT pg_sleep(0.2)')).rejects.toMatchObject({ code: '57014' }) + expect(JSON.parse(log.mock.calls[0]![0] as string)).toMatchObject({ + event: 'orca_relay_postgres_query_failed', + phase: 'execute', + code: '57014', + connectionTimeout: false + }) + expect(await database.query('SELECT 1 AS ok')).toEqual([{ ok: 1 }]) + }) + + it('distinguishes queue acquisition timeout without running the statement', async () => { + const log = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + let acquired!: () => void + const ready = new Promise((resolve) => { + acquired = resolve + }) + let release!: () => void + const wait = new Promise((resolve) => { + release = resolve + }) + const holder = database.transaction(async () => { + acquired() + await wait + }) + await ready + try { + await expect(database.query('SELECT pg_sleep(0.2)')).rejects.toThrow( + 'timeout exceeded when trying to connect' + ) + expect(JSON.parse(log.mock.calls[0]![0] as string)).toMatchObject({ + event: 'orca_relay_postgres_query_failed', + phase: 'acquire', + code: 'unknown', + connectionTimeout: true, + poolTotal: 1, + poolIdle: 0 + }) + } finally { + release() + await holder + } + expect(await database.query('SELECT 1 AS ok')).toEqual([{ ok: 1 }]) + }) +}) diff --git a/cloud/apps/relay/src/postgres-query-failure.test.ts b/cloud/apps/relay/src/postgres-query-failure.test.ts new file mode 100644 index 00000000000..7b42b6f5cb1 --- /dev/null +++ b/cloud/apps/relay/src/postgres-query-failure.test.ts @@ -0,0 +1,109 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const fakes = vi.hoisted(() => ({ + connectError: undefined as unknown, + query: vi.fn(async (_sql: string, _params?: unknown[]) => ({ rows: [], rowCount: 0 })), + release: vi.fn() +})) + +vi.mock('pg', () => ({ + default: { + Pool: class { + totalCount = 10 + idleCount = 0 + waitingCount = 7 + on = vi.fn() + async connect() { + if (fakes.connectError) throw fakes.connectError + return { query: fakes.query, release: fakes.release } + } + async end() {} + } + } +})) + +import { openRelayDatabase, type RelayDatabase } from './database.js' + +describe('PostgreSQL query failure diagnostics', () => { + let database: RelayDatabase + const sql = 'WITH assignment_state AS MATERIALIZED (SELECT $1) SELECT * FROM assignment_state' + + beforeEach(async () => { + fakes.connectError = undefined + fakes.query.mockReset().mockResolvedValue({ rows: [], rowCount: 0 }) + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + database = await openRelayDatabase({ databaseUrl: 'postgres://unused', dataDir: '' }) + fakes.query.mockClear() + fakes.release.mockClear() + vi.mocked(console.warn).mockClear() + }) + + afterEach(async () => { + await database.close() + vi.restoreAllMocks() + }) + + it('identifies acquisition failure without issuing SQL or changing the error', async () => { + const error = new Error('timeout exceeded when trying to connect: private detail') + fakes.connectError = error + await expect(database.query(sql, ['private-token'])).rejects.toBe(error) + expect(fakes.query).not.toHaveBeenCalled() + expect(fakes.release).not.toHaveBeenCalled() + expect(JSON.parse(vi.mocked(console.warn).mock.calls[0]![0] as string)).toEqual({ + event: 'orca_relay_postgres_query_failed', + phase: 'acquire', + operation: 'control-renewal', + code: 'unknown', + connectionTimeout: true, + elapsedMs: expect.any(Number), + poolTotal: 10, + poolIdle: 0, + poolWaiting: 7 + }) + expect(JSON.stringify(vi.mocked(console.warn).mock.calls)).not.toContain('private') + }) + + it.each(['57014', '55P03', 'ECONNRESET'])( + 'identifies execute failure %s and releases its client', + async (code) => { + const error = Object.assign(new Error('private-token'), { code, detail: sql }) + fakes.query.mockRejectedValueOnce(error) + await expect(database.query(sql, ['private-token'])).rejects.toBe(error) + expect(fakes.query).toHaveBeenCalledOnce() + expect(fakes.release).toHaveBeenCalledOnce() + expect(JSON.parse(vi.mocked(console.warn).mock.calls[0]![0] as string)).toMatchObject({ + phase: 'execute', + operation: 'control-renewal', + code, + connectionTimeout: false + }) + expect(JSON.stringify(vi.mocked(console.warn).mock.calls)).not.toContain('private-token') + expect(JSON.stringify(vi.mocked(console.warn).mock.calls)).not.toContain(sql) + } + ) + + it('does not emit an arbitrary error code, message, query, or parameter', async () => { + const error = { code: 'private-code', message: 'private-message' } + fakes.query.mockRejectedValueOnce(error) + await expect(database.query('SELECT private_column', ['private-param'])).rejects.toBe(error) + const log = vi.mocked(console.warn).mock.calls[0]![0] as string + expect(JSON.parse(log)).toMatchObject({ operation: 'other', code: 'unknown' }) + expect(log).not.toContain('private') + }) + + it('keeps the original error and releases the client if logging fails', async () => { + const error = new Error('database failure') + fakes.query.mockRejectedValueOnce(error) + vi.mocked(console.warn).mockImplementationOnce(() => { + throw new Error('logger failure') + }) + await expect(database.query(sql)).rejects.toBe(error) + expect(fakes.release).toHaveBeenCalledOnce() + }) + + it('does not log successful queries', async () => { + await database.query(sql) + expect(console.warn).not.toHaveBeenCalled() + expect(fakes.release).toHaveBeenCalledOnce() + }) +}) diff --git a/cloud/apps/relay/src/postgres-query-failure.ts b/cloud/apps/relay/src/postgres-query-failure.ts new file mode 100644 index 00000000000..26b536c5134 --- /dev/null +++ b/cloud/apps/relay/src/postgres-query-failure.ts @@ -0,0 +1,55 @@ +type QueryFailurePhase = 'acquire' | 'execute' + +const ERROR_CODES = new Set([ + '57014', + '55P03', + '40P01', + '40001', + '53300', + '57P01', + '57P02', + '57P03', + '08000', + '08001', + '08003', + '08006', + 'ECONNRESET', + 'ECONNREFUSED', + 'ETIMEDOUT', + 'EPIPE' +]) + +export function reportPostgresQueryFailure(input: { + error: unknown + phase: QueryFailurePhase + sql: string + elapsedMs: number + pool: { totalCount: number; idleCount: number; waitingCount: number } +}): void { + // Emit only bounded categories: error messages and SQL can contain credentials or identities. + try { + const error = input.error as { code?: unknown; message?: unknown } | null + const code = + typeof error?.code === 'string' && ERROR_CODES.has(error.code) ? error.code : 'unknown' + const connectionTimeout = + typeof error?.message === 'string' && + error.message.includes('timeout exceeded when trying to connect') + console.warn( + JSON.stringify({ + event: 'orca_relay_postgres_query_failed', + phase: input.phase, + operation: /^\s*WITH\s+assignment_state\s+AS\s+MATERIALIZED\b/i.test(input.sql) + ? 'control-renewal' + : 'other', + code, + connectionTimeout, + elapsedMs: Math.max(0, Math.round(input.elapsedMs)), + poolTotal: input.pool.totalCount, + poolIdle: input.pool.idleCount, + poolWaiting: input.pool.waitingCount + }) + ) + } catch { + // Diagnostics must not replace the original database failure. + } +} diff --git a/cloud/apps/relay/src/postgres-statement-stats-postgres.test.ts b/cloud/apps/relay/src/postgres-statement-stats-postgres.test.ts new file mode 100644 index 00000000000..251e3670759 --- /dev/null +++ b/cloud/apps/relay/src/postgres-statement-stats-postgres.test.ts @@ -0,0 +1,121 @@ +import { randomUUID } from 'node:crypto' +import pg from 'pg' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { openRelayDatabase } from './database.js' +import { POSTGRES_STATEMENT_STATS_MIGRATION } from './postgres-statement-stats.js' + +const databaseUrl = process.env.ORCA_RELAY_TEST_POSTGRES_URL +const describePostgres = databaseUrl ? describe : describe.skip + +describePostgres('optional PostgreSQL statement statistics', () => { + let admin: pg.Client + let preloaded: boolean + const databases: string[] = [] + const roles: string[] = [] + + beforeAll(async () => { + admin = new pg.Client({ connectionString: databaseUrl }) + await admin.connect() + const result = await admin.query<{ loaded: boolean }>( + `SELECT 'pg_stat_statements' = ANY(string_to_array( + replace(current_setting('shared_preload_libraries'), ' ', ''), ',' + )) AS loaded` + ) + preloaded = result.rows[0]!.loaded + }) + + afterAll(async () => { + for (const database of databases) await admin.query(`DROP DATABASE IF EXISTS ${database}`) + for (const role of roles) await admin.query(`DROP ROLE IF EXISTS ${role}`) + await admin.end() + }) + + async function freshDatabase(): Promise { + const name = `relay_stats_${randomUUID().replaceAll('-', '')}` + await admin.query(`CREATE DATABASE ${name}`) + databases.push(name) + const url = new URL(databaseUrl!) + url.pathname = `/${name}` + return url.toString() + } + + async function connect(url: string): Promise { + const client = new pg.Client({ connectionString: url, statement_timeout: 2_000 }) + await client.connect() + return client + } + + async function installed(client: pg.Client): Promise { + const result = await client.query<{ present: boolean }>( + `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') AS present` + ) + return result.rows[0]!.present + } + + it('exposes an existing collector idempotently, and skips servers without one', async () => { + const url = await freshDatabase() + const database = await openRelayDatabase({ databaseUrl: url, dataDir: '' }) + await database.close() + const client = await connect(url) + try { + expect(await installed(client)).toBe(preloaded) + if (preloaded) { + const before = await client.query('SELECT stats_reset FROM public.pg_stat_statements_info') + await client.query(POSTGRES_STATEMENT_STATS_MIGRATION) + const after = await client.query('SELECT stats_reset FROM public.pg_stat_statements_info') + expect(after.rows).toEqual(before.rows) + await client.query('SELECT calls, wal_bytes, shared_blks_dirtied FROM public.pg_stat_statements LIMIT 1') + } else { + await client.query(POSTGRES_STATEMENT_STATS_MIGRATION) + expect(await installed(client)).toBe(false) + } + } finally { + await client.end() + } + }) + + it.each([false, true])('tolerates missing extension privileges (read settings: %s)', async (readSettings) => { + const client = await connect(await freshDatabase()) + const role = `relay_stats_role_${randomUUID().replaceAll('-', '')}` + await admin.query(`CREATE ROLE ${role}`) + roles.push(role) + if (readSettings) await admin.query(`GRANT pg_read_all_settings TO ${role}`) + try { + await client.query(`SET ROLE ${role}`) + await client.query(POSTGRES_STATEMENT_STATS_MIGRATION) + expect(await installed(client)).toBe(false) + expect((await client.query<{ value: number }>('SELECT 42 AS value')).rows[0]!.value).toBe(42) + } finally { + await client.end() + } + }) + + it('serializes concurrent catalog creation across directors', async () => { + const url = await freshDatabase() + const clients = await Promise.all(Array.from({ length: 5 }, async () => await connect(url))) + try { + await Promise.all(clients.map(async (client) => await client.query(POSTGRES_STATEMENT_STATS_MIGRATION))) + expect(await installed(clients[0]!)).toBe(preloaded) + } finally { + await Promise.all(clients.map(async (client) => await client.end())) + } + }) + + it('yields to an in-progress installer instead of blocking startup', async () => { + const url = await freshDatabase() + const owner = await connect(url) + const contender = await connect(url) + try { + await owner.query('BEGIN') + await owner.query(`SELECT pg_advisory_xact_lock(hashtext('orca-relay'), hashtext('statement-stats'))`) + await contender.query(POSTGRES_STATEMENT_STATS_MIGRATION) + expect(await installed(contender)).toBe(false) + await owner.query('COMMIT') + await contender.query(POSTGRES_STATEMENT_STATS_MIGRATION) + expect(await installed(contender)).toBe(preloaded) + } finally { + await owner.end() + await contender.end() + } + }) +}) diff --git a/cloud/apps/relay/src/postgres-statement-stats.ts b/cloud/apps/relay/src/postgres-statement-stats.ts new file mode 100644 index 00000000000..61a2fee3b75 --- /dev/null +++ b/cloud/apps/relay/src/postgres-statement-stats.ts @@ -0,0 +1,28 @@ +// Expose an already-running collector; never preload a module or require elevated runtime privileges. +export const POSTGRES_STATEMENT_STATS_MIGRATION = ` +DO $relay_statement_stats$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_settings + WHERE name = 'shared_preload_libraries' + AND 'pg_stat_statements' = ANY(string_to_array(replace(setting, ' ', ''), ',')) + ) OR EXISTS ( + SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pg_stat_statements' + ) OR NOT EXISTS ( + SELECT 1 FROM pg_catalog.pg_available_extensions WHERE name = 'pg_stat_statements' + ) THEN + RETURN; + END IF; + + IF NOT pg_try_advisory_xact_lock(hashtext('orca-relay'), hashtext('statement-stats')) THEN + RETURN; + END IF; + + BEGIN + CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public; + EXCEPTION WHEN insufficient_privilege THEN + RAISE WARNING 'orca_relay_statement_stats_unavailable: insufficient privilege'; + END; +END +$relay_statement_stats$; +` diff --git a/cloud/docs/orca-relay-operations.md b/cloud/docs/orca-relay-operations.md index 4515048b29b..ea5717daa87 100644 --- a/cloud/docs/orca-relay-operations.md +++ b/cloud/docs/orca-relay-operations.md @@ -2,6 +2,21 @@ This runbook applies to the stable Cloud Run director and the production-shaped GCE cells in both environments. It does not authorize a full Terraform apply: staging and production contain unrelated drift, so inspect a saved targeted plan and its destroy count before every apply. +## PostgreSQL statement statistics + +Relay schema startup exposes `pg_stat_statements` when the server already preloads +that collector and the schema identity can install its extension. Servers without +the collector or the required privileges continue normally. Installation does not +change preload settings, reset collected counters, or require a database restart; +concurrent startups yield to one installer. An existing extension is left in place. + +For SQL incidents, inspect bounded aggregates of `calls`, `total_exec_time`, +`shared_blks_read`, `shared_blks_dirtied`, and `wal_bytes`, scoped to the relay +database and identified query IDs. Compare counter deltas over the same interval +as fleet runtime metrics; retain the statistics reset timestamp. Do not export +query text, identities, credentials, or invoke `pg_stat_statements_reset()` during +an investigation. Treat an unavailable view as missing evidence, not zero work. + The relay is automatically active for entitled signed-in desktops. There is no rollout flag, cohort, or user toggle. The emergency product kill switch is the auth plane refusing relay-token exchange; use cell drains only to move or terminate existing data-plane work. ## Safety rules diff --git a/cloud/docs/relay-database-failure-diagnostics.md b/cloud/docs/relay-database-failure-diagnostics.md new file mode 100644 index 00000000000..26d3d847a17 --- /dev/null +++ b/cloud/docs/relay-database-failure-diagnostics.md @@ -0,0 +1,29 @@ +# Relay database failure phases + +`orca_relay_postgres_query_failed` separates failure to acquire a pooled connection +(`phase=acquire`) from failure after acquisition (`phase=execute`). It covers +`PostgresDatabase.query`, including the single-statement control-renewal CTE. +Statements inside explicit transactions use a different query path and are not +covered. These events are diagnostic evidence, not a replacement for total SQL +failure counters. + +The event contains only an allowlisted error code, a connection-timeout boolean, +the operation category (`control-renewal` or `other`), total elapsed milliseconds, +and pool total/idle/waiting counts at failure. Total elapsed time includes acquisition. +An acquisition timeout can mean either waiting in the queue or establishing a new +connection; use the pool counts and independent server activity to distinguish them. +Unknown error codes stay `unknown`. Query text, parameters, error messages, and +identifiers are never emitted. Successful queries emit no additional event. + +Use structured GCE logs with `jsonPayload.event="orca_relay_postgres_query_failed"`. +Compare counts by phase, operation, and code with the same cell's renewal outcomes +and pool pressure, and with independent PostgreSQL wait samples. Establishing the +failure phase does not by itself establish why the pool backed up. + +For production observation, use an immutable image through the same-cap workflow +on one cell, with fresh monitor evidence and the exact predecessor digest. Verify +the serving digest and health, then inspect these events during a naturally +occurring failure. Do not deliberately induce a production database failure. +Rollback uses the same workflow and predecessor image; no schema or database +configuration changes are involved. Do not change rehome limits, timeouts, pool +sizes, or renewal scheduling merely to collect this evidence. diff --git a/config/oxlint-dead-classes.json b/config/oxlint-dead-classes.json new file mode 100644 index 00000000000..ad58853134f --- /dev/null +++ b/config/oxlint-dead-classes.json @@ -0,0 +1,77 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": [], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "jsPlugins": [ + { + "name": "shadcn", + "specifier": "@shadcn/lint" + } + ], + "settings": { + "shadcn": { + "note": "See docs/STYLEGUIDE.md for the role each token and primitive plays." + } + }, + "rules": {}, + "overrides": [ + { + "files": ["**/src/renderer/**/*.tsx"], + "rules": { + "shadcn/no-unknown-classes": [ + "error", + { + "allow": [ + "agent-map-*", + "comment-md-*", + "compact-agent-*", + "feature-wall-*", + "is-*", + "markdown-annotation-*", + "markdown-body", + "markdown-dark", + "markdown-doc-link*", + "markdown-light", + "markdown-preview", + "markdown-preview-search*", + "markdown-preview-shell", + "markdown-review-*", + "markdown-toc-*", + "mobile-browser-driver-banner", + "mobile-driver-banner", + "native-chat-*", + "orca-*", + "pdfViewer", + "popover-scroll-content", + "popover-wheel-scroll", + "ravpr-*", + "ravs-*", + "scrollbar-editor", + "scrollbar-sleek", + "scrollbar-sleek-lg", + "scrollbar-sleek-parent", + "toaster", + "worktree-sidebar-scrollbar", + "xterm-*" + ] + } + ] + } + }, + { + "files": ["**/*.test.tsx"], + "rules": { + "shadcn/no-unknown-classes": "off" + } + } + ], + "ignorePatterns": ["**/node_modules", "**/dist", "**/out", "cloud/**", "mobile/**"] +} diff --git a/config/oxlint-design-system.json b/config/oxlint-design-system.json new file mode 100644 index 00000000000..23b8df517d3 --- /dev/null +++ b/config/oxlint-design-system.json @@ -0,0 +1,54 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": [], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "jsPlugins": [ + { + "name": "shadcn", + "specifier": "@shadcn/lint" + } + ], + "settings": { + "shadcn": { + "note": "See docs/STYLEGUIDE.md for the role each token and primitive plays." + } + }, + "rules": {}, + "overrides": [ + { + "files": ["**/src/renderer/**/*.tsx"], + "rules": { + "shadcn/no-restyle": [ + "error", + { + "allow": ["layout"] + } + ], + "shadcn/no-raw-colors": [ + "error", + { + "allow": ["shadow-floating"] + } + ], + "shadcn/require-static-classes": "error" + } + }, + { + "files": ["**/*.test.tsx"], + "rules": { + "shadcn/no-restyle": "off", + "shadcn/no-raw-colors": "off", + "shadcn/require-static-classes": "off" + } + } + ], + "ignorePatterns": ["**/node_modules", "**/dist", "**/out", "cloud/**", "mobile/**"] +} diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs index a1b5b2fc88a..31b6d24953a 100644 --- a/config/scripts/check-changed-code-quality.mjs +++ b/config/scripts/check-changed-code-quality.mjs @@ -29,6 +29,12 @@ export const OXLINT_SCANS = [ { label: 'React Doctor', args: ['--config', 'config/oxlint-react-doctor.json'] + }, + { + // Why changed-lines only: the renderer carries ~4.7k pre-existing restyle/raw-color + // findings. Gating added lines holds the line without a repo-wide migration. + label: 'design system', + args: ['--config', 'config/oxlint-design-system.json'] } ] diff --git a/config/scripts/dev-channel-base-version.mjs b/config/scripts/dev-channel-base-version.mjs index 62a28c6a374..074af9ec3d9 100644 --- a/config/scripts/dev-channel-base-version.mjs +++ b/config/scripts/dev-channel-base-version.mjs @@ -25,8 +25,11 @@ function compareTriples(a, b) { * 2026-08-03 main read `1.4.165-rc.0` for twenty hours while 1.4.165, 1.4.166 and * 1.4.167 all shipped — so hourlies built from that main claimed 1.4.165 while * carrying code newer than 1.4.167, and sorted *below* the stable their user was - * already running. Published tags are the only honest answer to "what number is - * taken"; package.json is a floor, not a source of truth. + * already running. Git tags (not GitHub releases) are the honest answer to "what + * number is taken": unpublishing a buggy cut deletes the GitHub release and + * leaves the tag, which still owns that number. Channel tags (`1.4.203-hourly.*`) + * are a second floor so that unpublish cannot drag the series backwards. + * package.json is a floor, not a source of truth. */ export function resolveDevChannelBaseVersion(packageVersion, publishedVersions = []) { const fromPackage = parseVersionTriple(packageVersion) diff --git a/config/scripts/dev-channel-base-version.test.mjs b/config/scripts/dev-channel-base-version.test.mjs index d2631eff0e9..00c13f6817a 100644 --- a/config/scripts/dev-channel-base-version.test.mjs +++ b/config/scripts/dev-channel-base-version.test.mjs @@ -39,6 +39,27 @@ describe('dev channel base version', () => { ) }) + // Why tags rather than GitHub releases: unpublishing a buggy cut deletes the + // GitHub release and leaves the tag. Releases-only then treated 1.4.202 as + // free, so hourlies sat on 1.4.202-hourly and sorted below that tagged stable. + it('climbs past a tagged stable that has no GitHub release', () => { + expect(resolveDevChannelBaseVersion('1.4.197', ['v1.4.201', 'v1.4.202'])).toBe('1.4.203') + }) + + // 2026-09-14: v1.4.202's GitHub release was deleted for a bug after hourlies + // had already shipped as 1.4.203. Without the channel tags as a floor, the + // next hourlies would have been 1.4.202-hourly, which electron-updater will + // not install over 1.4.203-hourly. + it('does not drop below an already-published channel version', () => { + expect( + resolveDevChannelBaseVersion('1.4.197', [ + 'v1.4.201', + 'v1.4.202-hourly.202609141912', + 'v1.4.203-hourly.202609140417' + ]) + ).toBe('1.4.203') + }) + it('treats package.json as a floor when it leads the tags', () => { expect(resolveDevChannelBaseVersion('1.5.0-rc.0', ['v1.4.167'])).toBe('1.5.0') }) diff --git a/config/scripts/git-binary-compatibility-workflow.test.mjs b/config/scripts/git-binary-compatibility-workflow.test.mjs index afe5615bb44..35d2b5c60dc 100644 --- a/config/scripts/git-binary-compatibility-workflow.test.mjs +++ b/config/scripts/git-binary-compatibility-workflow.test.mjs @@ -2,42 +2,63 @@ import { readFileSync } from 'node:fs' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' +const BASELINE_DIR = '~/.cache/orca-git-compat/git-2.25.5' + +const gateSteps = () => + parse(readFileSync('.github/workflows/pr.yml', 'utf8')).jobs.git_compatibility.steps + +const stepNamed = (name) => gateSteps().find((step) => step.name === name) + describe('Git binary compatibility PR gate', () => { it('runs the real-binary contract at each compatibility boundary', () => { - const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) - const step = workflow.jobs.git_compatibility.steps.find( - (candidate) => candidate.name === 'Verify Git binary compatibility matrix' - ) + const run = stepNamed('Verify Git binary compatibility matrix')?.run - expect(step?.run).toContain('git-2.25.5.tar.gz') + expect(run).toContain('ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git"') + expect(run).toContain('alpine/git:edge-2.38.1|2.38.1') + expect(run).toContain('alpine/git:v2.49.1|2.49.1') + expect(run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"') + expect(run).toContain('src/shared/git-binary-compatibility.test.ts') + expect(run).toContain('pids+=("$!")') + expect(run).toContain('wait "$pid" || status=1') + }) + + it('builds the pinned baseline tarball into the cached directory', () => { + const run = stepNamed('Build the baseline Git binary')?.run + + expect(run).toContain('git-2.25.5.tar.gz') // Why asserted: the sha256 check only runs on the build path, so a cached binary // must come from a key that pins the same version the tarball line declares. - expect(step?.run).toContain('if [ ! -x "$source/git" ]; then') - expect(step?.run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf') - expect(step?.run).toContain('ORCA_GIT_COMPAT_BINARY="$source/git"') - expect(step?.run).toContain('alpine/git:edge-2.38.1|2.38.1') - expect(step?.run).toContain('alpine/git:v2.49.1|2.49.1') - expect(step?.run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"') - expect(step?.run).toContain('src/shared/git-binary-compatibility.test.ts') - expect(step?.run).toContain('-j"$(nproc)"') - expect(step?.run).toContain('pids+=("$!")') - expect(step?.run).toContain('wait "$pid" || status=1') - }) - - it('restores the baseline Git build before the matrix runs', () => { - const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) - const steps = workflow.jobs.git_compatibility.steps - const cacheIndex = steps.findIndex((step) => step.name === 'Cache baseline Git build') - const matrixIndex = steps.findIndex( - (step) => step.name === 'Verify Git binary compatibility matrix' - ) - - expect(cacheIndex).toBeGreaterThanOrEqual(0) - expect(cacheIndex).toBeLessThan(matrixIndex) + expect(run).toContain('if [ -x "$source/git" ]; then') + expect(run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf') + expect(run).toContain('-j"$(nproc)"') // The cached path and the build path must be the same directory or the guard // above would rebuild on every run while still reporting a cache hit. - expect(steps[cacheIndex].with.path).toBe('~/.cache/orca-git-compat/git-2.25.5') - expect(steps[matrixIndex].run).toContain('source="$HOME/.cache/orca-git-compat/git-2.25.5"') + expect(run).toContain('source="$HOME/.cache/orca-git-compat/git-2.25.5"') + }) + + it('finishes the baseline build before the timed lanes start', () => { + const steps = gateSteps() + const names = steps.map((step) => step.name) + const cacheIndex = names.indexOf('Cache baseline Git build') + const buildIndex = names.indexOf('Build the baseline Git binary') + const matrixIndex = names.indexOf('Verify Git binary compatibility matrix') + + expect(cacheIndex).toBeGreaterThanOrEqual(0) + expect(cacheIndex).toBeLessThan(buildIndex) + expect(buildIndex).toBeLessThan(matrixIndex) + // Why asserted: each lane is bounded by Vitest's per-test timeout while it waits on + // container starts, so a `make -j$(nproc)` sharing the runner shows up as a timeout + // in whichever boundary case is running rather than as a slow build. + expect(steps[matrixIndex].run).not.toContain('make -C') + expect(steps[cacheIndex].with.path).toBe(BASELINE_DIR) expect(steps[cacheIndex].with.key).toContain('2.25.5') }) + + it('pulls every matrix image before any lane runs', () => { + const run = stepNamed('Verify Git binary compatibility matrix')?.run + // A lazy pull inside one lane stalls whatever test the sibling lane is timing. + const [beforeLanes] = run.split('pids=()') + + expect(beforeLanes).toContain('docker pull --quiet "${spec%%|*}"') + }) }) diff --git a/config/scripts/hourly-build-version.test.mjs b/config/scripts/hourly-build-version.test.mjs index 08fc9d28b81..7438b16acbd 100644 --- a/config/scripts/hourly-build-version.test.mjs +++ b/config/scripts/hourly-build-version.test.mjs @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { createHourlyBuildVersion, formatHourlyReleaseName, + getHourlyBuildIdentity, nextHourlyBuildNumber } from './hourly-build-version.mjs' import { compareAppVersions } from '../../src/shared/app-version' @@ -120,3 +121,26 @@ describe('nextHourlyBuildNumber', () => { expect(nextHourlyBuildNumber('1.4.163', ['v1.4.163-hourly.202607311354', null, ''])).toBe(1) }) }) + +describe('getHourlyBuildIdentity', () => { + // 2026-09-14: v1.4.202's GitHub release was deleted for a bug after hourlies + // had climbed to 1.4.203. Passing the leftover tag and the already-shipped + // hourly keeps the next build on 1.4.203 so electron-updater will still + // install it. + it('stays on the already-shipped hourly base after a buggy main release is unpublished', () => { + const identity = getHourlyBuildIdentity(new Date('2026-09-14T20:00:00Z'), { + publishedVersions: [ + 'v1.4.201', + 'v1.4.202', + 'v1.4.202-hourly.202609141912', + 'v1.4.203-hourly.202609140417' + ], + releaseNames: [ + '1.4.202 • 14 • Sep 14, 12:12PM • 875b86d', + '1.4.203 • 04 • Sep 13, 9:17PM • 2ce252f' + ] + }) + expect(identity.version).toBe('1.4.203-hourly.202609142000') + expect(identity.buildNumber).toBe(5) + }) +}) diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index ed4e1b1f1c8..d18837a1573 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -15,6 +15,7 @@ const shellContractFiles = [ 'src/main/daemon/shell-ready.test.ts', 'src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts', 'src/main/providers/__tests__/shell-ready-framework-example.test.ts', + 'src/main/pty/omp-shell-wrapper-alias-safety.test.ts', 'src/main/pty/omp-shell-wrapper.node-pty.test.ts', 'src/main/shell-startup-feature-channel.test.ts', 'src/main/zsh-scoped-histfile.live-shell.test.ts', diff --git a/config/scripts/session-search-pass-benchmark.ts b/config/scripts/session-search-pass-benchmark.ts new file mode 100644 index 00000000000..6576baa9589 --- /dev/null +++ b/config/scripts/session-search-pass-benchmark.ts @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict' +import { mkdir, mkdtemp, rename, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join } from 'node:path' +import { isolatedScanRoots } from '../../src/main/ai-vault/session-scanner-test-fixtures' +import { resetSessionParseCacheForTests } from '../../src/main/ai-vault/session-scanner-parse-cache' +import { resetTranscriptConsumersForTests } from '../../src/main/ai-vault/session-transcript-consumers' +import { SessionSearchIndexer } from '../../src/main/ai-vault-search/session-search-indexer' +import { writeSyntheticTranscriptCorpus } from '../../src/main/ai-vault-search/session-search-synthetic-corpus' + +// Bundle with esbuild --bundle --platform=node, then run on the host under test. +// What a warm pass costs on a machine with a real number of transcripts: a cycle +// stats the newest N per agent, a sweep stats every file under every root, and +// neither reads anything the index already holds at its current stat. This is the +// number the reconcile interval is chosen against; it does not set one. +// Never point this at a real transcript tree. + +const SESSIONS = 5_000 +const TURNS_PER_SESSION = 4 +const PROJECTS = 40 + +const corpus = await writeSyntheticTranscriptCorpus({ + sessions: SESSIONS, + turnsPerSession: TURNS_PER_SESSION +}) +const root = await mkdtemp(join(tmpdir(), 'orca-search-pass-')) +const roots = isolatedScanRoots(root) +const databasePath = join(root, 'index', 'session-search.sqlite') + +try { + // A flat corpus is not what discovery walks: spread it over project directories + // so the readdir count is realistic rather than one enormous listing. + for (let index = 0; index < PROJECTS; index++) { + await mkdir(join(roots.claudeProjectsDir, `project-${index}`), { recursive: true }) + } + await Promise.all( + corpus.files.map((path, index) => + rename(path, join(roots.claudeProjectsDir, `project-${index % PROJECTS}`, basename(path))) + ) + ) + + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + const errors: unknown[] = [] + const indexer = new SessionSearchIndexer({ + databasePath, + roots, + historyDays: null, + // No wall-clock ceiling: the cold build has to finish before a warm pass can + // be measured, and a deadline would leave a backlog priced into every number. + passDeadlineMs: Number.MAX_SAFE_INTEGER, + onError: (error) => errors.push(error) + }) + try { + const coldStarted = performance.now() + await indexer.start() + const coldMs = performance.now() - coldStarted + assert.deepEqual(errors, []) + assert.equal(indexer.status().filesIndexed, SESSIONS, 'indexed file count') + + const sweepStarted = performance.now() + await indexer.reconcile({ full: true }) + const sweepMs = performance.now() - sweepStarted + + const cycleStarted = performance.now() + await indexer.reconcile({ full: false }) + const cycleMs = performance.now() - cycleStarted + + assert.deepEqual(errors, []) + assert.equal(indexer.status().filesDue, 0, 'nothing owed after a warm sweep') + console.log( + JSON.stringify( + { + transcripts: SESSIONS, + projectDirectories: PROJECTS, + transcriptMb: Math.round((corpus.transcriptBytes / (1024 * 1024)) * 100) / 100, + coldBuildMs: Math.round(coldMs), + warmSweepMs: Math.round(sweepMs), + warmCycleMs: Math.round(cycleMs) + }, + null, + 2 + ) + ) + } finally { + indexer.close() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + } +} finally { + await rm(corpus.root, { recursive: true, force: true }) + await rm(root, { recursive: true, force: true }) +} diff --git a/config/scripts/workflow-ref-mirror-case-safety.test.mjs b/config/scripts/workflow-ref-mirror-case-safety.test.mjs index 497a58653a0..8ebb2fced9d 100644 --- a/config/scripts/workflow-ref-mirror-case-safety.test.mjs +++ b/config/scripts/workflow-ref-mirror-case-safety.test.mjs @@ -30,6 +30,25 @@ describe('ref-mirroring vet steps', () => { ).toBe(true) }) + // Why matching-refs rather than `gh release list` on the main repo: a tagged + // stable still owns its number after its GitHub release is unpublished for a + // bug, and that unpublish must not drag the channel backwards. + it.each(['daily', 'hourly', 'adhoc'])( + '%s versions from git tags, not main GitHub releases', + (channel) => { + const step = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[ + `build-${channel}-mac` + ].steps.find((candidate) => candidate.name === `Compute ${channel} version`) + expect(step.run).toContain('git/matching-refs/tags/v') + expect(step.run).not.toMatch( + /gh release list[\s\S]*--repo "\$GITHUB_REPOSITORY"[\s\S]*--json tagName/ + ) + if (channel !== 'adhoc') { + expect(step.run).toContain('channel_tags=') + } + } + ) + it('retains release-cut history for version reservation and retry ancestry', () => { const checkout = readWorkflow('.github/workflows/release-cut.yml').jobs.cut.steps.find( (step) => step.uses === 'actions/checkout@v6' diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 7a0586b8b1e..bd7a18f8488 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 53m + + downloads: 54m @@ -15,7 +15,7 @@ downloads downloads - 53m - 53m + 54m + 54m diff --git a/docs/reference/git-compatibility.md b/docs/reference/git-compatibility.md index 1e19860385e..d537c4d94de 100644 --- a/docs/reference/git-compatibility.md +++ b/docs/reference/git-compatibility.md @@ -69,6 +69,12 @@ PR checks run the capability contract against real Git 2.25.5, 2.38.1, and 2.49.1 binaries. This spans the pre-2.29 serialized `FETCH_HEAD` fallback, the transitional `merge-tree --write-tree` behavior before `--merge-base`, and current Git. +The three lanes run in parallel and each Git call in the container lanes costs a +container start, so their wall clock is runner contention, not Git. Build the +2.25.5 binary and pull the images before the lanes start: anything heavy left +running alongside them is charged to whichever boundary case is in flight and +surfaces as a Vitest timeout rather than as a slow setup step. + Keep the unit tests alongside that matrix. They cover concurrent probes, native/WSL/SSH/relay isolation, and error-stream shapes that a single real binary invocation cannot exercise deterministically. diff --git a/docs/reference/malformed-worktree-registration-removal.md b/docs/reference/malformed-worktree-registration-removal.md new file mode 100644 index 00000000000..34a32e58caf --- /dev/null +++ b/docs/reference/malformed-worktree-registration-removal.md @@ -0,0 +1,43 @@ +# Malformed worktree registration removal + +Git can report a linked worktree at `/.git` when its administrative +`gitdir` backlink incorrectly ends in `.git/.git`. That reproduces #17316's +validation error. The reproduction establishes the malformed registration, not +which program created it; current OMP uses ordinary `git worktree add`. + +Orca's desktop and runtime removal entry points use registration-only recovery +when Git positively marks the row prunable, the row has a named local branch and +HEAD, it is neither main nor locked, and the execution filesystem confirms the +selected `.git` path is a regular file. Missing or unknown evidence does not +permit this recovery. A symlink or directory is not a regular-file proof. + +Recovery reuses `git worktree prune` followed by a strict worktree listing that +must confirm the selected registration is gone. It does not delete the selected +file, infer a parent path for deletion, or delete the branch. Archive hooks and +checkout teardown are skipped because the selected row is not a checkout. + +Two consequences are intentional: + +- Git's prune also clears other stale, unlocked registrations in the repository; + it is not a path-scoped command. Live and locked registrations remain Git's + responsibility, and Orca verifies that the requested registration disappeared. +- The surviving checkout's `.git` file points at removed administrative metadata. + Files and its named branch are preserved; recovery removes the broken navigation + entry and does not repair or claim to restore that checkout. + +Native and WSL checks use the existing execution-filesystem accessor. WSL prune +and verification use the same selected distro. Paired runtimes run the recovery +on their owning host. Direct SSH does not enter this local recovery: its current +provider has no registration-only removal operation, and a failed remote removal +never authorizes a local fallback. + +The Git commands already exist in the 2.25-compatible cleanup path. On an older +Git that cannot positively attest this file-shaped registration as prunable, Orca +refuses this recovery. Deferred deletion independently rejects non-directory and +symlink targets, so force cannot move a `.git` file into deletion trash. + +Regression coverage is in `worktree-prunable-git-file.test.ts`, +`worktrees-removal-recovery.test.ts`, and +`worktree-deferred-removal-real-git.test.ts`. The latter reproduces the exact +malformation against the installed Git binary in a disposable repository and +checks surviving file contents, branch HEAD, and removed registration. diff --git a/docs/reference/omp-history-titles.md b/docs/reference/omp-history-titles.md new file mode 100644 index 00000000000..f238574ef2a --- /dev/null +++ b/docs/reference/omp-history-titles.md @@ -0,0 +1,34 @@ +# OMP history titles + +The message-graph scanner uses persisted OMP names ahead of the first user prompt: +`session.title`, version-1 `title` slots, `title_change.title`, and legacy +`session_info.name`. Empty or unsupported metadata leaves the previous name or +prompt fallback intact. Non-OMP graph parsing keeps its existing title policy. + +Explicit user names outrank automatic names. Within the same source, timestamps +prevent the current first-line title slot from being replaced by older rename +entries later in the file. Newer appended renames still update the row. Legacy +records without timestamps retain file-order handling. + +The graph fold stores title authority alongside the existing accumulator. Clones +retain it without sharing mutable accumulator or preview state, while preserving +the existing identity and message-consumer contracts. Cached append parsing uses +the normal durable offset; no extra scan, process, poll or watcher is introduced. + +The parser is shared by local and remote content readers and uses transcript data +from the execution host. It performs no client-side path lookup and changes no +wire shape. Folder workspaces require no git metadata. + +Run actual persistence and cache validation with a read-only OMP checkout: + +```sh +ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-history-title-smoke.mjs /path/to/oh-my-pi +``` + +The smoke persists a first prompt, performs a real OMP user rename, and verifies +both cold and incrementally cached scans. It checks one full parse, one append +parse and identical-object reuse on an unchanged scan. All home/config/data roots +are disposable; no model requests are made. + +This is the OMP subset of the history-name behavior proposed in PR #15696 by +Brennan Benson. Pi naming and title changes in the terminal are separate concerns. diff --git a/mobile/README.md b/mobile/README.md index 64f1081b73c..e78c2c410bf 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -184,6 +184,7 @@ Connect from the app using endpoint `ws://localhost:6768` and token `mock-device ### Environment variables - `MOCK_NATIVE_CHAT=1` — serve the native-chat scenario (one live agent tab, empty transcript, image upload) instead of the default terminal fixtures. +- `MOCK_CHAT_AGENT=omp` — with `MOCK_NATIVE_CHAT=1`, present an OMP tab and four decoded transcript messages, including a tool call and result, instead of the default Claude scenario. It deliberately omits `transcriptPath` to exercise legacy-hook readability discovery; current OMP hooks may report a path. - `MOCK_SERVER_KEY_FILE` — persist the server keypair across restarts so a paired device keeps its public-key pin. A missing or invalid file is re-keyed with a warning, which forces a re-pair. ### Scenario control files diff --git a/mobile/app.json b/mobile/app.json index 6121923f775..cfe66109e22 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Orca", "slug": "orca-mobile", - "version": "0.0.48", + "version": "0.0.50", "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", diff --git a/mobile/rpc-foundation/goldens/b1.json b/mobile/rpc-foundation/goldens/b1.json index 604256d7437..62348307747 100644 --- a/mobile/rpc-foundation/goldens/b1.json +++ b/mobile/rpc-foundation/goldens/b1.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "23ffc912a432dcd3ff70be1903a8d518cf85634f27a2be6d21585963e338e7e3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0d903486cbe8": { "name": "files.list#2", diff --git a/mobile/rpc-foundation/goldens/b2.json b/mobile/rpc-foundation/goldens/b2.json index c919ca011f5..7a8c6d229a5 100644 --- a/mobile/rpc-foundation/goldens/b2.json +++ b/mobile/rpc-foundation/goldens/b2.json @@ -3,14 +3,15 @@ "family": "project-explicit-false", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "b31992be2f91bd61fbe1b8a5400da3b7a56753564b0b0b2b38bc5d549812d693", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0c4dced3e005": { "error": "", diff --git a/mobile/rpc-foundation/goldens/b3.json b/mobile/rpc-foundation/goldens/b3.json index 78a1038358e..94931d8971f 100644 --- a/mobile/rpc-foundation/goldens/b3.json +++ b/mobile/rpc-foundation/goldens/b3.json @@ -3,14 +3,15 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "130e493fcd7765e037405f59e6cc78a0cc1793b1ae092cad933ff9d5a9df8b7a", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "034a83431f03": { "name": "linear.getIssue#1", diff --git a/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json index 81d6adfb2f4..2202f2f90b8 100644 --- a/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/interruptions-inventory-lifecycle.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "d6c57a5153d915f0a0c0fd9e305cac70b41b7eb8be226fc865227ebf1821e5d1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2837f481a843": { "name": "files.list#1", diff --git a/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json index 0a1aca3b4d6..fbe4f1277c1 100644 --- a/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/interruptions-settings-bot-overrides-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "2d5c6dea28aa1a7bb9e4aa14a4c8441527d9ad401ad30161f05ea1f8da836bb2", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06eff8247d02": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/inventory-lifecycle.json b/mobile/rpc-foundation/goldens/inventory-lifecycle.json index ad3b9c43a9a..9c02a306ee0 100644 --- a/mobile/rpc-foundation/goldens/inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/inventory-lifecycle.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "3471f5bcd6923c7b8ba3a737bb45b5239689deb78c00e85a828f38a6d6d68a05", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2837f481a843": { "name": "files.list#1", diff --git a/mobile/rpc-foundation/goldens/inventory-repeat-query.json b/mobile/rpc-foundation/goldens/inventory-repeat-query.json index 64a3d7492f0..279b436b7a3 100644 --- a/mobile/rpc-foundation/goldens/inventory-repeat-query.json +++ b/mobile/rpc-foundation/goldens/inventory-repeat-query.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "73a468d5c7a51c2dbb7af2642f0050d05d861fce29295460c48d7c51f86bf57f", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "003a57e2bf31": { "files": ["alpha.ts"] diff --git a/mobile/rpc-foundation/goldens/lifecycle-b3.json b/mobile/rpc-foundation/goldens/lifecycle-b3.json index e0d25b4724f..89f552c989c 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-b3.json +++ b/mobile/rpc-foundation/goldens/lifecycle-b3.json @@ -3,14 +3,15 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "8be12d116865d27e8dfd37921d2c723d63da101ec1197b1f5b2d9510838e1943", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "034a83431f03": { "name": "linear.getIssue#1", diff --git a/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json b/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json index ca3b76527e9..15396d1d796 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json +++ b/mobile/rpc-foundation/goldens/lifecycle-inventory-lifecycle.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "46bbafcc57fe2e3aee41a14bc26a0375b7b56e58030705fe4c28841a272b2560", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2837f481a843": { "name": "files.list#1", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json index f43d942e36d..0f64df064b4 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-bot-overrides-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "eb80283956c93849778f23cbabf1dbf83b72744197af4f6f50335b2fc1590d87", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json index 151c59c919f..945bff9140f 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-task-hydration-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "3a8eab831602443d320ca0aa0f35dc269d8d511e76bdae8fd025c433561d068d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json index 488a700a09a..7176d0db11b 100644 --- a/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/lifecycle-settings-workspace-context-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "136fb1d8d5925ad12ba22f4dd6c72573a9ad03b6a6ec8308668f0d9cd71aa36d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06425d8da2e6": { "name": "linear.status#2", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json index 96fbed0b57d..8798fc37eb9 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.baserefdefault-1.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "5605a2984d7692aa80e5e38f804bdfed4b1ce8ac2102def5dc728b1a79dc1acf", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0208d586a748": { "name": "repo.baseRefDefault#1", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json index 347625dadf3..93d14399294 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-repo.list-1.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "ed0e97ead1aad0b45bdfc48f5fe4e498810d0cfee88f07d3c6228db56fda1dd9", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "089d79f002a1": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json index 2717fb396a0..197474a5f06 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.base-ref-chain-worktree.show-1.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "1db6919b94df3b8548838ff4c206fafa3a09ea096b17c04483f78f9321ccb1ba", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "089d79f002a1": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json b/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json index 21171bf6d16..d7b195213a4 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.commit-message-ai-git.generatecommitmessage-1.json @@ -3,14 +3,15 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "22ea5279155ecf749aaab521ffd570221ac3169b177fc1daf85ef93a49d38260", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "125fbea5f50a": { "name": "git.generateCommitMessage#1", diff --git a/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json b/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json index abed8bf074b..0c22c57019d 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.history-read-git.history-1.json @@ -3,14 +3,15 @@ "family": "git.history-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "86254ed87ad3427d6ee4631d7348075039ba2d4d7496d59f27f03f78580f35a1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "155f61ed496f": { "status": "rejected", diff --git a/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json index 97346d0e6b4..62e2a2ddb99 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.remote-prerequisite-git.push-1.json @@ -3,14 +3,15 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "5009c22df7e74a850bcea41fc110ea7d7eb4bdada623837279f32eaa5149a9b8", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "00e8a3bac22f": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json index 9f417b48053..57dd65fa040 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.review-preparation-git.status-1.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "485b2751006ee8fb4df28b228ea7adda85779feae83974eb0f7e795e31c500a1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json index ce182135651..5d3fe5d8c90 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-git.push-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "fcf5cdc7388457156dd81fe28a470f42fbabac7435ec5572cb19e209f410ca84", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06a94a810e5f": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json index 0e1fbba63c7..f388e41ecc2 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-hostedreview.create-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "26e666a57805f354602a5b3906a691b10c8d6db66c77acc96c67153279c515a7", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06a94a810e5f": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json index c65b6cf6558..663a04d18ba 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-chain-worktree.set-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "08c25b6cb5bc12a7f67e858f229d15cf66b98b2ad4601b11f18c4c03f4a59669", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "03696d515352": { "name": "worktree.set#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json index 8bbc14962a5..036ff020b49 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.bulkstage-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "ff9d1bfd6337607f3d3e8162692b589ecea4a32ae01b5ebb3c602f8f0a55642c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02652fe244f8": { "name": "git.bulkStage#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json index f457f4b0052..d9512719419 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.commit-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "efeb9b248aeb98fac71c043d50afe0036cf804d3c11edfccd4e050fe8f3d8f9b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json index 488238d3bca..1947eb747af 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.generatecommitmessage-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "7b0d9ddcb8df83fc4e465aa6b0dcf05aa0d8f266cd4bb8651969cb8321bcf549", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json index 20ba5f1e127..8212022310a 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.push-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "72c1f08739db1c0dfcd48adffaca582a3596116c1c377f95f7dab8b08b7e6cdc", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json index d4ce67f0673..13e2571c7e5 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "1f002f900c1a3c92e8f7c72261579ee5015ec1529c003a1b32bcf3eaf98b672d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0179846b4707": { "name": "git.status#2", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json index ffa68cf4114..0c3a44a5b0b 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-2.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "3a6487a07457e0e5aa6fc3fccfa43687acfb06d94334e621081728de937e4e8d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "006d7b20ed48": { "name": "git.status#2", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json index 75c0eae29c7..7503ae5ba1f 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-3.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "7355c45a707fa8a31f0999c4805a5b1dace4c65b727e711231f784b2f92c05ff", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "01bc4ad46170": { "outcome": { diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json index aefc48d7680..6a18291ed1c 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-git.status-4.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "45cda6757b76399d282d4b07992dab21bbb8236faadedba5e92eab8818e886bf", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "01bc4ad46170": { "outcome": { diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json index 5e25cfa990e..ec85a0974ac 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.create-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "0ca08d5e70e1780a6ee5c919491dcddb062a22623f803e9960a329825f274cbe", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "000efa3053f3": { "outcome": { diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json index 7fda51cd916..a4f83a9559d 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "658eb7bbf63b3a4b38eca0b1733e523962b6b6943644d65aab6f5c7e62534d6a", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json index 2c3dc706047..7e350e18a2a 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-hostedreview.getcreationeligibility-2.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "75ba135731290bf734a5eef0b65f9ad8b7cac453c4e2006faac88a5da9dbe3a3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json index 021c9f2d320..da2a5b2c4ab 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.create-intent-worktree.set-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "beb1161b98ffde8c5f1128e843766a1da3182d195f1f0a9012e12e5318ae01bc", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json b/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json index 04dd7052c2e..35613ec8d32 100644 --- a/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json +++ b/mobile/rpc-foundation/goldens/matrix-hostedreview.eligibility-hostedreview.getcreationeligibility-1.json @@ -3,14 +3,15 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "6c4de90e2617d204e82ca5e65eb17fc397acbcbb9dc0ec18594d2a7739e3528b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "099a55e691ed": { "name": "hostedReview.getCreationEligibility#1", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json index 9d016af3ca0..cfaa250b313 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-1.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "4f6472fb7add960be9bcc8596a748264d7cb0755a782ebe9e85753ab1d1d5710", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "003a57e2bf31": { "files": ["alpha.ts"] diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json index 4443cb77710..d35e26c6955 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-files.searchpaths-2.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "048c3ec55ec67d09d9b02e17822f1154adca577e57ffe6d3059102d552d2f759", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02a03f44e95f": { "name": "files.searchPaths#2", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json index dcab97a3a4e..5bcd165c55b 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-fresh-inventory.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "ad458a3407e3f1303343b46a1308b43535abef2c9ed2f68db59157db5b91daa1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0d903486cbe8": { "name": "files.list#2", diff --git a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json index ff0a6d66acc..fdab5a1a644 100644 --- a/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json +++ b/mobile/rpc-foundation/goldens/matrix-legacy-inventory-old-inventory.json @@ -3,14 +3,15 @@ "family": "legacy-inventory", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "262eaad263a45aa13ec5b27c12b59946b12c202474229fff7a5727dba6d702ca", "scenarioSha256": "52742d894d0ea53db89729101664a393b10794d9c2d2fe7b40b020643a13af81", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0d903486cbe8": { "name": "files.list#2", diff --git a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json index 098e80e7c82..01c46dfd792 100644 --- a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json +++ b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.getissue-1.json @@ -3,14 +3,15 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "8e00afc85e5b82d75bedecea0c748a3c8658cfc8545650e755c03f51fdc932d6", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1696f2f90218": { "name": "detailError", diff --git a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json index 397e91d5859..31ee0f4511b 100644 --- a/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json +++ b/mobile/rpc-foundation/goldens/matrix-linear-detail-barrier-linear.issuecomments-1.json @@ -3,14 +3,15 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "40289acce4a3542773f74681d255d67cfddadf6c42317928d6728f26a76f6cfb", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "034a83431f03": { "name": "linear.getIssue#1", diff --git a/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json b/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json index e564c087182..d5878ea9801 100644 --- a/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json +++ b/mobile/rpc-foundation/goldens/matrix-project-explicit-false-github.project.updateissuebyslug-1.json @@ -3,14 +3,15 @@ "family": "project-explicit-false", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "926f0d8c37a33d465bf3a04f056600cfc9f1669b1eca7e968aa1a1f797a74c61", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "057a0b5a420b": { "name": "projectRowDetailError", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json index b5bfcc6c34b..0e807700b9b 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.activate-1.json @@ -3,14 +3,15 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "73366226aeaec1581aeeb47219fc703917143cfd7f6a2eb01d7bd703a7c7612d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0f9f4df04699": { "name": "session.tabs.activate#1", diff --git a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json index eb85e61870d..cd298741f2c 100644 --- a/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-session.tab-reveal-session.tabs.list-1.json @@ -3,14 +3,15 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "c38f2bc5c9faca0774dfe202137877bada9deba165c5e9c955cbe67eae0cbdd9", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0f9f4df04699": { "name": "session.tabs.activate#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json index 748dbe75ae2..e100f255245 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-preflight.detectremoteagents-1.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "a0effc9a0be519ccd18c1b1abfc8b497cd3858b89ea8d345ac0f8bd6d195cf21", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "07d4c9b0eaf2": { "name": "preflight.detectRemoteAgents#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json index 9946296e487..f0d53f404ce 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-repo.list-1.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "c21b2e0e97fab86664f634cc99d77dd587df4af4d02e6286c8380e09844096b2", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06b63e0d9986": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json index 2063dab2889..8ac2bce1cc0 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-agent-read-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "46c4e32a921612c736c8cf45ff72ed513431c917ed3dd03f289c0ba4c28d6adb", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json b/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json index 2ab9a497475..bd5bdab3e2e 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings-best-effort-settings.update-1.json @@ -3,14 +3,15 @@ "family": "settings-best-effort", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "cf671da175d50a4c2e1336f4e8338c24c4752db111e1eafd226bee6ff3582b1d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "178d4ef77ad7": { "name": "settings.update#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json index 786467ef280..a8cebde9ba2 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.bot-overrides-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "01408cebcc193f8e30119381c8acf494fa5e29850fe010809deb330c2f9bcb36", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json index de544d6c810..837f1e41e21 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-linear.status-1.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "19b445b39da98d28bcbcdab6f70e47ce208ca68f165e7b62c5fe9762eee67c8d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0e7c79cad23f": { "name": "linear.status#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json index 8f1903277ee..37743f6dc2f 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-preflight.check-1.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "4953dd0de509ce620b9840d7f460e472dc74f54d53636694d12cba2e3bb51da8", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0dcc6f40d62e": { "name": "preflight.check#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json index 0b50cfc9f24..3b72882ed96 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.home-providers-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "70f601caeaee957bd3b172fc0fc12e85d6e2d6bed7683c86869559c6c9f25834", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090d7111bcf7": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json index 047c08fea63..43e18b8eada 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-host.platform-1.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "687b109bd2bcc0c85b7c858d553e68e2fc4cb5b281d9f8b32836dbacc4bdc8f2", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json index 4909dbc80b6..3239d2ffba2 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-repo.list-1.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "3199c745e22973b432b0a36c34bb0bdda994334a4a0cd7ad2daf8b172625ce8d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json index f2a4002299a..bc395524564 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "5e9c3ff57cf432b24a17ee046636b61b94116a687cfa506cd79dee464542b76b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json index 6745742e69d..16895484050 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.repo-metadata-ssh.listtargetsummaries-1.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a88061d3d1f03074b0ed2b663b523f1602362bc317ba614106f1f646d037d6e3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json index 4d6759cb4bb..7723f38ff63 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-folderworkspace.list-1.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "50ba7c7963cb494e4b3d484eb334977d21cc69018da57d75a7b6fd0c92860bd2", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json index 56603458116..819a294f60d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-projectgroup.list-1.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "8dfd4550b39f0cfcb9aaa72fab0631b11f9e776ed389b206b326359d7f4c2d6e", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "01bc8208ba89": { "name": "projectGroup.list#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json index 642386c57c2..78b46cb6476 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-repo.list-1.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "85dc526201f66409dd6a411c5e14615b82389791ec210efb9889078f5d580373", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0ae64c827aea": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json index de758cfca98..f8d9736641f 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "080e10ae774ef097082267da0c8b6d0ebacae582d57b04a189c123258d0e5131", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json index c4874e981ea..ecd963ba3a6 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.resume-metadata-worktree.ps-1.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "5988fa5ce0bf6b8585f7ec66918123ee086d5cdf1185a4eeff2e88904985064c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0620c0819077": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json index 5118bc610db..c7b1701896d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-linear.status-1.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "f5033a7a3567cc9e016bf09ac8bcd8ff381c3054c041dbccc773f7011918bf1d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json index dcd34e9ac43..b197694bd09 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-preflight.check-1.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "0931b3d35868e5452cb550962f2408b6ce7cd6c89e90a9cf2897425edbb4b42d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json index 76565b7a9e0..df24acfbdf6 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "d0f8d9bfe0e1469af3b0dab8b5c9799d91cc2234e72f0e031d6872059654077d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json index b248c54e214..a09d4cedb0c 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-status.get-1.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "3f448feff463b59c3927dae020ecd8d4931bb4a6036df6d6af080de2ec5fcf2b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json index fdee42d21bc..ce339e45ed0 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-hydration-ui.get-1.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "4438f9fd62876333bb980157612aaf457c5a9b9115659c8c941c3b373ad071dd", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json new file mode 100644 index 00000000000..a26c457561d --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json @@ -0,0 +1,861 @@ +{ + "operation": "settings.task-workspace-create", + "family": "settings.task-workspace-create", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", + "scenarioSha256": "7e4c5bb29e0f630cda8a09233575b9295e485f3d3e315ebdc0458c69515fcfc7", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "067cef118d9f": { + "name": "runtimeTaskSettings", + "value": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "090c88478661": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "0f72e7ee78c9": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "displayName": "ORC-1 Recorded issue", + "id": "wt-1" + } + } + } + } + }, + "0fc3e204e7ba": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "127ad2bdc042": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "180125f5d1a6": { + "name": "workspaceCreateDraft", + "value": { + "$rpc": "null" + } + }, + "2473f12c7cdd": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + } + } + } + }, + "2b3aa0da0852": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "33e3b949d4c5": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "3405a06dce84": { + "name": "error", + "value": "Selected agent is disabled. Choose an enabled agent before creating." + }, + "3f453dd79b03": { + "name": "workspaceAgent", + "value": "codex" + }, + "6a98511b6371": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "6eb4e79ad99a": { + "name": "setupPrompt", + "value": { + "$rpc": "null" + } + }, + "730f92993963": { + "name": "creatingKey", + "value": "linear:1" + }, + "7abdfe20af50": { + "creating": "linear:1", + "error": "", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, + "7ddcb1852b39": { + "name": "settings.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, + "7ee99993a895": { + "name": "navigation", + "value": "/h/host-1/session/wt-1?name=ORC-1+Recorded+issue&created=1" + }, + "82cd71d524c8": { + "name": "error", + "value": "" + }, + "8b77098df0c3": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "8f8296303a77": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "ac9996319e05": { + "name": "actionItem", + "value": { + "$rpc": "null" + } + }, + "adec34c2065c": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": {} + }, + "b759ab27e4dd": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "baa74a0ec378": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"orc-1\",\"displayName\":\"ORC-1 Recorded issue\",\"displayNameKind\":\"generated\",\"linkedLinearIssue\":\"ORC-1\",\"setupDecision\":\"inherit\",\"activate\":true,\"startupDraft\":\"https://linear.app/orca/issue/ORC-1\",\"createdWithAgent\":\"claude\"}}" + }, + "c9cb32059b8d": { + "name": "creatingKey", + "value": { + "$rpc": "null" + } + }, + "d27ce798af34": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "d5df3f6b123a": { + "creating": { + "$rpc": "null" + }, + "error": "Selected agent is disabled. Choose an enabled agent before creating.", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, + "dae7907f03cc": { + "name": "runtimeTaskSettings", + "value": {} + }, + "e0cf1af55a54": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "e1bd8b4a5d70": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f84a8688af61": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "matrix-settings.task-workspace-create-settings.get-1", + "checkpoints": [ + { + "id": "settings-task-workspace-create-linear.prelude:settings-pending", + "observation": { + "sender": ["090c88478661"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "7abdfe20af50", + "effects": ["730f92993963", "82cd71d524c8"] + } + }, + { + "id": "settings-task-workspace-create-linear.prelude:cleanup", + "observation": { + "sender": ["f84a8688af61"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "7abdfe20af50", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.normal:created", + "observation": { + "sender": ["2473f12c7cdd", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.result-absent:created", + "observation": { + "sender": ["e0cf1af55a54"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.result-null:created", + "observation": { + "sender": ["e1bd8b4a5d70"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-ok-missing:created", + "observation": { + "sender": ["0fc3e204e7ba", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "adec34c2065c", + "effects": [ + "730f92993963", + "82cd71d524c8", + "dae7907f03cc", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-false-string-error:created", + "observation": { + "sender": ["d27ce798af34", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "adec34c2065c", + "effects": [ + "730f92993963", + "82cd71d524c8", + "dae7907f03cc", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-false-object-error:created", + "observation": { + "sender": ["127ad2bdc042", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "adec34c2065c", + "effects": [ + "730f92993963", + "82cd71d524c8", + "dae7907f03cc", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.outer-refused:created", + "observation": { + "sender": ["8f8296303a77"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.outer-refused-no-message:created", + "observation": { + "sender": ["6a98511b6371"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.method-not-found:created", + "observation": { + "sender": ["b759ab27e4dd"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.transport-rejection:created", + "observation": { + "sender": ["8b77098df0c3"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.transport-rejection-no-message:created", + "observation": { + "sender": ["2b3aa0da0852"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "d5df3f6b123a", + "effects": [ + "730f92993963", + "82cd71d524c8", + "3f453dd79b03", + "ea709e13f0f0", + "3405a06dce84", + "c9cb32059b8d" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json new file mode 100644 index 00000000000..603e3535a72 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-worktree.create-1.json @@ -0,0 +1,1011 @@ +{ + "operation": "settings.task-workspace-create", + "family": "settings.task-workspace-create", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", + "scenarioSha256": "1d7713cf4c23d053105c2abb02340d81d5eb689f4311a0984932d8ebd031b4ce", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "067cef118d9f": { + "name": "runtimeTaskSettings", + "value": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "090c88478661": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "0ca3bb7ac195": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "0f72e7ee78c9": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "displayName": "ORC-1 Recorded issue", + "id": "wt-1" + } + } + } + } + }, + "12b9d0436b8a": { + "name": "error", + "value": "Cannot read properties of undefined (reading 'displayName')" + }, + "180125f5d1a6": { + "name": "workspaceCreateDraft", + "value": { + "$rpc": "null" + } + }, + "186f44bc465a": { + "name": "error", + "value": "Unknown method" + }, + "1bb065c2a768": { + "creating": { + "$rpc": "null" + }, + "error": "Unknown method", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "2473f12c7cdd": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + } + } + } + }, + "2e80de97dd3b": { + "name": "error", + "value": "Cannot read properties of null (reading 'worktree')" + }, + "2f13b6f74cc6": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "2fac0da15fae": { + "creating": { + "$rpc": "null" + }, + "error": "transport failure", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "31738898988e": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "33e3b949d4c5": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "37345621a939": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "5b8e61be1638": { + "name": "error", + "value": "Cannot read properties of undefined (reading 'worktree')" + }, + "67b44e804cc9": { + "creating": { + "$rpc": "null" + }, + "error": "Cannot read properties of null (reading 'worktree')", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "6eb4e79ad99a": { + "name": "setupPrompt", + "value": { + "$rpc": "null" + } + }, + "730f92993963": { + "name": "creatingKey", + "value": "linear:1" + }, + "7abdfe20af50": { + "creating": "linear:1", + "error": "", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, + "7b27297e7f2d": { + "creating": { + "$rpc": "null" + }, + "error": "outer refused", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "7ddcb1852b39": { + "name": "settings.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, + "7ee99993a895": { + "name": "navigation", + "value": "/h/host-1/session/wt-1?name=ORC-1+Recorded+issue&created=1" + }, + "82cd71d524c8": { + "name": "error", + "value": "" + }, + "841ba02855c9": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "89456eae5a16": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "945ea389c1ef": { + "name": "error", + "value": "transport failure" + }, + "97348f3fe285": { + "creating": { + "$rpc": "null" + }, + "error": "Cannot read properties of undefined (reading 'worktree')", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "97dc8fc98386": { + "creating": { + "$rpc": "null" + }, + "error": "Cannot read properties of undefined (reading 'displayName')", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "9f82f10075a3": { + "name": "error", + "value": "Connection closed" + }, + "ac9996319e05": { + "name": "actionItem", + "value": { + "$rpc": "null" + } + }, + "adfc4e9a82be": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "ba65a7abe43b": { + "name": "error", + "value": "outer refused" + }, + "baa74a0ec378": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"orc-1\",\"displayName\":\"ORC-1 Recorded issue\",\"displayNameKind\":\"generated\",\"linkedLinearIssue\":\"ORC-1\",\"setupDecision\":\"inherit\",\"activate\":true,\"startupDraft\":\"https://linear.app/orca/issue/ORC-1\",\"createdWithAgent\":\"claude\"}}" + }, + "c7d9517809c8": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "c9cb32059b8d": { + "name": "creatingKey", + "value": { + "$rpc": "null" + } + }, + "eb44ca9ac41f": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "eecc0c1b6490": { + "creating": "linear:1", + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "f73b6faeedba": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "ff28e2c78e1b": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + } + }, + "recording": { + "scenario": "matrix-settings.task-workspace-create-worktree.create-1", + "checkpoints": [ + { + "id": "settings-task-workspace-create-linear.prelude:settings-pending", + "observation": { + "sender": ["090c88478661"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "7abdfe20af50", + "effects": ["730f92993963", "82cd71d524c8"] + } + }, + { + "id": "settings-task-workspace-create-linear.prelude:cleanup", + "observation": { + "sender": ["2473f12c7cdd", "eb44ca9ac41f"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "eecc0c1b6490", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "9f82f10075a3", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.normal:created", + "observation": { + "sender": ["2473f12c7cdd", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.result-absent:created", + "observation": { + "sender": ["2473f12c7cdd", "841ba02855c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "97348f3fe285", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "5b8e61be1638", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.result-null:created", + "observation": { + "sender": ["2473f12c7cdd", "0ca3bb7ac195"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "67b44e804cc9", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "2e80de97dd3b", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-ok-missing:created", + "observation": { + "sender": ["2473f12c7cdd", "ff28e2c78e1b"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "97dc8fc98386", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "12b9d0436b8a", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-false-string-error:created", + "observation": { + "sender": ["2473f12c7cdd", "89456eae5a16"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "97dc8fc98386", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "12b9d0436b8a", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.inner-false-object-error:created", + "observation": { + "sender": ["2473f12c7cdd", "f73b6faeedba"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "97dc8fc98386", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "12b9d0436b8a", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.outer-refused:created", + "observation": { + "sender": ["2473f12c7cdd", "c7d9517809c8"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "7b27297e7f2d", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ba65a7abe43b", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.outer-refused-no-message:created", + "observation": { + "sender": ["2473f12c7cdd", "2f13b6f74cc6"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "82cd71d524c8", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.method-not-found:created", + "observation": { + "sender": ["2473f12c7cdd", "adfc4e9a82be"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "1bb065c2a768", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "186f44bc465a", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.transport-rejection:created", + "observation": { + "sender": ["2473f12c7cdd", "31738898988e"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "2fac0da15fae", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "945ea389c1ef", + "c9cb32059b8d" + ] + } + }, + { + "id": "settings-task-workspace-create-linear.transport-rejection-no-message:created", + "observation": { + "sender": ["2473f12c7cdd", "37345621a939"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "82cd71d524c8", + "c9cb32059b8d" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json index 1a43cd52057..5e2a9fb63d9 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "994ea8b4ddb05774a8c2d5902bb68bf5e8f25399a787262b8f23f458f2790698", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json index b71bb648aae..783270cf37d 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-linear.status-1.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "30b3f8d79589e9fb3d7ef804233554fa231f68ab88e5130ddfa78e79221e3c78", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json index cb92182e684..b1fac82a003 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-preflight.check-1.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a5366cbd31d899feeb7e1901edd0c78191c2c8c8179ad5d5b24b7ca22bd538f8", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02eac6141a1f": { "name": "preflight.check#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json index d73c6ab0aa7..2d91c6cc1d6 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "797ea410af6536410335ebe93b8bc354cd633cf980eb95efbb10bc46f5516cb7", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json index d9457d90a8f..08544474c90 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-context-ui.get-1.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "3cd29e7b6a1cdfd99796a58cf6ad6f9aa3dbac75dd6e989ea99ba6027c210028", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0039f2221403": { "name": "ui.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json index 34486618f06..47f42d572ba 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json @@ -3,14 +3,15 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "a89bdf93df71a958810aba72c80e42f663644781a29e934898e2ddf86c5dd5d5", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json new file mode 100644 index 00000000000..3e68dafc652 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.reposlug-1.json @@ -0,0 +1,1082 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "749f877ac0c08860f74fc56e34b07f51960dda5bd1fdcf9df847b5200bf67779", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "09b235c17bb0": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "0e9d6525a582": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "11ab96fde6c9": { + "name": "gitlab.workItemByPath#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemByPath\",\"params\":{\"repo\":\"id:repo-1\",\"host\":\"gitlab.com\",\"path\":\"group/project\",\"iid\":7,\"type\":\"issue\"}}" + }, + "135faf86ace7": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "error": "inner refused", + "ok": false + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "1c3567f57943": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true + } + } + }, + "2113a0cc7708": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "285ceb964a96": { + "name": "github.repoSlug#2", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-2" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "398515139d34": { + "name": "github.repoSlug#2", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-2\"}}" + }, + "46e234697d93": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'toLowerCase')", + "isRpcDeliveryUnknown": false + } + }, + "4a3429622287": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "4eec4620374a": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "error": { + "message": "inner refused" + }, + "ok": false + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "5248ebd8f08a": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "65342779da15": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "6662fbe6a28e": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-4", + "ok": false + } + } + }, + "731507dd2e23": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + } + }, + "7445a582a9c8": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + } + }, + "8f410b944069": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-4", + "ok": false + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9e6675f5d017": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "a091594f56e6": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "$rpc": "null" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "a3d7eef0da8a": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-4", + "ok": false + } + } + }, + "a45a7dd68af6": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "aaad292bbd1b": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "aaf80675fc49": { + "name": "github.workItem#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItem\",\"params\":{\"repo\":\"id:repo-1\",\"number\":12}}" + }, + "b303200fec39": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "bd533f6b0b40": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "cded841b4a1b": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "d48fa181d583": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "error": "refused" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "e1f537905a65": { + "name": "github.workItemByOwnerRepo#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItemByOwnerRepo\",\"params\":{\"repo\":\"id:repo-1\",\"owner\":\"owner\",\"ownerRepo\":\"repo\",\"number\":12,\"type\":\"issue\"}}" + }, + "e29333b1693f": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + } + }, + "e2af62b90b0b": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "$rpc": "null" + } + ], + [ + "repo-2", + { + "$rpc": "null" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "$rpc": "null" + } + }, + "e970eb27f5ca": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + }, + "f0486ebd441c": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "f9ea1f747023": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.paste-lookup-github.reposlug-1", + "checkpoints": [ + { + "id": "tw-paste-lookup-resolved.prelude:by-number", + "observation": { + "sender": ["65342779da15"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "731507dd2e23" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.prelude:by-slug", + "observation": { + "sender": ["65342779da15", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.prelude:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "4a3429622287", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "2113a0cc7708", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "1c3567f57943", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "aaad292bbd1b", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "f0486ebd441c"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "46e234697d93" + }, + "state": "d48fa181d583", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "9e6675f5d017"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "46e234697d93" + }, + "state": "135faf86ace7", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "cded841b4a1b"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "46e234697d93" + }, + "state": "4eec4620374a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "a3d7eef0da8a", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "8f410b944069", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "6662fbe6a28e"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "ee20a1dc39e7" + }, + "state": "e2af62b90b0b", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "5248ebd8f08a", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:repo-slug-matched", + "observation": { + "sender": [ + "65342779da15", + "f9ea1f747023", + "e29333b1693f", + "b303200fec39", + "285ceb964a96" + ], + "payloads": [ + "aaf80675fc49", + "e1f537905a65", + "11ab96fde6c9", + "a45a7dd68af6", + "398515139d34" + ], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "9270aeb7d9c6" + }, + "state": "a091594f56e6", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json new file mode 100644 index 00000000000..865b44c4852 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitem-1.json @@ -0,0 +1,1646 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "2a11156b7b6d3cf0773c8dc02a72e126bc187dcf62ad7d1bf5f30d7b27192b03", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "06c63d693b0e": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "09b235c17bb0": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "0e9d6525a582": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "11ab96fde6c9": { + "name": "gitlab.workItemByPath#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemByPath\",\"params\":{\"repo\":\"id:repo-1\",\"host\":\"gitlab.com\",\"path\":\"group/project\",\"iid\":7,\"type\":\"issue\"}}" + }, + "19bc74accf11": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "1e7d56be018c": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "2113a0cc7708": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "21981303c684": { + "by-number": { + "$rpc": "null" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "38e35cd6299a": { + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "3bae2da7492a": { + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "3c03bbd195d8": { + "by-number": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "3e4f8a2833ba": { + "by-number": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "4267f9cc3919": { + "by-number": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "46daeacd502c": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "refused", + "repoId": "repo-1" + } + }, + "4a3429622287": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "5827c760c69a": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "5b601868bb59": { + "by-number": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "65342779da15": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "731507dd2e23": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + } + }, + "73fe68f6a6fc": { + "by-number": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "cache": [] + }, + "7445a582a9c8": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + } + }, + "870d10fe8de9": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "894b40a0b814": { + "by-number": { + "$rpc": "null" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "896610e0c4e7": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "8aa944d2f7a1": { + "cache": [] + }, + "8f8ff0f7d554": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "90de73e52a3d": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9dcdd903c10f": { + "by-number": { + "error": "refused", + "repoId": "repo-1" + }, + "cache": [] + }, + "9e9f15f7df58": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + } + }, + "a45a7dd68af6": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "a7f4472cdb70": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "aaf80675fc49": { + "name": "github.workItem#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItem\",\"params\":{\"repo\":\"id:repo-1\",\"number\":12}}" + }, + "ad658847a638": { + "by-number": { + "error": "refused", + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "b19c59c5ee88": { + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "b70f6ec811e0": { + "by-number": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bd533f6b0b40": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "c392cc9aa63a": { + "by-number": { + "$rpc": "null" + }, + "cache": [] + }, + "c57df09a398c": { + "by-number": { + "error": "refused", + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "c6646b64fc57": { + "by-number": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "cache": [] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c9a1abec42e3": { + "by-number": { + "$rpc": "null" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "d0150efe4124": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "d65cceb204a7": { + "by-number": { + "error": "refused", + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "de772915aa03": { + "by-number": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "e1f537905a65": { + "name": "github.workItemByOwnerRepo#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItemByOwnerRepo\",\"params\":{\"repo\":\"id:repo-1\",\"owner\":\"owner\",\"ownerRepo\":\"repo\",\"number\":12,\"type\":\"issue\"}}" + }, + "e29333b1693f": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + } + }, + "e970eb27f5ca": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f9ea1f747023": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "fb69e80392e8": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.paste-lookup-github.workitem-1", + "checkpoints": [ + { + "id": "tw-paste-lookup-resolved.normal:by-number", + "observation": { + "sender": ["65342779da15"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "731507dd2e23" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:by-slug", + "observation": { + "sender": ["65342779da15", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "4a3429622287", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "2113a0cc7708", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:by-number", + "observation": { + "sender": ["fb69e80392e8"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "ee20a1dc39e7" + }, + "state": "c392cc9aa63a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:by-slug", + "observation": { + "sender": ["fb69e80392e8", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23" + }, + "state": "21981303c684", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:gitlab-path", + "observation": { + "sender": ["fb69e80392e8", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "c9a1abec42e3", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:repo-slug-matched", + "observation": { + "sender": ["fb69e80392e8", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "894b40a0b814", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:by-number", + "observation": { + "sender": ["5827c760c69a"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "ee20a1dc39e7" + }, + "state": "c392cc9aa63a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:by-slug", + "observation": { + "sender": ["5827c760c69a", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23" + }, + "state": "21981303c684", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:gitlab-path", + "observation": { + "sender": ["5827c760c69a", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "c9a1abec42e3", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:repo-slug-matched", + "observation": { + "sender": ["5827c760c69a", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "ee20a1dc39e7", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "894b40a0b814", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:by-number", + "observation": { + "sender": ["19bc74accf11"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "46daeacd502c" + }, + "state": "9dcdd903c10f", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:by-slug", + "observation": { + "sender": ["19bc74accf11", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "46daeacd502c", + "by-slug": "731507dd2e23" + }, + "state": "d65cceb204a7", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:gitlab-path", + "observation": { + "sender": ["19bc74accf11", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "46daeacd502c", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "c57df09a398c", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:repo-slug-matched", + "observation": { + "sender": ["19bc74accf11", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "46daeacd502c", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "ad658847a638", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:by-number", + "observation": { + "sender": ["d0150efe4124"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "9e9f15f7df58" + }, + "state": "73fe68f6a6fc", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:by-slug", + "observation": { + "sender": ["d0150efe4124", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "9e9f15f7df58", + "by-slug": "731507dd2e23" + }, + "state": "b70f6ec811e0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:gitlab-path", + "observation": { + "sender": ["d0150efe4124", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "9e9f15f7df58", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "3e4f8a2833ba", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:repo-slug-matched", + "observation": { + "sender": ["d0150efe4124", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "9e9f15f7df58", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3c03bbd195d8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:by-number", + "observation": { + "sender": ["896610e0c4e7"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "a7f4472cdb70" + }, + "state": "c6646b64fc57", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:by-slug", + "observation": { + "sender": ["896610e0c4e7", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "a7f4472cdb70", + "by-slug": "731507dd2e23" + }, + "state": "4267f9cc3919", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:gitlab-path", + "observation": { + "sender": ["896610e0c4e7", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "a7f4472cdb70", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "5b601868bb59", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:repo-slug-matched", + "observation": { + "sender": ["896610e0c4e7", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "a7f4472cdb70", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "de772915aa03", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:by-number", + "observation": { + "sender": ["90de73e52a3d"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "32a7c0ae7918" + }, + "state": "8aa944d2f7a1", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:by-slug", + "observation": { + "sender": ["90de73e52a3d", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "32a7c0ae7918", + "by-slug": "731507dd2e23" + }, + "state": "b19c59c5ee88", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:gitlab-path", + "observation": { + "sender": ["90de73e52a3d", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "32a7c0ae7918", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "38e35cd6299a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:repo-slug-matched", + "observation": { + "sender": ["90de73e52a3d", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "32a7c0ae7918", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3bae2da7492a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:by-number", + "observation": { + "sender": ["870d10fe8de9"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "f3b516f62081" + }, + "state": "8aa944d2f7a1", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:by-slug", + "observation": { + "sender": ["870d10fe8de9", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "f3b516f62081", + "by-slug": "731507dd2e23" + }, + "state": "b19c59c5ee88", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:gitlab-path", + "observation": { + "sender": ["870d10fe8de9", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "f3b516f62081", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "38e35cd6299a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:repo-slug-matched", + "observation": { + "sender": ["870d10fe8de9", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "f3b516f62081", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3bae2da7492a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:by-number", + "observation": { + "sender": ["06c63d693b0e"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "b948e8307e81" + }, + "state": "8aa944d2f7a1", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:by-slug", + "observation": { + "sender": ["06c63d693b0e", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "b948e8307e81", + "by-slug": "731507dd2e23" + }, + "state": "b19c59c5ee88", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:gitlab-path", + "observation": { + "sender": ["06c63d693b0e", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "b948e8307e81", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "38e35cd6299a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:repo-slug-matched", + "observation": { + "sender": ["06c63d693b0e", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "b948e8307e81", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3bae2da7492a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:by-number", + "observation": { + "sender": ["1e7d56be018c"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "a947768bc0ed" + }, + "state": "8aa944d2f7a1", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:by-slug", + "observation": { + "sender": ["1e7d56be018c", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "a947768bc0ed", + "by-slug": "731507dd2e23" + }, + "state": "b19c59c5ee88", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:gitlab-path", + "observation": { + "sender": ["1e7d56be018c", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "a947768bc0ed", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "38e35cd6299a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:repo-slug-matched", + "observation": { + "sender": ["1e7d56be018c", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "a947768bc0ed", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3bae2da7492a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:by-number", + "observation": { + "sender": ["8f8ff0f7d554"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "c7584e82c72f" + }, + "state": "8aa944d2f7a1", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:by-slug", + "observation": { + "sender": ["8f8ff0f7d554", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "c7584e82c72f", + "by-slug": "731507dd2e23" + }, + "state": "b19c59c5ee88", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:gitlab-path", + "observation": { + "sender": ["8f8ff0f7d554", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "c7584e82c72f", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "38e35cd6299a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:repo-slug-matched", + "observation": { + "sender": ["8f8ff0f7d554", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "c7584e82c72f", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "3bae2da7492a", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json new file mode 100644 index 00000000000..95e65525868 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-github.workitembyownerrepo-1.json @@ -0,0 +1,1514 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "4c2560ac236a1cc1ef239b7c97a0436e115b19a6ad3ddd96b3b77970aa631ae3", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "043383809888": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "09b235c17bb0": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "0e35851dfc19": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "0e9d6525a582": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "11ab96fde6c9": { + "name": "gitlab.workItemByPath#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemByPath\",\"params\":{\"repo\":\"id:repo-1\",\"host\":\"gitlab.com\",\"path\":\"group/project\",\"iid\":7,\"type\":\"issue\"}}" + }, + "1930e5b10aa4": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "2113a0cc7708": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "27ddfa6b2efa": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "cache": [] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "34d2c8648702": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "36efe7e0f4f2": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "refused", + "repoId": "repo-1" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "3b70826f7fe6": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "3c4b264bef1c": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "$rpc": "null" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "46daeacd502c": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "refused", + "repoId": "repo-1" + } + }, + "49054e5c9fe8": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "4a3429622287": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "518d26b50905": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "65342779da15": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "6ec2e8b6f903": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "$rpc": "null" + }, + "cache": [] + }, + "731507dd2e23": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + } + }, + "7445a582a9c8": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + } + }, + "825e80908bc2": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "873ee3388e70": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "90adb9377343": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "9221b9a7a4b0": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "92bb68fe4007": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "9e9f15f7df58": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + } + }, + "9fe9e3dcad5b": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "a45a7dd68af6": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "a7f4472cdb70": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "aaf80675fc49": { + "name": "github.workItem#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItem\",\"params\":{\"repo\":\"id:repo-1\",\"number\":12}}" + }, + "ab24157c895e": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "refused", + "repoId": "repo-1" + }, + "cache": [] + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bd533f6b0b40": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "bf5c299a7c14": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "$rpc": "null" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "c2fb4513d62f": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "refused", + "repoId": "repo-1" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "c4a429577522": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d58cca0b1bf7": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "dffa1578b2eb": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "e1f537905a65": { + "name": "github.workItemByOwnerRepo#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItemByOwnerRepo\",\"params\":{\"repo\":\"id:repo-1\",\"owner\":\"owner\",\"ownerRepo\":\"repo\",\"number\":12,\"type\":\"issue\"}}" + }, + "e29333b1693f": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + } + }, + "e970eb27f5ca": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "ebc4e477d2ce": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "cache": [] + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f9ea1f747023": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.paste-lookup-github.workitembyownerrepo-1", + "checkpoints": [ + { + "id": "tw-paste-lookup-resolved.prelude:by-number", + "observation": { + "sender": ["65342779da15"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "731507dd2e23" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:by-slug", + "observation": { + "sender": ["65342779da15", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "4a3429622287", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "2113a0cc7708", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:by-slug", + "observation": { + "sender": ["65342779da15", "3b70826f7fe6"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7" + }, + "state": "6ec2e8b6f903", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:gitlab-path", + "observation": { + "sender": ["65342779da15", "3b70826f7fe6", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7", + "gitlab-path": "bd533f6b0b40" + }, + "state": "3c4b264bef1c", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "3b70826f7fe6", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "bf5c299a7c14", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:by-slug", + "observation": { + "sender": ["65342779da15", "d58cca0b1bf7"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7" + }, + "state": "6ec2e8b6f903", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:gitlab-path", + "observation": { + "sender": ["65342779da15", "d58cca0b1bf7", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7", + "gitlab-path": "bd533f6b0b40" + }, + "state": "3c4b264bef1c", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "d58cca0b1bf7", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "ee20a1dc39e7", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "bf5c299a7c14", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:by-slug", + "observation": { + "sender": ["65342779da15", "c4a429577522"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "46daeacd502c" + }, + "state": "ab24157c895e", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:gitlab-path", + "observation": { + "sender": ["65342779da15", "c4a429577522", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "46daeacd502c", + "gitlab-path": "bd533f6b0b40" + }, + "state": "c2fb4513d62f", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "c4a429577522", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "46daeacd502c", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "36efe7e0f4f2", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:by-slug", + "observation": { + "sender": ["65342779da15", "90adb9377343"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "9e9f15f7df58" + }, + "state": "ebc4e477d2ce", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:gitlab-path", + "observation": { + "sender": ["65342779da15", "90adb9377343", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "9e9f15f7df58", + "gitlab-path": "bd533f6b0b40" + }, + "state": "92bb68fe4007", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "90adb9377343", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "9e9f15f7df58", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "873ee3388e70", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:by-slug", + "observation": { + "sender": ["65342779da15", "9221b9a7a4b0"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a7f4472cdb70" + }, + "state": "27ddfa6b2efa", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:gitlab-path", + "observation": { + "sender": ["65342779da15", "9221b9a7a4b0", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a7f4472cdb70", + "gitlab-path": "bd533f6b0b40" + }, + "state": "dffa1578b2eb", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "9221b9a7a4b0", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a7f4472cdb70", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "043383809888", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:by-slug", + "observation": { + "sender": ["65342779da15", "34d2c8648702"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "32a7c0ae7918" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:gitlab-path", + "observation": { + "sender": ["65342779da15", "34d2c8648702", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "32a7c0ae7918", + "gitlab-path": "bd533f6b0b40" + }, + "state": "49054e5c9fe8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "34d2c8648702", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "32a7c0ae7918", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "1930e5b10aa4", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:by-slug", + "observation": { + "sender": ["65342779da15", "9fe9e3dcad5b"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "f3b516f62081" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:gitlab-path", + "observation": { + "sender": ["65342779da15", "9fe9e3dcad5b", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "f3b516f62081", + "gitlab-path": "bd533f6b0b40" + }, + "state": "49054e5c9fe8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "9fe9e3dcad5b", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "f3b516f62081", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "1930e5b10aa4", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:by-slug", + "observation": { + "sender": ["65342779da15", "0e35851dfc19"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "b948e8307e81" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:gitlab-path", + "observation": { + "sender": ["65342779da15", "0e35851dfc19", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "b948e8307e81", + "gitlab-path": "bd533f6b0b40" + }, + "state": "49054e5c9fe8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "0e35851dfc19", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "b948e8307e81", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "1930e5b10aa4", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:by-slug", + "observation": { + "sender": ["65342779da15", "518d26b50905"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a947768bc0ed" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:gitlab-path", + "observation": { + "sender": ["65342779da15", "518d26b50905", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a947768bc0ed", + "gitlab-path": "bd533f6b0b40" + }, + "state": "49054e5c9fe8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "518d26b50905", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "a947768bc0ed", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "1930e5b10aa4", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:by-slug", + "observation": { + "sender": ["65342779da15", "825e80908bc2"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "c7584e82c72f" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:gitlab-path", + "observation": { + "sender": ["65342779da15", "825e80908bc2", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "c7584e82c72f", + "gitlab-path": "bd533f6b0b40" + }, + "state": "49054e5c9fe8", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "825e80908bc2", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "c7584e82c72f", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "1930e5b10aa4", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json new file mode 100644 index 00000000000..0ebc851bed1 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.paste-lookup-gitlab.workitembypath-1.json @@ -0,0 +1,1320 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "ab5511fa34181dc9de590df2fe57a0d061a261e7b204a6ef830c03bc53923d65", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "06c1311be9ff": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "$rpc": "null" + } + }, + "09b235c17bb0": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "0b3a62a33eb9": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "error": "refused", + "repoId": "repo-1" + } + }, + "0c4bd9fe5448": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "0e9d6525a582": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "11ab96fde6c9": { + "name": "gitlab.workItemByPath#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemByPath\",\"params\":{\"repo\":\"id:repo-1\",\"host\":\"gitlab.com\",\"path\":\"group/project\",\"iid\":7,\"type\":\"issue\"}}" + }, + "2113a0cc7708": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "22a0e7139433": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "237ac027130f": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + } + }, + "2c926252e701": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "46daeacd502c": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "refused", + "repoId": "repo-1" + } + }, + "4a3429622287": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "4ef10450d3fc": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-3", + "ok": false + } + } + }, + "5f9434462f8a": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "65342779da15": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "6945ba429114": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "error": "refused", + "repoId": "repo-1" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "70f924b6a03a": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "731507dd2e23": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + } + }, + "737574c61e8d": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-3", + "ok": false + } + } + }, + "7445a582a9c8": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + } + }, + "754e655d6508": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + } + }, + "7a02e1f7b185": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "9e9f15f7df58": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + } + }, + "a1e6f2b40722": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "$rpc": "null" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "a346acfeb887": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a45a7dd68af6": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "a7f4472cdb70": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "message": "inner refused" + }, + "ok": false, + "repoId": "repo-1" + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "aaf80675fc49": { + "name": "github.workItem#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItem\",\"params\":{\"repo\":\"id:repo-1\",\"number\":12}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bd533f6b0b40": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d1f460d3c414": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "error": "inner refused", + "ok": false, + "repoId": "repo-1" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "dfcabc236ad7": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "e1f537905a65": { + "name": "github.workItemByOwnerRepo#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItemByOwnerRepo\",\"params\":{\"repo\":\"id:repo-1\",\"owner\":\"owner\",\"ownerRepo\":\"repo\",\"number\":12,\"type\":\"issue\"}}" + }, + "e29333b1693f": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + } + }, + "e970eb27f5ca": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + }, + "f0766555428a": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "f215cf4a0ca3": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f9ea1f747023": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.paste-lookup-gitlab.workitembypath-1", + "checkpoints": [ + { + "id": "tw-paste-lookup-resolved.prelude:by-number", + "observation": { + "sender": ["65342779da15"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "731507dd2e23" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.prelude:by-slug", + "observation": { + "sender": ["65342779da15", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "4a3429622287", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.normal:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "2113a0cc7708", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "5f9434462f8a"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "ee20a1dc39e7" + }, + "state": "06c1311be9ff", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-absent:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "5f9434462f8a", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "ee20a1dc39e7", + "repo-slug": "0e9d6525a582" + }, + "state": "a1e6f2b40722", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "f215cf4a0ca3"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "ee20a1dc39e7" + }, + "state": "06c1311be9ff", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.result-null:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "f215cf4a0ca3", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "ee20a1dc39e7", + "repo-slug": "0e9d6525a582" + }, + "state": "a1e6f2b40722", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "22a0e7139433"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "46daeacd502c" + }, + "state": "0b3a62a33eb9", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-ok-missing:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "22a0e7139433", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "46daeacd502c", + "repo-slug": "0e9d6525a582" + }, + "state": "6945ba429114", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "0c4bd9fe5448"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "9e9f15f7df58" + }, + "state": "237ac027130f", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-string-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "0c4bd9fe5448", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "9e9f15f7df58", + "repo-slug": "0e9d6525a582" + }, + "state": "d1f460d3c414", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "dfcabc236ad7"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "a7f4472cdb70" + }, + "state": "754e655d6508", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.inner-false-object-error:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "dfcabc236ad7", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "a7f4472cdb70", + "repo-slug": "0e9d6525a582" + }, + "state": "70f924b6a03a", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "4ef10450d3fc"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "32a7c0ae7918" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "4ef10450d3fc", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "32a7c0ae7918", + "repo-slug": "0e9d6525a582" + }, + "state": "7a02e1f7b185", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "f0766555428a"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "f3b516f62081" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.outer-refused-no-message:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "f0766555428a", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "f3b516f62081", + "repo-slug": "0e9d6525a582" + }, + "state": "7a02e1f7b185", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "737574c61e8d"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "b948e8307e81" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.method-not-found:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "737574c61e8d", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "b948e8307e81", + "repo-slug": "0e9d6525a582" + }, + "state": "7a02e1f7b185", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "2c926252e701"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "a947768bc0ed" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "2c926252e701", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "a947768bc0ed", + "repo-slug": "0e9d6525a582" + }, + "state": "7a02e1f7b185", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "a346acfeb887"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "c7584e82c72f" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "tw-paste-lookup-resolved.transport-rejection-no-message:repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "a346acfeb887", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "c7584e82c72f", + "repo-slug": "0e9d6525a582" + }, + "state": "7a02e1f7b185", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json new file mode 100644 index 00000000000..f56d0808fa6 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-github.listworkitems-1.json @@ -0,0 +1,1886 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "ea5c32c4dbb67aae1ebaf809a28104d52d189e5153d9b28285f4d8a6d78753ea", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "05e9b743fb1d": { + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "13833f2512ec": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "13ebc07aa6fe": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "155f61ed496f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'items')", + "isRpcDeliveryUnknown": false + } + }, + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25716369cd8f": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "37c4b6aa154e": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "42f4c910f308": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "44136fa355b3": {}, + "50263a3726f9": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "518e8334f7d3": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "51a271295555": { + "github": [], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "61210ae02f8d": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "71764b0214a9": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "80cc566cdd55": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "867ee0f6f5d8": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "96555ad1314a": { + "github": [] + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a85937f48d97": { + "github": [], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c2bf4ae27078": { + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ce28e5229996": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "ef416ca3ea2c": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f51f34589c7a": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'items')", + "isRpcDeliveryUnknown": false + } + }, + "f7877799c609": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "f8f245caedb5": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.smart-source-search-github.listworkitems-1", + "checkpoints": [ + { + "id": "tw-smart-search-all-providers.normal:github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:github-items", + "observation": { + "sender": ["f8f245caedb5"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "f51f34589c7a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:gitlab-items", + "observation": { + "sender": ["f8f245caedb5", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "f51f34589c7a", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-search", + "observation": { + "sender": ["f8f245caedb5", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "f51f34589c7a", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:branch-refs", + "observation": { + "sender": ["f8f245caedb5", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "f51f34589c7a", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-assigned-listed", + "observation": { + "sender": [ + "f8f245caedb5", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "f51f34589c7a", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:github-items", + "observation": { + "sender": ["ef416ca3ea2c"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "155f61ed496f" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:gitlab-items", + "observation": { + "sender": ["ef416ca3ea2c", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "155f61ed496f", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-search", + "observation": { + "sender": ["ef416ca3ea2c", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "155f61ed496f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:branch-refs", + "observation": { + "sender": ["ef416ca3ea2c", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "155f61ed496f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-assigned-listed", + "observation": { + "sender": [ + "ef416ca3ea2c", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "155f61ed496f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:github-items", + "observation": { + "sender": ["f7877799c609"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "25716369cd8f" + }, + "state": "96555ad1314a", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:gitlab-items", + "observation": { + "sender": ["f7877799c609", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7" + }, + "state": "51a271295555", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-search", + "observation": { + "sender": ["f7877799c609", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "a85937f48d97", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:branch-refs", + "observation": { + "sender": ["f7877799c609", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "867ee0f6f5d8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-assigned-listed", + "observation": { + "sender": [ + "f7877799c609", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "518e8334f7d3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:github-items", + "observation": { + "sender": ["13833f2512ec"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "25716369cd8f" + }, + "state": "96555ad1314a", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:gitlab-items", + "observation": { + "sender": ["13833f2512ec", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7" + }, + "state": "51a271295555", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-search", + "observation": { + "sender": ["13833f2512ec", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "a85937f48d97", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:branch-refs", + "observation": { + "sender": ["13833f2512ec", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "867ee0f6f5d8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-assigned-listed", + "observation": { + "sender": [ + "13833f2512ec", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "518e8334f7d3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:github-items", + "observation": { + "sender": ["61210ae02f8d"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "25716369cd8f" + }, + "state": "96555ad1314a", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:gitlab-items", + "observation": { + "sender": ["61210ae02f8d", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7" + }, + "state": "51a271295555", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-search", + "observation": { + "sender": ["61210ae02f8d", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "a85937f48d97", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:branch-refs", + "observation": { + "sender": ["61210ae02f8d", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "867ee0f6f5d8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-assigned-listed", + "observation": { + "sender": [ + "61210ae02f8d", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "25716369cd8f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "518e8334f7d3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:github-items", + "observation": { + "sender": ["50263a3726f9"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "32a7c0ae7918" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:gitlab-items", + "observation": { + "sender": ["50263a3726f9", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "32a7c0ae7918", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-search", + "observation": { + "sender": ["50263a3726f9", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "32a7c0ae7918", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:branch-refs", + "observation": { + "sender": ["50263a3726f9", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "32a7c0ae7918", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-assigned-listed", + "observation": { + "sender": [ + "50263a3726f9", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "32a7c0ae7918", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:github-items", + "observation": { + "sender": ["ce28e5229996"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "f3b516f62081" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:gitlab-items", + "observation": { + "sender": ["ce28e5229996", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "f3b516f62081", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-search", + "observation": { + "sender": ["ce28e5229996", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "f3b516f62081", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:branch-refs", + "observation": { + "sender": ["ce28e5229996", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "f3b516f62081", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "ce28e5229996", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "f3b516f62081", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:github-items", + "observation": { + "sender": ["80cc566cdd55"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "b948e8307e81" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:gitlab-items", + "observation": { + "sender": ["80cc566cdd55", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "b948e8307e81", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-search", + "observation": { + "sender": ["80cc566cdd55", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "b948e8307e81", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:branch-refs", + "observation": { + "sender": ["80cc566cdd55", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "b948e8307e81", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-assigned-listed", + "observation": { + "sender": [ + "80cc566cdd55", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "b948e8307e81", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:github-items", + "observation": { + "sender": ["37c4b6aa154e"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "a947768bc0ed" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:gitlab-items", + "observation": { + "sender": ["37c4b6aa154e", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "a947768bc0ed", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-search", + "observation": { + "sender": ["37c4b6aa154e", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "a947768bc0ed", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:branch-refs", + "observation": { + "sender": ["37c4b6aa154e", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "a947768bc0ed", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-assigned-listed", + "observation": { + "sender": [ + "37c4b6aa154e", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "a947768bc0ed", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:github-items", + "observation": { + "sender": ["42f4c910f308"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "c7584e82c72f" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:gitlab-items", + "observation": { + "sender": ["42f4c910f308", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "c7584e82c72f", + "gitlab": "6e2d75e3bbd7" + }, + "state": "c2bf4ae27078", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-search", + "observation": { + "sender": ["42f4c910f308", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "c7584e82c72f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "05e9b743fb1d", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:branch-refs", + "observation": { + "sender": ["42f4c910f308", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "c7584e82c72f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "71764b0214a9", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "42f4c910f308", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "c7584e82c72f", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "13ebc07aa6fe", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json new file mode 100644 index 00000000000..82a1b00570a --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-gitlab.listworkitems-1.json @@ -0,0 +1,1773 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "541a282829d3aa5d6b66eeba06e783368f397178ff0d2bdf3e87bccdc62b690f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "24e67c350a20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [] + }, + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25716369cd8f": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "3698dc9e21e5": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "5b5689593188": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "67ba0246dbf8": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "6c647ccd3cff": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "6f4bd0fc6d8b": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "74bc6b65cdef": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "76fa023e535f": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "82f9caba201c": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "8eb709e28997": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "94d9f7a1e105": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "9f9af59ae576": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "a25ac3f73d46": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'error')", + "isRpcDeliveryUnknown": false + } + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b236fc09fef7": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "be85b10635d4": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'error')", + "isRpcDeliveryUnknown": false + } + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "e2325a86e69b": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "f01419051ddf": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "f0a975a83b87": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fb884a9370b1": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.smart-source-search-gitlab.listworkitems-1", + "checkpoints": [ + { + "id": "tw-smart-search-all-providers.prelude:github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "b236fc09fef7"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "be85b10635d4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-search", + "observation": { + "sender": ["5bce68072dc3", "b236fc09fef7", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "be85b10635d4", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "b236fc09fef7", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "be85b10635d4", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "b236fc09fef7", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "be85b10635d4", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "76fa023e535f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a25ac3f73d46" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-search", + "observation": { + "sender": ["5bce68072dc3", "76fa023e535f", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a25ac3f73d46", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "76fa023e535f", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a25ac3f73d46", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "76fa023e535f", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a25ac3f73d46", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "94d9f7a1e105"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f" + }, + "state": "24e67c350a20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-search", + "observation": { + "sender": ["5bce68072dc3", "94d9f7a1e105", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6" + }, + "state": "6c647ccd3cff", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "94d9f7a1e105", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "9f9af59ae576", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "94d9f7a1e105", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "74bc6b65cdef", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "8eb709e28997"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f" + }, + "state": "24e67c350a20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-search", + "observation": { + "sender": ["5bce68072dc3", "8eb709e28997", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6" + }, + "state": "6c647ccd3cff", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "8eb709e28997", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "9f9af59ae576", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "8eb709e28997", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "74bc6b65cdef", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "3698dc9e21e5"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f" + }, + "state": "24e67c350a20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-search", + "observation": { + "sender": ["5bce68072dc3", "3698dc9e21e5", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6" + }, + "state": "6c647ccd3cff", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "3698dc9e21e5", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "9f9af59ae576", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "3698dc9e21e5", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "25716369cd8f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "74bc6b65cdef", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "fb884a9370b1"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "32a7c0ae7918" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-search", + "observation": { + "sender": ["5bce68072dc3", "fb884a9370b1", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "32a7c0ae7918", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "fb884a9370b1", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "32a7c0ae7918", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "fb884a9370b1", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "32a7c0ae7918", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "5b5689593188"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "f3b516f62081" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-search", + "observation": { + "sender": ["5bce68072dc3", "5b5689593188", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "f3b516f62081", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "5b5689593188", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "f3b516f62081", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "5b5689593188", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "f3b516f62081", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "e2325a86e69b"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "b948e8307e81" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-search", + "observation": { + "sender": ["5bce68072dc3", "e2325a86e69b", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "b948e8307e81", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "e2325a86e69b", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "b948e8307e81", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "e2325a86e69b", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "b948e8307e81", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "67ba0246dbf8"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a947768bc0ed" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-search", + "observation": { + "sender": ["5bce68072dc3", "67ba0246dbf8", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a947768bc0ed", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "67ba0246dbf8", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a947768bc0ed", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "67ba0246dbf8", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "a947768bc0ed", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f01419051ddf"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "c7584e82c72f" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f01419051ddf", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "c7584e82c72f", + "linear": "a4ee5d16b4f6" + }, + "state": "6f4bd0fc6d8b", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f01419051ddf", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "c7584e82c72f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "82f9caba201c", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f01419051ddf", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "c7584e82c72f", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "f0a975a83b87", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json new file mode 100644 index 00000000000..ca2c35eb8fe --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.listissues-1.json @@ -0,0 +1,1200 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "767044526344237daee0a3f981a10615fbb9ebc3f45c2f6f41f9b8b16d362082", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "090ea9e6ac63": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "3c5eceeb8463": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-5", + "ok": false + } + } + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "7a91e9a2c1bb": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "957cc0c5ead6": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unexpected Linear tasks response", + "isRpcDeliveryUnknown": false + } + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "adb630f7c310": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-5", + "ok": false + } + } + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b68d510a9e89": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d7c2c3caeb26": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-5", + "ok": false + } + } + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ec10770e2214": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "f3a7d3f5dc3c": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f64e4725150b": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "f7b4f4fa8d5a": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.smart-source-search-linear.listissues-1", + "checkpoints": [ + { + "id": "tw-smart-search-all-providers.prelude:github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "f3a7d3f5dc3c" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "957cc0c5ead6" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "7a91e9a2c1bb" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "957cc0c5ead6" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "ec10770e2214" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "957cc0c5ead6" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "f7b4f4fa8d5a" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "957cc0c5ead6" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "b68d510a9e89" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "957cc0c5ead6" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "3c5eceeb8463" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "32a7c0ae7918" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "adb630f7c310" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "f3b516f62081" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "d7c2c3caeb26" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "b948e8307e81" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "f64e4725150b" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "a947768bc0ed" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "090ea9e6ac63" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "c7584e82c72f" + }, + "state": "c43e80126d82", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json new file mode 100644 index 00000000000..103a2851abd --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-linear.searchissues-1.json @@ -0,0 +1,1496 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "40a8ea6a916d4266bd80148e40fd817bbe80cefa02465b88d37c42cebed44f22", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0914e9c666b1": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "27fa02820da8": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "3351dd9fcc16": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-3", + "ok": false + } + } + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "5a41c0588421": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "619f7466012f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "78f8899bda05": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "957cc0c5ead6": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unexpected Linear tasks response", + "isRpcDeliveryUnknown": false + } + }, + "99e5be0a1b11": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b4185f815a19": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-3", + "ok": false + } + } + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c2b41e1dbaf8": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c770655d7a45": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fb4807630e1d": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.smart-source-search-linear.searchissues-1", + "checkpoints": [ + { + "id": "tw-smart-search-all-providers.prelude:github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "5a41c0588421"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "5a41c0588421", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "5a41c0588421", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "78f8899bda05"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "78f8899bda05", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "78f8899bda05", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "27fa02820da8"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "27fa02820da8", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "27fa02820da8", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "c770655d7a45"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "c770655d7a45", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "c770655d7a45", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "619f7466012f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "619f7466012f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "619f7466012f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "957cc0c5ead6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "3351dd9fcc16"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "32a7c0ae7918" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "3351dd9fcc16", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "32a7c0ae7918", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "3351dd9fcc16", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "32a7c0ae7918", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "fb4807630e1d"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "f3b516f62081" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "fb4807630e1d", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "f3b516f62081", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "fb4807630e1d", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "f3b516f62081", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "b4185f815a19"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "b948e8307e81" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "b4185f815a19", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "b948e8307e81", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "b4185f815a19", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "b948e8307e81", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "99e5be0a1b11"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a947768bc0ed" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "99e5be0a1b11", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a947768bc0ed", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "99e5be0a1b11", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a947768bc0ed", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "0914e9c666b1"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "c7584e82c72f" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "0914e9c666b1", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "c7584e82c72f", + "branches": "b015aaf3a53a" + }, + "state": "c2b41e1dbaf8", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "0914e9c666b1", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "c7584e82c72f", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json new file mode 100644 index 00000000000..cdb71469a91 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.smart-source-search-repo.searchrefs-1.json @@ -0,0 +1,1411 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "aa06630cd902fd5dde5181bddd38f5478ae5b48044a3105b090728158aa9a621", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "071e0e8e68dc": { + "branches": [], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25716369cd8f": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "3842f5bcd677": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "39576819ef3f": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "553cf244460a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "59e25358865a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-4", + "ok": false + } + } + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "5d85e47efa46": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-4", + "ok": false + } + } + }, + "5ff512429b6c": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "a4263a13d324": { + "branches": [], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b2d9361f1d3d": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "b721d1733537": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c30c54d734bc": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'refDetails')", + "isRpcDeliveryUnknown": false + } + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d1572a7d1ddb": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "d4061d056a75": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-4", + "ok": false + } + } + }, + "e6d2fd7367d3": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f582af356003": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'refDetails')", + "isRpcDeliveryUnknown": false + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.smart-source-search-repo.searchrefs-1", + "checkpoints": [ + { + "id": "tw-smart-search-all-providers.prelude:github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.prelude:linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.normal:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "5ff512429b6c"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "c30c54d734bc" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-absent:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "5ff512429b6c", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "c30c54d734bc", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "553cf244460a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "f582af356003" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.result-null:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "553cf244460a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "f582af356003", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "b2d9361f1d3d"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f" + }, + "state": "a4263a13d324", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-ok-missing:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "b2d9361f1d3d", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f", + "linear-assigned": "26dc3b7c8299" + }, + "state": "071e0e8e68dc", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "b721d1733537"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f" + }, + "state": "a4263a13d324", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-string-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "b721d1733537", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f", + "linear-assigned": "26dc3b7c8299" + }, + "state": "071e0e8e68dc", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "3842f5bcd677"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f" + }, + "state": "a4263a13d324", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.inner-false-object-error:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "3842f5bcd677", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "25716369cd8f", + "linear-assigned": "26dc3b7c8299" + }, + "state": "071e0e8e68dc", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "59e25358865a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "32a7c0ae7918" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "59e25358865a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "32a7c0ae7918", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "d4061d056a75"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "f3b516f62081" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.outer-refused-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "d4061d056a75", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "f3b516f62081", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "5d85e47efa46"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b948e8307e81" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.method-not-found:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "5d85e47efa46", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b948e8307e81", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "39576819ef3f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "a947768bc0ed" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "39576819ef3f", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "a947768bc0ed", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "e6d2fd7367d3"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "c7584e82c72f" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "tw-smart-search-all-providers.transport-rejection-no-message:linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "e6d2fd7367d3", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "c7584e82c72f", + "linear-assigned": "26dc3b7c8299" + }, + "state": "d1572a7d1ddb", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json new file mode 100644 index 00000000000..abd0097c78e --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.searchrefs-1.json @@ -0,0 +1,977 @@ +{ + "operation": "tasks.workspace-source", + "family": "tasks.workspace-source", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "9e91d46870cd69279cc7d8ebfd317ab8b13136ccff9662876c7601a3a83ecafe", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0045016f4149": { + "branchError": "Cannot read properties of null (reading 'refDetails')", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "273f4074a9b5": { + "name": "workspaceSparsePresetsLoaded", + "value": false + }, + "28f23529596e": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "2bd164983c10": { + "branchError": "outer refused", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "2e554aeab5d0": { + "branchError": "transport failure", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "35afa5cb107f": { + "name": "workspaceBaseBranchLoading", + "value": false + }, + "395368dea8ff": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "refs": ["main"] + } + } + } + }, + "46027e62015d": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"main\",\"limit\":20}}" + }, + "46f2f1c9bc6a": { + "name": "workspaceBaseBranchError", + "value": "transport failure" + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "4cedb91a2f7a": { + "name": "repo.sparsePresets#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.sparsePresets\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "52925a303ed6": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "5485811c08ca": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "57f06e6e349e": { + "branchError": "", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "58cb95babab2": { + "name": "workspaceBaseBranchLoading", + "value": true + }, + "5b0e628f442c": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "5f1c84e00d4f": { + "name": "workspaceBaseBranchResults", + "value": [] + }, + "6c344c5f4ac0": { + "name": "workspaceBaseBranchError", + "value": "" + }, + "7444e76d58f7": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "8353b8e1a426": { + "name": "workspaceSparsePresetsLoading", + "value": true + }, + "846e910f6579": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "8dbe7ea87a41": { + "name": "workspaceBaseBranchResults", + "value": [ + { + "localBranchName": "main", + "refName": "main" + } + ] + }, + "914268bb0636": { + "name": "workspaceBaseBranchError", + "value": "outer refused" + }, + "9357f7ea8445": { + "name": "workspaceSparsePresetId", + "value": { + "$rpc": "null" + } + }, + "94cafc85a34d": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "b78bcf7ca596": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "bb1a94f8cb3f": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "bd26306458d2": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "bd93f3a9862f": { + "branchError": "Unknown method", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "c8d4d05367d6": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + } + } + } + }, + "cfc8af2a7169": { + "name": "workspaceSparsePresetsLoading", + "value": false + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "e4bad139cb5f": { + "branchError": "Cannot read properties of undefined (reading 'refDetails')", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "e883c3f737f1": { + "name": "workspaceBaseBranchError", + "value": "Unknown method" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ec51e489da58": { + "name": "workspaceBaseBranchError", + "value": "Cannot read properties of null (reading 'refDetails')" + }, + "f6f9a9765c0c": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "fcca5f73b480": { + "name": "workspaceBaseBranchError", + "value": "Cannot read properties of undefined (reading 'refDetails')" + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-source-repo.searchrefs-1", + "checkpoints": [ + { + "id": "tw-workspace-source-presets.prelude:presets-loaded", + "observation": { + "sender": ["c8d4d05367d6"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.normal:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "b78bcf7ca596", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-absent:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "846e910f6579"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "e4bad139cb5f", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "fcca5f73b480", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-null:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "28f23529596e"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "0045016f4149", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "ec51e489da58", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-ok-missing:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "5485811c08ca"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-string-error:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "f6f9a9765c0c"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-object-error:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "94cafc85a34d"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "bb1a94f8cb3f"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "2bd164983c10", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "914268bb0636", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused-no-message:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "7444e76d58f7"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "6c344c5f4ac0", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.method-not-found:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "bd26306458d2"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "bd93f3a9862f", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "e883c3f737f1", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "5b0e628f442c"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "2e554aeab5d0", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "46f2f1c9bc6a", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection-no-message:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "52925a303ed6"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "5f1c84e00d4f", + "6c344c5f4ac0", + "35afa5cb107f" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json new file mode 100644 index 00000000000..81996baa630 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-source-repo.sparsepresets-1.json @@ -0,0 +1,1266 @@ +{ + "operation": "tasks.workspace-source", + "family": "tasks.workspace-source", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "2d4a681bffbc5ff9d3040ea0d6bb2603ee940c3c497269d0b63caca564fb25e1", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0e0e1c74c796": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "transport failure", + "presetsLoaded": false + }, + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "2399e995a370": { + "name": "workspaceSparsePresets", + "value": [] + }, + "273f4074a9b5": { + "name": "workspaceSparsePresetsLoaded", + "value": false + }, + "2d69fe330484": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "35afa5cb107f": { + "name": "workspaceBaseBranchLoading", + "value": false + }, + "395368dea8ff": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "refs": ["main"] + } + } + } + }, + "3dcbacca6ef0": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "46027e62015d": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"main\",\"limit\":20}}" + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "4c7522c66d03": { + "name": "workspaceSparsePresetsError", + "value": "transport failure" + }, + "4cedb91a2f7a": { + "name": "repo.sparsePresets#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.sparsePresets\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "51396e45f193": { + "name": "workspaceSparsePresetsError", + "value": "Cannot read properties of undefined (reading 'presets')" + }, + "513bb01f2f25": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "", + "presetsLoaded": true + }, + "57f06e6e349e": { + "branchError": "", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "58cb95babab2": { + "name": "workspaceBaseBranchLoading", + "value": true + }, + "5cc2ef1617e8": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "5e895d7d4949": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "transport failure", + "presetsLoaded": false + }, + "5f1c84e00d4f": { + "name": "workspaceBaseBranchResults", + "value": [] + }, + "62bc28c39ffc": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "", + "presetsLoaded": false + }, + "6c344c5f4ac0": { + "name": "workspaceBaseBranchError", + "value": "" + }, + "83043f6bd49a": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "8353b8e1a426": { + "name": "workspaceSparsePresetsLoading", + "value": true + }, + "841927d71fc6": { + "name": "workspaceSparsePresetsError", + "value": "outer refused" + }, + "844c1ccf1f9a": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "Cannot read properties of undefined (reading 'presets')", + "presetsLoaded": false + }, + "8b4d034d6e9e": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "8dbe7ea87a41": { + "name": "workspaceBaseBranchResults", + "value": [ + { + "localBranchName": "main", + "refName": "main" + } + ] + }, + "90bd9a937fe0": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "", + "presetsLoaded": false + }, + "9357f7ea8445": { + "name": "workspaceSparsePresetId", + "value": { + "$rpc": "null" + } + }, + "96f5a578e45b": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "981cb584cfe3": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "982d70c476ea": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "a18f0cdbc6fe": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "aaaeee84b4d2": { + "name": "workspaceSparsePresetsError", + "value": "Cannot read properties of null (reading 'presets')" + }, + "b78bcf7ca596": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "bf004df2bf3d": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "Cannot read properties of null (reading 'presets')", + "presetsLoaded": false + }, + "c58cdfec29bd": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "", + "presetsLoaded": true + }, + "c8d4d05367d6": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + } + } + } + }, + "c909c6a474d9": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "Cannot read properties of undefined (reading 'presets')", + "presetsLoaded": false + }, + "c9379c8a3ba8": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "outer refused", + "presetsLoaded": false + }, + "c9e6bb8f5e61": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "Cannot read properties of null (reading 'presets')", + "presetsLoaded": false + }, + "ce4aab89eed0": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "Unknown method", + "presetsLoaded": false + }, + "ce7d06abf495": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "Unknown method", + "presetsLoaded": false + }, + "cfc8af2a7169": { + "name": "workspaceSparsePresetsLoading", + "value": false + }, + "d075e587b820": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [], + "presetsError": "outer refused", + "presetsLoaded": false + }, + "db27fad68ce2": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "ea260eacb1db": { + "name": "workspaceSparsePresetsError", + "value": "Unknown method" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-source-repo.sparsepresets-1", + "checkpoints": [ + { + "id": "tw-workspace-source-presets.normal:presets-loaded", + "observation": { + "sender": ["c8d4d05367d6"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.normal:branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "b78bcf7ca596", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-absent:presets-loaded", + "observation": { + "sender": ["981cb584cfe3"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c909c6a474d9", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "51396e45f193", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-absent:branches-loaded", + "observation": { + "sender": ["981cb584cfe3", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "844c1ccf1f9a", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "51396e45f193", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-null:presets-loaded", + "observation": { + "sender": ["a18f0cdbc6fe"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c9e6bb8f5e61", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "aaaeee84b4d2", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.result-null:branches-loaded", + "observation": { + "sender": ["a18f0cdbc6fe", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "bf004df2bf3d", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "aaaeee84b4d2", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-ok-missing:presets-loaded", + "observation": { + "sender": ["3dcbacca6ef0"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c58cdfec29bd", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-ok-missing:branches-loaded", + "observation": { + "sender": ["3dcbacca6ef0", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "513bb01f2f25", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-string-error:presets-loaded", + "observation": { + "sender": ["982d70c476ea"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c58cdfec29bd", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-string-error:branches-loaded", + "observation": { + "sender": ["982d70c476ea", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "513bb01f2f25", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-object-error:presets-loaded", + "observation": { + "sender": ["8b4d034d6e9e"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c58cdfec29bd", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.inner-false-object-error:branches-loaded", + "observation": { + "sender": ["8b4d034d6e9e", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "513bb01f2f25", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused:presets-loaded", + "observation": { + "sender": ["5cc2ef1617e8"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "c9379c8a3ba8", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "841927d71fc6", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused:branches-loaded", + "observation": { + "sender": ["5cc2ef1617e8", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "d075e587b820", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "841927d71fc6", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused-no-message:presets-loaded", + "observation": { + "sender": ["db27fad68ce2"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "62bc28c39ffc", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "dba381378b08", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.outer-refused-no-message:branches-loaded", + "observation": { + "sender": ["db27fad68ce2", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "90bd9a937fe0", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "dba381378b08", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.method-not-found:presets-loaded", + "observation": { + "sender": ["83043f6bd49a"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "ce4aab89eed0", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "ea260eacb1db", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.method-not-found:branches-loaded", + "observation": { + "sender": ["83043f6bd49a", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "ce7d06abf495", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "ea260eacb1db", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection:presets-loaded", + "observation": { + "sender": ["96f5a578e45b"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "0e0e1c74c796", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "4c7522c66d03", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection:branches-loaded", + "observation": { + "sender": ["96f5a578e45b", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "5e895d7d4949", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "4c7522c66d03", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection-no-message:presets-loaded", + "observation": { + "sender": ["2d69fe330484"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "62bc28c39ffc", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "dba381378b08", + "cfc8af2a7169" + ] + } + }, + { + "id": "tw-workspace-source-presets.transport-rejection-no-message:branches-loaded", + "observation": { + "sender": ["2d69fe330484", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "90bd9a937fe0", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "dba381378b08", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json new file mode 100644 index 00000000000..8168589cfe0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-repo.savesparsepreset-1.json @@ -0,0 +1,941 @@ +{ + "operation": "tasks.workspace-sparse", + "family": "tasks.workspace-sparse", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "3d4637406e2d658b72f73153f0a5e176cb8b143593b72bce0cdf979bc6e4cbdd", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1923ab7dba76": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "1f453ea83df7": { + "presets": [], + "presetsError": "transport failure", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "23e798f4b47c": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "312ed3cbf468": { + "name": "workspaceSparsePresetId", + "value": "p1" + }, + "404305aa2e3a": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "42bbd034563e": { + "name": "workspaceSparseDraft", + "value": { + "$rpc": "null" + } + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "4c7522c66d03": { + "name": "workspaceSparsePresetsError", + "value": "transport failure" + }, + "4d66e995ff47": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + }, + "4e1228f5e0a8": { + "name": "workspaceSparsePresetsError", + "value": "Cannot read properties of null (reading 'preset')" + }, + "5c44ff5f6877": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "preset": { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + } + } + } + }, + "74c1230400a6": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "7993762437ad": { + "name": "workspaceSparsePresetsError", + "value": "Connection closed" + }, + "7fd0cde62993": { + "name": "workspaceSparseSaving", + "value": false + }, + "841927d71fc6": { + "name": "workspaceSparsePresetsError", + "value": "outer refused" + }, + "89aa7a3bd619": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "8e410711e308": { + "presets": [], + "presetsError": "Cannot read properties of undefined (reading 'preset')", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "92b857799ffd": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "95295c6eaa8d": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "990a149dc1be": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "bcfc7df6e4f2": { + "presets": [], + "presetsError": "", + "saving": true, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "bd0266b23771": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "befb68fa6c76": { + "presets": [], + "presetsError": "Failed to save sparse preset.", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "c33fee0d8294": { + "presets": [], + "presetsError": "Unknown method", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "c5a3f70f9b02": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "cea9d7e8986e": { + "name": "workspaceSparseSaving", + "value": true + }, + "d14de7ce4d84": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "d74bc3778ca8": { + "presets": [], + "presetsError": "outer refused", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "da3a01640280": { + "name": "workspaceSparsePresetsError", + "value": "Failed to save sparse preset." + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "dd63325a7802": { + "name": "workspaceSparsePresetsError", + "value": "Cannot read properties of undefined (reading 'preset')" + }, + "e52233a9ff71": { + "presets": [], + "presetsError": "Cannot read properties of null (reading 'preset')", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "ea260eacb1db": { + "name": "workspaceSparsePresetsError", + "value": "Unknown method" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ee3a941d5e9c": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "f4d4ba362712": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "fd758406cc2c": { + "name": "repo.saveSparsePreset#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.saveSparsePreset\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"docs\",\"directories\":[\"docs\"]}}" + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-sparse-repo.savesparsepreset-1", + "checkpoints": [ + { + "id": "tw-workspace-sparse-saved.prelude:ssh-state-read", + "observation": { + "sender": ["89aa7a3bd619"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "ee3a941d5e9c", + "effects": ["921f72d7827e"] + } + }, + { + "id": "tw-workspace-sparse-saved.prelude:cleanup", + "observation": { + "sender": ["89aa7a3bd619", "4d66e995ff47"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "bcfc7df6e4f2", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "7993762437ad", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.normal:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "404305aa2e3a", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.result-absent:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "95295c6eaa8d"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "8e410711e308", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "dd63325a7802", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.result-null:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "d14de7ce4d84"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "e52233a9ff71", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "4e1228f5e0a8", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-ok-missing:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "92b857799ffd"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "befb68fa6c76", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "da3a01640280", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-string-error:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "bd0266b23771"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "befb68fa6c76", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "da3a01640280", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-object-error:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "c5a3f70f9b02"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "befb68fa6c76", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "da3a01640280", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "1923ab7dba76"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "d74bc3778ca8", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "841927d71fc6", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused-no-message:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "990a149dc1be"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "ee3a941d5e9c", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "dba381378b08", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.method-not-found:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "74c1230400a6"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "c33fee0d8294", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "ea260eacb1db", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "f4d4ba362712"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "1f453ea83df7", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "4c7522c66d03", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection-no-message:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "23e798f4b47c"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "ee3a941d5e9c", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "dba381378b08", + "7fd0cde62993" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json new file mode 100644 index 00000000000..0b9326b05ca --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-sparse-ssh.getstate-1.json @@ -0,0 +1,1165 @@ +{ + "operation": "tasks.workspace-sparse", + "family": "tasks.workspace-sparse", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "3453024581230908d1e0e9335f5310e7b4ae03faf50d93654b9ff28c464fb95f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0a16839c6f87": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "0eabd872f405": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "14db652edf02": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "1703db1e81e4": { + "name": "workspaceSshState", + "value": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "25352a4de532": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "2d910059043a": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "312ed3cbf468": { + "name": "workspaceSparsePresetId", + "value": "p1" + }, + "404305aa2e3a": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "42bbd034563e": { + "name": "workspaceSparseDraft", + "value": { + "$rpc": "null" + } + }, + "44ca8518769a": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "57be9babbecd": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "5c44ff5f6877": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "preset": { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + } + } + } + }, + "68ca812c8120": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "6daeb33f37f8": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "7a26c9dceb4c": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "7e5ba73897a1": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "7fd0cde62993": { + "name": "workspaceSparseSaving", + "value": false + }, + "80431b2fc9cd": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "813df5a46a4a": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "81af687a998a": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "86cc01b1e541": { + "name": "workspaceSshState", + "value": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "89aa7a3bd619": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "9164e806ca12": { + "name": "workspaceSshState", + "value": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "9367b086d487": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "93dfd351c771": { + "name": "workspaceSshState", + "value": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "a16210531185": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "a209f2c7160e": { + "name": "workspaceSshState", + "value": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "b09dd4915f43": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "b705ba88a562": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "b7fa4557dcfa": { + "name": "workspaceSshState", + "value": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "cd050477e049": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "cea9d7e8986e": { + "name": "workspaceSparseSaving", + "value": true + }, + "d0fad8f739ca": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "e18278fce524": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ee3a941d5e9c": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "ef27a7ecb258": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "f36f17f8d448": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "f7885da6b9c0": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "fd758406cc2c": { + "name": "repo.saveSparsePreset#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.saveSparsePreset\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"docs\",\"directories\":[\"docs\"]}}" + }, + "ff6c3161dcc7": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-sparse-ssh.getstate-1", + "checkpoints": [ + { + "id": "tw-workspace-sparse-saved.normal:ssh-state-read", + "observation": { + "sender": ["89aa7a3bd619"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "ee3a941d5e9c", + "effects": ["921f72d7827e"] + } + }, + { + "id": "tw-workspace-sparse-saved.normal:preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "404305aa2e3a", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.result-absent:ssh-state-read", + "observation": { + "sender": ["14db652edf02"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "44ca8518769a", + "effects": ["1703db1e81e4"] + } + }, + { + "id": "tw-workspace-sparse-saved.result-absent:preset-saved", + "observation": { + "sender": ["14db652edf02", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "7a26c9dceb4c", + "effects": [ + "1703db1e81e4", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.result-null:ssh-state-read", + "observation": { + "sender": ["0eabd872f405"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "81af687a998a", + "effects": ["9164e806ca12"] + } + }, + { + "id": "tw-workspace-sparse-saved.result-null:preset-saved", + "observation": { + "sender": ["0eabd872f405", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "80431b2fc9cd", + "effects": [ + "9164e806ca12", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-ok-missing:ssh-state-read", + "observation": { + "sender": ["0a16839c6f87"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "68ca812c8120", + "effects": ["25352a4de532"] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-ok-missing:preset-saved", + "observation": { + "sender": ["0a16839c6f87", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "f7885da6b9c0", + "effects": [ + "25352a4de532", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-string-error:ssh-state-read", + "observation": { + "sender": ["b09dd4915f43"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "68ca812c8120", + "effects": ["25352a4de532"] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-string-error:preset-saved", + "observation": { + "sender": ["b09dd4915f43", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "f7885da6b9c0", + "effects": [ + "25352a4de532", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-object-error:ssh-state-read", + "observation": { + "sender": ["e18278fce524"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "68ca812c8120", + "effects": ["25352a4de532"] + } + }, + { + "id": "tw-workspace-sparse-saved.inner-false-object-error:preset-saved", + "observation": { + "sender": ["e18278fce524", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "f7885da6b9c0", + "effects": [ + "25352a4de532", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused:ssh-state-read", + "observation": { + "sender": ["d0fad8f739ca"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "a16210531185", + "effects": ["93dfd351c771"] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused:preset-saved", + "observation": { + "sender": ["d0fad8f739ca", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "9367b086d487", + "effects": [ + "93dfd351c771", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused-no-message:ssh-state-read", + "observation": { + "sender": ["ff6c3161dcc7"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "813df5a46a4a", + "effects": ["86cc01b1e541"] + } + }, + { + "id": "tw-workspace-sparse-saved.outer-refused-no-message:preset-saved", + "observation": { + "sender": ["ff6c3161dcc7", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "cd050477e049", + "effects": [ + "86cc01b1e541", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.method-not-found:ssh-state-read", + "observation": { + "sender": ["b705ba88a562"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "7e5ba73897a1", + "effects": ["a209f2c7160e"] + } + }, + { + "id": "tw-workspace-sparse-saved.method-not-found:preset-saved", + "observation": { + "sender": ["b705ba88a562", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "57be9babbecd", + "effects": [ + "a209f2c7160e", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection:ssh-state-read", + "observation": { + "sender": ["2d910059043a"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "6daeb33f37f8", + "effects": ["b7fa4557dcfa"] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection:preset-saved", + "observation": { + "sender": ["2d910059043a", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "ef27a7ecb258", + "effects": [ + "b7fa4557dcfa", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection-no-message:ssh-state-read", + "observation": { + "sender": ["f36f17f8d448"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "813df5a46a4a", + "effects": ["86cc01b1e541"] + } + }, + { + "id": "tw-workspace-sparse-saved.transport-rejection-no-message:preset-saved", + "observation": { + "sender": ["f36f17f8d448", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "cd050477e049", + "effects": [ + "86cc01b1e541", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json new file mode 100644 index 00000000000..8e8383f860d --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-local-preflight.detectagents-1.json @@ -0,0 +1,567 @@ +{ + "operation": "tasks.workspace-ssh-local", + "family": "tasks.workspace-ssh-local", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "28f7ba289c188bfc121ef7b969133711189e887fbfc7b37c8a5401ea7f30b56a", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "00d70c40c34c": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "0846bea730cf": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "1317fc33bdbe": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "163b91b6fe9c": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "327b46fb8bef": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "6e5fcf24648d": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "70d128c20ae4": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "71225024ccf5": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "7400f4eebe66": { + "agent": "claude", + "connecting": false, + "detected": ["codex", "claude"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "87d7d24a30d2": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9f152ed6e897": { + "name": "workspaceDetectedAgentIds", + "value": [] + }, + "cb93b17470e8": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex", "claude"] + } + } + }, + "cbb858a786ac": { + "name": "workspaceDetectedAgentIds", + "value": ["codex", "claude"] + }, + "cf32edc950ac": { + "name": "preflight.detectAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}" + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "fb640b2bca4c": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "fbb9eef78275": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-ssh-local-preflight.detectagents-1", + "checkpoints": [ + { + "id": "tw-workspace-ssh-local-agents.normal:local-agents-detected", + "observation": { + "sender": ["cb93b17470e8"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "7400f4eebe66", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "cbb858a786ac"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.result-absent:local-agents-detected", + "observation": { + "sender": ["6e5fcf24648d"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.result-null:local-agents-detected", + "observation": { + "sender": ["1317fc33bdbe"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.inner-ok-missing:local-agents-detected", + "observation": { + "sender": ["327b46fb8bef"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.inner-false-string-error:local-agents-detected", + "observation": { + "sender": ["0846bea730cf"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.inner-false-object-error:local-agents-detected", + "observation": { + "sender": ["00d70c40c34c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.outer-refused:local-agents-detected", + "observation": { + "sender": ["fb640b2bca4c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.outer-refused-no-message:local-agents-detected", + "observation": { + "sender": ["163b91b6fe9c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.method-not-found:local-agents-detected", + "observation": { + "sender": ["87d7d24a30d2"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.transport-rejection:local-agents-detected", + "observation": { + "sender": ["fbb9eef78275"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-local-agents.transport-rejection-no-message:local-agents-detected", + "observation": { + "sender": ["70d128c20ae4"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json new file mode 100644 index 00000000000..72d178b7168 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-preflight.detectremoteagents-1.json @@ -0,0 +1,1269 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "35771ab92d0d4ff44a1dd6e5f1e5d3137570a2e013bddea5ca77ecac7989ed53", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02a3532637b4": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "07d4c9b0eaf2": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "0ba2cee4b538": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + }, + "162a699815c1": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "1739575ac53e": { + "name": "workspaceSshConnecting", + "value": true + }, + "17e35b25d15d": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex"] + } + } + }, + "18e6a3ac6471": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "19b6093097ff": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "227c671c491b": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "3571f351281f": { + "name": "workspaceDetectedAgentIds", + "value": ["codex"] + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "43ead075ce12": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "43fd3e2f4b53": { + "name": "workspaceSshConnecting", + "value": false + }, + "71225024ccf5": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "71d817ffdd81": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "7a1b524f17d0": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "7c9498659f58": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "80a4af19f556": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": " pnpm install " + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + } + } + }, + "860046b4ce30": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "90dce4861972": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "95dee1165f95": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "9f0676ba0d67": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "9f152ed6e897": { + "name": "workspaceDetectedAgentIds", + "value": [] + }, + "a1f755a38636": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "c5eeac27af29": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "fbfdbb919268": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + }, + "fd04a7852302": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-ssh-preflight.detectremoteagents-1", + "checkpoints": [ + { + "id": "tw-workspace-ssh-connected.normal:agents-detected", + "observation": { + "sender": ["17e35b25d15d"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "18e6a3ac6471", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "3571f351281f"] + } + }, + { + "id": "tw-workspace-ssh-connected.normal:connected", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.normal:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:agents-detected", + "observation": { + "sender": ["90dce4861972"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:connected", + "observation": { + "sender": ["90dce4861972", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:setup-prompted", + "observation": { + "sender": ["90dce4861972", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:agents-detected", + "observation": { + "sender": ["02a3532637b4"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:connected", + "observation": { + "sender": ["02a3532637b4", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:setup-prompted", + "observation": { + "sender": ["02a3532637b4", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:agents-detected", + "observation": { + "sender": ["227c671c491b"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:connected", + "observation": { + "sender": ["227c671c491b", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:setup-prompted", + "observation": { + "sender": ["227c671c491b", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:agents-detected", + "observation": { + "sender": ["fd04a7852302"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:connected", + "observation": { + "sender": ["fd04a7852302", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:setup-prompted", + "observation": { + "sender": ["fd04a7852302", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:agents-detected", + "observation": { + "sender": ["162a699815c1"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:connected", + "observation": { + "sender": ["162a699815c1", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:setup-prompted", + "observation": { + "sender": ["162a699815c1", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:agents-detected", + "observation": { + "sender": ["c5eeac27af29"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:connected", + "observation": { + "sender": ["c5eeac27af29", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:setup-prompted", + "observation": { + "sender": ["c5eeac27af29", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:agents-detected", + "observation": { + "sender": ["19b6093097ff"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:connected", + "observation": { + "sender": ["19b6093097ff", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:setup-prompted", + "observation": { + "sender": ["19b6093097ff", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:agents-detected", + "observation": { + "sender": ["860046b4ce30"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:connected", + "observation": { + "sender": ["860046b4ce30", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:setup-prompted", + "observation": { + "sender": ["860046b4ce30", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:agents-detected", + "observation": { + "sender": ["07d4c9b0eaf2"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:connected", + "observation": { + "sender": ["07d4c9b0eaf2", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:setup-prompted", + "observation": { + "sender": ["07d4c9b0eaf2", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:agents-detected", + "observation": { + "sender": ["95dee1165f95"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "71225024ccf5", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "9f152ed6e897"] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:connected", + "observation": { + "sender": ["95dee1165f95", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "9f0676ba0d67", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:setup-prompted", + "observation": { + "sender": ["95dee1165f95", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "7a1b524f17d0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json new file mode 100644 index 00000000000..81ce3956258 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-repo.hooks-1.json @@ -0,0 +1,976 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "39451d3c811068754f91ac243fe1208f4ce742df53261314345d8209ba761e94", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02800add9d11": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "0ba2cee4b538": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + }, + "1712c415bebf": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "decision": "inherit", + "kind": "decision" + } + }, + "1739575ac53e": { + "name": "workspaceSshConnecting", + "value": true + }, + "17e35b25d15d": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex"] + } + } + }, + "18e6a3ac6471": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "1fcb0efb54e8": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "33cfd55c1890": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "3571f351281f": { + "name": "workspaceDetectedAgentIds", + "value": ["codex"] + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "43ead075ce12": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "43fd3e2f4b53": { + "name": "workspaceSshConnecting", + "value": false + }, + "5278c299d57a": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "6139c7d2716a": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'hooks')", + "isRpcDeliveryUnknown": false + } + }, + "70a84db3f870": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-3", + "ok": false + } + } + }, + "71d817ffdd81": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "7c7a826833e0": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-3", + "ok": false + } + } + }, + "7c9498659f58": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "7e1d82e5b5ed": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "decision": "inherit", + "kind": "decision" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "80a4af19f556": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": " pnpm install " + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + } + } + }, + "8c3bb432df5b": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "941b6aeb0d6f": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a1f755a38636": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "f21c4f69fe5a": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'hooks')", + "isRpcDeliveryUnknown": false + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f7b1983b91e9": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "fbfdbb919268": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + }, + "ff43290f6836": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-ssh-repo.hooks-1", + "checkpoints": [ + { + "id": "tw-workspace-ssh-connected.prelude:agents-detected", + "observation": { + "sender": ["17e35b25d15d"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "18e6a3ac6471", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "3571f351281f"] + } + }, + { + "id": "tw-workspace-ssh-connected.prelude:connected", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.normal:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "f7b1983b91e9"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "f21c4f69fe5a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "ff43290f6836"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "6139c7d2716a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "5278c299d57a"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "1712c415bebf" + }, + "state": "7e1d82e5b5ed", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "02800add9d11"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "1712c415bebf" + }, + "state": "7e1d82e5b5ed", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "8c3bb432df5b"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "1712c415bebf" + }, + "state": "7e1d82e5b5ed", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "7c7a826833e0"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "32a7c0ae7918" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "1fcb0efb54e8"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "f3b516f62081" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "70a84db3f870"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "b948e8307e81" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "941b6aeb0d6f"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "a947768bc0ed" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "33cfd55c1890"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "c7584e82c72f" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json new file mode 100644 index 00000000000..9649d6671ef --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-tasks.workspace-ssh-ssh.connect-1.json @@ -0,0 +1,1422 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "5cc1773d06d49d2616da72f2790753322edda6d67e12b5e41116971d34787391", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "09c18a29abf3": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "0ba2cee4b538": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + }, + "11181309201b": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "1703db1e81e4": { + "name": "workspaceSshState", + "value": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "1739575ac53e": { + "name": "workspaceSshConnecting", + "value": true + }, + "17e35b25d15d": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex"] + } + } + }, + "18e6a3ac6471": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "3571f351281f": { + "name": "workspaceDetectedAgentIds", + "value": ["codex"] + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "42fb94e15a80": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "43ead075ce12": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "43fd3e2f4b53": { + "name": "workspaceSshConnecting", + "value": false + }, + "4a24b4b276fa": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "4ca864d39d04": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "50b0f369719b": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "5313715e0fbb": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "654de1224bc2": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + }, + "671db70f932a": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "6d2db0d7fee0": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "71d817ffdd81": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "7c9498659f58": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "80a4af19f556": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": " pnpm install " + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + } + } + }, + "86cc01b1e541": { + "name": "workspaceSshState", + "value": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "8a5755fd3ffa": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "8dc4620dc1de": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "9164e806ca12": { + "name": "workspaceSshState", + "value": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "93dfd351c771": { + "name": "workspaceSshState", + "value": { + "error": "outer refused", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "9eb40e943577": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "a1f755a38636": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "a209f2c7160e": { + "name": "workspaceSshState", + "value": { + "error": "Unknown method", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "aa15b77aca73": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "aad86573b0be": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "b7fa4557dcfa": { + "name": "workspaceSshState", + "value": { + "error": "transport failure", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "c5608f9dd27c": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "c81e3c5c4429": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "cabfead2f0ff": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "ce4a98c7ed2f": { + "name": "workspaceSshState", + "value": { + "error": "Connection closed", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "ce5554d7557b": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "d86f0ed68c40": { + "agent": "claude", + "connecting": true, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + }, + "e17430747d93": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": "Cannot read properties of null (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "e29464ad65fd": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "e62fd21eb764": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f066aa754e25": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": "Cannot read properties of undefined (reading 'state')", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "fbfdbb919268": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + } + }, + "recording": { + "scenario": "matrix-tasks.workspace-ssh-ssh.connect-1", + "checkpoints": [ + { + "id": "tw-workspace-ssh-connected.prelude:agents-detected", + "observation": { + "sender": ["17e35b25d15d"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "18e6a3ac6471", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "3571f351281f"] + } + }, + { + "id": "tw-workspace-ssh-connected.prelude:cleanup", + "observation": { + "sender": ["17e35b25d15d", "654de1224bc2"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "d86f0ed68c40", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "ce4a98c7ed2f", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.normal:connected", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.normal:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:connected", + "observation": { + "sender": ["17e35b25d15d", "50b0f369719b"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "f066aa754e25", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "1703db1e81e4", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-absent:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "50b0f369719b", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "ce5554d7557b", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "1703db1e81e4", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:connected", + "observation": { + "sender": ["17e35b25d15d", "09c18a29abf3"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "cabfead2f0ff", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "9164e806ca12", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.result-null:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "09c18a29abf3", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "e17430747d93", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "9164e806ca12", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:connected", + "observation": { + "sender": ["17e35b25d15d", "11181309201b"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-ok-missing:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "11181309201b", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:connected", + "observation": { + "sender": ["17e35b25d15d", "c81e3c5c4429"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-string-error:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "c81e3c5c4429", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:connected", + "observation": { + "sender": ["17e35b25d15d", "e62fd21eb764"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.inner-false-object-error:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "e62fd21eb764", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:connected", + "observation": { + "sender": ["17e35b25d15d", "e29464ad65fd"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4ca864d39d04", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "93dfd351c771", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "e29464ad65fd", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "4a24b4b276fa", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "93dfd351c771", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:connected", + "observation": { + "sender": ["17e35b25d15d", "aad86573b0be"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "5313715e0fbb", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.outer-refused-no-message:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "aad86573b0be", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "aa15b77aca73", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:connected", + "observation": { + "sender": ["17e35b25d15d", "8a5755fd3ffa"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "8dc4620dc1de", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "a209f2c7160e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.method-not-found:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "8a5755fd3ffa", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "9eb40e943577", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "a209f2c7160e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:connected", + "observation": { + "sender": ["17e35b25d15d", "c5608f9dd27c"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "6d2db0d7fee0", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "b7fa4557dcfa", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "c5608f9dd27c", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "42fb94e15a80", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "b7fa4557dcfa", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:connected", + "observation": { + "sender": ["17e35b25d15d", "671db70f932a"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "5313715e0fbb", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + }, + { + "id": "tw-workspace-ssh-connected.transport-rejection-no-message:setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "671db70f932a", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "aa15b77aca73", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json new file mode 100644 index 00000000000..500f6774271 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.create-retry-worktree.create-1.json @@ -0,0 +1,653 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "e93a36ef900de27e1a566cdb2389ba4f900a330eadbb476b9bfb1ef05707b352", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0938d32a2ec2": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "12ace8a26229": { + "outcome": { + "error": "outer refused" + } + }, + "151fd59f40cd": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "199931225ca2": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'displayName')", + "isRpcDeliveryUnknown": false + } + }, + "240b0b1c72b2": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "" + } + }, + "2588fd63a157": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'worktree')", + "isRpcDeliveryUnknown": false + } + }, + "292579caa07d": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "2e78a1dad2ea": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "3f946ad0279c": { + "outcome": "uncreated" + }, + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "489c189aebca": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "worktree": { + "displayName": "kestrel", + "id": "repo-1::/w" + } + } + } + } + }, + "6bf7db287168": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "7665e4eb5ce2": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "outer refused" + } + }, + "7d651cae8837": { + "outcome": { + "error": "" + } + }, + "7fbbbeb1902c": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "Unknown method" + } + }, + "8cf7217b02de": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "96a4c62e654d": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b32227fdb10b": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "name": "kestrel", + "worktreeId": "repo-1::/w" + } + }, + "b5447f4dd931": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'worktree')", + "isRpcDeliveryUnknown": false + } + }, + "b6ebedadd49b": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "c26dcf914d04": { + "outcome": { + "error": "Unknown method" + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8b7b7e4da75": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "cf574d8c995b": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "df162b95f465": { + "outcome": { + "name": "kestrel", + "worktreeId": "repo-1::/w" + } + } + }, + "recording": { + "scenario": "matrix-worktree.create-retry-worktree.create-1", + "checkpoints": [ + { + "id": "tw-create-retry-created.normal:created", + "observation": { + "sender": ["489c189aebca"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "b32227fdb10b" + }, + "state": "df162b95f465", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.result-absent:created", + "observation": { + "sender": ["cf574d8c995b"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "2588fd63a157" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.result-null:created", + "observation": { + "sender": ["0938d32a2ec2"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "b5447f4dd931" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.inner-ok-missing:created", + "observation": { + "sender": ["6bf7db287168"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "199931225ca2" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.inner-false-string-error:created", + "observation": { + "sender": ["96a4c62e654d"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "199931225ca2" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.inner-false-object-error:created", + "observation": { + "sender": ["2e78a1dad2ea"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "199931225ca2" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.outer-refused:created", + "observation": { + "sender": ["c8b7b7e4da75"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "7665e4eb5ce2" + }, + "state": "12ace8a26229", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.outer-refused-no-message:created", + "observation": { + "sender": ["b6ebedadd49b"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "240b0b1c72b2" + }, + "state": "7d651cae8837", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.method-not-found:created", + "observation": { + "sender": ["151fd59f40cd"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "7fbbbeb1902c" + }, + "state": "c26dcf914d04", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.transport-rejection:created", + "observation": { + "sender": ["292579caa07d"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "a947768bc0ed" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "tw-create-retry-created.transport-rejection-no-message:created", + "observation": { + "sender": ["8cf7217b02de"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "c7584e82c72f" + }, + "state": "3f946ad0279c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json new file mode 100644 index 00000000000..13dc5e4d143 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolvemrbase-1.json @@ -0,0 +1,739 @@ +{ + "operation": "tasks.composer-hosted-base", + "family": "worktree.hosted-base", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "a84f8a5acfd428eb77b5c02a3de0fa8b780c666db31bbe574ecf76cdf84adeb2", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "08ecbab921e6": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "0e24d2a37a0d": { + "name": "worktree.resolvePrBase#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolvePrBase\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"headRefName\":\"feature\"}}" + }, + "156f5e61efd3": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "17ae65496a72": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "201cea1f9864": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "22f024eeb07c": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "236529aa012d": { + "mrBase": "unresolved", + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "2aaea8ee523e": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused", + "isRpcDeliveryUnknown": false + } + }, + "2c7f810cc819": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "[object Object]", + "isRpcDeliveryUnknown": false + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "336e99424dd0": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "382c27f806f2": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "3cca8119f144": { + "mrBase": { + "baseBranch": "develop" + }, + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "4febe923ceea": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + } + } + }, + "5428de0f5130": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "69afcaf1cb72": { + "name": "worktree.resolveMrBase#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolveMrBase\",\"params\":{\"repo\":\"id:repo-1\",\"mrIid\":7,\"sourceBranch\":\"feature\"}}" + }, + "7214459608bf": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot use 'in' operator to search for 'error' in null", + "isRpcDeliveryUnknown": false + } + }, + "93bb7cfeae89": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "96b186d42430": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "aad8e7ddeea2": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "baseBranch": "develop" + } + } + } + }, + "ae5862eb7a20": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot use 'in' operator to search for 'error' in undefined", + "isRpcDeliveryUnknown": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bd721565327b": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d05b2d417b9c": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "inner refused", + "isRpcDeliveryUnknown": false + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fd552ecb03da": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "develop" + } + } + }, + "recording": { + "scenario": "matrix-worktree.hosted-base-worktree.resolvemrbase-1", + "checkpoints": [ + { + "id": "tw-hosted-base-resolved.prelude:pr-base-resolved", + "observation": { + "sender": ["4febe923ceea"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "5428de0f5130" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.normal:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "fd552ecb03da" + }, + "state": "3cca8119f144", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-absent:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "93bb7cfeae89"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "ae5862eb7a20" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-null:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "336e99424dd0"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "7214459608bf" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-ok-missing:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "08ecbab921e6"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "2aaea8ee523e" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-string-error:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "96b186d42430"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "d05b2d417b9c" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-object-error:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "156f5e61efd3"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "2c7f810cc819" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "201cea1f9864"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "32a7c0ae7918" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused-no-message:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "382c27f806f2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "f3b516f62081" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.method-not-found:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "17ae65496a72"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "b948e8307e81" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "bd721565327b"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "a947768bc0ed" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection-no-message:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "22f024eeb07c"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "c7584e82c72f" + }, + "state": "236529aa012d", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json new file mode 100644 index 00000000000..f1c90037fcd --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.hosted-base-worktree.resolveprbase-1.json @@ -0,0 +1,869 @@ +{ + "operation": "tasks.composer-hosted-base", + "family": "worktree.hosted-base", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "45783a7cbb44b04dbbd6bfd6735799bb4c75e503f43f1821cf8640d11f7464ad", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0ace0141301c": { + "mrBase": { + "baseBranch": "develop" + }, + "prBase": "unresolved" + }, + "0e24d2a37a0d": { + "name": "worktree.resolvePrBase#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolvePrBase\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"headRefName\":\"feature\"}}" + }, + "236529aa012d": { + "mrBase": "unresolved", + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "28b232b6369b": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "2aaea8ee523e": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "refused", + "isRpcDeliveryUnknown": false + } + }, + "2c7f810cc819": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "[object Object]", + "isRpcDeliveryUnknown": false + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "388eebbe7dca": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "3cca8119f144": { + "mrBase": { + "baseBranch": "develop" + }, + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "4febe923ceea": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + } + } + }, + "5428de0f5130": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "5ce5558cd2f1": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "60f898896e1a": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "62d33be71d4d": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "658dfb6d27f2": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "69afcaf1cb72": { + "name": "worktree.resolveMrBase#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolveMrBase\",\"params\":{\"repo\":\"id:repo-1\",\"mrIid\":7,\"sourceBranch\":\"feature\"}}" + }, + "7214459608bf": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot use 'in' operator to search for 'error' in null", + "isRpcDeliveryUnknown": false + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "aad8e7ddeea2": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "baseBranch": "develop" + } + } + } + }, + "ae5862eb7a20": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot use 'in' operator to search for 'error' in undefined", + "isRpcDeliveryUnknown": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c57e06c96492": { + "mrBase": "unresolved", + "prBase": "unresolved" + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d05b2d417b9c": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "inner refused", + "isRpcDeliveryUnknown": false + } + }, + "d778e31ef5f7": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "e3d0229e3cdb": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f19c03489128": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f4bbce06e9b6": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "fd552ecb03da": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "develop" + } + } + }, + "recording": { + "scenario": "matrix-worktree.hosted-base-worktree.resolveprbase-1", + "checkpoints": [ + { + "id": "tw-hosted-base-resolved.normal:pr-base-resolved", + "observation": { + "sender": ["4febe923ceea"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "5428de0f5130" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.normal:mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "fd552ecb03da" + }, + "state": "3cca8119f144", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-absent:pr-base-resolved", + "observation": { + "sender": ["60f898896e1a"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "ae5862eb7a20" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-absent:mr-base-resolved", + "observation": { + "sender": ["60f898896e1a", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "ae5862eb7a20", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-null:pr-base-resolved", + "observation": { + "sender": ["f4bbce06e9b6"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "7214459608bf" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.result-null:mr-base-resolved", + "observation": { + "sender": ["f4bbce06e9b6", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "7214459608bf", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-ok-missing:pr-base-resolved", + "observation": { + "sender": ["658dfb6d27f2"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "2aaea8ee523e" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-ok-missing:mr-base-resolved", + "observation": { + "sender": ["658dfb6d27f2", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "2aaea8ee523e", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-string-error:pr-base-resolved", + "observation": { + "sender": ["62d33be71d4d"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "d05b2d417b9c" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-string-error:mr-base-resolved", + "observation": { + "sender": ["62d33be71d4d", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "d05b2d417b9c", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-object-error:pr-base-resolved", + "observation": { + "sender": ["5ce5558cd2f1"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "2c7f810cc819" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.inner-false-object-error:mr-base-resolved", + "observation": { + "sender": ["5ce5558cd2f1", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "2c7f810cc819", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused:pr-base-resolved", + "observation": { + "sender": ["f19c03489128"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "32a7c0ae7918" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused:mr-base-resolved", + "observation": { + "sender": ["f19c03489128", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "32a7c0ae7918", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused-no-message:pr-base-resolved", + "observation": { + "sender": ["e3d0229e3cdb"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "f3b516f62081" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.outer-refused-no-message:mr-base-resolved", + "observation": { + "sender": ["e3d0229e3cdb", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "f3b516f62081", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.method-not-found:pr-base-resolved", + "observation": { + "sender": ["28b232b6369b"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "b948e8307e81" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.method-not-found:mr-base-resolved", + "observation": { + "sender": ["28b232b6369b", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "b948e8307e81", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection:pr-base-resolved", + "observation": { + "sender": ["d778e31ef5f7"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "a947768bc0ed" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection:mr-base-resolved", + "observation": { + "sender": ["d778e31ef5f7", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "a947768bc0ed", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection-no-message:pr-base-resolved", + "observation": { + "sender": ["388eebbe7dca"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "c7584e82c72f" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "tw-hosted-base-resolved.transport-rejection-no-message:mr-base-resolved", + "observation": { + "sender": ["388eebbe7dca", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "c7584e82c72f", + "mr": "fd552ecb03da" + }, + "state": "0ace0141301c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json index 8ab2bcc788a..b792a3eaf66 100644 --- a/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json +++ b/mobile/rpc-foundation/goldens/matrix-worktree.review-link-worktree.set-1.json @@ -3,14 +3,15 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "2fc093ec505bfac04a4ff0adab991baeba985253486dbe9e3ec9884b8d5f0920", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0d39ad71ac82": { "linkedPR": "unread", diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json new file mode 100644 index 00000000000..550750f9c6c --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.runtime-capabilities-status.get-1.json @@ -0,0 +1,571 @@ +{ + "operation": "tasks.worktree-capabilities", + "family": "worktree.runtime-capabilities", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "f70c6b1753377b5a502bf7d1e69dc95617f24c42137471320eb393567efbe735", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "16cd464bf664": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "2698c9770ad3": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "4451bb95a76e": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "5242fad3532f": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["mobile.tasks.v1", "worktree.create-idempotency.v1"], + "platform": "linux", + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + } + } + }, + "62aaf19f0b16": { + "capabilities": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + }, + "7d3dd7f9381b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "86f7fa8089fe": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": false, + "worktreeCreateIdempotency": false + } + }, + "88200d49083c": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "89236e432861": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "944bf432f199": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "9cdf3c107e7b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "b33d34bddc4e": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + }, + "c71b2f8a6993": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "de87f6266897": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "f80e92134eb1": { + "capabilities": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": false, + "worktreeCreateIdempotency": false + } + } + }, + "recording": { + "scenario": "matrix-worktree.runtime-capabilities-status.get-1", + "checkpoints": [ + { + "id": "tw-capabilities-advertised.normal:probed", + "observation": { + "sender": ["5242fad3532f"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "b33d34bddc4e" + }, + "state": "62aaf19f0b16", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.result-absent:probed", + "observation": { + "sender": ["7d3dd7f9381b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.result-null:probed", + "observation": { + "sender": ["88200d49083c"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.inner-ok-missing:probed", + "observation": { + "sender": ["4451bb95a76e"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.inner-false-string-error:probed", + "observation": { + "sender": ["944bf432f199"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.inner-false-object-error:probed", + "observation": { + "sender": ["89236e432861"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.outer-refused:probed", + "observation": { + "sender": ["16cd464bf664"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.outer-refused-no-message:probed", + "observation": { + "sender": ["9cdf3c107e7b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.method-not-found:probed", + "observation": { + "sender": ["c71b2f8a6993"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.transport-rejection:probed", + "observation": { + "sender": ["de87f6266897"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + }, + { + "id": "tw-capabilities-advertised.transport-rejection-no-message:probed", + "observation": { + "sender": ["2698c9770ad3"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "86f7fa8089fe" + }, + "state": "f80e92134eb1", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json new file mode 100644 index 00000000000..34c5923d693 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.setup-hook-trust-ui.set-1.json @@ -0,0 +1,674 @@ +{ + "operation": "tasks.setup-hook-trust", + "family": "worktree.setup-hook-trust", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "8487fd14ed779708415b264e2b80b26b0d5094e379a04f4f32c2cd75ec469182", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0f68ccbfb8e9": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"trustedOrcaHooks\":{\"repo-1\":{\"setup\":{\"contentHash\":\"hash-1\",\"approvedAt\":1767225600000}}}}}" + }, + "229c35d1a4ba": { + "trust": "unapproved" + }, + "255cdc090b8a": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "2905cce95e1c": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "29fc0c5de3b0": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "32af950a57cc": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "6f009f61d89f": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "9deb505f7915": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "a406b068aeca": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "abd752b10f76": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "ac7d2d4aa85c": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "b2aa9ff12623": { + "trust": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "d1113bd291a2": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "e3e6506e1ed0": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e9c010ad58d3": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-worktree.setup-hook-trust-ui.set-1", + "checkpoints": [ + { + "id": "tw-setup-hook-trust-approved.normal:approved", + "observation": { + "sender": ["6f009f61d89f"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.result-absent:approved", + "observation": { + "sender": ["e9c010ad58d3"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.result-null:approved", + "observation": { + "sender": ["9deb505f7915"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.inner-ok-missing:approved", + "observation": { + "sender": ["ac7d2d4aa85c"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.inner-false-string-error:approved", + "observation": { + "sender": ["d1113bd291a2"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.inner-false-object-error:approved", + "observation": { + "sender": ["2905cce95e1c"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.outer-refused:approved", + "observation": { + "sender": ["e3e6506e1ed0"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "32a7c0ae7918" + }, + "state": "229c35d1a4ba", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.outer-refused-no-message:approved", + "observation": { + "sender": ["32af950a57cc"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "f3b516f62081" + }, + "state": "229c35d1a4ba", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.method-not-found:approved", + "observation": { + "sender": ["255cdc090b8a"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "b948e8307e81" + }, + "state": "229c35d1a4ba", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.transport-rejection:approved", + "observation": { + "sender": ["29fc0c5de3b0"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a947768bc0ed" + }, + "state": "229c35d1a4ba", + "effects": [] + } + }, + { + "id": "tw-setup-hook-trust-approved.transport-rejection-no-message:approved", + "observation": { + "sender": ["abd752b10f76"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "c7584e82c72f" + }, + "state": "229c35d1a4ba", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json index 5d21ce568a0..a11324d3bb7 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-both-refused.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "819fa73c7700b4d526da91c37558a6498008d745d1debcc26e6bb757550ebf99", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json index 68a52959edf..8fa02cf4b25 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-null-sibling-refused.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "500396d72abd2f73d11ef066bca3f88798c8cbdaef09fa7c1c8d1fbaf0b3b85a", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json b/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json index 5a74ca74542..6617e7fc33a 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-refused-sibling-rejects.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "daf68df8840ea6872521d823cc17e1e5de3f3a74a8855465fcf40cc276e9c2ce", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json b/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json index 441f1284661..9b093e9e6a8 100644 --- a/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json +++ b/mobile/rpc-foundation/goldens/probe-new-tab-rejects-sibling-refused.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "61e36caf6b3bb01c3ad0db282b7f0fbc0f300d40184f9cf3d7e4e3a3194a4f2a", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-default.json b/mobile/rpc-foundation/goldens/sc-base-ref-default.json index efc8a33b474..7dd990f81a6 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-default.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-default.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "374129def6baa0e06b808c067831820966638d79d7a782e96c1f2f891cc9dc86", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "089d79f002a1": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json b/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json index 8b451a3c47b..3140487f163 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-repo-fallback.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "97a8b8f5b9a7c7467745666becee07f5dfc57fb283d4e80dcbe7941509177598", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "594101d24d72": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json b/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json index 6f61749a0ff..e06a4eda1d0 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-unavailable.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "25a4762d735dfb4979e6ef31b9fdb380941a824a54b45b3d08ddb2cde25c2eb7", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "594101d24d72": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json b/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json index e134ff8d55d..4c709fa04c6 100644 --- a/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json +++ b/mobile/rpc-foundation/goldens/sc-base-ref-worktree-hit.json @@ -3,14 +3,15 @@ "family": "git.base-ref-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "f3b193f93f6c9de41d11e706ecbd99648eb2ed41ccb7c66cdb80c934e780ed7c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "26accd69bc48": { "name": "repo.list#1", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json b/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json index 5efc0525535..ec14ddf867c 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-cancel-rejected.json @@ -3,14 +3,15 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "013db25622b180a8333bb1ef27c22a5b1f8e04148201201e5cc3413a10640781", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "029ea2c16f05": { "name": "git.cancelGenerateCommitMessage#1", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json b/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json index 84768ce1373..aa585267ff0 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-canceled.json @@ -3,14 +3,15 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "15bf6c17f4b524dfbf5373b2eeed61ee2e659421cf8b6e3cff6c0378c7692cc1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0aeb6552c58a": { "name": "git.generateCommitMessage#1", diff --git a/mobile/rpc-foundation/goldens/sc-commit-message-generated.json b/mobile/rpc-foundation/goldens/sc-commit-message-generated.json index a8abfe60296..905123dc6fc 100644 --- a/mobile/rpc-foundation/goldens/sc-commit-message-generated.json +++ b/mobile/rpc-foundation/goldens/sc-commit-message-generated.json @@ -3,14 +3,15 @@ "family": "git.commit-message-ai", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "540f05d84d1cbffd566af933c547838c75500bb5d708e8558c21fe8131d724e3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "125fbea5f50a": { "name": "git.generateCommitMessage#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-existing-review.json b/mobile/rpc-foundation/goldens/sc-create-existing-review.json index e51f2ee4e8b..2cce136bf27 100644 --- a/mobile/rpc-foundation/goldens/sc-create-existing-review.json +++ b/mobile/rpc-foundation/goldens/sc-create-existing-review.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "7d6099248aa6a2ef19f2e169ff917af794649d9d64d139aa9ffeea6a41355ddc", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1617f98dc371": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json b/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json index 91ac688ebcb..5512be8da12 100644 --- a/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json +++ b/mobile/rpc-foundation/goldens/sc-create-intent-stage-commit-push-create.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-intent", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "447f9b3d697dbfe21cb7fb6e12d1bf5fa94b023b7e1697bdc2dc82ce7072183f", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json b/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json index 818cf613211..f2c81a1f63d 100644 --- a/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json +++ b/mobile/rpc-foundation/goldens/sc-create-link-failure-is-non-fatal.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "2deb0435ef63a3e0102e28f2f3f331039486d193d1e1ffdfb53ad86d3ff039f0", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "122ef8a1f0b9": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json b/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json index 32d59893c05..f2d5807370b 100644 --- a/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json +++ b/mobile/rpc-foundation/goldens/sc-create-pushes-then-creates.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "0f86b3e6059c48cd327c55df2452a9bc6ea85584ffbeacafad496f600c20e06f", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06a94a810e5f": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json b/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json index d810ad5a8c5..e77d1ab11c2 100644 --- a/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-create-refused-empty-message.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "5008e69a8e396b1deccd98712d92650a630f971cd76a02862a461afb8617b8a4", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1617f98dc371": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json b/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json index 337f070d679..06a31f9690e 100644 --- a/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-create-rejected-empty-message.json @@ -3,14 +3,15 @@ "family": "hostedReview.create-chain", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "ea71081982a101d0f8624707b59c99de9981e9b1d1bafa25c66d575e3f876ff4", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1617f98dc371": { "name": "hostedReview.create#1", diff --git a/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json b/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json index 5de6a24a2af..3fea01ffea6 100644 --- a/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json +++ b/mobile/rpc-foundation/goldens/sc-eligibility-fetched.json @@ -3,14 +3,15 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "f6a1595073abe11b33973e8865900a1d849f44221961da5c12ea13aa696f6490", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "24bd84c9fb40": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-history-loaded.json b/mobile/rpc-foundation/goldens/sc-history-loaded.json index 848a5c1acdc..2e6d7621018 100644 --- a/mobile/rpc-foundation/goldens/sc-history-loaded.json +++ b/mobile/rpc-foundation/goldens/sc-history-loaded.json @@ -3,14 +3,15 @@ "family": "git.history-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "83f61085a91458bad529905ecc6fe240c598cddfe44a56dd497b8aed9fb8a7e5", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "17bc1e177fe1": { "name": "git.history#1", diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json b/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json index c1a63194a95..87cb6d74b1a 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-hosted-review.json @@ -3,14 +3,15 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "42b0304b2fdce08b7ff52ec979dd9f199f368e5e4ef0b5370acc417909b592b3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1852c739af4f": { "linkedPR": "unread", diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-read.json b/mobile/rpc-foundation/goldens/sc-pr-link-read.json index 908b8db23be..bbe80639fe6 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-read.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-read.json @@ -3,14 +3,15 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "4520bd54a55eabfe6ec64a4b2f824f095f98b2fffd1bf22fe4f9ec7f63cbfa3f", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "17ab8ab0a9f4": { "linkedPR": 7, diff --git a/mobile/rpc-foundation/goldens/sc-pr-link-set.json b/mobile/rpc-foundation/goldens/sc-pr-link-set.json index b161e5d105f..d11a49779d4 100644 --- a/mobile/rpc-foundation/goldens/sc-pr-link-set.json +++ b/mobile/rpc-foundation/goldens/sc-pr-link-set.json @@ -3,14 +3,15 @@ "family": "worktree.review-link", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "b65bad4f8c0ae0b686f6c3db93bd43ffa072ae426f978f1a86ad8008fb24fa24", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1852c739af4f": { "linkedPR": "unread", diff --git a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json index 695d5f054b1..4cc19ec06cd 100644 --- a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json +++ b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-refusal.json @@ -3,14 +3,15 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "9de96287a4dfa6cf5c9a8b683cc696fdc2cd387f86f231e22ee3f100a2e778e3", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0710fc7e2b71": { "name": "hostedReview.getCreationEligibility#1", diff --git a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json index 2c6c73ee674..88d435bce53 100644 --- a/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json +++ b/mobile/rpc-foundation/goldens/sc-prefill-unavailable-on-rejection.json @@ -3,14 +3,15 @@ "family": "hostedReview.eligibility", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "adbdfcc3895cc04d830900de518e689c9e63f6f75569127e1fde24488658e8a0", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2f56274e5397": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json b/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json index d1358b6562e..e87bbbdcd9c 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-force-with-lease.json @@ -3,14 +3,15 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "ec2847b4af357d8564d8e0a9a1072713c1afd7ff86ba69e9c83c056a6841ee39", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "00e8a3bac22f": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json b/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json index dfb64a312d6..c6f5866b3bc 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-publish.json @@ -3,14 +3,15 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "6cf6ebd20adc4cc76a12d3424863ee9db2b24f36593664a1f4e0e05de9a53d39", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "00e8a3bac22f": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-push.json b/mobile/rpc-foundation/goldens/sc-prerequisite-push.json index ed73ee99553..da29fb11607 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-push.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-push.json @@ -3,14 +3,15 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "e6e8197a541cd73e5811a1f28b0dbfc414a4d34c1ae6929fc1bbae1213820674", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "00e8a3bac22f": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json b/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json index 1684dea4b6c..888440d3fb8 100644 --- a/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json +++ b/mobile/rpc-foundation/goldens/sc-prerequisite-skipped.json @@ -3,14 +3,15 @@ "family": "git.remote-prerequisite", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "523d1ee21e3871a4dffc32f48d2e28c31ecea48cbf3f842acffc8355be06b14b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "69f421c50546": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json b/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json index 16850663572..428027eb01e 100644 --- a/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json +++ b/mobile/rpc-foundation/goldens/sc-reveal-first-poll.json @@ -3,14 +3,15 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "00b26cb279b0a934df98d93ba98a4a0c79e302c7690c582e778dc0156ab4f235", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0f9f4df04699": { "name": "session.tabs.activate#1", diff --git a/mobile/rpc-foundation/goldens/sc-reveal-timeout.json b/mobile/rpc-foundation/goldens/sc-reveal-timeout.json index 0ef7c028dea..a008df59c41 100644 --- a/mobile/rpc-foundation/goldens/sc-reveal-timeout.json +++ b/mobile/rpc-foundation/goldens/sc-reveal-timeout.json @@ -3,14 +3,15 @@ "family": "session.tab-reveal", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5e47c7960b9352753f0751d4ccf5e3ff7fb7b74a63cc4e4df3c3ffdcc6c1e66b", "scenarioSha256": "fcd61f1ef46c42889827a87876239534b851c425f8ef7a9405ea95b8d07d2363", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "3dbdccea1da9": { "name": "session.tabs.list#3", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json b/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json index 8fbaaf8af5d..45bb2402e37 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-inner-failure.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "db1fb2a584cd806028b9be861283a63aa4836c83f61558f3d518ddbb7a59498d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "88185276c233": { "status": "fulfilled", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json b/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json index f739f278bf4..40beeb8b55b 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-refused-empty-message.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "dba1676583dc832ef059285a6bd4c3eefe6be0230e42100cb9d7a125755d136b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "45f289a0f3ae": { "committed": { diff --git a/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json b/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json index 2f5b022f361..078faa9be2f 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit-rejected.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "de079de9cc21bfb40da6e1431273b10c3e2a5b5402b91b2b8a85c8d7ac41bc97", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "01bab795ab1e": { "name": "git.commit#1", diff --git a/mobile/rpc-foundation/goldens/sc-review-commit.json b/mobile/rpc-foundation/goldens/sc-review-commit.json index c3092244e76..cf10a46d6ff 100644 --- a/mobile/rpc-foundation/goldens/sc-review-commit.json +++ b/mobile/rpc-foundation/goldens/sc-review-commit.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "a29d518b2d075e8e4811404e0fcbf8948fbfe53a3aefbf0948cb2f2e622e8cbb", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "17bb401abe83": { "committed": { diff --git a/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json b/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json index 0c03aa7a953..2ce6ecf5b47 100644 --- a/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json +++ b/mobile/rpc-foundation/goldens/sc-review-status-entries-not-array.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "37ba7780ca9525ab313c0a9c781ce5bb26344e63af9272c32ae889621f383b2b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "5e330d49c396": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/sc-review-status-normalized.json b/mobile/rpc-foundation/goldens/sc-review-status-normalized.json index 27735b6ec4f..c034531b532 100644 --- a/mobile/rpc-foundation/goldens/sc-review-status-normalized.json +++ b/mobile/rpc-foundation/goldens/sc-review-status-normalized.json @@ -3,14 +3,15 @@ "family": "git.review-preparation", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "c582ce355b0704b38d7e84cbce352630096c91231bf09aecd1f6dbb409fed17f", "scenarioSha256": "93193acd57d6f00abc8e6c22ec3a8f1ca6ce7c5808d906d0aa7c11e41dab4635", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0278e0f0d6cf": { "name": "git.status#1", diff --git a/mobile/rpc-foundation/goldens/schedules-b3.json b/mobile/rpc-foundation/goldens/schedules-b3.json index 8744ce77f9d..556c6383e4e 100644 --- a/mobile/rpc-foundation/goldens/schedules-b3.json +++ b/mobile/rpc-foundation/goldens/schedules-b3.json @@ -3,14 +3,15 @@ "family": "linear-detail-barrier", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "b59fb599dd3a5fbc79bb8602dcec4b1c51a392c662efab7efc8324fc718ce8de", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "034a83431f03": { "name": "linear.getIssue#1", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json index ea6b3307985..30d4c2b7289 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-home-providers-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "fbd311a377672a9335521c30734880eea1b04bab0aff367854c1deebcf66b105", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "1081ce76cc68": { "name": "linear.status#1", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json b/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json index 5fade18c847..84b00d9421e 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-new-tab-ssh.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "2726d71130f623e3ad02c168c13269979ca6f84703bf1c5aaf36bd4432dfb516", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06eff8247d02": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json index b211647b7ad..561713d4919 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-repo-metadata-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "664eba1468e229f9ac2dced262e7ad896ead01688dff4c570f397c3f8594efd7", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json index 6ba9db9e317..f68cf880e8c 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-resume-metadata-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "7bfba3fae1dc33acf40e8a955bbfccf28580b3daee3270dec6a15e6cefd45a84", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0447fbb835ad": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json index 46ae74ee9a7..9d742a9fe1c 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-task-hydration-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "02e3ca10296704b5478185e9d3dc0136596a2ee57580d7f9268672568dab4cd4", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json index 1d576fb6c54..7fb20d98a13 100644 --- a/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/schedules-settings-workspace-context-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "eb980fc027ca0200212ba6ad3bf9a1ab3460936a7bb4b04353a9462eecd287a1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "06eff8247d02": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json index 339e6c67c53..4f3de72a1cc 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "4b4b4a8d1acaaec1c8dde0233dc49a696ffe53466578477efcbcdb7263dbd617", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json index 47ff0ace506..35717335b1b 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-refresh-refused.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "cac4980465661fba372e187699741123a4edeb9270125a0a1cad7bbb6a6adebd", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json index f0ea7524e05..a26685776e0 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-refused.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "f8ebd2348373b2b39c167735e0c418dfe868511fb5306ecba90cb6f2a905b95e", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json b/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json index eb7b6d59568..7f9ec15954a 100644 --- a/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-bot-overrides-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.bot-overrides", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "f7e63144421a689f05cc50ad87ec901a9eaeb3163165656887be12a5f2753005", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-home-coalesced.json b/mobile/rpc-foundation/goldens/settings-home-coalesced.json index f75d79cd560..5c7c4a9132a 100644 --- a/mobile/rpc-foundation/goldens/settings-home-coalesced.json +++ b/mobile/rpc-foundation/goldens/settings-home-coalesced.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "d8d6f738ee11d84d6e9e546624f4babcb42476432f6bddbc74e8519d9ca18370", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "0241b27b279c": { "name": "settings.get#3", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json b/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json index b46ae96ec3e..d8c862a2805 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a32b2fc99e830c58460e6f7c857aed0048738a55501e508eb236604680b9c235", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "24054d93a95f": { "name": "providers", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json index 873318de0fc..2c4043afd7b 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-refuse-after-data.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "78c48025c4af6cc0f1f136448c0ede9f76b7485d7b33b1356d11dec017bd9053", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "078b082b9b55": { "name": "linear.status#2", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-refused.json b/mobile/rpc-foundation/goldens/settings-home-providers-refused.json index c0eed18e92b..2027ca29d96 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-refused.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-refused.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "c22f33a0ed28622d4d53ae31e934056f87d2c10e8dc4475831ae1ee5fd3a9b8b", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "24054d93a95f": { "name": "providers", diff --git a/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json b/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json index b01cf2c4ea4..8f831b94a86 100644 --- a/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-home-providers-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.home-providers", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "4f16b43dddfb9257828342b0297317868df24b03fd98a89c66f5cd1897829d73", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "163d57ce469e": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-refused.json b/mobile/rpc-foundation/goldens/settings-new-tab-refused.json index 635d3093dd9..a279bf2cf5c 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-refused.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-refused.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "b6fb40be3bb92d7d9f1a79d99dee077cf95097077917679dc99702f912241fa4", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json b/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json index af4bbc7873a..c3b1b41a809 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-ssh.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "31f8a348322551738b14207b3477bae492d48d45c5d51be4d97ffaca2fe2b6e1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json b/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json index 53721dfee13..67dcbaa2268 100644 --- a/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-new-tab-transport-error.json @@ -3,14 +3,15 @@ "family": "settings-agent-read", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "448cdbd12f4f6a14bb33947bfbbb1837aebeb28979db70c62f2ba2fbb4d89c8f", "scenarioSha256": "1347663aba0ada1eee8e88fac306757dc0d29fe21f0d062ac2d3968a30a2f214", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json b/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json index 1dcccf8c919..e18196945aa 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json +++ b/mobile/rpc-foundation/goldens/settings-repo-cache-expiry.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "adebd553e2648278d719a1d7299cb36683fce714682a1ab7b49d4c9027eea34e", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json index 1a996c52104..614d070572a 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "b45eba2007e8e2668f524cd7503b8a711eba67816c9c35af5c3725a1afe32d8d", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json index 06c148924b8..bd1a76e2157 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-refuse-after-data.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "b1c0b957b828c32e7ec388ec6668273fe84bbe5d11d8286b9a246fa92395a26e", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json index cc4100645ad..77e38a6976d 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-refused.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "867b6905c8a533ddd1c7c8174bf4aadd5fd725cc72bdddbcb2ea8af26e219078", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json index 4b6030d3e80..0434f40b769 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-single-host.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "d8a00a72849f1ed254c3b35ebcc330dd1bb15b189f006bd1517853a19e53de6c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2bd489a9fa29": { "repoColorsByName": [ diff --git a/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json b/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json index ed46e819bda..ec872c5ba5f 100644 --- a/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-repo-metadata-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.repo-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "741db13a84dbcec2e97e80605d742e69558954657c72f8450f3f8bc177dd01b6", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02449e890487": { "name": "host.platform#1", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json index 40c1299216d..e33bffa7169 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "7a8d0a5305aafea56733c229989b6e825fe9b8a681f48f6cef405350304520b6", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json index c2134a9da51..b719686e1b9 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-refuse-after-data.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a11756ecaf7c2d3955b9512aa7479ca55d810341f1492f472985abb538e140e8", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json index 06fcf005535..bdc37f9fa94 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-refused.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "def0640be601a8013f9537f161b60d8c14ac9551931e5ee4d4cc2acd3c2baf2a", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json b/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json index 27398aa2494..87613cf2a3e 100644 --- a/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-resume-metadata-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.resume-metadata", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "17c31d0c2b7ae5322fd59bafcfa1d2779ae9eff841e12c0cf16b27e454b49f13", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "14a657727096": { "name": "worktree.ps#1", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json b/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json index 0487f1b4564..328f578b80c 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "c08cce5d1f71761dbf504863b736e5546abb42b9ff4ab8ced65c7c42e3d66c0e", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json index 73654e37ced..1b522f55949 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-refuse-after-data.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "b23c3076081901c89e8a8fb8d20028e03f030db040c9cd793b6f2c7cd49d8f25", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json b/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json index c22817884ae..258690da249 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-refused.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a510ff7505cddbd6dad3c7e5a2dcde206a5dab1940901511d72c97aca576a6f1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json b/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json index 38b2cd42359..e215c4800c6 100644 --- a/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-task-hydration-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.task-hydration", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "d4a2fef3aefb78bdb4aed94fda982124f24a3af3832227654d324735f44aaeeb", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "002ad269dd44": { "name": "showLinearConnect", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json b/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json new file mode 100644 index 00000000000..ffa8395a545 --- /dev/null +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-create-linear.json @@ -0,0 +1,242 @@ +{ + "operation": "settings.task-workspace-create", + "family": "settings.task-workspace-create", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", + "scenarioSha256": "662c3e04e31bce5757f09f91e3e3739fb9d57767b7443be4dc936705b64b1432", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "067cef118d9f": { + "name": "runtimeTaskSettings", + "value": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "090c88478661": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "0f72e7ee78c9": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "displayName": "ORC-1 Recorded issue", + "id": "wt-1" + } + } + } + } + }, + "180125f5d1a6": { + "name": "workspaceCreateDraft", + "value": { + "$rpc": "null" + } + }, + "2473f12c7cdd": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + } + } + } + }, + "33e3b949d4c5": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "6eb4e79ad99a": { + "name": "setupPrompt", + "value": { + "$rpc": "null" + } + }, + "730f92993963": { + "name": "creatingKey", + "value": "linear:1" + }, + "7abdfe20af50": { + "creating": "linear:1", + "error": "", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, + "7ddcb1852b39": { + "name": "settings.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, + "7ee99993a895": { + "name": "navigation", + "value": "/h/host-1/session/wt-1?name=ORC-1+Recorded+issue&created=1" + }, + "82cd71d524c8": { + "name": "error", + "value": "" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "ac9996319e05": { + "name": "actionItem", + "value": { + "$rpc": "null" + } + }, + "baa74a0ec378": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"orc-1\",\"displayName\":\"ORC-1 Recorded issue\",\"displayNameKind\":\"generated\",\"linkedLinearIssue\":\"ORC-1\",\"setupDecision\":\"inherit\",\"activate\":true,\"startupDraft\":\"https://linear.app/orca/issue/ORC-1\",\"createdWithAgent\":\"claude\"}}" + }, + "c9cb32059b8d": { + "name": "creatingKey", + "value": { + "$rpc": "null" + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "settings-task-workspace-create-linear", + "checkpoints": [ + { + "id": "settings-pending", + "observation": { + "sender": ["090c88478661"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "7abdfe20af50", + "effects": ["730f92993963", "82cd71d524c8"] + } + }, + { + "id": "created", + "observation": { + "sender": ["2473f12c7cdd", "0f72e7ee78c9"], + "payloads": ["7ddcb1852b39", "baa74a0ec378"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "730f92993963", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "7ee99993a895", + "c9cb32059b8d" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json b/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json new file mode 100644 index 00000000000..870d6724ba7 --- /dev/null +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-create-pr-start-point.json @@ -0,0 +1,336 @@ +{ + "operation": "settings.task-workspace-create", + "family": "settings.task-workspace-create", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", + "scenarioSha256": "8ae9e1dbb32d404eac9e01f71dacf1c37497030220a8e988c0093bb7ed2d159b", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "067cef118d9f": { + "name": "runtimeTaskSettings", + "value": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "090c88478661": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "180125f5d1a6": { + "name": "workspaceCreateDraft", + "value": { + "$rpc": "null" + } + }, + "2473f12c7cdd": { + "name": "settings.get#1", + "args": [ + { + "name": "method", + "value": "settings.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + } + } + } + }, + "33e3b949d4c5": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "3dc266b1bda1": { + "creating": "github:7", + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": [] + } + }, + "52051fd3214e": { + "creating": "github:7", + "error": "", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, + "6eb4e79ad99a": { + "name": "setupPrompt", + "value": { + "$rpc": "null" + } + }, + "7ddcb1852b39": { + "name": "settings.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, + "82cd71d524c8": { + "name": "error", + "value": "" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9e9f36142bbd": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "baseBranch": "main", + "createdWithAgent": "claude", + "displayName": "Recorded pull request", + "displayNameKind": "generated", + "linkedPR": 7, + "name": "pr-7", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://github.com/o/r/pull/7" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "warning": "shallow clone", + "worktree": { + "id": "wt-2" + } + } + } + } + }, + "a49b109c46d4": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "baseBranch": "main", + "createdWithAgent": "claude", + "displayName": "Recorded pull request", + "displayNameKind": "generated", + "linkedPR": 7, + "name": "pr-7", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://github.com/o/r/pull/7" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "ac9996319e05": { + "name": "actionItem", + "value": { + "$rpc": "null" + } + }, + "b66f6d1958b9": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"pr-7\",\"displayName\":\"Recorded pull request\",\"displayNameKind\":\"generated\",\"setupDecision\":\"inherit\",\"activate\":true,\"startupDraft\":\"https://github.com/o/r/pull/7\",\"createdWithAgent\":\"claude\",\"baseBranch\":\"main\",\"linkedPR\":7}}" + }, + "be0ebed89b2b": { + "name": "navigation", + "value": "/h/host-1/session/wt-2?name=Recorded+pull+request&created=1&warning=shallow+clone" + }, + "c9cb32059b8d": { + "name": "creatingKey", + "value": { + "$rpc": "null" + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ecc2acd4d70d": { + "name": "worktree.resolvePrBase#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolvePrBase\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":7}}" + }, + "ecefc28694cb": { + "name": "creatingKey", + "value": "github:7" + }, + "f9e183f427ee": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "prNumber": 7, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "baseBranch": "main" + } + } + } + } + }, + "recording": { + "scenario": "settings-task-workspace-create-pr-start-point", + "checkpoints": [ + { + "id": "settings-pending", + "observation": { + "sender": ["090c88478661"], + "payloads": ["7ddcb1852b39"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "52051fd3214e", + "effects": ["ecefc28694cb", "82cd71d524c8"] + } + }, + { + "id": "pr-base-resolved", + "observation": { + "sender": ["2473f12c7cdd", "f9e183f427ee", "a49b109c46d4"], + "payloads": ["7ddcb1852b39", "ecc2acd4d70d", "b66f6d1958b9"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "3dc266b1bda1", + "effects": ["ecefc28694cb", "82cd71d524c8", "067cef118d9f"] + } + }, + { + "id": "created-from-pr-base", + "observation": { + "sender": ["2473f12c7cdd", "f9e183f427ee", "9e9f36142bbd"], + "payloads": ["7ddcb1852b39", "ecc2acd4d70d", "b66f6d1958b9"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "eb79a9b3682a" + }, + "state": "33e3b949d4c5", + "effects": [ + "ecefc28694cb", + "82cd71d524c8", + "067cef118d9f", + "ac9996319e05", + "180125f5d1a6", + "6eb4e79ad99a", + "be0ebed89b2b", + "c9cb32059b8d" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json index cc7121f4087..08cead301ea 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "5c4c890e4c71e80fa8847a5e29700fc9df3ac3bd634bad6289db37522fadd621", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json index 809b521ec44..2db425571a4 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json @@ -3,14 +3,15 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "a699a0a5b128fa422dab0c7557b5aa18599b2d23fa6685cdcc02e17edf328af1", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json index e21150ea4cd..0a1bfb48a55 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.task-workspace", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "a5e812cd508826b3f01ec3798c621ab4303de6536a364113f01a4770dd197bb5", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-task-write.json b/mobile/rpc-foundation/goldens/settings-task-write.json index 000079c0ef8..688d8fded98 100644 --- a/mobile/rpc-foundation/goldens/settings-task-write.json +++ b/mobile/rpc-foundation/goldens/settings-task-write.json @@ -3,14 +3,15 @@ "family": "settings-best-effort", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", "scenarioSha256": "bbcdefe16b07068a81f3c46ae60df01ccb0fbe5a7c1eade3f584f6f0130c23fe", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "2369258c9999": { "name": "settings.update#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json b/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json index 8d7523fc0ec..37794ac1b16 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "287f94e469548f28c9d5591ff6ffb916fa22caa18776b091542b75704c9e1fee", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json b/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json index 7636c928522..2868142028e 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-refuse-after-data.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "76793a56e7b9e596d8c42e9a5a1c47337db32d7437bec2e41d6e7253943f3fd8", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "02f9384f5305": { "name": "settings.get#2", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json b/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json index 998f1287287..2f851cefb93 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-refused.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "ad19fc24973b49ee7d14bc31460a7e4af5d207db6a2375b52b5a0aa878e09205", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json b/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json index 5c97bea5c98..ad3a4e73368 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-context-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.workspace-context", "namedDeltas": ["new-workspace-runtime-context-null-results-degrade-to-absent"], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "fc73116d95985606fe32f22d3f14f434a6d5cd49219416c9fa0f47f971fa6ff0", "scenarioSha256": "a2c80c9cdbb631f3a8fa648dfbb9e691418467d6ead8fe769d72e7e1d8b552b4", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json index ed74d84b1c7..dac6def8701 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json @@ -3,14 +3,15 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "b10ff86086c134284cb0446e8857cd4b55f5ff2bd0507388ec659a95f25e2a19", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json index 4b691635314..8742b9ad421 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json @@ -3,14 +3,15 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "373ea3743dac4e0845df01d5c8f75c909563b8c517f3858293a478234dc9ca5c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json index a16a6b6b3c4..e940e5bc40c 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json @@ -3,14 +3,15 @@ "family": "settings.workspace-submit", "namedDeltas": [], "runnerVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", - "recorderSha256": "58a461dbc99f5ce803aaec650e72eaa2934fccd405e56b77932286f05742ec6b", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "411f2288f09b7940ceb46304c7fc3325e248bf009ff3a7cc12839d521cfad599", "scenarioSha256": "dfbacbd6392ae8e8199550952fe917e7c01182349df6c99a06eb0682cfd9175c", "platform": "darwin", "scenarioVersion": 1, "projectionVersion": 2, - "goldenFormatVersion": 4, + "goldenFormatVersion": 5, "values": { "090c88478661": { "name": "settings.get#1", diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json b/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json new file mode 100644 index 00000000000..2c69c70fbf1 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-capabilities-advertised.json @@ -0,0 +1,100 @@ +{ + "operation": "tasks.worktree-capabilities", + "family": "worktree.runtime-capabilities", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "9d79bcfd6957d11d5ce8c3296f1038a3cfab81eedad7f990b44071104dfd0f91", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "5242fad3532f": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["mobile.tasks.v1", "worktree.create-idempotency.v1"], + "platform": "linux", + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + } + } + }, + "62aaf19f0b16": { + "capabilities": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + }, + "b33d34bddc4e": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + } + } + } + }, + "recording": { + "scenario": "tw-capabilities-advertised", + "checkpoints": [ + { + "id": "probed", + "observation": { + "sender": ["5242fad3532f"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "b33d34bddc4e" + }, + "state": "62aaf19f0b16", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json b/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json new file mode 100644 index 00000000000..bed52964116 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-capabilities-cutover-retried.json @@ -0,0 +1,186 @@ +{ + "operation": "tasks.worktree-capabilities", + "family": "worktree.runtime-capabilities", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "fb9c0e4b7c34f9bd1bd355b606c6ba75a7583cff3788000b7ed013612f5574fe", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "32354557bece": { + "capabilities": "unprobed" + }, + "4e6e53404f59": { + "capabilities": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": false + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a8bcef1e95ed": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": true, + "worktreeCreateIdempotency": false + } + }, + "ae9ff6b74ec1": { + "name": "status.get#2", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "capabilities": ["mobile.tasks.v1"] + } + } + } + }, + "c0c86e67c300": { + "name": "status.get#2", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "c9c0513fdcb9": { + "name": "status.get#2", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "edf54746317d": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "LogicalClientCutoverError", + "message": "RPC interrupted by connection migration", + "isRpcDeliveryUnknown": true, + "cause": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + } + } + }, + "recording": { + "scenario": "tw-capabilities-cutover-retried", + "checkpoints": [ + { + "id": "reprobing-after-cutover", + "observation": { + "sender": ["edf54746317d", "c9c0513fdcb9"], + "payloads": ["1e5b32902af7", "c0c86e67c300"], + "settlements": { + "probe": "9270aeb7d9c6", + "migrate": "eb79a9b3682a" + }, + "state": "32354557bece", + "effects": [] + } + }, + { + "id": "probed-on-replacement", + "observation": { + "sender": ["edf54746317d", "ae9ff6b74ec1"], + "payloads": ["1e5b32902af7", "c0c86e67c300"], + "settlements": { + "probe": "a8bcef1e95ed", + "migrate": "eb79a9b3682a" + }, + "state": "4e6e53404f59", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json b/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json new file mode 100644 index 00000000000..feb90229213 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-capabilities-legacy-idempotency.json @@ -0,0 +1,96 @@ +{ + "operation": "tasks.worktree-capabilities", + "family": "worktree.runtime-capabilities", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "d73fed49e0a7e3e054d5c2fa75780f98bf78b6fa7e02f1ccb2fdc49465cf2fe5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "03f6a4ac937a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": false, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 60000 + } + } + }, + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "3b0c9705ec9a": { + "capabilities": { + "hostPlatform": { + "$rpc": "null" + }, + "tasksSupported": false, + "worktreeCreateIdempotency": { + "dedupeTtlMs": 60000 + } + } + }, + "488c988b5918": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["worktree.create-idempotency.v1"] + } + } + } + } + }, + "recording": { + "scenario": "tw-capabilities-legacy-idempotency", + "checkpoints": [ + { + "id": "legacy-host-window", + "observation": { + "sender": ["488c988b5918"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "03f6a4ac937a" + }, + "state": "3b0c9705ec9a", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json new file mode 100644 index 00000000000..9bd05e45c6e --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-after-drop.json @@ -0,0 +1,110 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "a99bb80a5826af1df8d74114fcc5654aa42c9208747b512cea9bd5ca65b64ccb", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "3b42c7d5a39b": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection lost", + "isRpcDeliveryUnknown": true + } + } + }, + "3f946ad0279c": { + "outcome": "uncreated" + }, + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f0d75436c3f2": { + "status": "rejected", + "startedAt": 0, + "settledAt": 20000, + "error": { + "category": "Error", + "message": "Connection lost", + "isRpcDeliveryUnknown": true + } + } + }, + "recording": { + "scenario": "tw-create-retry-ambiguous-after-drop", + "checkpoints": [ + { + "id": "waiting-for-reconnect", + "observation": { + "sender": ["3b42c7d5a39b"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "9270aeb7d9c6", + "drop": "eb79a9b3682a" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "replay-window-abandoned", + "observation": { + "sender": ["3b42c7d5a39b"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "f0d75436c3f2", + "drop": "eb79a9b3682a" + }, + "state": "3f946ad0279c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json new file mode 100644 index 00000000000..8a20a9af0ca --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-while-connected.json @@ -0,0 +1,84 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "3cda09e4a4ad4092f5f9a48b7c9715a99a51eb3bc4bed00f7054537a9e21cea9", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "3f946ad0279c": { + "outcome": "uncreated" + }, + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "4b9d2713abf2": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Request timed out", + "isRpcDeliveryUnknown": true + } + }, + "50eee544463d": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Request timed out", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "tw-create-retry-ambiguous-while-connected", + "checkpoints": [ + { + "id": "unknown-not-failed", + "observation": { + "sender": ["50eee544463d"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "4b9d2713abf2" + }, + "state": "3f946ad0279c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json new file mode 100644 index 00000000000..fbbad83c2d1 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-ambiguous-without-idempotency.json @@ -0,0 +1,83 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "61097c13b262a4454510936fa9c9554a07865b610f18407f1b67bdd74476df08", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "3f946ad0279c": { + "outcome": "uncreated" + }, + "6d0209806267": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection lost", + "isRpcDeliveryUnknown": true + } + }, + "99d539e63c12": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\"}}" + }, + "a179866627c5": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection lost", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "tw-create-retry-ambiguous-without-idempotency", + "checkpoints": [ + { + "id": "unstamped-create-is-not-replayed", + "observation": { + "sender": ["a179866627c5"], + "payloads": ["99d539e63c12"], + "settlements": { + "create": "6d0209806267" + }, + "state": "3f946ad0279c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-created.json b/mobile/rpc-foundation/goldens/tw-create-retry-created.json new file mode 100644 index 00000000000..02e54ab56de --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-created.json @@ -0,0 +1,91 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "0278216fee698c00118bb0e73a7fe755dc59c3b8e2b0459153edae64f155774c", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "489c189aebca": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "worktree": { + "displayName": "kestrel", + "id": "repo-1::/w" + } + } + } + } + }, + "b32227fdb10b": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "name": "kestrel", + "worktreeId": "repo-1::/w" + } + }, + "df162b95f465": { + "outcome": { + "name": "kestrel", + "worktreeId": "repo-1::/w" + } + } + }, + "recording": { + "scenario": "tw-create-retry-created", + "checkpoints": [ + { + "id": "created", + "observation": { + "sender": ["489c189aebca"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "b32227fdb10b" + }, + "state": "df162b95f465", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json b/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json new file mode 100644 index 00000000000..d56190b9c77 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-name-collision.json @@ -0,0 +1,177 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "6cb658dadc9e146c4f36c6ce643451e72300b8cdda19cc684ffa9fb2b0822c0a", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "2b7c07c2d2af": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "conflict", + "message": "Branch \"kestrel\" already exists." + }, + "id": "frame-1", + "ok": false + } + } + }, + "3f946ad0279c": { + "outcome": "uncreated" + }, + "3fa75e508233": { + "name": "worktree.create#2", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel-2\",\"clientMutationId\":\"mutation-2\"}}" + }, + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "96ffe866d064": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "name": "kestrel-2", + "worktreeId": "repo-1::/w2" + } + }, + "c277d86477c3": { + "name": "worktree.create#2", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-2", + "name": "kestrel-2", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "d0c9ecd48c97": { + "name": "worktree.create#2", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-2", + "name": "kestrel-2", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "displayName": "kestrel-2", + "id": "repo-1::/w2" + } + } + } + } + }, + "db70004d62eb": { + "outcome": { + "name": "kestrel-2", + "worktreeId": "repo-1::/w2" + } + } + }, + "recording": { + "scenario": "tw-create-retry-name-collision", + "checkpoints": [ + { + "id": "retrying", + "observation": { + "sender": ["2b7c07c2d2af", "c277d86477c3"], + "payloads": ["43a221c63628", "3fa75e508233"], + "settlements": { + "create": "9270aeb7d9c6" + }, + "state": "3f946ad0279c", + "effects": [] + } + }, + { + "id": "created-suffixed", + "observation": { + "sender": ["2b7c07c2d2af", "d0c9ecd48c97"], + "payloads": ["43a221c63628", "3fa75e508233"], + "settlements": { + "create": "96ffe866d064" + }, + "state": "db70004d62eb", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json b/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json new file mode 100644 index 00000000000..d5805149736 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-unretryable-refusal.json @@ -0,0 +1,87 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "16abe9e1a8d4cff17b3ea29d40277ae30a3d555a4a9efa08b2745c3a85b02740", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "240b0b1c72b2": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": "" + } + }, + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "43e8315bc2fe": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "internal", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "7d651cae8837": { + "outcome": { + "error": "" + } + } + }, + "recording": { + "scenario": "tw-create-retry-unretryable-refusal", + "checkpoints": [ + { + "id": "refused-empty-message", + "observation": { + "sender": ["43e8315bc2fe"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "240b0b1c72b2" + }, + "state": "7d651cae8837", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json b/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json new file mode 100644 index 00000000000..e13d7a8cf50 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-create-retry-warning-kept.json @@ -0,0 +1,93 @@ +{ + "operation": "tasks.worktree-create-retry", + "family": "worktree.create-retry", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "f469b2b7d61e7fc500fa97b5548f5a3732b0dbcdb405412a514a609f786dfbb5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "43a221c63628": { + "name": "worktree.create#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"kestrel\",\"clientMutationId\":\"mutation-1\"}}" + }, + "6ad759c47a41": { + "name": "worktree.create#1", + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "clientMutationId": "mutation-1", + "name": "kestrel", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "warning": " startup terminal failed ", + "worktree": { + "id": "repo-1::/w" + } + } + } + } + }, + "97555d579c32": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "name": "kestrel", + "warning": "startup terminal failed", + "worktreeId": "repo-1::/w" + } + }, + "f800fc04633e": { + "outcome": { + "name": "kestrel", + "warning": "startup terminal failed", + "worktreeId": "repo-1::/w" + } + } + }, + "recording": { + "scenario": "tw-create-retry-warning-kept", + "checkpoints": [ + { + "id": "created-with-warning", + "observation": { + "sender": ["6ad759c47a41"], + "payloads": ["43a221c63628"], + "settlements": { + "create": "97555d579c32" + }, + "state": "f800fc04633e", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json b/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json new file mode 100644 index 00000000000..8ca7295111f --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-hosted-base-resolved.json @@ -0,0 +1,159 @@ +{ + "operation": "tasks.composer-hosted-base", + "family": "worktree.hosted-base", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "b3734c24f8a083d3efcdd995ea6a57e608d3d3ae3bbbd514db19dcf69448fa48", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0e24d2a37a0d": { + "name": "worktree.resolvePrBase#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolvePrBase\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"headRefName\":\"feature\"}}" + }, + "236529aa012d": { + "mrBase": "unresolved", + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "3cca8119f144": { + "mrBase": { + "baseBranch": "develop" + }, + "prBase": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "4febe923ceea": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + } + } + }, + "5428de0f5130": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + }, + "69afcaf1cb72": { + "name": "worktree.resolveMrBase#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolveMrBase\",\"params\":{\"repo\":\"id:repo-1\",\"mrIid\":7,\"sourceBranch\":\"feature\"}}" + }, + "aad8e7ddeea2": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "baseBranch": "develop" + } + } + } + }, + "fd552ecb03da": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "baseBranch": "develop" + } + } + }, + "recording": { + "scenario": "tw-hosted-base-resolved", + "checkpoints": [ + { + "id": "pr-base-resolved", + "observation": { + "sender": ["4febe923ceea"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "5428de0f5130" + }, + "state": "236529aa012d", + "effects": [] + } + }, + { + "id": "mr-base-resolved", + "observation": { + "sender": ["4febe923ceea", "aad8e7ddeea2"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "5428de0f5130", + "mr": "fd552ecb03da" + }, + "state": "3cca8119f144", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json b/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json new file mode 100644 index 00000000000..b2515930252 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-hosted-base-soft-error.json @@ -0,0 +1,149 @@ +{ + "operation": "tasks.composer-hosted-base", + "family": "worktree.hosted-base", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "451e74a430d2549976fa360a0e43e76a8d855b1470b8e313797019770ceca4cb", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0e24d2a37a0d": { + "name": "worktree.resolvePrBase#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolvePrBase\",\"params\":{\"repo\":\"id:repo-1\",\"prNumber\":12,\"headRefName\":\"feature\"}}" + }, + "69afcaf1cb72": { + "name": "worktree.resolveMrBase#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.resolveMrBase\",\"params\":{\"repo\":\"id:repo-1\",\"mrIid\":7,\"sourceBranch\":\"feature\"}}" + }, + "723a115a3810": { + "name": "worktree.resolveMrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolveMrBase" + }, + { + "name": "params", + "value": { + "mrIid": 7, + "repo": "id:repo-1", + "sourceBranch": "feature" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "" + } + } + } + }, + "ae0c82b12f2a": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "pull request not found", + "isRpcDeliveryUnknown": false + } + }, + "c57e06c96492": { + "mrBase": "unresolved", + "prBase": "unresolved" + }, + "eb01c2306db5": { + "name": "worktree.resolvePrBase#1", + "args": [ + { + "name": "method", + "value": "worktree.resolvePrBase" + }, + { + "name": "params", + "value": { + "headRefName": "feature", + "prNumber": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "pull request not found" + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "tw-hosted-base-soft-error", + "checkpoints": [ + { + "id": "in-band-error", + "observation": { + "sender": ["eb01c2306db5"], + "payloads": ["0e24d2a37a0d"], + "settlements": { + "pr": "ae0c82b12f2a" + }, + "state": "c57e06c96492", + "effects": [] + } + }, + { + "id": "in-band-empty-error", + "observation": { + "sender": ["eb01c2306db5", "723a115a3810"], + "payloads": ["0e24d2a37a0d", "69afcaf1cb72"], + "settlements": { + "pr": "ae0c82b12f2a", + "mr": "f3b516f62081" + }, + "state": "c57e06c96492", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json new file mode 100644 index 00000000000..d22f91f1bbe --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-resolved.json @@ -0,0 +1,341 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "1fb1cdc8a2544e25547175760143a61355900a3ed4b87e08a1fa0dd2409e317d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "09b235c17bb0": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "0e9d6525a582": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "11ab96fde6c9": { + "name": "gitlab.workItemByPath#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.workItemByPath\",\"params\":{\"repo\":\"id:repo-1\",\"host\":\"gitlab.com\",\"path\":\"group/project\",\"iid\":7,\"type\":\"issue\"}}" + }, + "2113a0cc7708": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [ + [ + "repo-1", + { + "owner": "owner", + "repo": "repo" + } + ] + ], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + }, + "repo-slug": { + "displayName": "Repo", + "id": "repo-1", + "slug": { + "$rpc": "null" + } + } + }, + "4a3429622287": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [], + "gitlab-path": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "65342779da15": { + "name": "github.workItem#1", + "args": [ + { + "name": "method", + "value": "github.workItem" + }, + { + "name": "params", + "value": { + "number": 12, + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + }, + "731507dd2e23": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + } + }, + "7445a582a9c8": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + } + }, + "a45a7dd68af6": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "aaf80675fc49": { + "name": "github.workItem#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItem\",\"params\":{\"repo\":\"id:repo-1\",\"number\":12}}" + }, + "bd533f6b0b40": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "iid": 7, + "repoId": "repo-1", + "title": "seven" + } + }, + "e1f537905a65": { + "name": "github.workItemByOwnerRepo#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.workItemByOwnerRepo\",\"params\":{\"repo\":\"id:repo-1\",\"owner\":\"owner\",\"ownerRepo\":\"repo\",\"number\":12,\"type\":\"issue\"}}" + }, + "e29333b1693f": { + "name": "gitlab.workItemByPath#1", + "args": [ + { + "name": "method", + "value": "gitlab.workItemByPath" + }, + { + "name": "params", + "value": { + "host": "gitlab.com", + "iid": 7, + "path": "group/project", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + } + }, + "e970eb27f5ca": { + "by-number": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "by-slug": { + "number": 12, + "repoId": "repo-1", + "title": "twelve" + }, + "cache": [] + }, + "f9ea1f747023": { + "name": "github.workItemByOwnerRepo#1", + "args": [ + { + "name": "method", + "value": "github.workItemByOwnerRepo" + }, + { + "name": "params", + "value": { + "number": 12, + "owner": "owner", + "ownerRepo": "repo", + "repo": "id:repo-1", + "type": "issue" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + } + } + }, + "recording": { + "scenario": "tw-paste-lookup-resolved", + "checkpoints": [ + { + "id": "by-number", + "observation": { + "sender": ["65342779da15"], + "payloads": ["aaf80675fc49"], + "settlements": { + "by-number": "731507dd2e23" + }, + "state": "09b235c17bb0", + "effects": [] + } + }, + { + "id": "by-slug", + "observation": { + "sender": ["65342779da15", "f9ea1f747023"], + "payloads": ["aaf80675fc49", "e1f537905a65"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23" + }, + "state": "e970eb27f5ca", + "effects": [] + } + }, + { + "id": "gitlab-path", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40" + }, + "state": "4a3429622287", + "effects": [] + } + }, + { + "id": "repo-slug-matched", + "observation": { + "sender": ["65342779da15", "f9ea1f747023", "e29333b1693f", "7445a582a9c8"], + "payloads": ["aaf80675fc49", "e1f537905a65", "11ab96fde6c9", "a45a7dd68af6"], + "settlements": { + "by-number": "731507dd2e23", + "by-slug": "731507dd2e23", + "gitlab-path": "bd533f6b0b40", + "repo-slug": "0e9d6525a582" + }, + "state": "2113a0cc7708", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json new file mode 100644 index 00000000000..8317ae46784 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-refused.json @@ -0,0 +1,136 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "e9d85c576adf93063a8f56d49d28869c402cad38122732b46b8ec021d25db5e3", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0b81b65669e6": { + "name": "github.repoSlug#2", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-2" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "2d9e475c68c7": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "no access" + }, + "id": "frame-1", + "ok": false + } + } + }, + "35f3e39a1c50": { + "cache": [ + [ + "repo-1", + { + "$rpc": "null" + } + ], + [ + "repo-2", + { + "$rpc": "null" + } + ] + ], + "repo-slug": { + "$rpc": "null" + } + }, + "5f7cab1e0f03": { + "name": "github.repoSlug#2", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-2\"}}" + }, + "6530ef4dbd15": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + } + }, + "recording": { + "scenario": "tw-paste-lookup-slug-refused", + "checkpoints": [ + { + "id": "refusal-is-per-repo", + "observation": { + "sender": ["2d9e475c68c7", "0b81b65669e6"], + "payloads": ["6530ef4dbd15", "5f7cab1e0f03"], + "settlements": { + "repo-slug": "ee20a1dc39e7" + }, + "state": "35f3e39a1c50", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json new file mode 100644 index 00000000000..a4f99bffb06 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-paste-lookup-slug-unsupported.json @@ -0,0 +1,134 @@ +{ + "operation": "tasks.paste-lookup", + "family": "tasks.paste-lookup", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "14509b4c1cc3beb00cc329b6bae46913f59b3938f76c0bd3bf3e374f34fb680d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0f35c09b3d1e": { + "name": "github.repoSlug#1", + "args": [ + { + "name": "method", + "value": "github.repoSlug" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "176039835400": { + "cache": [ + [ + "repo-1", + { + "$rpc": "null" + } + ], + [ + "repo-2", + { + "$rpc": "null" + } + ] + ], + "repo-slug": { + "$rpc": "null" + }, + "repo-slug-again": { + "$rpc": "null" + } + }, + "35f3e39a1c50": { + "cache": [ + [ + "repo-1", + { + "$rpc": "null" + } + ], + [ + "repo-2", + { + "$rpc": "null" + } + ] + ], + "repo-slug": { + "$rpc": "null" + } + }, + "6530ef4dbd15": { + "name": "github.repoSlug#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.repoSlug\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "ee20a1dc39e7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "null" + } + } + }, + "recording": { + "scenario": "tw-paste-lookup-slug-unsupported", + "checkpoints": [ + { + "id": "host-wide-probe-cached", + "observation": { + "sender": ["0f35c09b3d1e"], + "payloads": ["6530ef4dbd15"], + "settlements": { + "repo-slug": "ee20a1dc39e7" + }, + "state": "35f3e39a1c50", + "effects": [] + } + }, + { + "id": "no-second-probe", + "observation": { + "sender": ["0f35c09b3d1e"], + "payloads": ["6530ef4dbd15"], + "settlements": { + "repo-slug": "ee20a1dc39e7", + "repo-slug-again": "ee20a1dc39e7" + }, + "state": "176039835400", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json new file mode 100644 index 00000000000..bc7ee4dd338 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-always.json @@ -0,0 +1,91 @@ +{ + "operation": "tasks.setup-hook-trust", + "family": "worktree.setup-hook-trust", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "0d2e3f48aadf45abbf6927b72ed5fef4caa8e3eb2efd1046339a3fbfab6f9f18", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "229c35d1a4ba": { + "trust": "unapproved" + }, + "2fde86b1acca": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "all": { + "approvedAt": 1767225600000 + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "341f646a48a2": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"trustedOrcaHooks\":{\"repo-1\":{\"all\":{\"approvedAt\":1767225600000}}}}}" + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "tw-setup-hook-trust-always", + "checkpoints": [ + { + "id": "refused-empty-message", + "observation": { + "sender": ["2fde86b1acca"], + "payloads": ["341f646a48a2"], + "settlements": { + "approve": "f3b516f62081" + }, + "state": "229c35d1a4ba", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json new file mode 100644 index 00000000000..1859fa4e483 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-setup-hook-trust-approved.json @@ -0,0 +1,101 @@ +{ + "operation": "tasks.setup-hook-trust", + "family": "worktree.setup-hook-trust", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "f54f5c6dbbe7dbcf8e85e9bd36b27ca9bea7de65d5e35dabace49b3fc766a403", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0f68ccbfb8e9": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"trustedOrcaHooks\":{\"repo-1\":{\"setup\":{\"contentHash\":\"hash-1\",\"approvedAt\":1767225600000}}}}}" + }, + "6f009f61d89f": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "a406b068aeca": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + }, + "b2aa9ff12623": { + "trust": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + "recording": { + "scenario": "tw-setup-hook-trust-approved", + "checkpoints": [ + { + "id": "approved", + "observation": { + "sender": ["6f009f61d89f"], + "payloads": ["0f68ccbfb8e9"], + "settlements": { + "approve": "a406b068aeca" + }, + "state": "b2aa9ff12623", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json b/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json new file mode 100644 index 00000000000..36ba7152a68 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-smart-search-all-providers.json @@ -0,0 +1,490 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "283849e17fb47ad5f9c128cef37a18e869a132357b332b40bec955292db2af3f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "253629bd0d20": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "25f88995b39a": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-4", + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + } + }, + "26dc3b7c8299": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-3" + } + ] + }, + "2cfd107b9660": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "36290ab254a4": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ] + }, + "3828d5880c35": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "41d2452d4ebe": { + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "5bce68072dc3": { + "name": "github.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "github.listWorkItems" + }, + { + "name": "params", + "value": { + "limit": 36, + "query": "bug", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + } + }, + "6107c951646f": { + "name": "linear.searchIssues#1", + "args": [ + { + "name": "method", + "value": "linear.searchIssues" + }, + { + "name": "params", + "value": { + "limit": 50, + "query": "bug", + "workspaceId": "linear-workspace" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + } + }, + "6e2d75e3bbd7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ] + }, + "a4ee5d16b4f6": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-1" + } + ] + }, + "a92cd1dd05af": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-3" + } + ] + }, + "b015aaf3a53a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ] + }, + "b8b02a30b6b8": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"bug\",\"limit\":20}}" + }, + "c43e80126d82": { + "branches": [ + { + "localBranchName": "main", + "refName": "main" + }, + { + "localBranchName": "release", + "refName": "release" + } + ], + "github": [ + { + "number": 1, + "repoId": "repo-1", + "title": "one" + } + ], + "gitlab": [ + { + "iid": 2, + "repoId": "repo-1", + "title": "two" + } + ], + "linear": [ + { + "id": "issue-1" + } + ] + }, + "e97e5a589476": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-5\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "ead829dd6d03": { + "name": "linear.searchIssues#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"linear.searchIssues\",\"params\":{\"query\":\"bug\",\"limit\":50,\"workspaceId\":\"linear-workspace\"}}" + }, + "ee6fe4f97b01": { + "name": "github.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"github.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"limit\":36,\"query\":\"bug\"}}" + }, + "f32ad26605d0": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "missing", + "type": "not_found" + }, + "items": [ + { + "iid": 2, + "title": "two" + } + ] + } + } + } + }, + "fe7f60b5d785": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-5", + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + } + } + }, + "recording": { + "scenario": "tw-smart-search-all-providers", + "checkpoints": [ + { + "id": "github-items", + "observation": { + "sender": ["5bce68072dc3"], + "payloads": ["ee6fe4f97b01"], + "settlements": { + "github": "36290ab254a4" + }, + "state": "253629bd0d20", + "effects": [] + } + }, + { + "id": "gitlab-items", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0"], + "payloads": ["ee6fe4f97b01", "3828d5880c35"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7" + }, + "state": "2cfd107b9660", + "effects": [] + } + }, + { + "id": "linear-search", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6" + }, + "state": "41d2452d4ebe", + "effects": [] + } + }, + { + "id": "branch-refs", + "observation": { + "sender": ["5bce68072dc3", "f32ad26605d0", "6107c951646f", "25f88995b39a"], + "payloads": ["ee6fe4f97b01", "3828d5880c35", "ead829dd6d03", "b8b02a30b6b8"], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a" + }, + "state": "c43e80126d82", + "effects": [] + } + }, + { + "id": "linear-assigned-listed", + "observation": { + "sender": [ + "5bce68072dc3", + "f32ad26605d0", + "6107c951646f", + "25f88995b39a", + "fe7f60b5d785" + ], + "payloads": [ + "ee6fe4f97b01", + "3828d5880c35", + "ead829dd6d03", + "b8b02a30b6b8", + "e97e5a589476" + ], + "settlements": { + "github": "36290ab254a4", + "gitlab": "6e2d75e3bbd7", + "linear": "a4ee5d16b4f6", + "branches": "b015aaf3a53a", + "linear-assigned": "26dc3b7c8299" + }, + "state": "a92cd1dd05af", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json b/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json new file mode 100644 index 00000000000..6a4d57f7e27 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-smart-search-gitlab-provider-error.json @@ -0,0 +1,166 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "4537bf83f7a030521eec549adf4490da5be183471d5a9f9e58b71815b29481ff", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "44136fa355b3": {}, + "46027e62015d": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"main\",\"limit\":20}}" + }, + "51e012c25ebf": { + "branches": [ + { + "localBranchName": "main", + "refName": "origin/main" + } + ] + }, + "522d9e5c292e": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "refDetails": [ + { + "localBranchName": "main", + "refName": "origin/main" + } + ] + } + } + } + }, + "791f6fc629fc": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "localBranchName": "main", + "refName": "origin/main" + } + ] + }, + "86057be07bd0": { + "name": "gitlab.listWorkItems#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"gitlab.listWorkItems\",\"params\":{\"repo\":\"id:repo-1\",\"state\":\"opened\",\"page\":1,\"perPage\":50,\"query\":\"bug\"}}" + }, + "97c2301d5d8c": { + "name": "gitlab.listWorkItems#1", + "args": [ + { + "name": "method", + "value": "gitlab.listWorkItems" + }, + { + "name": "params", + "value": { + "page": 1, + "perPage": 50, + "query": "bug", + "repo": "id:repo-1", + "state": "opened" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "rate limited", + "type": "quota" + }, + "items": [] + } + } + } + }, + "bf7ab976b200": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "rate limited", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "tw-smart-search-gitlab-provider-error", + "checkpoints": [ + { + "id": "in-band-provider-error", + "observation": { + "sender": ["97c2301d5d8c"], + "payloads": ["86057be07bd0"], + "settlements": { + "gitlab": "bf7ab976b200" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "branch-ref-details", + "observation": { + "sender": ["97c2301d5d8c", "522d9e5c292e"], + "payloads": ["86057be07bd0", "46027e62015d"], + "settlements": { + "gitlab": "bf7ab976b200", + "branches": "791f6fc629fc" + }, + "state": "51e012c25ebf", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json b/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json new file mode 100644 index 00000000000..c8a387e7c1a --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-smart-search-linear-listed.json @@ -0,0 +1,94 @@ +{ + "operation": "tasks.smart-source-search", + "family": "tasks.smart-source-search", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "343b97826782820b7aecaddd13a4c8cdbbbca057c83021863d577a677934f3a3", + "scenarioSha256": "34ab4edb621856980c8678629bd809c100d27dcd0747db5abbf4508c7231b7e5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "4e3aac46030e": { + "name": "linear.listIssues#1", + "args": [ + { + "name": "method", + "value": "linear.listIssues" + }, + { + "name": "params", + "value": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$rpc": "undefined" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": [ + { + "id": "issue-2" + } + ] + } + } + }, + "a95bdb94e589": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": [ + { + "id": "issue-2" + } + ] + }, + "b107467b4d7c": { + "name": "linear.listIssues#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"linear.listIssues\",\"params\":{\"filter\":\"assigned\",\"limit\":50}}" + }, + "f2b981b0b281": { + "linear": [ + { + "id": "issue-2" + } + ] + } + }, + "recording": { + "scenario": "tw-smart-search-linear-listed", + "checkpoints": [ + { + "id": "linear-assigned", + "observation": { + "sender": ["4e3aac46030e"], + "payloads": ["b107467b4d7c"], + "settlements": { + "linear": "a95bdb94e589" + }, + "state": "f2b981b0b281", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json b/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json new file mode 100644 index 00000000000..a7c76afba35 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-task-preferences-resume-write.json @@ -0,0 +1,155 @@ +{ + "operation": "settings.task-preferences", + "family": "settings-best-effort", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "d3b7f33d810e1fa420ac41a628cde9fe4a9e65fd57f89fbca0a40fc7d74951ab", + "scenarioSha256": "31bfa49f888b0eb3f72873bf4a3af26129e78c23126e8fc8fe45b952caa60904", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "43a97b36b849": { + "name": "ui.set#2", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"trustedOrcaHooks\":{\"repo-1\":{\"setup\":{\"contentHash\":\"hash-1\",\"approvedAt\":1767225600000}}}}}" + }, + "8214f29cee6d": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"taskResumeState\":{\"githubItemsPreset\":\"issues\"}}}" + }, + "a569eb8ebbdd": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "taskResumeState": { + "githubItemsPreset": "issues" + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "no access" + }, + "id": "frame-1", + "ok": false + } + } + }, + "ae0c8e22f430": { + "preset": "all" + }, + "bea815de84ac": { + "name": "ui.set#2", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "approvedAt": 1767225600000, + "contentHash": "hash-1" + } + } + } + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "tw-task-preferences-resume-write", + "checkpoints": [ + { + "id": "best-effort-resume-write", + "observation": { + "sender": ["a569eb8ebbdd"], + "payloads": ["8214f29cee6d"], + "settlements": { + "mount": "eb79a9b3682a", + "resume": "eb79a9b3682a" + }, + "state": "ae0c8e22f430", + "effects": [] + } + }, + { + "id": "awaited-trust-write-refused", + "observation": { + "sender": ["a569eb8ebbdd", "bea815de84ac"], + "payloads": ["8214f29cee6d", "43a97b36b849"], + "settlements": { + "mount": "eb79a9b3682a", + "resume": "eb79a9b3682a", + "trust": "f3b516f62081" + }, + "state": "ae0c8e22f430", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json b/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json new file mode 100644 index 00000000000..120bc19d8c5 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-source-presets-refused.json @@ -0,0 +1,137 @@ +{ + "operation": "tasks.workspace-source", + "family": "tasks.workspace-source", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "2cd1e8972f226572744dad7da82afffbdf0452a121c1cd8c3334d5c3fde5d57c", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "2399e995a370": { + "name": "workspaceSparsePresets", + "value": [] + }, + "273f4074a9b5": { + "name": "workspaceSparsePresetsLoaded", + "value": false + }, + "35afa5cb107f": { + "name": "workspaceBaseBranchLoading", + "value": false + }, + "4cedb91a2f7a": { + "name": "repo.sparsePresets#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.sparsePresets\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "5bad21b1e042": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "5f1c84e00d4f": { + "name": "workspaceBaseBranchResults", + "value": [] + }, + "62bc28c39ffc": { + "branchError": "", + "branches": [], + "presets": [], + "presetsError": "", + "presetsLoaded": false + }, + "6c344c5f4ac0": { + "name": "workspaceBaseBranchError", + "value": "" + }, + "8353b8e1a426": { + "name": "workspaceSparsePresetsLoading", + "value": true + }, + "9357f7ea8445": { + "name": "workspaceSparsePresetId", + "value": { + "$rpc": "null" + } + }, + "cfc8af2a7169": { + "name": "workspaceSparsePresetsLoading", + "value": false + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "tw-workspace-source-presets-refused", + "checkpoints": [ + { + "id": "presets-refused-empty-message", + "observation": { + "sender": ["5bad21b1e042"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "62bc28c39ffc", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "2399e995a370", + "273f4074a9b5", + "9357f7ea8445", + "dba381378b08", + "cfc8af2a7169" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json b/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json new file mode 100644 index 00000000000..dd3c263152a --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-source-presets.json @@ -0,0 +1,255 @@ +{ + "operation": "tasks.workspace-source", + "family": "tasks.workspace-source", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "046dd3a125a3c9abcf5a0dd122818939b516adb91cbda2554b3409d4bb3a7980", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "273f4074a9b5": { + "name": "workspaceSparsePresetsLoaded", + "value": false + }, + "35afa5cb107f": { + "name": "workspaceBaseBranchLoading", + "value": false + }, + "395368dea8ff": { + "name": "repo.searchRefs#1", + "args": [ + { + "name": "method", + "value": "repo.searchRefs" + }, + { + "name": "params", + "value": { + "limit": 20, + "query": "main", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "refs": ["main"] + } + } + } + }, + "46027e62015d": { + "name": "repo.searchRefs#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.searchRefs\",\"params\":{\"repo\":\"id:repo-1\",\"query\":\"main\",\"limit\":20}}" + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "4cedb91a2f7a": { + "name": "repo.sparsePresets#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.sparsePresets\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "57f06e6e349e": { + "branchError": "", + "branches": [], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "58cb95babab2": { + "name": "workspaceBaseBranchLoading", + "value": true + }, + "5f1c84e00d4f": { + "name": "workspaceBaseBranchResults", + "value": [] + }, + "6c344c5f4ac0": { + "name": "workspaceBaseBranchError", + "value": "" + }, + "8353b8e1a426": { + "name": "workspaceSparsePresetsLoading", + "value": true + }, + "8dbe7ea87a41": { + "name": "workspaceBaseBranchResults", + "value": [ + { + "localBranchName": "main", + "refName": "main" + } + ] + }, + "9357f7ea8445": { + "name": "workspaceSparsePresetId", + "value": { + "$rpc": "null" + } + }, + "b78bcf7ca596": { + "branchError": "", + "branches": [ + { + "localBranchName": "main", + "refName": "main" + } + ], + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "presetsLoaded": true + }, + "c8d4d05367d6": { + "name": "repo.sparsePresets#1", + "args": [ + { + "name": "method", + "value": "repo.sparsePresets" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + } + } + } + }, + "cfc8af2a7169": { + "name": "workspaceSparsePresetsLoading", + "value": false + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "tw-workspace-source-presets", + "checkpoints": [ + { + "id": "presets-loaded", + "observation": { + "sender": ["c8d4d05367d6"], + "payloads": ["4cedb91a2f7a"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "57f06e6e349e", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169" + ] + } + }, + { + "id": "branches-loaded", + "observation": { + "sender": ["c8d4d05367d6", "395368dea8ff"], + "payloads": ["4cedb91a2f7a", "46027e62015d"], + "settlements": { + "mount": "eb79a9b3682a", + "branch-query": "eb79a9b3682a" + }, + "state": "b78bcf7ca596", + "effects": [ + "8353b8e1a426", + "273f4074a9b5", + "dba381378b08", + "5f1c84e00d4f", + "35afa5cb107f", + "6c344c5f4ac0", + "4856f62b3650", + "1d9a7d969446", + "9357f7ea8445", + "cfc8af2a7169", + "58cb95babab2", + "6c344c5f4ac0", + "8dbe7ea87a41", + "35afa5cb107f" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json b/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json new file mode 100644 index 00000000000..ab187d6c990 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-sparse-missing-preset.json @@ -0,0 +1,160 @@ +{ + "operation": "tasks.workspace-sparse", + "family": "tasks.workspace-sparse", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "865a659012dd882fd6073813585e2911a1d6252404fbf5a5e273f062b89fc91d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "193c0bc3cf2a": { + "presets": [], + "presetsError": "Failed to save sparse preset.", + "saving": false, + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "7fd0cde62993": { + "name": "workspaceSparseSaving", + "value": false + }, + "86cc01b1e541": { + "name": "workspaceSshState", + "value": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "a6bf06ff84e0": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": {} + } + } + }, + "cea9d7e8986e": { + "name": "workspaceSparseSaving", + "value": true + }, + "da3a01640280": { + "name": "workspaceSparsePresetsError", + "value": "Failed to save sparse preset." + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f22d3216eb8d": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "fd758406cc2c": { + "name": "repo.saveSparsePreset#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.saveSparsePreset\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"docs\",\"directories\":[\"docs\"]}}" + } + }, + "recording": { + "scenario": "tw-workspace-sparse-missing-preset", + "checkpoints": [ + { + "id": "saved-without-preset", + "observation": { + "sender": ["f22d3216eb8d", "a6bf06ff84e0"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "193c0bc3cf2a", + "effects": [ + "86cc01b1e541", + "cea9d7e8986e", + "dba381378b08", + "da3a01640280", + "7fd0cde62993" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json b/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json new file mode 100644 index 00000000000..68ec9130db0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-sparse-saved.json @@ -0,0 +1,230 @@ +{ + "operation": "tasks.workspace-sparse", + "family": "tasks.workspace-sparse", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "124f664e339bfd83a1d892e1cd953a78fdf0dc4b20c4272356d24079c72a3e04", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1d9a7d969446": { + "name": "workspaceSparsePresetsLoaded", + "value": true + }, + "312ed3cbf468": { + "name": "workspaceSparsePresetId", + "value": "p1" + }, + "404305aa2e3a": { + "presets": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "42bbd034563e": { + "name": "workspaceSparseDraft", + "value": { + "$rpc": "null" + } + }, + "4856f62b3650": { + "name": "workspaceSparsePresets", + "value": [ + { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + ] + }, + "5c44ff5f6877": { + "name": "repo.saveSparsePreset#1", + "args": [ + { + "name": "method", + "value": "repo.saveSparsePreset" + }, + { + "name": "params", + "value": { + "directories": ["docs"], + "name": "docs", + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "preset": { + "directories": ["docs"], + "id": "p1", + "name": "docs" + } + } + } + } + }, + "7fd0cde62993": { + "name": "workspaceSparseSaving", + "value": false + }, + "89aa7a3bd619": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "cea9d7e8986e": { + "name": "workspaceSparseSaving", + "value": true + }, + "dba381378b08": { + "name": "workspaceSparsePresetsError", + "value": "" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ee3a941d5e9c": { + "presets": [], + "presetsError": "", + "saving": false, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "fd758406cc2c": { + "name": "repo.saveSparsePreset#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"repo.saveSparsePreset\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"docs\",\"directories\":[\"docs\"]}}" + } + }, + "recording": { + "scenario": "tw-workspace-sparse-saved", + "checkpoints": [ + { + "id": "ssh-state-read", + "observation": { + "sender": ["89aa7a3bd619"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "ee3a941d5e9c", + "effects": ["921f72d7827e"] + } + }, + { + "id": "preset-saved", + "observation": { + "sender": ["89aa7a3bd619", "5c44ff5f6877"], + "payloads": ["f9dfbe0c0ea7", "fd758406cc2c"], + "settlements": { + "mount": "eb79a9b3682a", + "save": "eb79a9b3682a" + }, + "state": "404305aa2e3a", + "effects": [ + "921f72d7827e", + "cea9d7e8986e", + "dba381378b08", + "4856f62b3650", + "1d9a7d969446", + "312ed3cbf468", + "42bbd034563e", + "7fd0cde62993" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json new file mode 100644 index 00000000000..96bd529a316 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connect-refused.json @@ -0,0 +1,278 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "dd25391fdd3dc864ae493f72d013e789884a21e9c71522edc79323bc2b6c7f76", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "12826f529c2a": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "ssh_failed", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "1739575ac53e": { + "name": "workspaceSshConnecting", + "value": true + }, + "27b09a2898b9": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection lost", + "isRpcDeliveryUnknown": true + } + } + }, + "2d313c57ddf7": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "decision": "skip", + "kind": "decision", + "setupTrust": { + "$rpc": "undefined" + } + } + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "43fd3e2f4b53": { + "name": "workspaceSshConnecting", + "value": false + }, + "55904d40a00f": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": { + "decision": "skip", + "kind": "decision", + "setupTrust": { + "$rpc": "undefined" + } + }, + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "7c9498659f58": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "8509334ad6ae": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": "unresolved", + "ssh": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "86cc01b1e541": { + "name": "workspaceSshState", + "value": { + "error": "", + "reconnectAttempt": 0, + "status": "error", + "targetId": "ssh-1" + } + }, + "9f152ed6e897": { + "name": "workspaceDetectedAgentIds", + "value": [] + }, + "b302d21e1567": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": "pnpm i" + } + }, + "setupRunPolicy": "never" + } + } + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "fbfdbb919268": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + } + }, + "recording": { + "scenario": "tw-workspace-ssh-connect-refused", + "checkpoints": [ + { + "id": "connect-refused-empty-message", + "observation": { + "sender": ["27b09a2898b9", "12826f529c2a"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "8509334ad6ae", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + }, + { + "id": "setup-skipped", + "observation": { + "sender": ["27b09a2898b9", "12826f529c2a", "b302d21e1567"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "2d313c57ddf7" + }, + "state": "55904d40a00f", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "1739575ac53e", + "fbfdbb919268", + "86cc01b1e541", + "43fd3e2f4b53" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json new file mode 100644 index 00000000000..8893595a9b8 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-connected.json @@ -0,0 +1,320 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "495f51d9c2f7f3d71f53a53e88786b8d1f767a5bf66b8655c28222d2909a964c", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0ba2cee4b538": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + }, + "1739575ac53e": { + "name": "workspaceSshConnecting", + "value": true + }, + "17e35b25d15d": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex"] + } + } + }, + "18e6a3ac6471": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "3571f351281f": { + "name": "workspaceDetectedAgentIds", + "value": ["codex"] + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "43ead075ce12": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": { + "command": "pnpm install", + "kind": "prompt", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "43fd3e2f4b53": { + "name": "workspaceSshConnecting", + "value": false + }, + "71d817ffdd81": { + "name": "ssh.connect#1", + "args": [ + { + "name": "method", + "value": "ssh.connect" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 120000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "7c9498659f58": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "80a4af19f556": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": " pnpm install " + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + }, + "source": "repo" + } + } + } + }, + "921f72d7827e": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "a1f755a38636": { + "agent": "claude", + "connecting": false, + "detected": ["codex"], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "fbfdbb919268": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connecting", + "targetId": "ssh-1" + } + } + }, + "recording": { + "scenario": "tw-workspace-ssh-connected", + "checkpoints": [ + { + "id": "agents-detected", + "observation": { + "sender": ["17e35b25d15d"], + "payloads": ["37921d9fdeb7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "18e6a3ac6471", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "3571f351281f"] + } + }, + { + "id": "connected", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81"], + "payloads": ["37921d9fdeb7", "7c9498659f58"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "a1f755a38636", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + }, + { + "id": "setup-prompted", + "observation": { + "sender": ["17e35b25d15d", "71d817ffdd81", "80a4af19f556"], + "payloads": ["37921d9fdeb7", "7c9498659f58", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a", + "setup": "0ba2cee4b538" + }, + "state": "43ead075ce12", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "3571f351281f", + "1739575ac53e", + "fbfdbb919268", + "921f72d7827e", + "43fd3e2f4b53" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json new file mode 100644 index 00000000000..c62985ba0ea --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-local-agents.json @@ -0,0 +1,104 @@ +{ + "operation": "tasks.workspace-ssh-local", + "family": "tasks.workspace-ssh-local", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "af0623c2d106d2ed18ef9149d4990539f9ed82146ae091a872a3e1d792efeffe", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "7400f4eebe66": { + "agent": "claude", + "connecting": false, + "detected": ["codex", "claude"], + "setup": "unresolved", + "ssh": { + "$rpc": "null" + } + }, + "cb93b17470e8": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": ["codex", "claude"] + } + } + }, + "cbb858a786ac": { + "name": "workspaceDetectedAgentIds", + "value": ["codex", "claude"] + }, + "cf32edc950ac": { + "name": "preflight.detectAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}" + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + } + }, + "recording": { + "scenario": "tw-workspace-ssh-local-agents", + "checkpoints": [ + { + "id": "local-agents-detected", + "observation": { + "sender": ["cb93b17470e8"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "7400f4eebe66", + "effects": ["ed6189938d78", "ea709e13f0f0", "41b0d115f434", "cbb858a786ac"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json b/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json new file mode 100644 index 00000000000..c4eeb37ecd0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/tw-workspace-ssh-not-ready.json @@ -0,0 +1,269 @@ +{ + "operation": "tasks.workspace-ssh", + "family": "tasks.workspace-ssh", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "4e567302ac8acffcfd602c9b323ecf8b5b7c0c4692bda1a4c881011a91d98979", + "scenarioSha256": "3aa23f15da8fe9972e47c767db454b41750ca353ab10797082fde4514ffe9da0", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0adf11d42d1a": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "0f1cf505ed63": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connect Repo before creating a workspace.", + "isRpcDeliveryUnknown": false + } + }, + "15d9dbcfd2ce": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "hooks": { + "scripts": {} + } + } + } + } + }, + "1712c415bebf": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "decision": "inherit", + "kind": "decision" + } + }, + "25352a4de532": { + "name": "workspaceSshState", + "value": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "28be8cfc5f01": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "forbidden", + "message": "no access" + }, + "id": "frame-1", + "ok": false + } + } + }, + "37921d9fdeb7": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "41b0d115f434": { + "name": "workspaceDetectedAgentIds", + "value": { + "$rpc": "null" + } + }, + "8ecc31aa9892": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + } + } + } + }, + "9a9cd2877569": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": { + "decision": "inherit", + "kind": "decision" + }, + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "9f152ed6e897": { + "name": "workspaceDetectedAgentIds", + "value": [] + }, + "d3698fc526a8": { + "agent": "claude", + "connecting": false, + "detected": [], + "setup": "unresolved", + "ssh": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "disconnected", + "targetId": "ssh-1" + } + }, + "ea709e13f0f0": { + "name": "workspaceAgentOverridden", + "value": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ed6189938d78": { + "name": "workspaceAgent", + "value": "claude" + }, + "f0a9f62da106": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + } + }, + "recording": { + "scenario": "tw-workspace-ssh-not-ready", + "checkpoints": [ + { + "id": "ensure-rejected", + "observation": { + "sender": ["28be8cfc5f01", "8ecc31aa9892"], + "payloads": ["37921d9fdeb7", "0adf11d42d1a"], + "settlements": { + "mount": "eb79a9b3682a", + "ensure": "0f1cf505ed63" + }, + "state": "d3698fc526a8", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "25352a4de532" + ] + } + }, + { + "id": "no-setup-script", + "observation": { + "sender": ["28be8cfc5f01", "8ecc31aa9892", "15d9dbcfd2ce"], + "payloads": ["37921d9fdeb7", "0adf11d42d1a", "f0a9f62da106"], + "settlements": { + "mount": "eb79a9b3682a", + "ensure": "0f1cf505ed63", + "setup": "1712c415bebf" + }, + "state": "9a9cd2877569", + "effects": [ + "ed6189938d78", + "ea709e13f0f0", + "41b0d115f434", + "9f152ed6e897", + "25352a4de532" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 01652ad047a..1443895a1a8 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "baseline": "e53f1557e1b5a6e26e85cfca390b34f7f8a7d26c", + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", "scenarios": [ { "id": "b1", @@ -1830,6 +1830,173 @@ } ] }, + { + "id": "settings-task-workspace-create-linear", + "operation": "settings.task-workspace-create", + "version": 1, + "family": "settings.task-workspace-create", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "submit", + "id": "submit", + "args": { + "item": { + "key": "linear:1", + "provider": "linear", + "title": "Recorded issue", + "source": { + "identifier": "ORC-1", + "title": "Recorded issue", + "url": "https://linear.app/orca/issue/ORC-1" + } + } + } + }, + { + "checkpoint": "settings-pending" + }, + { + "complete": "settings.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "settings": { + "disabledTuiAgents": [], + "defaultTuiAgent": "codex" + } + } + } + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "orc-1", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "setupDecision": "inherit", + "activate": true, + "startupDraft": "https://linear.app/orca/issue/ORC-1", + "createdWithAgent": "claude" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "id": "wt-1", + "displayName": "ORC-1 Recorded issue" + } + } + } + }, + { + "checkpoint": "created" + } + ] + }, + { + "id": "settings-task-workspace-create-pr-start-point", + "operation": "settings.task-workspace-create", + "version": 1, + "family": "settings.task-workspace-create", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "submit", + "id": "submit", + "args": { + "item": { + "key": "github:7", + "provider": "github", + "title": "Recorded pull request", + "source": { + "type": "pr", + "repoId": "repo-1", + "number": 7, + "title": "Recorded pull request", + "url": "https://github.com/o/r/pull/7" + } + } + } + }, + { + "checkpoint": "settings-pending" + }, + { + "complete": "settings.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "settings": { + "disabledTuiAgents": [], + "defaultTuiAgent": "codex" + } + } + } + }, + { + "complete": "worktree.resolvePrBase#1", + "params": { + "repo": "id:repo-1", + "prNumber": 7 + }, + "reply": { + "ok": true, + "result": { + "baseBranch": "main" + } + } + }, + { + "checkpoint": "pr-base-resolved" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "pr-7", + "displayName": "Recorded pull request", + "displayNameKind": "generated", + "setupDecision": "inherit", + "activate": true, + "startupDraft": "https://github.com/o/r/pull/7", + "createdWithAgent": "claude", + "baseBranch": "main", + "linkedPR": 7 + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "id": "wt-2" + }, + "warning": "shallow clone" + } + } + }, + { + "checkpoint": "created-from-pr-base" + } + ] + }, { "id": "settings-new-tab-refused", "operation": "settings.new-tab-agents", @@ -5137,6 +5304,1455 @@ "checkpoint": "settled" } ] + }, + { + "id": "tw-create-retry-created", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel", + "clientMutationId": "mutation-1" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "id": "repo-1::/w", + "displayName": "kestrel" + } + } + } + }, + { + "checkpoint": "created" + } + ] + }, + { + "id": "tw-create-retry-warning-kept", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel", + "clientMutationId": "mutation-1" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "id": "repo-1::/w" + }, + "warning": " startup terminal failed " + } + } + }, + { + "checkpoint": "created-with-warning" + } + ] + }, + { + "id": "tw-create-retry-name-collision", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel", + "clientMutationId": "mutation-1" + }, + "reply": { + "ok": false, + "error": { + "code": "conflict", + "message": "Branch \"kestrel\" already exists." + } + } + }, + { + "checkpoint": "retrying" + }, + { + "complete": "worktree.create#2", + "params": { + "repo": "id:repo-1", + "name": "kestrel-2", + "clientMutationId": "mutation-2" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "id": "repo-1::/w2", + "displayName": "kestrel-2" + } + } + } + }, + { + "checkpoint": "created-suffixed" + } + ] + }, + { + "id": "tw-create-retry-unretryable-refusal", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel", + "clientMutationId": "mutation-1" + }, + "reply": { + "ok": false, + "error": { + "code": "internal", + "message": "" + } + } + }, + { + "checkpoint": "refused-empty-message" + } + ] + }, + { + "id": "tw-create-retry-ambiguous-while-connected", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel", + "clientMutationId": "mutation-1" + }, + "reject": { + "message": "Request timed out", + "deliveryUnknown": true + } + }, + { + "checkpoint": "unknown-not-failed" + } + ] + }, + { + "id": "tw-create-retry-ambiguous-after-drop", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create" + }, + { + "action": "disconnect", + "id": "drop" + }, + { + "checkpoint": "waiting-for-reconnect" + }, + { + "advance": 20000 + }, + { + "checkpoint": "replay-window-abandoned" + } + ] + }, + { + "id": "tw-create-retry-ambiguous-without-idempotency", + "operation": "tasks.worktree-create-retry", + "version": 1, + "family": "worktree.create-retry", + "sites": ["mobile/src/tasks/worktree-create-retry.ts"], + "schedules": [], + "steps": [ + { + "action": "create", + "id": "create", + "args": { + "idempotency": false + } + }, + { + "complete": "worktree.create#1", + "params": { + "repo": "id:repo-1", + "name": "kestrel" + }, + "reject": { + "message": "Connection lost", + "deliveryUnknown": true + } + }, + { + "checkpoint": "unstamped-create-is-not-replayed" + } + ] + }, + { + "id": "tw-capabilities-advertised", + "operation": "tasks.worktree-capabilities", + "version": 1, + "family": "worktree.runtime-capabilities", + "sites": ["mobile/src/tasks/worktree-create-capability.ts"], + "schedules": [], + "steps": [ + { + "action": "probe", + "id": "probe" + }, + { + "complete": "status.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["mobile.tasks.v1", "worktree.create-idempotency.v1"], + "worktreeCreateIdempotency": { + "dedupeTtlMs": 45000 + }, + "platform": "linux" + } + } + }, + { + "checkpoint": "probed" + } + ] + }, + { + "id": "tw-capabilities-legacy-idempotency", + "operation": "tasks.worktree-capabilities", + "version": 1, + "family": "worktree.runtime-capabilities", + "sites": ["mobile/src/tasks/worktree-create-capability.ts"], + "schedules": [], + "steps": [ + { + "action": "probe", + "id": "probe" + }, + { + "complete": "status.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["worktree.create-idempotency.v1"] + } + } + }, + { + "checkpoint": "legacy-host-window" + } + ] + }, + { + "id": "tw-capabilities-cutover-retried", + "operation": "tasks.worktree-capabilities", + "version": 1, + "family": "worktree.runtime-capabilities", + "sites": ["mobile/src/tasks/worktree-create-capability.ts"], + "schedules": [], + "steps": [ + { + "action": "probe", + "id": "probe" + }, + { + "action": "cutover", + "id": "migrate" + }, + { + "bind": "status-after-cutover", + "request": "status.get#2", + "params": { + "$undefined": true + } + }, + { + "checkpoint": "reprobing-after-cutover" + }, + { + "complete": "status-after-cutover", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["mobile.tasks.v1"] + } + } + }, + { + "checkpoint": "probed-on-replacement" + } + ] + }, + { + "id": "tw-hosted-base-resolved", + "operation": "tasks.composer-hosted-base", + "version": 1, + "family": "worktree.hosted-base", + "sites": ["mobile/src/tasks/composer-source-base-resolve.ts"], + "schedules": [], + "steps": [ + { + "action": "pr-base", + "id": "pr" + }, + { + "complete": "worktree.resolvePrBase#1", + "params": { + "repo": "id:repo-1", + "prNumber": 12, + "headRefName": "feature" + }, + "reply": { + "ok": true, + "result": { + "baseBranch": "main", + "compareBaseRef": "origin/main" + } + } + }, + { + "checkpoint": "pr-base-resolved" + }, + { + "action": "mr-base", + "id": "mr" + }, + { + "complete": "worktree.resolveMrBase#1", + "params": { + "repo": "id:repo-1", + "mrIid": 7, + "sourceBranch": "feature" + }, + "reply": { + "ok": true, + "result": { + "baseBranch": "develop" + } + } + }, + { + "checkpoint": "mr-base-resolved" + } + ] + }, + { + "id": "tw-hosted-base-soft-error", + "operation": "tasks.composer-hosted-base", + "version": 1, + "family": "worktree.hosted-base", + "sites": ["mobile/src/tasks/composer-source-base-resolve.ts"], + "schedules": [], + "steps": [ + { + "action": "pr-base", + "id": "pr" + }, + { + "complete": "worktree.resolvePrBase#1", + "params": { + "repo": "id:repo-1", + "prNumber": 12, + "headRefName": "feature" + }, + "reply": { + "ok": true, + "result": { + "error": "pull request not found" + } + } + }, + { + "checkpoint": "in-band-error" + }, + { + "action": "mr-base", + "id": "mr" + }, + { + "complete": "worktree.resolveMrBase#1", + "params": { + "repo": "id:repo-1", + "mrIid": 7, + "sourceBranch": "feature" + }, + "reply": { + "ok": true, + "result": { + "error": "" + } + } + }, + { + "checkpoint": "in-band-empty-error" + } + ] + }, + { + "id": "tw-setup-hook-trust-approved", + "operation": "tasks.setup-hook-trust", + "version": 1, + "family": "worktree.setup-hook-trust", + "sites": ["mobile/src/tasks/setup-hook-trust.ts"], + "schedules": [], + "steps": [ + { + "action": "approve", + "id": "approve" + }, + { + "complete": "ui.set#1", + "params": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "contentHash": "hash-1", + "approvedAt": 1767225600000 + } + } + } + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "checkpoint": "approved" + } + ] + }, + { + "id": "tw-setup-hook-trust-always", + "operation": "tasks.setup-hook-trust", + "version": 1, + "family": "worktree.setup-hook-trust", + "sites": ["mobile/src/tasks/setup-hook-trust.ts"], + "schedules": [], + "steps": [ + { + "action": "approve", + "id": "approve", + "args": { + "always": true + } + }, + { + "complete": "ui.set#1", + "params": { + "trustedOrcaHooks": { + "repo-1": { + "all": { + "approvedAt": 1767225600000 + } + } + } + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "" + } + } + }, + { + "checkpoint": "refused-empty-message" + } + ] + }, + { + "id": "tw-smart-search-all-providers", + "operation": "tasks.smart-source-search", + "version": 1, + "family": "tasks.smart-source-search", + "sites": ["mobile/src/tasks/smart-source-search-requests.ts"], + "schedules": [], + "steps": [ + { + "action": "github", + "id": "github" + }, + { + "complete": "github.listWorkItems#1", + "params": { + "repo": "id:repo-1", + "limit": 36, + "query": "bug" + }, + "reply": { + "ok": true, + "result": { + "items": [ + { + "number": 1, + "title": "one" + } + ] + } + } + }, + { + "checkpoint": "github-items" + }, + { + "action": "gitlab", + "id": "gitlab" + }, + { + "complete": "gitlab.listWorkItems#1", + "params": { + "repo": "id:repo-1", + "state": "opened", + "page": 1, + "perPage": 50, + "query": "bug" + }, + "reply": { + "ok": true, + "result": { + "items": [ + { + "iid": 2, + "title": "two" + } + ], + "error": { + "type": "not_found", + "message": "missing" + } + } + } + }, + { + "checkpoint": "gitlab-items" + }, + { + "action": "linear", + "id": "linear" + }, + { + "complete": "linear.searchIssues#1", + "params": { + "query": "bug", + "limit": 50, + "workspaceId": "linear-workspace" + }, + "reply": { + "ok": true, + "result": { + "items": [ + { + "id": "issue-1" + } + ] + } + } + }, + { + "checkpoint": "linear-search" + }, + { + "action": "branches", + "id": "branches" + }, + { + "complete": "repo.searchRefs#1", + "params": { + "repo": "id:repo-1", + "query": "bug", + "limit": 20 + }, + "reply": { + "ok": true, + "result": { + "refs": ["main", "release"] + } + } + }, + { + "checkpoint": "branch-refs" + }, + { + "action": "linear", + "id": "linear-assigned", + "args": { + "query": " ", + "workspace": null + } + }, + { + "complete": "linear.listIssues#1", + "params": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$undefined": true + } + }, + "reply": { + "ok": true, + "result": [ + { + "id": "issue-3" + } + ] + } + }, + { + "checkpoint": "linear-assigned-listed" + } + ] + }, + { + "id": "tw-smart-search-linear-listed", + "operation": "tasks.smart-source-search", + "version": 1, + "family": "tasks.smart-source-search", + "sites": ["mobile/src/tasks/smart-source-search-requests.ts"], + "schedules": [], + "steps": [ + { + "action": "linear", + "id": "linear", + "args": { + "query": " ", + "workspace": null + } + }, + { + "complete": "linear.listIssues#1", + "params": { + "filter": "assigned", + "limit": 50, + "workspaceId": { + "$undefined": true + } + }, + "reply": { + "ok": true, + "result": [ + { + "id": "issue-2" + } + ] + } + }, + { + "checkpoint": "linear-assigned" + } + ] + }, + { + "id": "tw-smart-search-gitlab-provider-error", + "operation": "tasks.smart-source-search", + "version": 1, + "family": "tasks.smart-source-search", + "sites": ["mobile/src/tasks/smart-source-search-requests.ts"], + "schedules": [], + "steps": [ + { + "action": "gitlab", + "id": "gitlab" + }, + { + "complete": "gitlab.listWorkItems#1", + "params": { + "repo": "id:repo-1", + "state": "opened", + "page": 1, + "perPage": 50, + "query": "bug" + }, + "reply": { + "ok": true, + "result": { + "items": [], + "error": { + "type": "quota", + "message": "rate limited" + } + } + } + }, + { + "checkpoint": "in-band-provider-error" + }, + { + "action": "branches", + "id": "branches", + "args": { + "query": " main " + } + }, + { + "complete": "repo.searchRefs#1", + "params": { + "repo": "id:repo-1", + "query": "main", + "limit": 20 + }, + "reply": { + "ok": true, + "result": { + "refDetails": [ + { + "refName": "origin/main", + "localBranchName": "main" + } + ] + } + } + }, + { + "checkpoint": "branch-ref-details" + } + ] + }, + { + "id": "tw-paste-lookup-resolved", + "operation": "tasks.paste-lookup", + "version": 1, + "family": "tasks.paste-lookup", + "sites": ["mobile/src/tasks/smart-source-paste-intent.ts"], + "schedules": [], + "steps": [ + { + "action": "by-number", + "id": "by-number" + }, + { + "complete": "github.workItem#1", + "params": { + "repo": "id:repo-1", + "number": 12 + }, + "reply": { + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + }, + { + "checkpoint": "by-number" + }, + { + "action": "by-slug", + "id": "by-slug" + }, + { + "complete": "github.workItemByOwnerRepo#1", + "params": { + "repo": "id:repo-1", + "owner": "owner", + "ownerRepo": "repo", + "number": 12, + "type": "issue" + }, + "reply": { + "ok": true, + "result": { + "number": 12, + "title": "twelve" + } + } + }, + { + "checkpoint": "by-slug" + }, + { + "action": "gitlab-path", + "id": "gitlab-path" + }, + { + "complete": "gitlab.workItemByPath#1", + "params": { + "repo": "id:repo-1", + "host": "gitlab.com", + "path": "group/project", + "iid": 7, + "type": "issue" + }, + "reply": { + "ok": true, + "result": { + "iid": 7, + "title": "seven" + } + } + }, + { + "checkpoint": "gitlab-path" + }, + { + "action": "repo-slug", + "id": "repo-slug" + }, + { + "complete": "github.repoSlug#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "owner": "owner", + "repo": "repo" + } + } + }, + { + "checkpoint": "repo-slug-matched" + } + ] + }, + { + "id": "tw-paste-lookup-slug-unsupported", + "operation": "tasks.paste-lookup", + "version": 1, + "family": "tasks.paste-lookup", + "sites": ["mobile/src/tasks/smart-source-paste-intent.ts"], + "schedules": [], + "steps": [ + { + "action": "repo-slug", + "id": "repo-slug" + }, + { + "complete": "github.repoSlug#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": false, + "error": { + "code": "method_not_found", + "message": "Unknown method" + } + } + }, + { + "checkpoint": "host-wide-probe-cached" + }, + { + "action": "repo-slug-again", + "id": "repo-slug-again" + }, + { + "checkpoint": "no-second-probe" + } + ] + }, + { + "id": "tw-paste-lookup-slug-refused", + "operation": "tasks.paste-lookup", + "version": 1, + "family": "tasks.paste-lookup", + "sites": ["mobile/src/tasks/smart-source-paste-intent.ts"], + "schedules": [], + "steps": [ + { + "action": "repo-slug", + "id": "repo-slug" + }, + { + "complete": "github.repoSlug#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "no access" + } + } + }, + { + "complete": "github.repoSlug#2", + "params": { + "repo": "id:repo-2" + }, + "reply": { + "ok": true, + "result": null + } + }, + { + "checkpoint": "refusal-is-per-repo" + } + ] + }, + { + "id": "tw-workspace-source-presets", + "operation": "tasks.workspace-source", + "version": 1, + "family": "tasks.workspace-source", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "repo.sparsePresets#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "presets": [ + { + "id": "p1", + "name": "docs", + "directories": ["docs"] + } + ] + } + } + }, + { + "checkpoint": "presets-loaded" + }, + { + "action": "branch-query", + "id": "branch-query" + }, + { + "complete": "repo.searchRefs#1", + "params": { + "repo": "id:repo-1", + "query": "main", + "limit": 20 + }, + "reply": { + "ok": true, + "result": { + "refs": ["main"] + } + } + }, + { + "checkpoint": "branches-loaded" + } + ] + }, + { + "id": "tw-workspace-source-presets-refused", + "operation": "tasks.workspace-source", + "version": 1, + "family": "tasks.workspace-source", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "repo.sparsePresets#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "" + } + } + }, + { + "checkpoint": "presets-refused-empty-message" + } + ] + }, + { + "id": "tw-workspace-sparse-saved", + "operation": "tasks.workspace-sparse", + "version": 1, + "family": "tasks.workspace-sparse", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "ssh.getState#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": true, + "result": { + "state": { + "targetId": "ssh-1", + "status": "connected", + "error": null, + "reconnectAttempt": 0 + } + } + } + }, + { + "checkpoint": "ssh-state-read" + }, + { + "action": "save-preset", + "id": "save" + }, + { + "complete": "repo.saveSparsePreset#1", + "params": { + "repo": "id:repo-1", + "name": "docs", + "directories": ["docs"] + }, + "reply": { + "ok": true, + "result": { + "preset": { + "id": "p1", + "name": "docs", + "directories": ["docs"] + } + } + } + }, + { + "checkpoint": "preset-saved" + } + ] + }, + { + "id": "tw-workspace-sparse-missing-preset", + "operation": "tasks.workspace-sparse", + "version": 1, + "family": "tasks.workspace-sparse", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "ssh.getState#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "" + } + } + }, + { + "action": "save-preset", + "id": "save" + }, + { + "complete": "repo.saveSparsePreset#1", + "params": { + "repo": "id:repo-1", + "name": "docs", + "directories": ["docs"] + }, + "reply": { + "ok": true, + "result": {} + } + }, + { + "checkpoint": "saved-without-preset" + } + ] + }, + { + "id": "tw-workspace-ssh-connected", + "operation": "tasks.workspace-ssh", + "version": 1, + "family": "tasks.workspace-ssh", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "preflight.detectRemoteAgents#1", + "params": { + "connectionId": "ssh-1" + }, + "reply": { + "ok": true, + "result": ["codex"] + } + }, + { + "checkpoint": "agents-detected" + }, + { + "action": "connect", + "id": "connect" + }, + { + "complete": "ssh.connect#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": true, + "result": { + "state": { + "targetId": "ssh-1", + "status": "connected", + "error": null, + "reconnectAttempt": 0 + } + } + } + }, + { + "checkpoint": "connected" + }, + { + "action": "resolve-setup", + "id": "setup" + }, + { + "complete": "repo.hooks#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": " pnpm install " + } + }, + "setupRunPolicy": "ask", + "source": "repo", + "setupTrust": { + "contentHash": "hash-1", + "scriptContent": "pnpm install" + } + } + } + }, + { + "checkpoint": "setup-prompted" + } + ] + }, + { + "id": "tw-workspace-ssh-not-ready", + "operation": "tasks.workspace-ssh", + "version": 1, + "family": "tasks.workspace-ssh", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "preflight.detectRemoteAgents#1", + "params": { + "connectionId": "ssh-1" + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "no access" + } + } + }, + { + "action": "ensure-ready", + "id": "ensure" + }, + { + "complete": "ssh.getState#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": true, + "result": { + "state": { + "targetId": "ssh-1", + "status": "disconnected", + "error": null, + "reconnectAttempt": 0 + } + } + } + }, + { + "checkpoint": "ensure-rejected" + }, + { + "action": "resolve-setup", + "id": "setup" + }, + { + "complete": "repo.hooks#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "hooks": { + "scripts": {} + } + } + } + }, + { + "checkpoint": "no-setup-script" + } + ] + }, + { + "id": "tw-workspace-ssh-connect-refused", + "operation": "tasks.workspace-ssh", + "version": 1, + "family": "tasks.workspace-ssh", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "preflight.detectRemoteAgents#1", + "params": { + "connectionId": "ssh-1" + }, + "reject": { + "message": "Connection lost", + "deliveryUnknown": true + } + }, + { + "action": "connect", + "id": "connect" + }, + { + "complete": "ssh.connect#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": false, + "error": { + "code": "ssh_failed", + "message": "" + } + } + }, + { + "checkpoint": "connect-refused-empty-message" + }, + { + "action": "resolve-setup", + "id": "setup" + }, + { + "complete": "repo.hooks#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": "pnpm i" + } + }, + "setupRunPolicy": "never" + } + } + }, + { + "checkpoint": "setup-skipped" + } + ] + }, + { + "id": "tw-workspace-ssh-local-agents", + "operation": "tasks.workspace-ssh-local", + "version": 1, + "family": "tasks.workspace-ssh-local", + "sites": ["mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "complete": "preflight.detectAgents#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": ["codex", "claude"] + } + }, + { + "checkpoint": "local-agents-detected" + } + ] + }, + { + "id": "tw-task-preferences-resume-write", + "operation": "settings.task-preferences", + "version": 1, + "family": "settings-best-effort", + "sites": ["mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "resume", + "id": "resume" + }, + { + "complete": "ui.set#1", + "params": { + "taskResumeState": { + "githubItemsPreset": "issues" + } + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "no access" + } + } + }, + { + "checkpoint": "best-effort-resume-write" + }, + { + "action": "trust", + "id": "trust" + }, + { + "complete": "ui.set#2", + "params": { + "trustedOrcaHooks": { + "repo-1": { + "setup": { + "contentHash": "hash-1", + "approvedAt": 1767225600000 + } + } + } + }, + "reply": { + "ok": false, + "error": { + "code": "forbidden", + "message": "" + } + } + }, + { + "checkpoint": "awaited-trust-write-refused" + } + ] } ] } diff --git a/mobile/scripts/mock-server-native-chat-scenario.ts b/mobile/scripts/mock-server-native-chat-scenario.ts index d2f65e0b635..b9d18348f7b 100644 --- a/mobile/scripts/mock-server-native-chat-scenario.ts +++ b/mobile/scripts/mock-server-native-chat-scenario.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import type { WebSocket } from 'ws' import type { AgentStatusEntry } from '../../src/shared/agent-status-types' +import type { NativeChatMessage } from '../../src/shared/native-chat-types' import type { RuntimeMobileSessionTabsResult, RuntimeMobileSessionTerminalClientTab @@ -25,6 +26,10 @@ const TAB_ID = 'chat-tab-1' const SESSION_ID = 'mock-chat-session' const TRANSCRIPT_PATH = join(tmpdir(), 'mock-transcript.jsonl') const MOCK_IMAGE_PATH = join(tmpdir(), 'mock-image.png') +// Exercise legacy OMP hooks without a transcript path; current hooks may include one. +const CHAT_AGENT = process.env.MOCK_CHAT_AGENT === 'omp' ? 'omp' : 'claude' +const CHAT_TITLE = CHAT_AGENT === 'omp' ? 'OMP' : 'Claude Code' +const TRANSCRIPT_START = Date.now() - 1000 * 60 * 5 function readControl(file: string): string { try { @@ -41,28 +46,27 @@ const agentStatus: AgentStatusEntry = { prompt: '', updatedAt: Date.now(), stateStartedAt: Date.now(), - agentType: 'claude', + agentType: CHAT_AGENT, paneKey: `${TAB_ID}:leaf-1`, terminalHandle: TERMINAL_HANDLE, stateHistory: [], - providerSession: { - key: 'session_id', - id: SESSION_ID, - transcriptPath: TRANSCRIPT_PATH - } + providerSession: + CHAT_AGENT === 'omp' + ? { key: 'session_id', id: SESSION_ID } + : { key: 'session_id', id: SESSION_ID, transcriptPath: TRANSCRIPT_PATH } } function buildTab(): RuntimeMobileSessionTerminalClientTab { return { type: 'terminal', id: TAB_ID, - title: 'Claude Code', + title: CHAT_TITLE, parentTabId: TAB_ID, leafId: 'leaf-1', ptyId: 'pty-1', status: 'ready', terminal: TERMINAL_HANDLE, - launchAgent: 'claude', + launchAgent: CHAT_AGENT, agentStatus, viewMode: 'chat', isActive: true @@ -104,6 +108,51 @@ function worktreeOf(request: RpcRequest): string { return typeof raw === 'string' ? raw : 'id:mock-worktree' } +// Why: shapes mirror what the runtime's omp decoder emits for a real session +// (thinking→text on the assistant turn, toolCall blocks, toolResult turns), so +// the phone exercises the same render path a live omp pane would. +function mockTranscript(): NativeChatMessage[] { + if (CHAT_AGENT !== 'omp') { + return [] + } + const t = TRANSCRIPT_START + return [ + { + id: 'omp-1', + role: 'user', + blocks: [{ type: 'text', text: 'why is my deploy failing?' }], + timestamp: t, + source: 'transcript' + }, + { + id: 'omp-2', + role: 'assistant', + blocks: [ + { type: 'text', text: 'Let me check the deploy logs first.' }, + { type: 'tool-call', name: 'bash', input: { command: 'kubectl get pods' } } + ], + timestamp: t + 1000, + source: 'transcript' + }, + { + id: 'omp-3', + role: 'tool', + blocks: [{ type: 'tool-result', output: 'api-7f9c 0/1 CrashLoopBackOff' }], + timestamp: t + 2000, + source: 'transcript' + }, + { + id: 'omp-4', + role: 'assistant', + blocks: [ + { type: 'text', text: 'The API pod is crash-looping. Check its logs with kubectl logs.' } + ], + timestamp: t + 3000, + source: 'transcript' + } + ] +} + // Why: unsubscribe correlates by worktree, not request id, and a socket that // navigates A->B->A would otherwise stack one push loop per subscribe. const tabsPushLoops = new Map>>() @@ -144,7 +193,7 @@ type Respond = (response: RpcResponse) => void type Success = (id: string, result: unknown, streaming?: boolean) => RpcResponse type Failure = (id: string, code: string, message: string) => RpcResponse -/** Mock backend for the native-chat surface: session tabs, an empty transcript +/** Mock backend for the native-chat surface: session tabs, a fixture transcript * snapshot, terminal send, and image upload. Opt-in via MOCK_NATIVE_CHAT=1 * because it replaces the default terminal fixtures. No transcript or terminal * output frames are pushed. Returns false for methods it does not own. */ @@ -194,7 +243,7 @@ export function handleMockNativeChatRequest( const entry = (handle: string) => ({ handle, worktreeId, - title: 'Claude Code', + title: CHAT_TITLE, isActive: true, hasRunningProcess: true }) @@ -209,11 +258,13 @@ export function handleMockNativeChatRequest( } case 'nativeChat.subscribe': - respond(success(request.id, { type: 'snapshot', messages: [], hasMore: false }, true)) + respond( + success(request.id, { type: 'snapshot', messages: mockTranscript(), hasMore: false }, true) + ) return true case 'nativeChat.readSession': - respond(success(request.id, { messages: [], hasMore: false })) + respond(success(request.id, { messages: mockTranscript(), hasMore: false })) return true case 'terminal.subscribe': { diff --git a/mobile/scripts/rpc-recording.mts b/mobile/scripts/rpc-recording.mts index 392a086e621..a166fe7a38a 100644 --- a/mobile/scripts/rpc-recording.mts +++ b/mobile/scripts/rpc-recording.mts @@ -1,6 +1,7 @@ import { createRequire } from 'node:module' import { resolve } from 'node:path' import { runProcess } from '../../src/shared/child-process/run-process.ts' +import { RECORDING_DRIVERS } from '../src/test-support/rpc-recording/recording-drivers.ts' import { readScenarios } from '../src/test-support/rpc-recording/scenario-input.ts' if (process.argv[2] !== '--record' || process.env.RPC_FOUNDATION_RECORD !== '1') { @@ -56,8 +57,7 @@ const result = await runProcess({ args: [ resolve(require.resolve('vitest/package.json'), '../vitest.mjs'), 'run', - 'src/test-support/rpc-recording/pilot-recordings.test.ts', - 'src/test-support/rpc-recording/family-recordings.test.ts' + ...RECORDING_DRIVERS.map((driver) => `src/test-support/rpc-recording/${driver}`) ], cwd: resolve(root, 'mobile'), timeoutMs: 120_000, diff --git a/mobile/src/session/MobileNativeChatOverlay.tsx b/mobile/src/session/MobileNativeChatOverlay.tsx index 389beb8eaad..a72b91ff29c 100644 --- a/mobile/src/session/MobileNativeChatOverlay.tsx +++ b/mobile/src/session/MobileNativeChatOverlay.tsx @@ -83,6 +83,7 @@ export function MobileNativeChatOverlay({ onDismissAsk={controller.dismissNativeChatAsk} onAnswerAsk={controller.handleNativeChatAnswerAsk} onCancelAsk={controller.handleNativeChatCancelAsk} + onCancelPrompt={controller.handleNativeChatCancelPrompt} question={controller.nativeChatQuestion} onAnswerQuestion={controller.handleNativeChatQuestionAnswer} permission={controller.nativeChatPermission} diff --git a/mobile/src/session/MobileNativeChatPermission.test.ts b/mobile/src/session/MobileNativeChatPermission.test.ts index b39188b381f..88de20a7ca5 100644 --- a/mobile/src/session/MobileNativeChatPermission.test.ts +++ b/mobile/src/session/MobileNativeChatPermission.test.ts @@ -10,7 +10,7 @@ vi.mock('react-native', () => ({ View: 'View' })) -vi.mock('lucide-react-native', () => ({ ShieldQuestion: 'ShieldQuestion' })) +vi.mock('lucide-react-native', () => ({ ShieldQuestion: 'ShieldQuestion', X: 'X' })) describe('MobileNativeChatPermission', () => { let renderer: ReactTestRenderer | null = null @@ -42,4 +42,24 @@ describe('MobileNativeChatPermission', () => { expect(onRespond).toHaveBeenCalledOnce() await act(async () => resolveResponse(true)) }) + + it('passes the rendered prompt identity to cancel', async () => { + const onCancel = vi.fn(async () => true) + await act(async () => { + renderer = create( + createElement(MobileNativeChatPermission, { + permission: { + title: 'Approve?', + prompt: { itemId: 'approval-1', expectedRevision: 4 }, + options: [{ label: 'Allow', send: '1' }] + }, + onRespond: vi.fn(async () => true), + onCancel + }) + ) + }) + const cancel = renderer.root.findByProps({ accessibilityLabel: 'Cancel' }) + await act(async () => cancel.props.onPress()) + expect(onCancel).toHaveBeenCalledWith({ itemId: 'approval-1', expectedRevision: 4 }) + }) }) diff --git a/mobile/src/session/MobileNativeChatPermission.tsx b/mobile/src/session/MobileNativeChatPermission.tsx index ad26d578d93..47ad7a52022 100644 --- a/mobile/src/session/MobileNativeChatPermission.tsx +++ b/mobile/src/session/MobileNativeChatPermission.tsx @@ -1,6 +1,6 @@ import { memo, useRef, useState } from 'react' import { Pressable, StyleSheet, Text, View } from 'react-native' -import { ShieldQuestion } from 'lucide-react-native' +import { ShieldQuestion, X } from 'lucide-react-native' import { colors, radii, spacing, typography } from '../theme/mobile-theme' import type { MobileChatPermission } from './mobile-native-chat-permission' @@ -9,10 +9,12 @@ import type { MobileChatPermission } from './mobile-native-chat-permission' // accent button so the affirmative choice reads as distinct from the rest. function MobileNativeChatPermissionImpl({ permission, - onRespond + onRespond, + onCancel }: { permission: MobileChatPermission onRespond: (send: string) => Promise + onCancel?: (prompt?: NonNullable) => Promise }): React.JSX.Element { const [submitting, setSubmitting] = useState(false) const submittingRef = useRef(false) @@ -33,6 +35,17 @@ function MobileNativeChatPermissionImpl({ {permission.title} + {onCancel ? ( + void onCancel(permission.prompt)} + disabled={submitting} + > + + + ) : null} {permission.detail ? {permission.detail} : null} @@ -80,10 +93,17 @@ const styles = StyleSheet.create({ gap: spacing.sm }, title: { + flex: 1, color: colors.textPrimary, fontSize: typography.bodySize, fontWeight: '600' }, + cancel: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center' + }, detail: { color: colors.textSecondary, fontSize: typography.metaSize, diff --git a/mobile/src/session/MobileNativeChatPromptCard.tsx b/mobile/src/session/MobileNativeChatPromptCard.tsx index 470ba2ee8b6..31a007801dc 100644 --- a/mobile/src/session/MobileNativeChatPromptCard.tsx +++ b/mobile/src/session/MobileNativeChatPromptCard.tsx @@ -15,6 +15,7 @@ export function MobileNativeChatPromptCard({ onDismissAsk, onAnswerAsk, onCancelAsk, + onCancelPrompt, permission, onRespondPermission, question, @@ -25,6 +26,7 @@ export function MobileNativeChatPromptCard({ onDismissAsk?: () => void onAnswerAsk?: (prompt: AskPrompt, selections: AskAnswerSelection[]) => Promise onCancelAsk?: () => Promise + onCancelPrompt?: (prompt?: NonNullable) => Promise permission?: MobileChatPermission | null onRespondPermission?: (send: string) => Promise question?: MobileChatQuestion | null @@ -58,6 +60,7 @@ export function MobileNativeChatPromptCard({ key={JSON.stringify(permission)} permission={permission} onRespond={async (send) => (await onRespondPermission?.(send)) ?? false} + onCancel={onCancelPrompt} /> ) } @@ -67,6 +70,7 @@ export function MobileNativeChatPromptCard({ key={mobileChatQuestionKey(question)} question={question} onAnswer={async (text) => (await onAnswerQuestion?.(text)) ?? false} + onCancel={onCancelPrompt} /> ) } diff --git a/mobile/src/session/MobileNativeChatQuestion.test.tsx b/mobile/src/session/MobileNativeChatQuestion.test.tsx index be9777a0b69..96cae6556a4 100644 --- a/mobile/src/session/MobileNativeChatQuestion.test.tsx +++ b/mobile/src/session/MobileNativeChatQuestion.test.tsx @@ -14,7 +14,8 @@ vi.mock('react-native', () => ({ vi.mock('lucide-react-native', () => ({ ArrowUp: 'ArrowUp', Check: 'Check', - CircleHelp: 'CircleHelp' + CircleHelp: 'CircleHelp', + X: 'X' })) describe('MobileNativeChatQuestion', () => { @@ -103,4 +104,27 @@ describe('MobileNativeChatQuestion', () => { expect(onAnswer).toHaveBeenCalledWith('east-token, other-token:ap-south') }) + + it('passes the rendered prompt identity to cancel', async () => { + const onCancel = vi.fn(async () => true) + await act(async () => { + renderer = create( + createElement(MobileNativeChatQuestion, { + question: { + question: 'Pick one', + prompt: { itemId: 'question-1', expectedRevision: 7 }, + options: ['Choice'], + multiSelect: false, + allowOther: false, + optionTokens: ['choice-token'] + }, + onAnswer: vi.fn(async () => true), + onCancel + }) + ) + }) + const cancel = renderer.root.findByProps({ accessibilityLabel: 'Cancel' }) + await act(async () => cancel.props.onPress()) + expect(onCancel).toHaveBeenCalledWith({ itemId: 'question-1', expectedRevision: 7 }) + }) }) diff --git a/mobile/src/session/MobileNativeChatQuestion.tsx b/mobile/src/session/MobileNativeChatQuestion.tsx index 9eae7210bc8..1cb530c7682 100644 --- a/mobile/src/session/MobileNativeChatQuestion.tsx +++ b/mobile/src/session/MobileNativeChatQuestion.tsx @@ -1,6 +1,6 @@ import { useMemo, useRef, useState } from 'react' import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native' -import { ArrowUp, Check, CircleHelp } from 'lucide-react-native' +import { ArrowUp, Check, CircleHelp, X } from 'lucide-react-native' import { colors, radii, spacing, typography } from '../theme/mobile-theme' import { formatQuestionAnswerByIndexes, @@ -12,13 +12,18 @@ import { type Props = { question: MobileChatQuestion onAnswer: (text: string) => Promise + onCancel?: (prompt?: NonNullable) => Promise } /** Renders an agent's choice prompt as a tappable card. Single-select answers * on tap; multi-select toggles then Submits; an always-present text entry lets * the user answer freely (the escape hatch) when the heuristic misreads the * options or none apply. */ -export function MobileNativeChatQuestion({ question, onAnswer }: Props): React.JSX.Element { +export function MobileNativeChatQuestion({ + question, + onAnswer, + onCancel +}: Props): React.JSX.Element { const [selectedOptionIndexes, setSelectedOptionIndexes] = useState([]) const [freeText, setFreeText] = useState('') const [sending, setSending] = useState(false) @@ -102,6 +107,17 @@ export function MobileNativeChatQuestion({ question, onAnswer }: Props): React.J {question.question} + {onCancel ? ( + void onCancel(question.prompt)} + disabled={sending} + > + + + ) : null} {hasOptions ? ( @@ -214,6 +230,12 @@ const styles = StyleSheet.create({ fontWeight: '600', lineHeight: typography.bodySize + 7 }, + cancel: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center' + }, options: { gap: spacing.xs }, diff --git a/mobile/src/session/MobileNativeChatView.test.ts b/mobile/src/session/MobileNativeChatView.test.ts index 2e67b1eaa9f..a245a1fe7bb 100644 --- a/mobile/src/session/MobileNativeChatView.test.ts +++ b/mobile/src/session/MobileNativeChatView.test.ts @@ -85,6 +85,9 @@ type Overrides = { turnIndicator?: Parameters[0]['turnIndicator'] agentWorking?: boolean canStop?: boolean + ask?: Parameters[0]['ask'] + question?: Parameters[0]['question'] + permission?: Parameters[0]['permission'] sendSurfaceId?: string keyboardInset?: number hasMore?: boolean @@ -681,6 +684,72 @@ describe('MobileNativeChatView', () => { expect(workingIndicators()).toHaveLength(0) }) + it.each([ + { + label: 'structured question', + cardType: 'ChatAsk', + interaction: { + ask: { + questions: [ + { + question: 'Pick destination', + multiSelect: false, + options: [{ label: 'Choice A' }, { label: 'Choice B' }] + } + ] + } + } + }, + { + label: 'question', + cardType: 'ChatQuestion', + interaction: { + question: { + question: 'Pick destination', + options: ['Choice A', 'Choice B'], + multiSelect: false, + allowOther: true, + optionTokens: ['choice-a', 'choice-b'] + } + } + }, + { + label: 'approval', + cardType: 'ChatPermission', + interaction: { + permission: { + title: 'Allow command?', + detail: 'pnpm test', + options: [ + { label: 'Allow', send: 'allow' }, + { label: 'Deny', send: 'deny' } + ] + } + } + } + ])('hides live turn activity for a pending $label without settling it', async (testCase) => { + const folded = [userTurn('u1', 'go'), assistantTurn('a1', 'waiting for input')] + const working = { + messages: folded, + folded, + structuredActivityUi: true, + agentWorking: true, + canStop: true + } + await render({ ...working, ...testCase.interaction }) + + expect(footerProps()).toBeNull() + expect(rowProps('a1').activeTurnIsWorking).toBe(true) + expect( + renderer!.root.findAll((node) => node.props.accessibilityLabel === 'Stop the agent') + ).toHaveLength(1) + expect(renderer!.root.findAll((node) => node.type === testCase.cardType)).toHaveLength(1) + + await update(working) + expect(footerProps()).toMatchObject({ thinking: false, workedSeconds: null }) + expect(rowProps('a1').activeTurnIsWorking).toBe(true) + }) + it('reports the live turn as thinking only when its journal says it is reasoning', async () => { const folded = [userTurn('u1', 'go')] await render({ diff --git a/mobile/src/session/MobileNativeChatView.tsx b/mobile/src/session/MobileNativeChatView.tsx index be974b989f3..49f20fa5d5d 100644 --- a/mobile/src/session/MobileNativeChatView.tsx +++ b/mobile/src/session/MobileNativeChatView.tsx @@ -122,6 +122,8 @@ type Props = { * into selector keystrokes (Claude) or pasted label text (other agents). */ onAnswerAsk?: (prompt: AskPrompt, selections: AskAnswerSelection[]) => Promise onCancelAsk?: () => Promise + /** Cancel a structured approval/question with exact item identity when supported. */ + onCancelPrompt?: (prompt?: { itemId: string; expectedRevision: number }) => Promise question?: MobileChatQuestion | null onAnswerQuestion?: (text: string) => Promise permission?: MobileChatPermission | null @@ -178,6 +180,7 @@ export function MobileNativeChatView({ onDismissAsk, onAnswerAsk, onCancelAsk, + onCancelPrompt, question, onAnswerQuestion, permission, @@ -266,6 +269,8 @@ export function MobileNativeChatView({ activityText: turnIndicator?.activityText ?? null, scopeKey: sendSurfaceId }) + const hasPendingStructuredInteraction = + structuredActivityUi && (ask != null || permission != null || question != null) const renderItem = useCallback( ({ item, index }: { item: NativeChatMessage; index: number }) => ( @@ -329,7 +334,10 @@ export function MobileNativeChatView({ ) : null } ListFooterComponent={ - structuredActivityUi && agentWorking && turns.active ? ( + structuredActivityUi && + agentWorking && + !hasPendingStructuredInteraction && + turns.active ? ( Promise handleNativeChatCancelAsk: () => Promise + handleNativeChatCancelPrompt?: (prompt?: { + itemId: string + expectedRevision: number + }) => Promise handleNativeChatRespondPermission: (text: string) => Promise handleNativeChatStop: () => void nativeChatFilePaths: string[] diff --git a/mobile/src/session/mobile-native-chat-permission.ts b/mobile/src/session/mobile-native-chat-permission.ts index fe3f9a0d89b..53799718eb4 100644 --- a/mobile/src/session/mobile-native-chat-permission.ts +++ b/mobile/src/session/mobile-native-chat-permission.ts @@ -11,6 +11,8 @@ export type MobileChatPermission = { title: string detail?: string + /** Structured prompt identity, present only when the host can cancel it exactly. */ + prompt?: { itemId: string; expectedRevision: number } options: Array<{ label: string; send: string }> } diff --git a/mobile/src/session/mobile-native-chat-question.ts b/mobile/src/session/mobile-native-chat-question.ts index 59ba3d72aba..66ebb75919f 100644 --- a/mobile/src/session/mobile-native-chat-question.ts +++ b/mobile/src/session/mobile-native-chat-question.ts @@ -5,6 +5,8 @@ export type MobileChatQuestion = { question: string + /** Structured prompt identity, present only for durable host prompts. */ + prompt?: { itemId: string; expectedRevision: number } options: string[] multiSelect: boolean /** Structured questions hide the free-text row when the provider does not accept it. */ diff --git a/mobile/src/session/mobile-session-route-parity.test.ts b/mobile/src/session/mobile-session-route-parity.test.ts index 7d0c5c6de46..7b3b91af7a6 100644 --- a/mobile/src/session/mobile-session-route-parity.test.ts +++ b/mobile/src/session/mobile-session-route-parity.test.ts @@ -62,12 +62,12 @@ const HOST_COMPONENT_NAMES = new Set([ 'View' ]) -const HEAD_MAIN_HOOK_SHA256 = '11cd92aec686a6e47b23114ec31da86152a850b064821578b165fabfbce53b27' -const HEAD_HOOK_BINDING_SHA256 = 'f8bce7101a26b4d794bb58dee54702424a4965cc81dec5c758ca56cd5a6f4ce8' +const HEAD_MAIN_HOOK_SHA256 = 'c7a1bbc0588a5d27797bbab13168e76eb20200288921fdc3347632c2b4afd0ae' +const HEAD_HOOK_BINDING_SHA256 = '06edf1a4314eba41b1d3e1cb67b0cfab2a936aef7d127c5dc48e789c9adc6c8f' const HEAD_CALLBACK_IDENTITY_SHA256 = '2a9e4825df007f6ef53b81aa5004991d6318eee7507b44d625c07e630be432eb' const HEAD_CALLBACK_BODY_SHA256 = '85c4f4605e66c45e2b6bc7de739cb3493d9e2d0db9c9242c379db8ed34a8cefe' -const HEAD_EFFECT_SHA256 = 'd9ebfaabc1e79773cdada7ab370b20459ed972f1f8edce1652199f4d0391cd13' +const HEAD_EFFECT_SHA256 = '73d80845e0a4b6363cfb4bb55551af97965b1f676b97adf0b2a8504219b9a501' const HEAD_CONTENT_HOOK_SHA256 = '9c3b612fef3f370d66873aefdbe1d701f20cb64ded31fef5cc45fde6f8189581' const HEAD_NESTED_FUNCTION_SHA256 = '97ce5457d8059974f500022a4382ff687074e26843d6c1525be938d6c0537928' @@ -87,7 +87,7 @@ const HEAD_STYLE_REFERENCE_SHA256 = const HEAD_IDENTITY_FIELD_SHA256 = '91146853930a34dd1f3d80e5c97fbacd7cf19fb93dd26fe8fc6f29169622f9d6' const HEAD_NAVIGATION_SHA256 = '9d96f5dad7de555d6553eac39c0fab00efad507470fd562cb9beaa32db16f512' -const HEAD_CAPABILITY_SHA256 = 'ca219f7909a091717110b823d5b94a20770ad3ae51894e0fa765e8628309392d' +const HEAD_CAPABILITY_SHA256 = '67c3154b71b542bb63a4365d3ea75aef19ef133c02f509318619618221786fab' type Definition = { declaration: ts.FunctionDeclaration; sourceFile: ts.SourceFile } type HookFacts = { @@ -472,7 +472,7 @@ describe('mobile session route extraction parity', () => { const contentBindings = CONTENT_COMPONENT_NAMES.flatMap( (name) => readHookFacts(name, definitions).bindings ) - expect(main.hooks).toHaveLength(268) + expect(main.hooks).toHaveLength(269) expect(hash(main.hooks)).toBe(HEAD_MAIN_HOOK_SHA256) expect(hash(main.bindings)).toBe(HEAD_HOOK_BINDING_SHA256) expect(main.callbacks).toHaveLength(77) @@ -511,7 +511,7 @@ describe('mobile session route extraction parity', () => { expect(hash(compatibility.identityFields)).toBe(HEAD_IDENTITY_FIELD_SHA256) expect(compatibility.navigation).toHaveLength(6) expect(hash(compatibility.navigation)).toBe(HEAD_NAVIGATION_SHA256) - expect(compatibility.capabilities).toHaveLength(5) + expect(compatibility.capabilities).toHaveLength(6) expect(hash(compatibility.capabilities)).toBe(HEAD_CAPABILITY_SHA256) }) diff --git a/mobile/src/session/mobile-structured-agent-prompts.ts b/mobile/src/session/mobile-structured-agent-prompts.ts index 61425597721..82d619e49e6 100644 --- a/mobile/src/session/mobile-structured-agent-prompts.ts +++ b/mobile/src/session/mobile-structured-agent-prompts.ts @@ -134,6 +134,7 @@ export function projectStructuredPermission( } return { title: prompt.body.title, + prompt: { itemId: prompt.itemId, expectedRevision: prompt.revision }, ...(prompt.body.detail ? { detail: prompt.body.detail } : {}), options: prompt.body.options.map((option) => ({ label: option.label, @@ -158,12 +159,14 @@ export function projectStructuredQuestion( return projectGroupedQuestion( prompt.body.questions, groupedDraft, - groupedQuestionPromptKey(prompt.itemId, prompt.revision) + groupedQuestionPromptKey(prompt.itemId, prompt.revision), + { itemId: prompt.itemId, expectedRevision: prompt.revision } ) } const optionDescriptions = prompt.body.options.map((option) => option.description) return { question: prompt.body.question, + prompt: { itemId: prompt.itemId, expectedRevision: prompt.revision }, options: prompt.body.options.map((option) => option.label), ...(optionDescriptions.some(Boolean) ? { optionDescriptions } : {}), multiSelect: false, diff --git a/mobile/src/session/mobile-structured-agent-session-cancel.ts b/mobile/src/session/mobile-structured-agent-session-cancel.ts new file mode 100644 index 00000000000..9c67e7480d0 --- /dev/null +++ b/mobile/src/session/mobile-structured-agent-session-cancel.ts @@ -0,0 +1,78 @@ +import type { AgentSessionCancelResult } from '../../../src/shared/agent-session-wire' +import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' +import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer' +import { activeStructuredAgentSessionTurnId } from '../../../src/shared/structured-agent-session-live-turn' +import type { RpcClient } from '../transport/rpc-client' +import { + requestStructuredAgentSessionMutation, + retainStructuredSessionOperationId, + type StructuredAgentSessionMutationCallResult +} from './mobile-structured-agent-session-rpc' + +type PromptIdentity = { itemId: string; expectedRevision: number } + +export function pendingStructuredPromptIdentity( + items: readonly AgentJournalRenderItem[] +): PromptIdentity | undefined { + const prompt = items.find((item) => + item.body.kind === 'approval' || item.body.kind === 'question' + ? item.body.resolution.state === 'pending' + : false + ) + return prompt ? { itemId: prompt.itemId, expectedRevision: prompt.revision } : undefined +} + +export async function requestMobileStructuredAgentSessionCancel(args: { + client: RpcClient | null + sessionId: string | null + enabled: boolean + stateRef: { readonly current: StructuredAgentSessionState } + sessionKey: string + operationIds: Map + promptCancelSupported: boolean | null + prompt?: PromptIdentity + onSendError: (message: string) => void +}): Promise { + const { client, enabled, onSendError, operationIds, sessionId, sessionKey, stateRef } = args + const current = stateRef.current + const turnId = activeStructuredAgentSessionTurnId(current.items) + if (!client || !sessionId || !enabled || current.fence === null || !turnId) { + onSendError('Stop not sent') + return false + } + // Check the capability before fields enter either the fingerprint or operation key. + const fields = { + turnId, + ...(args.prompt && args.promptCancelSupported === true ? { prompt: args.prompt } : {}) + } + const key = `${sessionKey}:agentSession.cancel:${JSON.stringify(fields)}` + const clientOperationId = retainStructuredSessionOperationId( + operationIds, + key, + operationIds.get(key) + ) + const result: StructuredAgentSessionMutationCallResult = + await requestStructuredAgentSessionMutation({ + client, + method: 'agentSession.cancel', + fingerprintMethod: 'agentSession.cancel', + sessionId, + expectedRuntimeFence: current.fence, + fields, + clientOperationId + }) + if (result.status !== 'unknown') { + operationIds.delete(key) + } + if (result.status === 'accepted') { + return true + } + if (result.status === 'unknown') { + onSendError('Stop unconfirmed — check chat before retrying') + } else if (result.status === 'refused') { + onSendError(result.message) + } else if (result.status === 'failed') { + onSendError(result.message === 'Request not sent' ? 'Stop not sent' : result.message) + } + return false +} diff --git a/mobile/src/session/mobile-structured-grouped-question.ts b/mobile/src/session/mobile-structured-grouped-question.ts index 17a716cd032..031cdf3f10f 100644 --- a/mobile/src/session/mobile-structured-grouped-question.ts +++ b/mobile/src/session/mobile-structured-grouped-question.ts @@ -105,7 +105,8 @@ function answersFor( export function projectGroupedQuestion( questions: readonly AgentJournalQuestion[], draft: GroupedQuestionDraft | null, - promptKey: string + promptKey: string, + promptIdentity?: { itemId: string; expectedRevision: number } ): MobileChatQuestion | null { const answered = answersFor(draft, promptKey).length const question = questions[answered] @@ -117,6 +118,7 @@ export function projectGroupedQuestion( return { question: questions.length > 1 ? `${heading} (${answered + 1} of ${questions.length})` : heading, + ...(promptIdentity ? { prompt: promptIdentity } : {}), options: question.options.map((option) => option.label), ...(optionDescriptions.some(Boolean) ? { optionDescriptions } : {}), multiSelect: question.multiSelect, diff --git a/mobile/src/session/use-mobile-native-chat-controller.test.ts b/mobile/src/session/use-mobile-native-chat-controller.test.ts index c90033e2404..00eee9651a0 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.test.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.test.ts @@ -17,6 +17,7 @@ const viewMode = { isTabChatView: (_tabId: string) => true } const sessionState = { messages: [] as unknown[], status: 'ready', transcriptLoading: false } const structuredSendWithOutcome = vi.fn() const structuredCancel = vi.fn() +const structuredCancelPrompt = vi.fn(async () => true) const structuredRespondPermission = vi.fn(async () => true) const structuredRespondQuestion = vi.fn(async () => true) const structuredSetOption = vi.fn(async () => true) @@ -90,6 +91,7 @@ vi.mock('./use-mobile-structured-agent-session', () => ({ ...structuredActivity, sendWithOutcome: structuredSendWithOutcome, cancel: structuredCancel, + cancelPrompt: structuredCancelPrompt, permission: structuredPermission, question: structuredQuestion, optionSnapshot: structuredOptionSnapshot, @@ -227,6 +229,10 @@ describe('useMobileNativeChatController handleNativeChatSend', () => { controller = null }) + it('leaves structured prompt cancellation unavailable on the legacy bridge lane', () => { + expect(controller?.handleNativeChatCancelPrompt).toBeUndefined() + }) + it('clears an orphaned image paste before a question-card answer (#10228)', async () => { // The chat overlay wires the question card straight to this send, bypassing // the image hook that used to own the only heal. diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index 987546a4f48..b8b99742dd8 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -35,6 +35,8 @@ export function useMobileNativeChatController(args: { nativeChatInputLeaseReady: boolean /** Live socket state; the lease collapses on disconnect but one render later. */ connState: ConnectionState + /** Host capability fact from the shared runtime status probe. */ + agentSessionPromptCancelSupported?: boolean | null onSendError: (message: string) => void /** Retires a held failure banner. Any accepted chat write clears it — a delivered * answer or permission reply must not sit under a stale "not sent". */ @@ -51,6 +53,7 @@ export function useMobileNativeChatController(args: { nativeChatTranscriptIsLocalReadable, nativeChatInputLeaseReady, connState, + agentSessionPromptCancelSupported = null, onSendError, onSendResolved } = args @@ -90,6 +93,7 @@ export function useMobileNativeChatController(args: { callerIdentity: deviceTokenRef.current ?? '', enabled: showNativeChat, connState, + promptCancelSupported: agentSessionPromptCancelSupported, onSendError }) const { @@ -258,6 +262,10 @@ export function useMobileNativeChatController(args: { ? structuredNativeChat.respondPermission : legacyHandleNativeChatRespondPermission const respond = useNativeChatAcceptedAction(handleNativeChatRespondPermission, onSendResolved) + const structuredCancelPrompt = useNativeChatAcceptedAction( + activeChatStructured ? structuredNativeChat.cancelPrompt : async () => false, + onSendResolved + ) return { isTabChatView, @@ -292,6 +300,9 @@ export function useMobileNativeChatController(args: { dismissNativeChatAsk, handleNativeChatAnswerAsk: answerAsk, handleNativeChatCancelAsk: cancelAsk, + // Heuristic/legacy cards have no durable prompt identity, so keep their + // cancel affordance absent instead of exposing a dead action. + handleNativeChatCancelPrompt: activeChatStructured ? structuredCancelPrompt : undefined, handleNativeChatRespondPermission: respond, handleNativeChatStop: activeChatStructured ? structuredNativeChat.cancel : handleNativeChatStop, nativeChatFilePaths, diff --git a/mobile/src/session/use-mobile-native-chat-session-lane.ts b/mobile/src/session/use-mobile-native-chat-session-lane.ts index d337d8ba929..bfeb1b06945 100644 --- a/mobile/src/session/use-mobile-native-chat-session-lane.ts +++ b/mobile/src/session/use-mobile-native-chat-session-lane.ts @@ -15,6 +15,7 @@ export function useMobileNativeChatSessionLane({ sessionId, sourceIdentity, callerIdentity, + promptCancelSupported, enabled, connState, onSendError @@ -29,6 +30,7 @@ export function useMobileNativeChatSessionLane({ sessionId: string | null sourceIdentity: Parameters[0]['sourceIdentity'] callerIdentity: string + promptCancelSupported?: boolean | null enabled: boolean connState: ConnectionState onSendError: (message: string) => void @@ -48,6 +50,7 @@ export function useMobileNativeChatSessionLane({ sessionId: structured ? sessionId : null, sourceIdentity, callerIdentity, + promptCancelSupported, enabled, // Holds are connection-scoped; dropping this on transport loss lets the hook // reacquire the provider without clearing the cached transcript. diff --git a/mobile/src/session/use-mobile-session-feedback-capabilities.ts b/mobile/src/session/use-mobile-session-feedback-capabilities.ts index 8c619fb1f7f..f0231189309 100644 --- a/mobile/src/session/use-mobile-session-feedback-capabilities.ts +++ b/mobile/src/session/use-mobile-session-feedback-capabilities.ts @@ -32,6 +32,11 @@ export function useMobileSessionFeedbackCapabilities(scope: MobileSessionTermina null ) const [quickCommandsSupported, setQuickCommandsSupported] = useState(null) + // Prompt cancellation is negotiated with the same host capability probe as + // the other session surfaces; consumers never maintain a second status cache. + const [agentSessionPromptCancelSupported, setAgentSessionPromptCancelSupported] = useState< + boolean | null + >(null) // Why: stable callbacks (handleFileTap) read the live value via this ref, since // the capability probe resolves after the callbacks are created. const browserScreencastSupportedRef = useRef(browserScreencastSupported) @@ -115,6 +120,8 @@ export function useMobileSessionFeedbackCapabilities(scope: MobileSessionTermina setAgentSessionHistorySupported, quickCommandsSupported, setQuickCommandsSupported, + agentSessionPromptCancelSupported, + setAgentSessionPromptCancelSupported, browserScreencastSupportedRef, reconciledCreateWarningState, createWarning, diff --git a/mobile/src/session/use-mobile-session-native-chat-dictation.ts b/mobile/src/session/use-mobile-session-native-chat-dictation.ts index 6046cba1059..535b4034ec8 100644 --- a/mobile/src/session/use-mobile-session-native-chat-dictation.ts +++ b/mobile/src/session/use-mobile-session-native-chat-dictation.ts @@ -27,6 +27,7 @@ export function useMobileSessionNativeChatDictation( worktreeId, client, connState, + agentSessionPromptCancelSupported, setInput, liveInputTerminalHandles, activeHandle, @@ -72,6 +73,7 @@ export function useMobileSessionNativeChatDictation( nativeChatTranscriptIsLocalReadable, nativeChatInputLeaseReady, connState, + agentSessionPromptCancelSupported, onSendError: nativeChatSendError.show, onSendResolved: nativeChatSendError.clear }) diff --git a/mobile/src/session/use-mobile-session-tab-reconciliation.ts b/mobile/src/session/use-mobile-session-tab-reconciliation.ts index be4641dd297..da7a48e035c 100644 --- a/mobile/src/session/use-mobile-session-tab-reconciliation.ts +++ b/mobile/src/session/use-mobile-session-tab-reconciliation.ts @@ -2,7 +2,10 @@ import { useEffect, useRef, useCallback, useMemo, useState } from 'react' import { startRuntimeCapabilityProbe } from '../transport/runtime-capability-probe' import { supportsMobileQuickCommands } from '../terminal/quick-commands' import { MOBILE_AI_VAULT_CAPABILITY } from '../agent-history/agent-history-capability' -import { TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version' +import { + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY, + TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY +} from '../../../src/shared/protocol-version' import { runAcceptedMobileSessionTabsEffects } from './mobile-session-tabs-accepted-effects' import type { SessionTabsStreamSource } from './mobile-session-tabs-stream-health' import { useMobileSessionTabsFetchReporting } from './use-mobile-session-tabs-fetch-reporting' @@ -31,6 +34,7 @@ export function useMobileSessionTabReconciliation(scope: MobileSessionMarkdownAc switchSessionTabRef, setBrowserScreencastSupported, setAgentSessionHistorySupported, + setAgentSessionPromptCancelSupported, setQuickCommandsSupported, nativeChatStream, fetchTerminals, @@ -148,6 +152,7 @@ export function useMobileSessionTabReconciliation(scope: MobileSessionMarkdownAc if (!client || connState !== 'connected') { setBrowserScreencastSupported(null) setAgentSessionHistorySupported(null) + setAgentSessionPromptCancelSupported(null) setQuickCommandsSupported(null) setShowQuickCommands(false) hostQueryReplyInputSupportedRef.current = false @@ -157,6 +162,7 @@ export function useMobileSessionTabReconciliation(scope: MobileSessionMarkdownAc // host; clear the prior capability before exposing host-specific actions. setBrowserScreencastSupported(null) setAgentSessionHistorySupported(null) + setAgentSessionPromptCancelSupported(null) setQuickCommandsSupported(null) setShowQuickCommands(false) hostQueryReplyInputSupportedRef.current = false @@ -165,6 +171,9 @@ export function useMobileSessionTabReconciliation(scope: MobileSessionMarkdownAc return startRuntimeCapabilityProbe(client, (capabilities) => { setBrowserScreencastSupported(capabilities.includes('browser.screencast.v1')) setAgentSessionHistorySupported(capabilities.includes(MOBILE_AI_VAULT_CAPABILITY)) + setAgentSessionPromptCancelSupported( + capabilities.includes(AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY) + ) setQuickCommandsSupported(supportsMobileQuickCommands(capabilities)) // Why: hosts without this capability strip inputKind from terminal.send, // so a forwarded xterm reply would become floor-stealing shell input. diff --git a/mobile/src/session/use-mobile-structured-agent-session-prompt-cancel.test.tsx b/mobile/src/session/use-mobile-structured-agent-session-prompt-cancel.test.tsx new file mode 100644 index 00000000000..48b44e78ff5 --- /dev/null +++ b/mobile/src/session/use-mobile-structured-agent-session-prompt-cancel.test.tsx @@ -0,0 +1,221 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' +import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer' +import type { RpcClient } from '../transport/rpc-client' + +const mocks = vi.hoisted(() => ({ sendRequest: vi.fn() })) +vi.mock('./use-mobile-structured-agent-state', () => ({ + useMobileStructuredAgentState: () => ({ + state, + stateRef, + loadingOlder: false, + loadEarlier: vi.fn() + }) +})) +vi.mock('./use-mobile-structured-agent-options', () => ({ + useMobileStructuredAgentOptions: () => ({ + conversationCommands: [], + invokeStructuredOption: vi.fn(), + optionSnapshot: [], + optionSurface: { getSnapshot: () => [], subscribe: () => () => {} }, + pendingOptionId: null, + setStructuredOption: vi.fn() + }) +})) +vi.mock('./use-mobile-structured-prompt-responses', () => ({ + useMobileStructuredPromptResponses: () => ({ + groupedDraft: null, + respondPermission: vi.fn(), + respondQuestion: vi.fn() + }) +})) +vi.mock('./use-mobile-structured-send-operation-reconciliation', () => ({ + useMobileStructuredSendOperationReconciliation: vi.fn() +})) + +import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session' + +const pendingApproval = (): AgentJournalRenderItem => ({ + itemId: 'approval-1', + revision: 4, + sequence: 2, + observedAt: 2, + body: { + kind: 'approval', + title: 'Allow Bash?', + detail: null, + options: [{ id: 'allow', label: 'Allow' }], + resolution: { + state: 'pending', + selectedOptionId: null, + resolvedBy: null, + resolvedAt: null + } + } +}) + +const runningTurn = (): AgentJournalRenderItem => ({ + itemId: 'turn-status', + revision: 1, + sequence: 1, + observedAt: 1, + body: { + kind: 'status', + text: 'Waiting', + turnLifecycle: { turnId: 'turn-1', state: 'running' } + } +}) + +const pendingQuestion = (): AgentJournalRenderItem => ({ + itemId: 'question-1', + revision: 7, + sequence: 2, + observedAt: 2, + body: { + kind: 'question', + question: 'Pick a destination', + options: [{ id: 'local', label: 'Local' }], + resolution: { + state: 'pending', + selectedOptionId: null, + resolvedBy: null, + resolvedAt: null + } + } +}) + +let state: StructuredAgentSessionState +const stateRef = { + get current(): StructuredAgentSessionState { + return state + } +} +const client: RpcClient = { + sendRequest: mocks.sendRequest, + subscribe: () => () => {}, + updateTerminalSubscriptionViewport: () => {}, + getState: () => 'connected', + getReconnectAttempt: () => 0, + getLastConnectedAt: () => null, + onStateChange: () => () => {}, + notifyForeground: () => {}, + close: () => {} +} + +function Harness({ promptCancelSupported }: { promptCancelSupported: boolean }): null { + hook = useMobileStructuredAgentSession({ + client, + sessionId: 'session-1', + sourceIdentity: 'host-a\0workspace-a', + enabled: true, + connected: true, + agent: 'codex', + promptCancelSupported, + onSendError: vi.fn() + }) + return null +} + +let hook: ReturnType +let renderer: ReactTestRenderer | null = null + +describe('mobile structured prompt cancellation', () => { + beforeEach(() => { + vi.clearAllMocks() + state = { + epoch: 'epoch-1', + cursor: { epoch: 'epoch-1', sequence: 2 }, + fence: 3, + items: [runningTurn(), pendingApproval()], + submissions: [], + retainedItemLimit: 1024, + hasOlder: false, + status: 'ready', + handoff: null + } + mocks.sendRequest.mockResolvedValue({ + ok: true, + result: { + ok: true, + replayed: false, + fence: 3, + cursor: { epoch: 'epoch-1', sequence: 3 }, + value: { turnId: 'turn-1', cancelled: true } + } + }) + renderer = null + }) + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + it('sends the clicked prompt identity on capable hosts', async () => { + act(() => { + renderer = create(createElement(Harness, { promptCancelSupported: true })) + }) + await act(async () => { + expect(await hook.cancelPrompt()).toBe(true) + }) + expect(mocks.sendRequest).toHaveBeenCalledWith( + 'agentSession.cancel', + expect.objectContaining({ + turnId: 'turn-1', + prompt: { itemId: 'approval-1', expectedRevision: 4 } + }), + expect.any(Object) + ) + }) + + it('downgrades to turn-only cancellation on an old host', async () => { + act(() => { + renderer = create(createElement(Harness, { promptCancelSupported: false })) + }) + await act(async () => { + expect(await hook.cancelPrompt()).toBe(true) + }) + const call = mocks.sendRequest.mock.calls.find(([method]) => method === 'agentSession.cancel') + expect(call?.[1]).toMatchObject({ turnId: 'turn-1' }) + expect(call?.[1]).not.toHaveProperty('prompt') + }) + + it('cancels a question card with its item identity', async () => { + state = { ...state, items: [runningTurn(), pendingQuestion()] } + act(() => { + renderer = create(createElement(Harness, { promptCancelSupported: true })) + }) + await act(async () => { + expect(await hook.cancelPrompt({ itemId: 'question-1', expectedRevision: 7 })).toBe(true) + }) + expect(mocks.sendRequest).toHaveBeenCalledWith( + 'agentSession.cancel', + expect.objectContaining({ + turnId: 'turn-1', + prompt: { itemId: 'question-1', expectedRevision: 7 } + }), + expect.any(Object) + ) + }) + + it('uses the rendered prompt identity when the journal changes before tap', async () => { + act(() => { + renderer = create(createElement(Harness, { promptCancelSupported: true })) + }) + const renderedIdentity = { itemId: 'approval-1', expectedRevision: 4 } + state = { + ...state, + items: [runningTurn(), { ...pendingApproval(), itemId: 'approval-new', revision: 9 }] + } + // The hook API accepts the identity captured by the card; the state is intentionally newer. + await act(async () => { + expect(await hook.cancelPrompt(renderedIdentity)).toBe(true) + }) + expect(mocks.sendRequest).toHaveBeenCalledWith( + 'agentSession.cancel', + expect.objectContaining({ prompt: renderedIdentity }), + expect.any(Object) + ) + }) +}) diff --git a/mobile/src/session/use-mobile-structured-agent-session.ts b/mobile/src/session/use-mobile-structured-agent-session.ts index 301c9b7b0ca..2a24d38a38e 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.ts +++ b/mobile/src/session/use-mobile-structured-agent-session.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { dispatchMobileStructuredCommand } from './mobile-structured-composer-command' -import type { AgentSessionCancelResult } from '../../../src/shared/agent-session-wire' import { structuredAgentSessionSendBody, type StructuredAgentSessionAttachment @@ -37,6 +36,10 @@ import { useMobileStructuredAgentOptions } from './use-mobile-structured-agent-o import { useMobileStructuredAgentTurnTiming } from './use-mobile-structured-agent-turn-timing' import { sendMobileStructuredAgentSessionMessage } from './mobile-structured-agent-session-send' import { useMobileStructuredSendOperationReconciliation } from './use-mobile-structured-send-operation-reconciliation' +import { + pendingStructuredPromptIdentity, + requestMobileStructuredAgentSessionCancel +} from './mobile-structured-agent-session-cancel' type StructuredMobileAttachment = StructuredAgentSessionAttachment & { id?: string @@ -61,6 +64,7 @@ type StructuredMobileSession = ReturnType Promise respondQuestion: (answer: string) => Promise + cancelPrompt: (prompt?: { itemId: string; expectedRevision: number }) => Promise } export function useMobileStructuredAgentSession(args: { @@ -73,6 +77,8 @@ export function useMobileStructuredAgentSession(args: { enabled: boolean /** Live transport only; gates the connection-scoped hold, nothing else. */ connected: boolean + /** Capability fact from the shared runtime status probe; null follows legacy cancellation. */ + promptCancelSupported?: boolean | null agent: string | null onSendError: (message: string) => void }): StructuredMobileSession { @@ -84,14 +90,13 @@ export function useMobileStructuredAgentSession(args: { sessionId, sourceIdentity = '', enabled, - onSendError + onSendError, + promptCancelSupported = null } = args const sessionKey = encodeNativeChatTranscriptIdentity([sourceIdentity, agent, sessionId]) const operationIdsRef = useRef(new Map()) const commandPendingRef = useRef(false) useEffect(() => () => operationIdsRef.current.clear(), []) - const retainOperationId = (key: string, operationId?: string): string => - retainStructuredOpId(operationIdsRef.current, key, operationId) const stateArgs = { client, sessionId, sessionKey, enabled, connected } const { state, stateRef, loadingOlder, loadEarlier } = useMobileStructuredAgentState(stateArgs) useMobileStructuredSendOperationReconciliation(state.submissions) @@ -108,7 +113,11 @@ export function useMobileStructuredAgentSession(args: { } const targetFence = current.fence const key = `${sessionKey}:${fingerprintMethod}:${JSON.stringify(fields)}` - const clientOperationId = retainOperationId(key, operationIdsRef.current.get(key)) + const clientOperationId = retainStructuredOpId( + operationIdsRef.current, + key, + operationIdsRef.current.get(key) + ) const result = await requestStructuredAgentSessionMutation({ client, method, @@ -127,9 +136,6 @@ export function useMobileStructuredAgentSession(args: { } } if (result.status === 'unknown') { - // Prompt/option plans cannot repeat a harmful effect under a fresh id; - // issue a fresh id so a retry can be admitted after the user checks the - // stream. Sends keep theirs — see `mobile-structured-send-delivery.ts`. operationIdsRef.current.delete(key) return result } @@ -230,7 +236,6 @@ export function useMobileStructuredAgentSession(args: { setStructuredOption ] ) - const { groupedDraft, respondPermission, respondQuestion } = useMobileStructuredPromptResponses({ stateRef, sessionKey, @@ -238,37 +243,21 @@ export function useMobileStructuredAgentSession(args: { onSendError }) - const cancel = useCallback(() => { - const current = stateRef.current - const turnId = activeStructuredAgentSessionTurnId(current.items) - if (!client || !sessionId || !enabled || current.fence === null || !turnId) { - onSendError('Stop not sent') - return - } - const fields = { turnId } - const key = `${sessionKey}:agentSession.cancel:${JSON.stringify(fields)}` - const clientOperationId = retainOperationId(key, operationIdsRef.current.get(key)) - void requestStructuredAgentSessionMutation({ - client, - method: 'agentSession.cancel', - fingerprintMethod: 'agentSession.cancel', - sessionId, - expectedRuntimeFence: current.fence, - fields, - clientOperationId - }).then((result) => { - if (result.status !== 'unknown') { - operationIdsRef.current.delete(key) - } - if (result.status === 'unknown') { - onSendError('Stop unconfirmed — check chat before retrying') - } else if (result.status === 'refused') { - onSendError(result.message) - } else if (result.status === 'failed') { - onSendError(result.message === 'Request not sent' ? 'Stop not sent' : result.message) - } - }) - }, [client, enabled, onSendError, sessionId, sessionKey]) + const requestCancel = useCallback( + (prompt?: { itemId: string; expectedRevision: number }): Promise => + requestMobileStructuredAgentSessionCancel({ + client, + enabled, + onSendError, + operationIds: operationIdsRef.current, + prompt, + promptCancelSupported, + sessionId, + sessionKey, + stateRef + }), + [client, enabled, onSendError, promptCancelSupported, sessionId, sessionKey, stateRef] + ) const messages = useMemo( () => projectStructuredAgentSessionMessages(state.items, [], state.submissions), @@ -279,8 +268,6 @@ export function useMobileStructuredAgentSession(args: { const activityText = selectStructuredAgentTurnActivity(state.items, turnId, state.activity)?.text ?? null const thinking = isStructuredAgentSessionThinking(state.items) - // Stable while the readings hold, so a streaming turn does not re-render the - // whole chat surface on every journal batch. const turnIndicator = useMemo(() => ({ thinking, activityText }), [thinking, activityText]) const status = state.status === 'idle' ? 'idle' : state.status const approvalPrompt = useMemo( @@ -291,7 +278,6 @@ export function useMobileStructuredAgentSession(args: { () => state.items.find(pendingStructuredQuestion) ?? null, [state.items] ) - return { ...options, session: { @@ -303,7 +289,6 @@ export function useMobileStructuredAgentSession(args: { loadingEarlier: loadingOlder, loadEarlier }, - // A dispatch the provider has not answered yet is already work — see the desktop hook. isWorking: turnId !== null || hasUnansweredStructuredAgentSessionDispatch(state.submissions, state.fence), @@ -311,7 +296,11 @@ export function useMobileStructuredAgentSession(args: { turnIndicator, ...turnTiming, sendWithOutcome, - cancel, + cancel: () => { + void requestCancel() + }, + cancelPrompt: (prompt?: { itemId: string; expectedRevision: number }) => + requestCancel(prompt ?? pendingStructuredPromptIdentity(stateRef.current.items)), permission: projectStructuredPermission(approvalPrompt), question: projectStructuredQuestion(questionPrompt, groupedDraft), respondPermission, diff --git a/mobile/src/source-control/mobile-git-read-operations.ts b/mobile/src/source-control/mobile-git-read-operations.ts index e611abe982c..3b815f3eb5b 100644 --- a/mobile/src/source-control/mobile-git-read-operations.ts +++ b/mobile/src/source-control/mobile-git-read-operations.ts @@ -1,6 +1,9 @@ import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' import type { RpcCompatibleReader } from '../transport/rpc-operation-contract' -import { rpcPayloadMember, rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { + rpcUncheckedMemberReader, + rpcUncheckedPayloadReader +} from '../transport/rpc-reader-payload' import { readMobileGitStatusResult } from '../session/mobile-diff-review-rpc' import type { MobileGitStatusResult } from './mobile-git-status' @@ -58,27 +61,18 @@ export const gitHistoryRead = bindDeferredRpcOperation( }) ) -const commitCompareEntriesReader: RpcCompatibleReader< - unknown, - 'commit-compare-entries', - unknown -> = (raw) => ({ - compatible: true, - variant: 'commit-compare-entries', - // Keeps the property-read exception the expanded-commit list already relies on: a null result - // throws inside the load, which is what leaves an already-loaded file list alone. - value: rpcPayloadMember(raw, 'entries'), - salvage: { droppedPaths: [], droppedCount: 0 } -}) - -/** A refused compare leaves the row's file list untouched, so refusal is a skip, not a throw. */ +/** + * A refused compare leaves the row's file list untouched, so refusal is a skip, not a throw. The + * member read keeps the property-read exception a null result throws, which is what leaves an + * already-loaded file list alone. + */ export const gitCommitCompareRead = bindDeferredRpcOperation( defineRpcOperation({ name: 'git.commit-compare-entries-or-skip', method: 'git.commitCompare', acceptance: 'success-result-or-skip', barrier: 'after-caller-barrier', - read: commitCompareEntriesReader + read: rpcUncheckedMemberReader('commit-compare-entries', 'entries') }) ) diff --git a/mobile/src/tasks/blank-workspace-create.ts b/mobile/src/tasks/blank-workspace-create.ts index 3c38ac37447..ea3c827c3b9 100644 --- a/mobile/src/tasks/blank-workspace-create.ts +++ b/mobile/src/tasks/blank-workspace-create.ts @@ -4,6 +4,7 @@ import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktr import type { WorktreeCreateIdempotencyProbe } from './worktree-create-idempotency-policy' import { agentLaunchCreateFields, + type WorkspaceCreateParams, type WorkspaceCreateSetupDecision } from './workspace-create-params' @@ -28,7 +29,7 @@ export async function createBlankWorkspace(args: { nameWasGenerated: args.nameWasGenerated, worktreeCreateIdempotency: args.worktreeCreateIdempotency, buildParams: (name) => { - const params: Record = { + const params: WorkspaceCreateParams = { repo: `id:${args.repoId}`, setupDecision: args.setupDecision, name, diff --git a/mobile/src/tasks/composer-source-base-resolve.ts b/mobile/src/tasks/composer-source-base-resolve.ts index 423c3b05294..40419993970 100644 --- a/mobile/src/tasks/composer-source-base-resolve.ts +++ b/mobile/src/tasks/composer-source-base-resolve.ts @@ -1,6 +1,6 @@ import type { RpcClient } from '../transport/rpc-client' -import type { RpcSuccess } from '../transport/types' import type { GitHubPrStartPoint } from '../../../src/shared/worktree/types' +import { worktreeMrBaseResolve, worktreePrBaseResolve } from './mobile-workspace-create-operations' // The resolved start point for a linked PR/MR: the base branch to create from // plus the optional review-compare ref, push target, and exact branch name. @@ -23,8 +23,8 @@ export async function resolveComposerPrBase(args: { isCrossRepository?: boolean }): Promise { const { client, repoId, prNumber, headRefName, baseRefName, isCrossRepository } = args - const response = await client.sendRequest( - 'worktree.resolvePrBase', + const reply = await worktreePrBaseResolve.request( + client, { repo: `id:${repoId}`, prNumber, @@ -34,10 +34,8 @@ export async function resolveComposerPrBase(args: { }, { timeoutMs: 30_000 } ) - if (!response.ok) { - throw new Error(response.error.message) - } - const result = (response as RpcSuccess).result as GitHubPrStartPoint | { error: string } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreePrBaseResolve.interpret(reply) as GitHubPrStartPoint | { error: string } if ('error' in result) { throw new Error(result.error) } @@ -54,8 +52,8 @@ export async function resolveComposerMrBase(args: { isCrossRepository?: boolean }): Promise { const { client, repoId, mrIid, sourceBranch, targetBranch, isCrossRepository } = args - const response = await client.sendRequest( - 'worktree.resolveMrBase', + const reply = await worktreeMrBaseResolve.request( + client, { repo: `id:${repoId}`, mrIid, @@ -65,10 +63,8 @@ export async function resolveComposerMrBase(args: { }, { timeoutMs: 30_000 } ) - if (!response.ok) { - throw new Error(response.error.message) - } - const result = (response as RpcSuccess).result as HostedBaseResult + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreeMrBaseResolve.interpret(reply) as HostedBaseResult if ('error' in result) { throw new Error(result.error) } diff --git a/mobile/src/tasks/mobile-task-runtime-operations.ts b/mobile/src/tasks/mobile-task-runtime-operations.ts new file mode 100644 index 00000000000..c69651af666 --- /dev/null +++ b/mobile/src/tasks/mobile-task-runtime-operations.ts @@ -0,0 +1,87 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { + rpcUncheckedMemberReader, + rpcUncheckedPayloadReader +} from '../transport/rpc-reader-payload' + +// What the Tasks screen reads once per host to hydrate, and the preferences it writes back. + +/** + * status.get read for task hydration, the first of two policies on this method. A refused status + * stops hydration with the host's own message; the create-time probe in + * mobile-workspace-create-operations.ts degrades instead. One reader serves both. + */ +export const taskRuntimeStatusRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'status.task-runtime', + method: 'status.get', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('runtime-status') + }) +) + +/** + * Persisted UI state, read at the hydration barrier alongside preflight and Linear status. A + * refused read leaves the screen on its defaults rather than failing hydration, so it is a skip. + */ +export const taskUiStateRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ui.task-state-or-skip', + method: 'ui.get', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('ui-state-member', 'ui') + }) +) + +/** Whether `glab` is installed, which gates the GitLab provider. Advisory, so refusal skips. */ +export const taskPreflightRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'preflight.task-tooling-or-skip', + method: 'preflight.check', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('task-preflight') + }) +) + +/** Whether Linear is connected. Also advisory: an unanswered probe means "not connected". */ +export const taskLinearStatusRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'linear.task-status-or-skip', + method: 'linear.status', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('linear-status') + }) +) + +/** + * Writing persisted UI state. Two of its three call sites await it and surface the host's refusal + * message; the third is fire-and-forget and never interprets the reply, so no acceptance applies + * there. The payload is unread either way. + */ +export const taskUiStateWrite = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ui.set-task-state', + method: 'ui.set', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('ui-state-written') + }) +) + +/** + * Writing a host setting from the Tasks screen. Every call site is best-effort — the in-memory + * picker already reflects the change — so a refusal is a skip, and none of them reads the payload. + */ +export const taskSettingsWrite = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'settings.update-task-preference-or-skip', + method: 'settings.update', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('setting-written') + }) +) diff --git a/mobile/src/tasks/mobile-task-source-search-operations.ts b/mobile/src/tasks/mobile-task-source-search-operations.ts new file mode 100644 index 00000000000..7460c42bfad --- /dev/null +++ b/mobile/src/tasks/mobile-task-source-search-operations.ts @@ -0,0 +1,100 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import type { RpcCompatibleReader } from '../transport/rpc-operation-contract' +import { rpcReadUnchecked, rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' +import { extractLinearIssueReadItems } from './linear-mobile-issue-read' + +// The Smart workspace-source picker's provider reads: per-repo search, and the single-item lookups +// a pasted link or number resolves to. Provider-specific fallbacks stay at their own call sites. + +export const githubWorkItemSearchRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'github.work-item-search', + method: 'github.listWorkItems', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('github-work-items') + }) +) + +/** GitLab answers in-band too: an accepted reply can carry a provider `error` the caller raises. */ +export const gitlabWorkItemSearchRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'gitlab.work-item-search', + method: 'gitlab.listWorkItems', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('gitlab-work-items') + }) +) + +// Linear replies either as a bare array or as an `{ items }` envelope, and the picker has always +// accepted both through this projection. Two operations share it because the empty-query path asks +// a different method, not because the two answers differ. +const linearIssueReader: RpcCompatibleReader = (raw) => + rpcReadUnchecked('linear-issues', extractLinearIssueReadItems(raw)) + +export const linearIssueSearchRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'linear.issue-search', + method: 'linear.searchIssues', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: linearIssueReader + }) +) + +export const linearAssignedIssueListRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'linear.assigned-issue-list', + method: 'linear.listIssues', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: linearIssueReader + }) +) + +/** + * A repo's owner/repo slug, asked per repo so a pasted cross-repo URL can be matched without + * assuming github.com syntax. A refusal means "this repo cannot answer", which the caller caches + * as no slug rather than failing the paste — so refusal is a skip. The caller still reads the + * refusal code directly, because `method_not_found` is host-wide and retires the whole probe. + */ +export const githubRepoSlugRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'github.repo-slug-or-skip', + method: 'github.repoSlug', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('repo-slug') + }) +) + +export const githubWorkItemByNumberRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'github.work-item-by-number', + method: 'github.workItem', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('github-work-item') + }) +) + +export const githubWorkItemBySlugRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'github.work-item-by-owner-repo', + method: 'github.workItemByOwnerRepo', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('github-work-item') + }) +) + +export const gitlabWorkItemByPathRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'gitlab.work-item-by-path', + method: 'gitlab.workItemByPath', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('gitlab-work-item') + }) +) diff --git a/mobile/src/tasks/mobile-tasks-refactor-parity.test.ts b/mobile/src/tasks/mobile-tasks-refactor-parity.test.ts index 111b119db8e..dcd84ba1656 100644 --- a/mobile/src/tasks/mobile-tasks-refactor-parity.test.ts +++ b/mobile/src/tasks/mobile-tasks-refactor-parity.test.ts @@ -16,12 +16,17 @@ const hash = (parts: string[] | string): string => .update(Array.isArray(parts) ? parts.join('\n') : parts) .digest('hex') -// Bound settings requests change source signatures; their behavior is covered by settings-read-operations.test.ts. -const SETTINGS_RPC_SCREEN_HOOKS = 'fb2d873e06001fbae7cee78d079b3df9dc2eedb56ab2f03c7ffb431bc8666191' +// Bound workspace-creation requests change source signatures the same way bound settings requests +// did: the method string and the envelope read leave the screen and an operation name arrives. The +// behaviour they used to pin is pinned by the recordings in mobile/rpc-foundation/goldens instead, +// which did not move. Statement, declaration, render and style counts are unchanged; `semantics` +// loses exactly the 22 `rpc:` signatures and 22 method literals the migration deleted. +const WORKSPACE_RPC_SCREEN_HOOKS = + '26ed5700089a9de13ea984274eb10ddea62f72b28135992514e3c16ef8e47e30' const PRE_REFACTOR_DIFF_HOOKS = '93c7189b32bed8456cc51814fffa8ce80cf62011ef968a9d53ddec2b9686f58f' -const SETTINGS_RPC_STATEMENTS = '1c99d6382f74c37c0ff896dfa634fb503c9fe8062e2280328d0b82f79f658fdb' +const WORKSPACE_RPC_STATEMENTS = 'c25179660e089fd602b06e8c235e5f92d62e63d6d4add4c33ff89a4b5f9493cc' const MAIN_REBASED_DECLARATIONS = '6ad0397123e59fc1047a14049c86ff31d81723673a7a7f5c41677471aec58415' -const SETTINGS_RPC_SEMANTICS = '2431b1c07dfe9a9c94f5d3f4e91415ed99bd9e1bce3794f8bd5f094a29134d77' +const WORKSPACE_RPC_SEMANTICS = '7a00e700fe7293df9b5b68470185197c56a27007d89038a183153b29326113c0' const PRE_REFACTOR_STYLES = '1db6af69c791d9963928541ad5310942fcbda6d984b422c90b6eb92b6816579a' const PRE_REFACTOR_RENDER_TREE = '2111145136b1e4fbca150d4792d735a90e992488e9934cfc1a8b8f3be981f39f' @@ -29,7 +34,7 @@ describe('Mobile Tasks refactor parity', () => { it('preserves recursively flattened hook and dependency order', () => { const screenHooks = readFlattenedMobileTasksHookSignatures('MobileTasksScreen') expect(screenHooks).toHaveLength(350) - expect(hash(screenHooks)).toBe(SETTINGS_RPC_SCREEN_HOOKS) + expect(hash(screenHooks)).toBe(WORKSPACE_RPC_SCREEN_HOOKS) const diffHooks = readFlattenedMobileTasksHookSignatures('GitHubPrFileDiff') expect(diffHooks).toHaveLength(3) @@ -39,7 +44,7 @@ describe('Mobile Tasks refactor parity', () => { it('preserves every screen statement in execution order', () => { const statements = readFlattenedMobileTasksCoreStatements() expect(statements).toHaveLength(417) - expect(hash(statements)).toBe(SETTINGS_RPC_STATEMENTS) + expect(hash(statements)).toBe(WORKSPACE_RPC_STATEMENTS) }) it('preserves every moved top-level declaration', () => { @@ -50,8 +55,8 @@ describe('Mobile Tasks refactor parity', () => { it('preserves RPC calls, runtime strings, and JSX host signatures', () => { const semantics = readMobileTasksSemanticSource() - expect(semantics.split('\n')).toHaveLength(3_496) - expect(hash(semantics)).toBe(SETTINGS_RPC_SEMANTICS) + expect(semantics.split('\n')).toHaveLength(3_452) + expect(hash(semantics)).toBe(WORKSPACE_RPC_SEMANTICS) }) it('preserves render expressions and event handlers in tree order', () => { diff --git a/mobile/src/tasks/mobile-workspace-create-operations.ts b/mobile/src/tasks/mobile-workspace-create-operations.ts new file mode 100644 index 00000000000..55cad2f373f --- /dev/null +++ b/mobile/src/tasks/mobile-workspace-create-operations.ts @@ -0,0 +1,64 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +// Creating a workspace from a task. Every reply here is one the call site only re-typed, so the +// readers are unchecked: moving a shape check in would be a validation change, not a migration. + +/** + * worktree.create. A lost reply is *unknown*, never failed — `worktree-create-retry.ts` replays on + * the same clientMutationId — so this operation never interprets a transport rejection: `request` + * hands back the transport promise itself and the delivery-unknown mark reaches the retry loop on + * the original rejection object. + */ +export const worktreeCreateRun = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.create', + method: 'worktree.create', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('created-worktree') + }) +) + +/** + * The start point for a workspace created from a linked pull request. Refusal throws the host's + * message; an accepted reply can still carry a soft `{ error }` the caller raises itself. + */ +export const worktreePrBaseResolve = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.resolve-pr-base', + method: 'worktree.resolvePrBase', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('pr-start-point') + }) +) + +/** The GitLab merge-request equivalent; same acceptance, same soft-error convention. */ +export const worktreeMrBaseResolve = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.resolve-mr-base', + method: 'worktree.resolveMrBase', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('mr-start-point') + }) +) + +/** + * status.get read for create-time capabilities, the second of two policies on this method. + * + * Both policies named because the two callers disagree about what a refused status means: the + * Tasks screen cannot hydrate without it and surfaces the host's message (`taskRuntimeStatusRead`), + * while create-time capability probing degrades to "no capabilities" and creates anyway, so here a + * refusal is a skip. One reader serves both — the payload is unchecked in each. + */ +export const worktreeCreateCapabilityRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'status.create-capabilities-or-skip', + method: 'status.get', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('runtime-status') + }) +) diff --git a/mobile/src/tasks/mobile-workspace-source-operations.ts b/mobile/src/tasks/mobile-workspace-source-operations.ts new file mode 100644 index 00000000000..3126682c9a1 --- /dev/null +++ b/mobile/src/tasks/mobile-workspace-source-operations.ts @@ -0,0 +1,101 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { + rpcUncheckedMemberReader, + rpcUncheckedPayloadReader +} from '../transport/rpc-reader-payload' + +// The repo and SSH reads the workspace-create drawer runs: connection state, agent detection, +// repo-owned setup hooks, sparse presets and base-branch search. + +const sshConnectionStateReader = rpcUncheckedMemberReader('ssh-connection-state', 'state') + +/** Connecting an SSH repo before create. The reply's only read field is `state`. */ +export const sshRepoConnectRun = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ssh.connect-repo', + method: 'ssh.connect', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: sshConnectionStateReader + }) +) + +/** The same field, read by the drawer's state effect and by the pre-create readiness check. */ +export const sshRepoStateRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ssh.repo-state', + method: 'ssh.getState', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: sshConnectionStateReader + }) +) + +// Agent detection is advisory: a refused or failed probe leaves the drawer with an empty set and +// the runtime still validates availability before spawning, so refusal is a skip. +export const remoteAgentDetectionRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'preflight.detect-remote-agents-or-skip', + method: 'preflight.detectRemoteAgents', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('detected-agent-ids') + }) +) + +/** The local host's agents, for a repo with no SSH connection. */ +export const localAgentDetectionRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'preflight.detect-agents-or-skip', + method: 'preflight.detectAgents', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('detected-agent-ids') + }) +) + +/** The repo's orca.yaml hooks, which decide whether create must ask before running setup. */ +export const repoSetupHooksRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.setup-hooks', + method: 'repo.hooks', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('repo-hooks') + }) +) + +export const repoSparsePresetListRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.sparse-preset-list', + method: 'repo.sparsePresets', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('sparse-presets', 'presets') + }) +) + +export const repoSparsePresetSaveRun = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.save-sparse-preset', + method: 'repo.saveSparsePreset', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('saved-sparse-preset', 'preset') + }) +) + +/** + * Base-branch search. The payload is unchecked: both callers — the drawer's picker effect and the + * Smart source picker — spell their own `refDetails ?? refs.map(...)` fallback, and reproducing + * that in the reader would need a type assertion the operation fence rightly bans. + */ +export const repoBaseRefSearchRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.base-ref-search', + method: 'repo.searchRefs', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('base-ref-search') + }) +) diff --git a/mobile/src/tasks/setup-hook-trust.ts b/mobile/src/tasks/setup-hook-trust.ts index 72381393989..e6cb492e617 100644 --- a/mobile/src/tasks/setup-hook-trust.ts +++ b/mobile/src/tasks/setup-hook-trust.ts @@ -1,5 +1,6 @@ import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' import type { RpcClient } from '../transport/rpc-client' +import { taskUiStateWrite } from './mobile-task-runtime-operations' export type SetupHookTrust = { contentHash: string @@ -45,10 +46,9 @@ export async function persistSetupHookTrustApproval(args: { alwaysTrust: boolean }): Promise { const next = trustedOrcaHooksWithSetupApproval(args) - const response = await args.client.sendRequest('ui.set', { trustedOrcaHooks: next }) - if (!response.ok) { - throw new Error(response.error.message) - } + taskUiStateWrite.interpret( + await taskUiStateWrite.request(args.client, { trustedOrcaHooks: next }) + ) return next } diff --git a/mobile/src/tasks/smart-source-paste-intent.ts b/mobile/src/tasks/smart-source-paste-intent.ts index 21afd4157de..87715eab039 100644 --- a/mobile/src/tasks/smart-source-paste-intent.ts +++ b/mobile/src/tasks/smart-source-paste-intent.ts @@ -9,7 +9,13 @@ import { import { parseGitLabIssueOrMRLink } from '../../../src/shared/new-workspace/gitlab-links' import { isSmartWorkspaceSourceQueryWithinLimit } from '../../../src/shared/new-workspace/smart-workspace-source-results' import type { RpcClient } from '../transport/rpc-client' -import type { RpcSuccess } from '../transport/types' +import { isMethodNotFoundRefusal } from '../transport/rpc-acceptance-policies' +import { + githubRepoSlugRead, + githubWorkItemByNumberRead, + githubWorkItemBySlugRead, + gitlabWorkItemByPathRead +} from './mobile-task-source-search-operations' import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key' // A repo the picker can switch to for a cross-repo GitHub paste. Slug is derived @@ -108,14 +114,18 @@ export async function findRepoMatchingSlugForPaste( let resolved = cache.get(repo.id) if (!cache.has(repo.id)) { try { - const response = await client.sendRequest('github.repoSlug', { repo: `id:${repo.id}` }) - if (!response.ok && response.error.code === 'method_not_found') { + const reply = await githubRepoSlugRead.request(client, { repo: `id:${repo.id}` }) + // Why the raw refusal: a missing method retires the probe host-wide, and the acceptance + // policy reports only that the reply was refused, not with which code. + if (isMethodNotFoundRefusal(reply)) { // Why: RPC availability is host-wide; avoid repeating an unsupported // probe for every repo or on the next paste attempt. repos.forEach((candidate) => cache.set(candidate.id, null)) return null } - resolved = response.ok ? ((response as RpcSuccess).result as RepoSlug | null) : null + const slug = githubRepoSlugRead.interpret(reply) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + resolved = slug.accepted ? (slug.value as RepoSlug | null) : null } catch { resolved = null } @@ -133,11 +143,12 @@ export async function lookupGitHubItemByNumber( repoId: string, number: number ): Promise { - const response = await client.sendRequest('github.workItem', { repo: `id:${repoId}`, number }) - if (!response.ok) { - throw new Error(response.error.message) - } - const item = (response as RpcSuccess).result as GitHubWorkItem | null + const reply = await githubWorkItemByNumberRead.request(client, { + repo: `id:${repoId}`, + number + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const item = githubWorkItemByNumberRead.interpret(reply) as GitHubWorkItem | null return item ? { ...item, repoId } : null } @@ -148,7 +159,7 @@ export async function lookupGitHubItemByOwnerRepo( number: number, type: 'issue' | 'pr' ): Promise { - const response = await client.sendRequest('github.workItemByOwnerRepo', { + const reply = await githubWorkItemBySlugRead.request(client, { repo: `id:${repoId}`, owner: slug.owner, ownerRepo: slug.repo, @@ -156,10 +167,8 @@ export async function lookupGitHubItemByOwnerRepo( number, type }) - if (!response.ok) { - throw new Error(response.error.message) - } - const item = (response as RpcSuccess).result as GitHubWorkItem | null + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const item = githubWorkItemBySlugRead.interpret(reply) as GitHubWorkItem | null return item ? { ...item, repoId } : null } @@ -168,16 +177,14 @@ export async function lookupGitLabItemByPath( repoId: string, link: NonNullable> ): Promise { - const response = await client.sendRequest('gitlab.workItemByPath', { + const reply = await gitlabWorkItemByPathRead.request(client, { repo: `id:${repoId}`, host: link.slug.host, path: link.slug.path, iid: link.number, type: link.type }) - if (!response.ok) { - throw new Error(response.error.message) - } - const item = (response as RpcSuccess).result as GitLabWorkItem | null + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const item = gitlabWorkItemByPathRead.interpret(reply) as GitLabWorkItem | null return item ? { ...item, repoId } : null } diff --git a/mobile/src/tasks/smart-source-search-requests.ts b/mobile/src/tasks/smart-source-search-requests.ts index 876e2ef1a20..432e6f1c6bc 100644 --- a/mobile/src/tasks/smart-source-search-requests.ts +++ b/mobile/src/tasks/smart-source-search-requests.ts @@ -3,8 +3,13 @@ import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' import type { LinearIssue } from '../../../src/shared/linear/issue-types' import type { BaseRefSearchResult } from '../../../src/shared/repo-types' import type { RpcClient } from '../transport/rpc-client' -import type { RpcSuccess } from '../transport/types' -import { extractLinearIssueReadItems } from './linear-mobile-issue-read' +import { repoBaseRefSearchRead } from './mobile-workspace-source-operations' +import { + githubWorkItemSearchRead, + gitlabWorkItemSearchRead, + linearAssignedIssueListRead, + linearIssueSearchRead +} from './mobile-task-source-search-operations' import { PER_REPO_FETCH_LIMIT } from './mobile-work-items' import type { MrStateFilter } from './mobile-composer-source-types' @@ -26,15 +31,13 @@ export async function searchGitHubItems( repoId: string, query: string ): Promise { - const response = await client.sendRequest('github.listWorkItems', { + const reply = await githubWorkItemSearchRead.request(client, { repo: `id:${repoId}`, limit: PER_REPO_FETCH_LIMIT, query: scopeGitHubQuery(query) }) - if (!response.ok) { - throw new Error(response.error.message) - } - const envelope = (response as RpcSuccess).result as { items: GitHubWorkItem[] } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const envelope = githubWorkItemSearchRead.interpret(reply) as { items: GitHubWorkItem[] } // Stamp repoId so the shared row builder + create flow can attribute each item // to the searched repo (the runtime omits it, like the desktop fetcher). return (envelope.items ?? []).map((item) => ({ ...item, repoId })) @@ -46,17 +49,15 @@ export async function searchGitLabItems( query: string, state: MrStateFilter ): Promise { - const response = await client.sendRequest('gitlab.listWorkItems', { + const reply = await gitlabWorkItemSearchRead.request(client, { repo: `id:${repoId}`, state, page: 1, perPage: GITLAB_PER_PAGE, query: query.trim() || undefined }) - if (!response.ok) { - throw new Error(response.error.message) - } - const envelope = (response as RpcSuccess).result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const envelope = gitlabWorkItemSearchRead.interpret(reply) as { items: GitLabWorkItem[] error?: { type?: string; message: string } } @@ -72,25 +73,27 @@ export async function searchLinearIssues( linearWorkspaceId: string | null | undefined ): Promise { const trimmed = query.trim() - const response = trimmed - ? await client.sendRequest('linear.searchIssues', { - query: trimmed, - limit: LINEAR_LIMIT, - workspaceId: linearWorkspaceId ?? undefined - }) - : await client.sendRequest('linear.listIssues', { - // Empty query lists the viewer's assigned issues, matching desktop's - // Smart picker default (SmartWorkspaceNameField uses listLinearIssues('assigned')). - filter: 'assigned', - limit: LINEAR_LIMIT, - workspaceId: linearWorkspaceId ?? undefined - }) - if (!response.ok) { - throw new Error(response.error.message) - } - // extractLinearIssueReadItems yields the mobile issue-read shape; the fields the - // row builder/create flow read (id/identifier/title/url/state/team) are a subset. - return extractLinearIssueReadItems((response as RpcSuccess).result) as unknown as LinearIssue[] + // The reader yields the mobile issue-read shape; the fields the row builder/create flow read + // (id/identifier/title/url/state/team) are a subset. + const issues = trimmed + ? linearIssueSearchRead.interpret( + await linearIssueSearchRead.request(client, { + query: trimmed, + limit: LINEAR_LIMIT, + workspaceId: linearWorkspaceId ?? undefined + }) + ) + : linearAssignedIssueListRead.interpret( + await linearAssignedIssueListRead.request(client, { + // Empty query lists the viewer's assigned issues, matching desktop's + // Smart picker default (SmartWorkspaceNameField uses listLinearIssues('assigned')). + filter: 'assigned', + limit: LINEAR_LIMIT, + workspaceId: linearWorkspaceId ?? undefined + }) + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + return issues as LinearIssue[] } export async function searchBranches( @@ -98,15 +101,13 @@ export async function searchBranches( repoId: string, query: string ): Promise { - const response = await client.sendRequest( - 'repo.searchRefs', + const reply = await repoBaseRefSearchRead.request( + client, { repo: `id:${repoId}`, query: query.trim(), limit: BRANCH_LIMIT }, { timeoutMs: 30_000 } ) - if (!response.ok) { - throw new Error(response.error.message) - } - const result = (response as RpcSuccess).result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = repoBaseRefSearchRead.interpret(reply) as { refDetails?: BaseRefSearchResult[] refs?: string[] } diff --git a/mobile/src/tasks/source-workspace-create.ts b/mobile/src/tasks/source-workspace-create.ts index 6e666005ac3..30dd16f89fb 100644 --- a/mobile/src/tasks/source-workspace-create.ts +++ b/mobile/src/tasks/source-workspace-create.ts @@ -9,6 +9,7 @@ import type { WorkspaceAgentChoice } from './workspace-agent-selection' import { agentLaunchCreateFields, buildTaskWorkspaceCreateParams, + type WorkspaceCreateParams, type WorkspaceCreateSetupDecision, type WorkspaceCreateTaskItem } from './workspace-create-params' @@ -167,7 +168,7 @@ async function createBranchWorkspace(args: { const createdWithAgentId = agent.choice === 'blank' ? undefined : agent.choice const comment = note?.trim() const manualDisplayName = nameIsAutoManaged === true ? undefined : workspaceName?.trim() - const applyCommon = (params: Record): Record => { + const applyCommon = (params: WorkspaceCreateParams): WorkspaceCreateParams => { Object.assign(params, agentLaunchCreateFields(createdWithAgentId)) if (comment) { params.comment = comment @@ -213,7 +214,7 @@ async function createBranchWorkspace(args: { baseName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, buildParams: (candidate) => { - const params: Record = { + const params: WorkspaceCreateParams = { repo: `id:${targetRepoId}`, name: candidate, setupDecision, @@ -263,7 +264,7 @@ async function createNewBranchWorkspace(args: { baseName: selection.branchName, worktreeCreateIdempotency: args.worktreeCreateIdempotency, buildParams: (candidate) => { - const params: Record = { + const params: WorkspaceCreateParams = { repo: `id:${targetRepoId}`, name: candidate, setupDecision, diff --git a/mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx b/mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx index fe048e7a5b7..ade7b46cb8f 100644 --- a/mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx +++ b/mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx @@ -7,12 +7,8 @@ import { useLayoutEffect, useState } from './mobile-tasks-dependencies' -import { - type GitHubPreset, - type RepoSummary, - type TaskResumeState, - isSuccess -} from './mobile-tasks-legacy-foundation' +import type { GitHubPreset, RepoSummary, TaskResumeState } from './mobile-tasks-legacy-foundation' +import { taskSettingsWrite, taskUiStateWrite } from './mobile-task-runtime-operations' export function useMobileTasksClientSettingsActions(model: ProjectRepositoryResolutionModel) { const { @@ -106,7 +102,7 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso } const next = { ...taskResumeRef.current, ...updates } taskResumeRef.current = next - void client.sendRequest('ui.set', { taskResumeState: next }).catch(() => { + void taskUiStateWrite.request(client, { taskResumeState: next }).catch(() => { // Best-effort: desktop treats task resume as a convenience preference. }) }, @@ -143,7 +139,7 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso if (!client || !taskUiReady) { return } - void client.sendRequest('settings.update', { defaultTaskSource: nextProvider }).catch(() => { + void taskSettingsWrite.request(client, { defaultTaskSource: nextProvider }).catch(() => { // Best-effort: a failed settings write should not block switching views. }) }, @@ -158,11 +154,9 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso const nextSelection = selection.size === 0 || selection.size === allRepos.length ? null : [...selection] defaultRepoSelectionRef.current = nextSelection - void client - .sendRequest('settings.update', { defaultRepoSelection: nextSelection }) - .catch(() => { - // Best-effort: the in-memory repo picker already reflects the change. - }) + void taskSettingsWrite.request(client, { defaultRepoSelection: nextSelection }).catch(() => { + // Best-effort: the in-memory repo picker already reflects the change. + }) }, [client, taskUiReady] ) @@ -173,7 +167,7 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso if (!client || !taskUiReady) { return } - void client.sendRequest('settings.update', { defaultTaskViewPreset: preset }).catch(() => { + void taskSettingsWrite.request(client, { defaultTaskViewPreset: preset }).catch(() => { // Best-effort: the current session still uses the selected preset. }) }, @@ -186,7 +180,7 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso if (!client || !taskUiReady) { return } - void client.sendRequest('settings.update', { githubProjects: nextSettings }).catch(() => { + void taskSettingsWrite.request(client, { githubProjects: nextSettings }).catch(() => { // Best-effort: project selection can still work for the current session. }) }, @@ -204,10 +198,7 @@ export function useMobileTasksClientSettingsActions(model: ProjectRepositoryReso contentHash, alwaysTrust }) - const response = await client.sendRequest('ui.set', { trustedOrcaHooks: next }) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } + taskUiStateWrite.interpret(await taskUiStateWrite.request(client, { trustedOrcaHooks: next })) setTrustedOrcaHooks(next) }, [client, trustedOrcaHooks] diff --git a/mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx b/mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx index 1ededc361bc..2372ea272c9 100644 --- a/mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx +++ b/mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx @@ -1,5 +1,11 @@ import { settingsRead } from '../transport/settings-read-operations' import type { ClientSettingsActionsModel } from './use-mobile-tasks-client-settings-actions' +import { + taskLinearStatusRead, + taskPreflightRead, + taskRuntimeStatusRead, + taskUiStateRead +} from './mobile-task-runtime-operations' import { MOBILE_TASKS_CAPABILITY, type PersistedTrustedOrcaHooks, @@ -18,7 +24,6 @@ import { type TaskRuntimeStatus, getTaskPresetQuery, githubKindFromQuery, - isSuccess, isTaskProvider, normalizeGitHubPreset, normalizeLinearFilter, @@ -192,14 +197,14 @@ export function useMobileTasksRuntimeHydration(model: ClientSettingsActionsModel resetWorkspaceCreateState() const hydrateTaskState = async (): Promise => { - const statusResponse = await client.sendRequest('status.get') + const statusReply = await taskRuntimeStatusRead.request(client) if (stale) { return } - if (!isSuccess(statusResponse)) { - throw new Error(statusResponse.error.message) - } - const status = statusResponse.result as TaskRuntimeStatus + // The guard stays between the request and the interpretation: a screen that has moved on + // must not raise a refusal it no longer owns. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const status = taskRuntimeStatusRead.interpret(statusReply) as TaskRuntimeStatus if (!status.capabilities?.includes(MOBILE_TASKS_CAPABILITY)) { // Why: Tasks is additive RPC surface, so old desktop builds can still // pair but must not receive the newer task-specific method calls. @@ -249,13 +254,15 @@ export function useMobileTasksRuntimeHydration(model: ClientSettingsActionsModel } setTasksSupportState({ kind: 'supported', client }) setError('') - const [settingsResponse, uiResponse, preflightResponse, linearStatusResponse] = - await Promise.all([ - settingsRead.request(client), - client.sendRequest('ui.get'), - client.sendRequest('preflight.check'), - client.sendRequest('linear.status') - ]) + // Why raw requests in the group and not startRpcOperation: main's Promise.all rejects as soon + // as one leg rejects, and interpreting at an all-settled barrier would instead wait for the + // slowest peer and let a later policy surface a different error. + const [settingsResponse, uiReply, preflightReply, linearStatusReply] = await Promise.all([ + settingsRead.request(client), + taskUiStateRead.request(client), + taskPreflightRead.request(client), + taskLinearStatusRead.request(client) + ]) if (stale) { return } @@ -266,26 +273,30 @@ export function useMobileTasksRuntimeHydration(model: ClientSettingsActionsModel ((settingsResult.value ?? {}) as RuntimeTaskSettings) : {} setRuntimeTaskSettings(settings) - const uiState = isSuccess(uiResponse) - ? ( - uiResponse.result as { - ui?: { + const uiRead = taskUiStateRead.interpret(uiReply) + const uiState = uiRead.accepted + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (uiRead.value as + | { taskResumeState?: TaskResumeState trustedOrcaHooks?: PersistedTrustedOrcaHooks } - } - ).ui + | undefined) : null setTrustedOrcaHooks(uiState?.trustedOrcaHooks ?? {}) const resume = uiState?.taskResumeState ?? {} taskResumeRef.current = resume setGithubProjectHiddenFieldIdsByView(resume.githubProjectHiddenFieldIdsByView ?? {}) - const preflight = isSuccess(preflightResponse) - ? (preflightResponse.result as { glab?: { installed?: boolean } }) + const preflightRead = taskPreflightRead.interpret(preflightReply) + const preflight = preflightRead.accepted + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (preflightRead.value as { glab?: { installed?: boolean } }) : null - const linearStatus = isSuccess(linearStatusResponse) - ? (linearStatusResponse.result as LinearStatusResponse) + const linearRead = taskLinearStatusRead.interpret(linearStatusReply) + const linearStatus = linearRead.accepted + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (linearRead.value as LinearStatusResponse) : null const preferredProviders = normalizeVisibleTaskProviders(settings.visibleTaskProviders) const linearIsConnected = linearStatus?.connected === true diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx index 5befaddaebd..0cfaa2e3d42 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx @@ -11,13 +11,18 @@ import { useCallback, wasSetupHookPreviouslyApproved } from './mobile-tasks-dependencies' -import { - type ActionableTaskItem, - type GitPushTarget, - type RuntimeTaskSettings, - type SetupDecision, - isSuccess +import type { + ActionableTaskItem, + GitPushTarget, + RuntimeTaskSettings, + SetupDecision } from './mobile-tasks-legacy-foundation' +import type { WorkspaceCreateParams } from './workspace-create-params' +import { + worktreeCreateRun, + worktreeMrBaseResolve, + worktreePrBaseResolve +} from './mobile-workspace-create-operations' export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateModel) { const { @@ -154,7 +159,7 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod const trimmedWorkspaceName = workspaceNameOverride?.trim() ?? '' const nameIsAutoManaged = !trimmedWorkspaceName || trimmedWorkspaceName === workspaceLastAutoName - let params: Record + let params: WorkspaceCreateParams if (item.provider === 'github') { const source = item.source let prStartPoint: { baseBranch: string; pushTarget?: GitPushTarget } | undefined @@ -164,8 +169,8 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod baseBranchOverride }) ) { - const response = await client.sendRequest( - 'worktree.resolvePrBase', + const reply = await worktreePrBaseResolve.request( + client, { repo: `id:${source.repoId}`, prNumber: source.number, @@ -176,10 +181,8 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod }, { timeoutMs: 30_000 } ) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const result = response.result as + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreePrBaseResolve.interpret(reply) as | { baseBranch: string; pushTarget?: GitPushTarget } | { error: string } if ('error' in result) { @@ -209,8 +212,8 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod baseBranchOverride }) ) { - const response = await client.sendRequest( - 'worktree.resolveMrBase', + const reply = await worktreeMrBaseResolve.request( + client, { repo: `id:${source.repoId}`, mrIid: source.number, @@ -221,10 +224,8 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod }, { timeoutMs: 30_000 } ) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const result = response.result as + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreeMrBaseResolve.interpret(reply) as | { baseBranch: string; pushTarget?: GitPushTarget } | { error: string } if ('error' in result) { @@ -259,13 +260,11 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod nameIsAutoManaged }) } - const response = await client.sendRequest('worktree.create', params, { + const createReply = await worktreeCreateRun.request(client, params, { timeoutMs: WORKTREE_CREATE_TIMEOUT_MS }) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const result = response.result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreeCreateRun.interpret(createReply) as { worktree: { id: string; displayName?: string } warning?: string } diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx index 93d0d45de09..ea780216170 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx @@ -1,6 +1,9 @@ import type { WorkspaceCreateProjectionModel } from './use-mobile-tasks-workspace-create-projection' import { type BaseRefSearchResult, type SparsePreset, useEffect } from './mobile-tasks-dependencies' -import { isSuccess } from './mobile-tasks-legacy-foundation' +import { + repoBaseRefSearchRead, + repoSparsePresetListRead +} from './mobile-workspace-source-operations' export function useMobileTasksWorkspaceSourceEffects(model: WorkspaceCreateProjectionModel) { const { @@ -45,16 +48,15 @@ export function useMobileTasksWorkspaceSourceEffects(model: WorkspaceCreateProje setWorkspaceSparsePresetsLoading(true) setWorkspaceSparsePresetsLoaded(false) setWorkspaceSparsePresetsError('') - void client - .sendRequest('repo.sparsePresets', { repo: `id:${workspaceCreateTargetRepo.id}` }) - .then((response) => { + void repoSparsePresetListRead + .request(client, { repo: `id:${workspaceCreateTargetRepo.id}` }) + .then((reply) => { if (stale) { return } - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const presets = (response.result as { presets?: SparsePreset[] }).presets ?? [] + const presets = + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (repoSparsePresetListRead.interpret(reply) as SparsePreset[] | undefined) ?? [] setWorkspaceSparsePresets(presets) setWorkspaceSparsePresetsLoaded(true) setWorkspaceSparsePresetId((current) => @@ -112,20 +114,18 @@ export function useMobileTasksWorkspaceSourceEffects(model: WorkspaceCreateProje let stale = false setWorkspaceBaseBranchLoading(true) setWorkspaceBaseBranchError('') - void client - .sendRequest( - 'repo.searchRefs', + void repoBaseRefSearchRead + .request( + client, { repo: `id:${workspaceCreateTargetRepo.id}`, query, limit: 20 }, { timeoutMs: 30_000 } ) - .then((response) => { + .then((reply) => { if (stale) { return } - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const result = response.result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = repoBaseRefSearchRead.interpret(reply) as { refDetails?: BaseRefSearchResult[] refs?: string[] } diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx index d2b9551032f..ef2ecabd4fe 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx @@ -5,7 +5,8 @@ import { useCallback, useEffect } from './mobile-tasks-dependencies' -import { isSuccess, sortSparsePresetsByName } from './mobile-tasks-legacy-foundation' +import { sortSparsePresetsByName } from './mobile-tasks-legacy-foundation' +import { repoSparsePresetSaveRun, sshRepoStateRead } from './mobile-workspace-source-operations' export function useMobileTasksWorkspaceSparseActions(model: WorkspaceSourceEffectsModel) { const { @@ -82,16 +83,14 @@ export function useMobileTasksWorkspaceSparseActions(model: WorkspaceSourceEffec setWorkspaceSparseSaving(true) setWorkspaceSparsePresetsError('') try { - const response = await client.sendRequest('repo.saveSparsePreset', { + const reply = await repoSparsePresetSaveRun.request(client, { repo: `id:${workspaceCreateTargetRepo.id}`, ...(workspaceSparseDraft.presetId ? { id: workspaceSparseDraft.presetId } : {}), name: workspaceSparseDraftName, directories: workspaceSparseDraftParsed.directories }) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const saved = (response.result as { preset?: SparsePreset }).preset + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const saved = repoSparsePresetSaveRun.interpret(reply) as SparsePreset | undefined if (!saved) { throw new Error('Failed to save sparse preset.') } @@ -130,16 +129,15 @@ export function useMobileTasksWorkspaceSparseActions(model: WorkspaceSourceEffec } let stale = false - void client - .sendRequest('ssh.getState', { targetId: workspaceCreateTargetConnectionId }) - .then((response) => { + void sshRepoStateRead + .request(client, { targetId: workspaceCreateTargetConnectionId }) + .then((reply) => { if (stale) { return } - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const state = (response.result as { state?: SshConnectionState | null }).state ?? null + const state = + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (sshRepoStateRead.interpret(reply) as SshConnectionState | null | undefined) ?? null setWorkspaceSshState( state ?? { targetId: workspaceCreateTargetConnectionId, diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx index d1a408aed22..5e0966ca9ba 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx @@ -8,12 +8,18 @@ import { useEffect, useMemo } from './mobile-tasks-dependencies' -import { - type RepoHooksResponse, - type RepoSummary, - type SetupDecision, - isSuccess +import type { + RepoHooksResponse, + RepoSummary, + SetupDecision } from './mobile-tasks-legacy-foundation' +import { + localAgentDetectionRead, + remoteAgentDetectionRead, + repoSetupHooksRead, + sshRepoConnectRun, + sshRepoStateRead +} from './mobile-workspace-source-operations' export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsModel) { const { @@ -47,15 +53,13 @@ export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsMod reconnectAttempt: 0 }) try { - const response = await client.sendRequest( - 'ssh.connect', + const reply = await sshRepoConnectRun.request( + client, { targetId: workspaceCreateTargetConnectionId }, { timeoutMs: 120_000 } ) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const state = (response.result as { state?: SshConnectionState | null }).state + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const state = sshRepoConnectRun.interpret(reply) as SshConnectionState | null | undefined setWorkspaceSshState( state ?? { targetId: workspaceCreateTargetConnectionId, @@ -87,11 +91,10 @@ export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsMod ) { return } - const response = await client.sendRequest('ssh.getState', { targetId: repo.connectionId }) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const state = (response.result as { state?: SshConnectionState | null }).state ?? null + const reply = await sshRepoStateRead.request(client, { targetId: repo.connectionId }) + const state = + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (sshRepoStateRead.interpret(reply) as SshConnectionState | null | undefined) ?? null if (state) { setWorkspaceSshState(state) } @@ -115,18 +118,23 @@ export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsMod } let stale = false setWorkspaceDetectedAgentIds(null) - const request = workspaceCreateTargetRepo.connectionId - ? client.sendRequest('preflight.detectRemoteAgents', { - connectionId: workspaceCreateTargetRepo.connectionId - }) - : client.sendRequest('preflight.detectAgents') - void request - .then((response) => { + const detection = workspaceCreateTargetRepo.connectionId + ? { + operation: remoteAgentDetectionRead, + reply: remoteAgentDetectionRead.request(client, { + connectionId: workspaceCreateTargetRepo.connectionId + }) + } + : { operation: localAgentDetectionRead, reply: localAgentDetectionRead.request(client) } + void detection.reply + .then((reply) => { if (stale) { return } + const detected = detection.operation.interpret(reply) setWorkspaceDetectedAgentIds( - isSuccess(response) ? new Set(response.result as string[]) : new Set() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + detected.accepted ? new Set(detected.value as string[]) : new Set() ) }) .catch(() => { @@ -190,11 +198,9 @@ export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsMod if (!client || !tasksSupported) { return { kind: 'decision', decision: override ?? 'inherit' } } - const response = await client.sendRequest('repo.hooks', { repo: `id:${repo.id}` }) - if (!isSuccess(response)) { - throw new Error(response.error.message) - } - const result = response.result as RepoHooksResponse + const reply = await repoSetupHooksRead.request(client, { repo: `id:${repo.id}` }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = repoSetupHooksRead.interpret(reply) as RepoHooksResponse const setupCommand = result.hooks?.scripts?.setup?.trim() const setupTrust = normalizeSetupHookTrust(result.setupTrust) ?? undefined if (!setupCommand) { diff --git a/mobile/src/tasks/workspace-create-params.ts b/mobile/src/tasks/workspace-create-params.ts index 218c4fe37b0..5f5adbecf5f 100644 --- a/mobile/src/tasks/workspace-create-params.ts +++ b/mobile/src/tasks/workspace-create-params.ts @@ -4,6 +4,7 @@ import type { SetupDecision } from '../../../src/shared/worktree/create-types' import type { GitPushTarget } from '../../../src/shared/worktree/types' +import type { RpcSendParams } from '../transport/rpc-params-contract' import { getWorkspaceSourceName } from '../../../src/shared/new-workspace/workspace-source' import { resolveMobileWorkspaceCreateName } from './mobile-workspace-name' import type { WorkspaceAgentChoice } from './workspace-agent-selection' @@ -55,7 +56,8 @@ export type WorkspaceCreateTaskItem = | WorkspaceCreateGitLabItem | WorkspaceCreateLinearItem -export type WorkspaceCreateParams = Record +/** The outgoing worktree.create params, so the builder and the operation agree by type. */ +export type WorkspaceCreateParams = RpcSendParams<'worktree.create'> /** * `worktree.create` fields for launching the picked agent in a fresh session. diff --git a/mobile/src/tasks/worktree-create-capability.ts b/mobile/src/tasks/worktree-create-capability.ts index c4ca8170312..63a67dda2e8 100644 --- a/mobile/src/tasks/worktree-create-capability.ts +++ b/mobile/src/tasks/worktree-create-capability.ts @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' import type { RpcClient } from '../transport/rpc-client' import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client' -import type { RpcSuccess } from '../transport/types' import { readMobileRuntimeHostPlatform } from '../transport/mobile-runtime-host-platform' +import { worktreeCreateCapabilityRead } from './mobile-workspace-create-operations' import { MOBILE_TASKS_CAPABILITY } from './mobile-tasks-capability' import { WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS, @@ -36,11 +36,14 @@ export async function readNewWorktreeRuntimeCapabilities( ): Promise { for (let migrationRetry = 0; ; migrationRetry += 1) { try { - const response = await client.sendRequest('status.get') - if (!response.ok) { + const status = worktreeCreateCapabilityRead.interpret( + await worktreeCreateCapabilityRead.request(client) + ) + if (!status.accepted) { return UNSUPPORTED_CAPABILITIES } - const result = (response as RpcSuccess).result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = status.value as { capabilities?: string[] worktreeCreateIdempotency?: unknown } diff --git a/mobile/src/tasks/worktree-create-retry.test.ts b/mobile/src/tasks/worktree-create-retry.test.ts index 1611ed74c19..b61ab20c30e 100644 --- a/mobile/src/tasks/worktree-create-retry.test.ts +++ b/mobile/src/tasks/worktree-create-retry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { RpcClient } from '../transport/rpc-client' -import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' +import { isRpcDeliveryUnknown, markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import type { ConnectionState } from '../transport/types' import { @@ -789,6 +789,49 @@ describe('createWorktreeWithNameRetry', () => { expect(attempts).toHaveLength(1) }) + // The delivery-unknown mark is a WeakSet keyed on the rejection object, so the create must reach + // the caller as the very object the transport rejected with. `worktreeCreateRun.request` returns + // the transport promise itself for exactly this reason; an operation that wrapped, re-threw or + // re-created the error would turn "the host may have built it" into "it failed". + it('rethrows the transport rejection object itself, mark and all', async () => { + const attempts: Attempt[] = [] + const connection = connectionController() + const marked = markRpcDeliveryUnknown(new Error('Connection lost')) + const client = scriptedClient([{ throws: marked }], attempts, connection) + // Idempotency off, so the resilient sender rethrows on the first ambiguity instead of replaying + // and the object under test is the one the transport produced, not a later attempt's. + const caught = await createWorktreeWithNameRetry({ + client, + baseName: 'kestrel', + buildParams: (name) => ({ repo: 'id:r', name }), + worktreeCreateIdempotency: false + }).then( + () => null, + (error: unknown) => error + ) + expect(caught).toBe(marked) + expect(isRpcDeliveryUnknown(caught)).toBe(true) + }) + + // The other direction: a definite failure must not acquire a mark on the way out, or a create the + // host never received would be replayed as a reconciliation and build a second worktree. + it('does not mark a rejection the transport left unmarked', async () => { + const attempts: Attempt[] = [] + const unmarked = new Error('Socket closed before send') + const client = scriptedClient([{ throws: unmarked }], attempts, connectionController()) + const caught = await createWorktreeWithNameRetry({ + client, + baseName: 'kestrel', + buildParams: (name) => ({ repo: 'id:r', name }), + worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT + }).then( + () => null, + (error: unknown) => error + ) + expect(caught).toBe(unmarked) + expect(isRpcDeliveryUnknown(caught)).toBe(false) + }) + it('keeps the replay window strictly inside the host dedupe TTL', () => { // The window is measured from a lower bound on when the host could have resolved, so // it has to leave the record room for the replay to still be in flight. Widening it diff --git a/mobile/src/tasks/worktree-create-retry.ts b/mobile/src/tasks/worktree-create-retry.ts index b0f8f618773..fae847367d0 100644 --- a/mobile/src/tasks/worktree-create-retry.ts +++ b/mobile/src/tasks/worktree-create-retry.ts @@ -1,6 +1,7 @@ import type { RpcClient } from '../transport/rpc-client' -import type { RpcResponse, RpcSuccess } from '../transport/types' +import type { RpcResponse } from '../transport/types' import { isRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' +import { worktreeCreateRun } from './mobile-workspace-create-operations' import { waitForRpcClientReconnected } from '../transport/rpc-client-reconnect-wait' import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import { @@ -10,6 +11,7 @@ import { isRetryableWorktreeCreateConflict } from '../../../src/shared/new-workspace/worktree-create-retry-policy' import { WORKTREE_CREATE_TIMEOUT_MS } from './workspace-create-timeout' +import type { WorkspaceCreateParams } from './workspace-create-params' import { getWorktreeCreateReplayWindowMs, type WorktreeCreateIdempotencyProbe, @@ -49,7 +51,7 @@ export type CreateWorktreeWithNameRetryArgs = { client: RpcClient baseName: string nameWasGenerated?: boolean - buildParams: (name: string) => Record + buildParams: (name: string) => WorkspaceCreateParams worktreeCreateIdempotency: WorktreeCreateIdempotencyProbe maxAttempts?: number // Injected in tests; production mints a fresh idempotency key per candidate. @@ -83,8 +85,11 @@ export async function createWorktreeWithNameRetry( ? { ...candidateParams, clientMutationId: mintMutationId() } : candidateParams const response = await sendWorktreeCreateResilient(client, params, worktreeCreateIdempotency) + // Why the raw refusal: the retry decision below is `isRetryableWorktreeCreateConflict` over the + // host's message, and no acceptance policy carries a refusal message through without throwing. if (response.ok) { - const result = (response as RpcSuccess).result as { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = worktreeCreateRun.interpret(response) as { worktree: { id: string; displayName?: string } warning?: string } @@ -116,7 +121,7 @@ export async function createWorktreeWithNameRetry( // is returned to the caller untouched. async function sendWorktreeCreateResilient( client: RpcClient, - params: Record, + params: WorkspaceCreateParams, worktreeCreateIdempotency: WorktreeCreateIdempotencySupport | false ): Promise { let migrationRetry = 0 @@ -125,7 +130,9 @@ async function sendWorktreeCreateResilient( let replayDeadlineAt: number | null = null for (;;) { try { - return await client.sendRequest('worktree.create', params, { + // `request` is the transport promise itself, so a delivery-unknown rejection reaches the + // catch below as the object the transport marked — the WeakSet cannot see through a wrapper. + return await worktreeCreateRun.request(client, params, { timeoutMs: WORKTREE_CREATE_TIMEOUT_MS }) } catch (error) { diff --git a/mobile/src/test-support/rpc-recording/README.md b/mobile/src/test-support/rpc-recording/README.md index 7dd7d87b602..b6d5f1379ba 100644 --- a/mobile/src/test-support/rpc-recording/README.md +++ b/mobile/src/test-support/rpc-recording/README.md @@ -1,8 +1,10 @@ # Main RPC recordings Test infrastructure only. `pilot-scenarios.json` binds logical operations/actions to small -mount adapters. The adapters execute the actual product modules from the selected source -root, using React's test renderer; they do not reconstruct acceptance or lifecycle logic. +mount adapters, which live one module per domain under `adapters/` and are registered in +`adapters/mounted-operation-modules.ts`. The adapters execute the actual product modules from the +selected source root, using React's test renderer; they do not reconstruct acceptance or lifecycle +logic. The module loader transpiles the real source with TypeScript and resolves task barrels lazily so unused native views do not need a device. Accessing an unspecified native import fails. The history metadata function is exposed to its adapter without rewriting its body. @@ -58,14 +60,55 @@ visible, it does not make the reduction itself observable. ## Golden schema Each file records `runnerVersion`, `baseline`, `lockfileSha256` (mobile's lockfile), -`recorderSha256`, `scenarioSha256`, `platform`, `scenarioVersion`, `projectionVersion`, -`goldenFormatVersion`, `operation`, `family`, and `namedDeltas`. `platform` and `lockfileSha256` -are provenance and are not compared: a dependency or OS that changes behaviour changes the trace -itself, so comparing them would only fail candidates on unrelated bumps. The rest are pinned. +`recorderSha256`, `adapterSha256`, `scenarioSha256`, `platform`, `scenarioVersion`, +`projectionVersion`, `goldenFormatVersion`, `operation`, `family`, and `namedDeltas`. `platform` +and `lockfileSha256` are provenance and are not compared: a dependency or OS that changes +behaviour changes the trace itself, so comparing them would only fail candidates on unrelated +bumps. The rest are pinned. -`recorderSha256` covers every non-markdown file under this directory, so the runner that produced a -golden is as pinned as the product baseline: editing an adapter projection or a fixture fails -candidate mode on the header and forces a deliberate re-record of everything. +`recorderSha256` covers every non-markdown file under this directory **except `adapters/`**, so the +engine that produced a golden is as pinned as the product baseline: editing the runner, the +transport, the projection or a fixture fails candidate mode on the header of every golden and +forces a deliberate re-record of all of them. + +`adapterSha256` covers the source of the mount adapter module _that golden_ was recorded through — +the file under `adapters/` that mounts each operation its scenarios drive, read off the same +`mounts` calls that build the table the recording runs against, so the pin cannot name a file the +runner did not use. Adding a domain's module re-digests nothing that was already recorded, and +editing one fails exactly the goldens mounted through it. The adapters used to sit in +`recorderSha256` with the engine, which made every golden's header a function of every other +family's adapter: #20568 added two task modules and put a conflict on that one line in 153 files, +against every domain branch in flight. + +`mutants/` is excluded for a different reason: nothing there is pinned by anything. A file that +cannot change a recording is not provenance for one, and pinning it would claim a provenance the +golden does not have — while charging every domain that adds a mutant a re-record of all 153 +files. +The mutant table, the per-family mutant registry, the reference states and the suites that apply +them all live there. What makes the exclusion sound is that no recording can reach them: the loader +takes a resolved mutation spec instead of importing a table by name. +`mutants/mutant-seam.test.ts` is the check, and it proves reachability forward, walking the static +import graph from the two recording drivers and failing if any module under `mutants/` appears in +it. Naming the directory is rejected too, in either spelling, for the paths a module can be read by +rather than imported; `MUTANT_DIRECTORY` is not exported for the same reason. A path assembled at +runtime from fragments would defeat both, which is the seam's remaining edge. + +For the same reason `recorderSha256` pins only the suites in `recording-drivers.ts`. A golden's +bytes come from `pilot-recordings.test.ts` or `family-recordings.test.ts` and from what they +import; a suite that reads goldens, or writes one to a scratch directory, puts no observation in a +recorded file. `scripts/rpc-recording.mts` records exactly that list, so the two cannot drift apart. + +A module-private product export an adapter drives is exposed by its own module — see +`settingsMountExposures` — not by a shared table, because the exposure text does change what a +recording loads. Each domain module gets its own loader carrying its own exposures, and one +recording mounts one adapter, so `adapterSha256` pins exactly the exposures that reached it. + +The adapter seam is the directory, not a filename convention, because a convention is a rule nobody +enforces. `adapter-seam.test.ts` enforces this one: every file under `adapters/` is a registered +module, every registered module is declared in the file it is registered under, no adapter module +imports a sibling (which would leave a golden pinned to one module and driven by two), and +`pilotMountAdapters` mounts nothing of its own — an adapter defined in an engine file would be +pinned by `recorderSha256` on all 153 goldens instead of by `adapterSha256` on its own. `scenarioSha256` covers the scenario input _that golden_ was recorded from — one manifest scenario for a pilot golden, the generated variants and any hoisted prelude for a matrix or schedule golden, @@ -75,8 +118,8 @@ it. The manifest used to be an input to `recorderSha256` instead, which made eve a function of every other family's scenarios: adding one domain's family re-digested all 153 files and put a conflict on that line in every domain branch in flight. Which goldens a manifest derives lives in `derived-goldens.ts`, so the digest is a function of the same derivation that records the -file rather than of a restatement of it; `golden-header-digest.test.ts` pins the four properties -that separation buys. +file rather than of a restatement of it; `golden-header-digest.test.ts` pins what both separations +buy. Checkpoints contain ordered sender calls and serialized physical application payloads, action and request settlements, projected state, and ordered external effects. Sender args have three @@ -86,10 +129,10 @@ times and errors stay observable. Errors contain category, message and `isRpcDel stack paths, plus `code` and a recursively captured `cause` when the thrown error carries them. Platform is provenance; candidate comparison does not require the same operating system. -Format version 4 adds `scenarioSha256`. A version-3 golden would already fail this reader's byte -compare, so the bump buys the diagnosis rather than the rejection: `readGolden` names the stale -format and says to re-record, instead of reporting an opaque `(encoding)` difference. The bump moved -no observation. +Format version 4 added `scenarioSha256` and version 5 adds `adapterSha256`. A stale golden would +already fail this reader's byte compare, so each bump buys the diagnosis rather than the rejection: +`readGolden` names the stale format and says to re-record, instead of reporting an opaque +`(encoding)` difference. Neither bump moved an observation. ### Value pool @@ -192,20 +235,20 @@ Task-model projections record setter invocations and resulting model values, not ## Commands and checker contract Record only from unchanged pinned product sources and lockfile. The fence exempts only -`mobile/src/test-support/rpc-recording`, which `recorderSha256` pins instead; every other -test-support path is compared against the baseline like product code: +`mobile/src/test-support/rpc-recording`, which `recorderSha256` and `adapterSha256` pin between +them; every other test-support path is compared against the baseline like product code: ```sh ORCA_BACKGROUND_LAUNCH=1 RPC_FOUNDATION_RECORD=1 pnpm --dir mobile exec tsx scripts/rpc-recording.mts --record ORCA_BACKGROUND_LAUNCH=1 pnpm --dir mobile test src/test-support/rpc-recording ``` -Mutants are the defect evidence. `operation-mutations.ts` holds one anchored source edit per -adapter family, and every family's recording must change visible state when its mutant is applied, -which is what shows that family's `state()` projection observes the operation's real output. -Anchors are asserted to match exactly one site, because a repeated anchor would half-apply while -still counting as applied. Mutants replace the expression in memory, then run the same real hook. -`runRecordingMutant` accepts a mutated mounting adapter, scheduler, baseline and optional +Mutants are the defect evidence. `mutants/operation-mutations.ts` holds one anchored source edit +per adapter family, and every family's recording must change visible state when its mutant is +applied, which is what shows that family's `state()` projection observes the operation's real +output. Anchors are asserted to match exactly one site, because a repeated anchor would half-apply +while still counting as applied. Mutants replace the expression in memory, then run the same real +hook. `runRecordingMutant` accepts a mutated mounting adapter, scheduler, baseline and optional observation projection, and returns `{verdict: "killed" | "survived", recording}`. Every mutant test requires the mutation to apply exactly once and change visible state to count as killed. @@ -244,9 +287,10 @@ It is not a substitute for reading the diff. Three facts bound it, all learned t reply kills it on five matrix goldens. The lesson is about the skip, not about that call site: a generator that opts a family out without failing is indistinguishable from coverage. -`probe-hole-witness.test.ts` closes the first two and keeps them closed. It asserts the hole and the closure -together: each probe must kill its mutation _and_ every pre-probe scenario of the same operation -must still survive it. A probe that stops being load-bearing fails instead of lingering. +`mutants/probe-hole-witness.test.ts` closes the first two and keeps them closed. It asserts the +hole and the closure together: each probe must kill its mutation _and_ every pre-probe scenario of +the same operation must still survive it. A probe that stops being load-bearing fails instead of +lingering. What is still not covered: what the count-based raw-port inventory covers instead (which files reach `sendRequest`, and how often), native storage, transport skew, the `subscribe`/ @@ -286,18 +330,26 @@ ORCA_BACKGROUND_LAUNCH=1 RPC_FOUNDATION_RECORD=1 \ A re-record is a claim about behaviour. State the cause in the commit; every golden the refresh moves should have one. -Editing the recorder itself on a migration branch is the awkward case: `recorderSha256` moves, so +Editing the recorder engine on a migration branch is the awkward case: `recorderSha256` moves, so every golden needs rewriting, but the product tree no longer matches `baseline`, and bumping `baseline` to the branch would record the migrated source and make the parity claim circular. Record -from the pinned commit instead, with this branch's recorder laid over it — a detached checkout or a -`git archive` extraction of `baseline`, this tree's `rpc-recording/` and `pilot-scenarios.json` -copied in, `node_modules` symlinked, `RPC_FOUNDATION_GOLDENS` pointed at a scratch directory — then -copy the result back and run the candidate suite here. Format the recorder before recording: an -`oxfmt` pass afterwards moves `recorderSha256` again. +from the pinned commit instead, with this branch's recorder laid over it: `git worktree add +--detach `, this tree's `rpc-recording/` and `pilot-scenarios.json` copied in, +`node_modules` symlinked, `RPC_FOUNDATION_GOLDENS` pointed at a scratch directory — then copy the +result back and run the candidate suite here. It must be a worktree, not a `git archive` +extraction: the fence runs `git diff --quiet ` and an untracked-file check, both of which +need a real `.git`, so an archive tree fails as `Product sources or lockfile differ from the pinned +main baseline` — a product mismatch that is not there. Format the recorder before recording: an +`oxfmt` pass afterwards moves `recorderSha256` again. A recorder-only branch that has merged main +is not the awkward case: its product tree is main's, so repin `baseline` to main's tip and record +in place — there is no migrated source for the goldens to be recorded against. Adding or editing +one domain's module under `adapters/` no longer needs any of this: only that domain's goldens move, +and they re-record from its own branch like any other behaviour change. Adding a mutant, a probe or +a suite that does not record needs none of it either, and moves no golden at all. -If your call site carries a mutation anchor in `operation-mutations.ts`, rewriting it will make the -anchor match zero sites. Re-anchor the same defect at its new home rather than deleting the mutant: -#20499 broke five anchors that way, and each one had a new home. +If your call site carries a mutation anchor in `mutants/operation-mutations.ts`, rewriting it will +make the anchor match zero sites. Re-anchor the same defect at its new home rather than deleting +the mutant: #20499 broke five anchors that way, and each one had a new home. `live-probe/` holds the runtime companion: `mock-desktop-settings-reply-modes.patch` teaches the mock desktop server to answer `settings.get` with a refusal, `method_not_found`, a null or absent diff --git a/mobile/src/test-support/rpc-recording/adapter-digest.ts b/mobile/src/test-support/rpc-recording/adapter-digest.ts new file mode 100644 index 00000000000..81966d4b35c --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapter-digest.ts @@ -0,0 +1,73 @@ +import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' +import { join, posix } from 'node:path' +import { MOUNTED_OPERATION_MODULES } from './adapters/mounted-operation-modules' +import { operationModuleLoader } from './operation-module-loader' +import { ADAPTER_DIRECTORY } from './recorder-digest' +import type { MountedOperationModule } from './mounted-operation-module' +import type { RecordingScenario } from './recording-scenario' + +const digests = new Map() + +/** + * Which module mounts each operation, read off the same `mounts` calls that build the table a + * recording runs against. A restated map could agree with itself while naming the wrong source. + */ +export function adapterSourceByOperation( + root: string, + registered: readonly MountedOperationModule[] = MOUNTED_OPERATION_MODULES +): Map { + const modules = operationModuleLoader(root) + const owners = new Map() + for (const module of registered) { + for (const operation of Object.keys(module.mounts(modules, {}))) { + owners.set(operation, module.source) + } + } + return owners +} + +/** + * The adapter source one golden was recorded through: the module mounting each operation its + * scenarios drive, deduplicated and ordered by file name. + * + * Pinned per golden rather than over the whole `adapters/` directory so a domain that adds its + * module moves nothing that was already recorded, and editing a module fails exactly the goldens + * that mounted it. A golden whose operation no module mounts has no runner to be attributed to, + * so it throws rather than digesting an empty set. + */ +export function adapterSha256( + root: string, + scenarios: readonly RecordingScenario[], + registered: readonly MountedOperationModule[] = MOUNTED_OPERATION_MODULES +): string { + const owners = adapterSourceByOperation(root, registered) + const sources = [ + ...new Set( + scenarios.map((scenario) => { + const source = owners.get(scenario.operation) + if (source === undefined) { + throw new Error(`No adapter module mounts ${scenario.operation}`) + } + return source + }) + ) + ].sort() + const key = `${root}\0${sources.join('\0')}` + const cached = digests.get(key) + if (cached !== undefined) { + return cached + } + const digest = createHash('sha256') + .update( + sources + .map( + (source) => + `${source}:${readFileSync(join(root, ...ADAPTER_DIRECTORY.split(posix.sep), source))}` + ) + .join('\n') + ) + .digest('hex') + digests.set(key, digest) + return digest +} diff --git a/mobile/src/test-support/rpc-recording/adapter-seam.test.ts b/mobile/src/test-support/rpc-recording/adapter-seam.test.ts new file mode 100644 index 00000000000..24219daff47 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapter-seam.test.ts @@ -0,0 +1,185 @@ +import { readFileSync, readdirSync } from 'node:fs' +import { join, resolve, sep } from 'node:path' +import ts from 'typescript' +import { describe, expect, it } from 'vitest' +import { adapterSourceByOperation } from './adapter-digest' +import { MOUNTED_OPERATION_MODULES } from './adapters/mounted-operation-modules' +import { operationModuleLoader } from './operation-module-loader' +import { pilotMountAdapters } from './pilot-mount-adapters' +import { ADAPTER_DIRECTORY, RECORDER_DIRECTORY } from './recorder-digest' +import { readScenarios } from './scenario-input' + +const root = resolve(import.meta.dirname, '../../../..') +const manifest = readScenarios( + process.env.RPC_FOUNDATION_SCENARIOS ?? + resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json') +).scenarios +const engine = join(root, RECORDER_DIRECTORY) +const directory = join(root, ADAPTER_DIRECTORY) +/** The register is the seam's own index, not an adapter: no golden is recorded through it. */ +const REGISTER = 'mounted-operation-modules.ts' +const registerPath = join(directory, REGISTER).replace(/\.ts$/, '') +const sources = MOUNTED_OPERATION_MODULES.map((module) => module.source) + +/** Relative specifiers, resolved against the importing file's directory, extension dropped. */ +function imports(from: string, contents: string): { specifier: string; target: string }[] { + return [...contents.matchAll(/(?:from|import\()\s*'(\.[^']*)'/g)].map((match) => ({ + specifier: match[1]!, + target: resolve(from, match[1]!).replace(/\.tsx?$/, '') + })) +} + +function read(source: string): string { + return readFileSync(join(directory, source), 'utf8') +} + +/** Every named import the register makes, local name to specifier. */ +function registerImports(file: ts.SourceFile): Map { + const bindings = new Map() + for (const statement of file.statements) { + const clause = ts.isImportDeclaration(statement) ? statement.importClause : undefined + if (!clause || clause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier!)) { + continue + } + const named = clause.namedBindings + if (!named || !ts.isNamedImports(named)) { + continue + } + for (const element of named.elements) { + if (!element.isTypeOnly) { + bindings.set(element.name.text, statement.moduleSpecifier.text) + } + } + } + return bindings +} + +/** The registered entries as written, so a literal in the register is visible as a literal. */ +function registerEntries(file: ts.SourceFile): ts.ObjectLiteralExpression[] { + for (const statement of file.statements) { + if (!ts.isVariableStatement(statement)) { + continue + } + for (const declaration of statement.declarationList.declarations) { + const initializer = declaration.initializer + if ( + declaration.name.getText() === 'MOUNTED_OPERATION_MODULES' && + initializer && + ts.isArrayLiteralExpression(initializer) + ) { + return initializer.elements.filter((element) => ts.isObjectLiteralExpression(element)) + } + } + } + throw new Error('MOUNTED_OPERATION_MODULES is not an array literal in the register') +} + +function property(entry: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined { + for (const member of entry.properties) { + if (ts.isPropertyAssignment(member) && member.name.getText() === name) { + return member.initializer + } + } + return undefined +} + +/** + * `recorderSha256` covers the engine and `adapterSha256` covers one module per golden, so a file on + * the wrong side of this directory is pinned by the wrong thing — an engine file here escapes every + * golden, and an adapter outside re-digests all of them. Both fail here on the move instead. + */ +describe('the engine/adapter seam', () => { + it('registers every file in the adapter directory', () => { + const present = readdirSync(directory).filter((file) => file !== REGISTER) + expect(present.sort()).toEqual([...sources].sort()) + }) + + it('pairs each registered module with the file that declares it', () => { + expect(new Set(sources).size).toBe(sources.length) + const unpaired = MOUNTED_OPERATION_MODULES.filter( + ({ source, mounts }) => !read(source).includes(`export function ${mounts.name}(`) + ).map(({ source, mounts }) => `${mounts.name} is not declared in ${source}`) + expect(unpaired).toEqual([]) + }) + + // An adapter reaching sideways would leave a golden pinned to one module and driven by two. + // Resolved, not spelled: `../adapters/other` climbs out and back in, and reads as an escape. + it('leaves the seam for every import an adapter module makes', () => { + const inward = sources.flatMap((source) => + imports(directory, read(source)) + .filter(({ target }) => target.startsWith(`${directory}${sep}`)) + .map(({ specifier }) => `${source} imports ${specifier}`) + ) + expect(inward).toEqual([]) + }) + + /** + * The same seam from the other side. `recorderSha256` skips this directory and `adapterSha256` + * names one module per golden, so an engine file that imports an adapter executes code that + * every golden recorded through a different domain leaves out of its header. Only the register + * may be crossed to, because it is the one file here that carries nothing of its own. + */ + it('reaches the adapter directory only through the register, from every engine file', () => { + const crossings = readdirSync(engine, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.ts')) + .flatMap((entry) => + imports(engine, readFileSync(join(engine, entry.name), 'utf8')) + .filter( + ({ target }) => target.startsWith(`${directory}${sep}`) && target !== registerPath + ) + .map(({ specifier }) => `${entry.name} imports ${specifier}`) + ) + expect(crossings).toEqual([]) + }) + + /** + * The register is pinned by nothing: the engine digest skips this directory and `adapterSha256` + * reads each entry's `source`, never the register. That is only sound while the register is an + * index — a `mounts` or `exposes` written inline here would drive a recording that no digest + * covers. Both must be identifiers imported from the entry's own module, so whatever they carry + * lives in the file that golden already pins. + */ + it("carries no behaviour of its own, only bindings from each entry's module", () => { + const file = ts.createSourceFile(REGISTER, read(REGISTER), ts.ScriptTarget.Latest, true) + const bindings = registerImports(file) + const entries = registerEntries(file) + expect(entries.length).toBe(MOUNTED_OPERATION_MODULES.length) + const carried = entries.flatMap((entry) => { + const source = property(entry, 'source') + if (!source || !ts.isStringLiteral(source)) { + return ['an entry declares no literal source'] + } + const expected = `./${source.text.replace(/\.ts$/, '')}` + return ['mounts', 'exposes'].flatMap((field) => { + const value = property(entry, field) + if (!value) { + return field === 'mounts' ? [`${source.text} registers no mounts`] : [] + } + if (!ts.isIdentifier(value)) { + return [`${source.text} writes ${field} inline instead of importing it`] + } + const from = bindings.get(value.text) + return from === expected + ? [] + : [`${source.text} takes ${field} from ${from ?? 'no import'}`] + }) + }) + expect(carried).toEqual([]) + }) + + it('mounts nothing outside a registered module', () => { + const modules = operationModuleLoader(root) + const registered = MOUNTED_OPERATION_MODULES.flatMap((module) => + Object.keys(module.mounts(modules, {})) + ) + expect(Object.keys(pilotMountAdapters(root).adapters).sort()).toEqual([...registered].sort()) + }) + + it('attributes every recorded operation to a registered module', () => { + const owners = adapterSourceByOperation(root) + const orphans = [...new Set(manifest.map((scenario) => scenario.operation))] + .filter((operation) => !owners.has(operation)) + .sort() + expect(orphans).toEqual([]) + }) +}) diff --git a/mobile/src/test-support/rpc-recording/adapters/file-inventory-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/file-inventory-mount-adapters.ts new file mode 100644 index 00000000000..898fbc92716 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/file-inventory-mount-adapters.ts @@ -0,0 +1,61 @@ +import { hookMount, performHookAction } from '../hook-mount' +import type { MountOptions } from '../mounted-operation-module' +import { operationModuleLoader } from '../operation-module-loader' +import type { MountAdapter } from '../recording-scenario' + +/** The native-chat file search behind the legacy inventory seeds. */ +export function fileInventoryMountAdapters( + modules: ReturnType, + options: MountOptions +): Record { + return { + 'workspace.file-inventory': ({ client }) => { + const useSearch = modules.load< + typeof import('../../../session/use-mobile-native-chat-file-search') + >('mobile/src/session/use-mobile-native-chat-file-search.ts').useMobileNativeChatFileSearch + const operations = options.reference + ? modules + .load('mobile/src/session/native-host-session-native-chat-operations.ts') + .nativeHostSessionNativeChatOperations(client) + : undefined + let workspace = 'A' + let state: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + state = useSearch({ client, operations, worktreeId: workspace } as Parameters< + typeof useSearch + >[0]) + }) + return { + action(name, args) { + if (name === 'mount' || name === 'remount') { + return hook.mount() + } + if (name === 'unmount') { + return hook.unmount() + } + if (name === 'select') { + workspace = String(args.workspace) + return hook.update() + } + if (name === 'reset') { + const previous = workspace + workspace = `${workspace}-reset` + hook.update() + workspace = previous + return hook.update() + } + if (name === 'query') { + return performHookAction(() => state.loadNativeChatFiles(String(args.query))) + } + if (name === 'blur') { + return + } + throw new Error(`Unknown inventory action: ${name}`) + }, + state: () => ({ files: state?.nativeChatFilePaths ?? [] }), + dispose: hook.unmount + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/hosted-review-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/hosted-review-mount-adapters.ts similarity index 90% rename from mobile/src/test-support/rpc-recording/hosted-review-mount-adapters.ts rename to mobile/src/test-support/rpc-recording/adapters/hosted-review-mount-adapters.ts index df836e7bc13..2528923ce4c 100644 --- a/mobile/src/test-support/rpc-recording/hosted-review-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/hosted-review-mount-adapters.ts @@ -1,5 +1,5 @@ -import type { MountAdapter } from './recording-scenario' -import { operationModuleLoader } from './operation-module-loader' +import type { MountAdapter } from '../recording-scenario' +import { operationModuleLoader } from '../operation-module-loader' const WORKTREE = 'repo42::/p' @@ -17,7 +17,7 @@ export function hostedReviewMountAdapters( return { 'source-control.review-git-preparation': ({ client }) => { const preparation = modules.load< - typeof import('../../source-control/mobile-hosted-review-git-preparation') + typeof import('../../../source-control/mobile-hosted-review-git-preparation') >('mobile/src/source-control/mobile-hosted-review-git-preparation.ts') let status: unknown = 'unread' let committed: unknown = 'uncommitted' @@ -42,7 +42,7 @@ export function hostedReviewMountAdapters( }, 'source-control.remote-prerequisite': (context) => { const apply = modules.load< - typeof import('../../source-control/mobile-hosted-review-remote-prerequisite') + typeof import('../../../source-control/mobile-hosted-review-remote-prerequisite') >( 'mobile/src/source-control/mobile-hosted-review-remote-prerequisite.ts' ).applyMobileHostedReviewRemotePrerequisite @@ -74,7 +74,7 @@ export function hostedReviewMountAdapters( }, 'source-control.hosted-review-eligibility': ({ client }) => { const service = modules.load< - typeof import('../../source-control/mobile-hosted-review-service') + typeof import('../../../source-control/mobile-hosted-review-service') >('mobile/src/source-control/mobile-hosted-review-service.ts') let eligibility: unknown = 'unfetched' let prefill: unknown = 'unresolved' @@ -104,7 +104,7 @@ export function hostedReviewMountAdapters( }, 'source-control.hosted-review-create': ({ client }) => { const create = modules.load< - typeof import('../../source-control/mobile-hosted-review-service') + typeof import('../../../source-control/mobile-hosted-review-service') >('mobile/src/source-control/mobile-hosted-review-service.ts').createMobileHostedReview let outcome: unknown = 'uncreated' return { @@ -127,7 +127,7 @@ export function hostedReviewMountAdapters( }, 'source-control.create-intent': (context) => { const run = modules.load< - typeof import('../../source-control/mobile-hosted-review-create-intent-runner') + typeof import('../../../source-control/mobile-hosted-review-create-intent-runner') >( 'mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts' ).runMobileHostedReviewCreateIntent diff --git a/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts b/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts new file mode 100644 index 00000000000..137ee683f8e --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts @@ -0,0 +1,37 @@ +import { fileInventoryMountAdapters } from './file-inventory-mount-adapters' +import { hostedReviewMountAdapters } from './hosted-review-mount-adapters' +import { newTabAgentMountAdapters } from './new-tab-agent-mount-adapters' +import { settingsMountAdapters, settingsMountExposures } from './settings-mount-adapters' +import { sourceControlMountAdapters } from './source-control-mount-adapters' +import { taskMountAdapters } from './task-mount-adapters' +import { taskWorkspaceHookMountAdapters } from './task-workspace-hook-mount-adapters' +import { taskWorkspaceSenderMountAdapters } from './task-workspace-sender-mount-adapters' +import { workspaceSettingsMounts } from './workspace-settings-mounts' +import type { MountedOperationModule } from '../mounted-operation-module' + +/** + * Every domain's mount adapters, paired with the file each one lives in. The register lives inside + * the seam it registers, so adding a domain edits no engine file and moves no existing golden; + * `adapter-seam.test.ts` checks each pairing names the file that declares it. + */ +export const MOUNTED_OPERATION_MODULES: readonly MountedOperationModule[] = [ + { source: 'file-inventory-mount-adapters.ts', mounts: fileInventoryMountAdapters }, + { source: 'hosted-review-mount-adapters.ts', mounts: hostedReviewMountAdapters }, + { source: 'new-tab-agent-mount-adapters.ts', mounts: newTabAgentMountAdapters }, + { + source: 'settings-mount-adapters.ts', + mounts: settingsMountAdapters, + exposes: settingsMountExposures + }, + { source: 'source-control-mount-adapters.ts', mounts: sourceControlMountAdapters }, + { source: 'task-mount-adapters.ts', mounts: taskMountAdapters }, + { + source: 'task-workspace-hook-mount-adapters.ts', + mounts: taskWorkspaceHookMountAdapters + }, + { + source: 'task-workspace-sender-mount-adapters.ts', + mounts: taskWorkspaceSenderMountAdapters + }, + { source: 'workspace-settings-mounts.ts', mounts: workspaceSettingsMounts } +] diff --git a/mobile/src/test-support/rpc-recording/adapters/new-tab-agent-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/new-tab-agent-mount-adapters.ts new file mode 100644 index 00000000000..7dec78b53e3 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/new-tab-agent-mount-adapters.ts @@ -0,0 +1,21 @@ +import { operationModuleLoader } from '../operation-module-loader' +import type { MountAdapter } from '../recording-scenario' + +/** New-tab agent options: a plain loader call, so no React host is needed. */ +export function newTabAgentMountAdapters( + modules: ReturnType +): Record { + return { + 'settings.new-tab-agents': ({ client }) => { + const load = modules.load( + 'mobile/src/session/mobile-new-tab-agent-loader.ts' + ).loadMobileNewTabAgentOptions + return { + action: (_name, args) => + load({ client, worktreeId: String(args.workspace ?? 'repo-1::/folder') }), + state: () => ({}), + dispose: () => {} + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/settings-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/settings-mount-adapters.ts similarity index 83% rename from mobile/src/test-support/rpc-recording/settings-mount-adapters.ts rename to mobile/src/test-support/rpc-recording/adapters/settings-mount-adapters.ts index f453c16ef13..5c6fe0ef14d 100644 --- a/mobile/src/test-support/rpc-recording/settings-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/settings-mount-adapters.ts @@ -1,16 +1,25 @@ -import type { MountAdapter } from './recording-scenario' -import { hookMount } from './hook-mount' -import { observableModel, projectObservable } from './observable-model' -import { operationModuleLoader } from './operation-module-loader' +import type { OperationExposure } from '../operation-module-loader' +import type { MountAdapter } from '../recording-scenario' +import { hookMount } from '../hook-mount' +import { observableModel, projectObservable } from '../observable-model' +import { operationModuleLoader } from '../operation-module-loader' + +/** `loadMobileResumeMetadata` is module-private in the panel; exposing it beats editing pinned source. */ +export const settingsMountExposures: readonly OperationExposure[] = [ + [ + 'MobileAgentSessionHistoryPanel.tsx', + '\nexports.loadMobileResumeMetadata = loadMobileResumeMetadata;' + ] +] export function settingsMountAdapters( modules: ReturnType ): Record { return { 'settings.bot-overrides': ({ client }) => { - const useOverrides = modules.load( - 'mobile/src/session/use-pr-bot-author-overrides.ts' - ).usePRBotAuthorOverrides + const useOverrides = modules.load< + typeof import('../../../session/use-pr-bot-author-overrides') + >('mobile/src/session/use-pr-bot-author-overrides.ts').usePRBotAuthorOverrides let state: ReadonlySet = new Set() let revision = 1 const hook = hookMount(() => { @@ -39,7 +48,7 @@ export function settingsMountAdapters( }, 'settings.workspace-context': ({ client }) => { const useContext = modules.load< - typeof import('../../components/use-new-workspace-runtime-context') + typeof import('../../../components/use-new-workspace-runtime-context') >('mobile/src/components/use-new-workspace-runtime-context.ts').useNewWorkspaceRuntimeContext let state: ReturnType let visible = true @@ -107,9 +116,9 @@ export function settingsMountAdapters( return { action: () => load(client), state: () => ({}), dispose: () => {} } }, 'settings.repo-metadata': (context) => { - const useMetadata = modules.load( - 'mobile/src/host-screen/use-host-repo-metadata.ts' - ).useHostRepoMetadata + const useMetadata = modules.load< + typeof import('../../../host-screen/use-host-repo-metadata') + >('mobile/src/host-screen/use-host-repo-metadata.ts').useHostRepoMetadata const state = observableModel(context, { clientRef: { current: context.client }, fetchRepoMetadataInFlightRef: { current: new Set() }, @@ -142,7 +151,7 @@ export function settingsMountAdapters( }, 'settings.task-hydration': (context) => { const useHydration = modules.load< - typeof import('../../tasks/use-mobile-tasks-runtime-hydration') + typeof import('../../../tasks/use-mobile-tasks-runtime-hydration') >('mobile/src/tasks/use-mobile-tasks-runtime-hydration.tsx').useMobileTasksRuntimeHydration const model = observableModel(context, { client: context.client, diff --git a/mobile/src/test-support/rpc-recording/source-control-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/source-control-mount-adapters.ts similarity index 86% rename from mobile/src/test-support/rpc-recording/source-control-mount-adapters.ts rename to mobile/src/test-support/rpc-recording/adapters/source-control-mount-adapters.ts index 81d2db75eb1..5602b916f70 100644 --- a/mobile/src/test-support/rpc-recording/source-control-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/source-control-mount-adapters.ts @@ -1,5 +1,5 @@ -import type { MountAdapter } from './recording-scenario' -import { operationModuleLoader } from './operation-module-loader' +import type { MountAdapter } from '../recording-scenario' +import { operationModuleLoader } from '../operation-module-loader' const WORKTREE = 'repo42::/p' @@ -12,7 +12,7 @@ export function sourceControlMountAdapters( ): Record { return { 'source-control.branch-base-ref': ({ client }) => { - const resolve = modules.load( + const resolve = modules.load( 'mobile/src/source-control/mobile-branch-base-ref.ts' ).resolveMobileBranchCompareBaseRef let baseRef: unknown = 'unresolved' @@ -27,7 +27,7 @@ export function sourceControlMountAdapters( } }, 'source-control.git-history': ({ client }) => { - const history = modules.load( + const history = modules.load( 'mobile/src/source-control/mobile-git-history.ts' ) let rows: unknown = 'unloaded' @@ -42,7 +42,7 @@ export function sourceControlMountAdapters( } }, 'source-control.commit-message': ({ client }) => { - const ai = modules.load( + const ai = modules.load( 'mobile/src/source-control/mobile-commit-message-ai.ts' ) let generated: unknown = 'ungenerated' @@ -61,7 +61,7 @@ export function sourceControlMountAdapters( } }, 'source-control.pr-link': ({ client }) => { - const link = modules.load( + const link = modules.load( 'mobile/src/source-control/mobile-pr-link.ts' ) let outcome: unknown = 'unlinked' @@ -93,7 +93,7 @@ export function sourceControlMountAdapters( }, 'source-control.session-diff-reveal': ({ client }) => { const reveal = modules.load< - typeof import('../../source-control/reveal-mobile-source-control-session-diff') + typeof import('../../../source-control/reveal-mobile-source-control-session-diff') >( 'mobile/src/source-control/reveal-mobile-source-control-session-diff.ts' ).revealMobileSourceControlSessionDiff diff --git a/mobile/src/test-support/rpc-recording/adapters/task-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/task-mount-adapters.ts new file mode 100644 index 00000000000..a1909007be1 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/task-mount-adapters.ts @@ -0,0 +1,170 @@ +import { hookMount } from '../hook-mount' +import type { MountOptions } from '../mounted-operation-module' +import { observableModel } from '../observable-model' +import { operationModuleLoader } from '../operation-module-loader' +import type { MountAdapter } from '../recording-scenario' + +/** Task-model mounts: GitHub project metadata, Linear issue detail and task client settings. */ +export function taskMountAdapters( + modules: ReturnType, + options: MountOptions +): Record { + return { + 'project.update-metadata': (context) => { + const useMetadata = modules.load< + typeof import('../../../tasks/use-mobile-tasks-project-metadata-actions') + >( + 'mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx' + ).useMobileTasksProjectMetadataActions + const row = { + id: 'item-1', + itemType: 'ISSUE', + content: { repository: 'owner/repo', number: 1, labels: [], assignees: [] } + } + const model = observableModel(context, { + projectMutating: false, + projectRowDetailError: '', + projectRowItem: row, + githubProjectTable: { rows: [row] }, + projectRowDetail: null, + projectFieldDrafts: {} + }) + Object.assign(model, { + client: context.client, + activeGitHubProjectHost: 'github.enterprise.test' + }) + if (options.reference) { + model.taskOperations = { + projectMutation: modules + .load('mobile/src/tasks/native-host-task-project-mutation-operations.ts') + .nativeHostTaskProjectMutationOperations(context.client) + } + } + let actions: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + actions = useMetadata(model as unknown as Parameters[0]) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'submit') { + return actions.mutateProjectRowMetadata( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scenario supplies the row as JSON, not as a typed model. + row as unknown as Parameters[0], + { addLabels: ['recorded'] } + ) + } + throw new Error(`Unknown project action: ${name}`) + }, + state: () => ({ + mutating: model.projectMutating, + error: model.projectRowDetailError, + row: model.projectRowItem + }), + dispose: hook.unmount + } + }, + 'linear.issue-detail': (context) => { + const useDetail = modules.load< + typeof import('../../../tasks/use-mobile-tasks-item-detail-loading') + >('mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx').useMobileTasksItemDetailLoading + const model = observableModel(context, { + actionItem: { + provider: 'linear', + source: { id: 'issue-1', workspaceId: 'linear-workspace' } + }, + detailLoading: false, + detailError: '', + detailPayload: null, + items: [] + }) + Object.assign(model, { client: context.client, tasksSupported: true, detailRefreshSeq: 0 }) + if (options.reference) { + model.taskOperations = { + detail: modules + .load('mobile/src/tasks/native-host-task-detail-operations.ts') + .nativeHostTaskDetailOperations(context.client) + } + } + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + useDetail(model as unknown as Parameters[0]) + }) + return { + action(name) { + if (name === 'mount' || name === 'remount') { + return hook.mount() + } + if (name === 'unmount') { + return hook.unmount() + } + if (name === 'reset') { + model.detailRefreshSeq = Number(model.detailRefreshSeq) + 1 + return hook.update() + } + if (name === 'blur') { + return + } + throw new Error(`Unknown detail action: ${name}`) + }, + state: () => ({ + loading: model.detailLoading, + error: model.detailError, + payload: model.detailPayload + }), + dispose: hook.unmount + } + }, + 'settings.task-preferences': (context) => { + const usePreferences = modules.load< + typeof import('../../../tasks/use-mobile-tasks-client-settings-actions') + >( + 'mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx' + ).useMobileTasksClientSettingsActions + const model = observableModel(context, { + defaultGitHubPreset: 'all', + githubProjectSettings: {} + }) + Object.assign(model, { + client: context.client, + clientRef: { current: context.client }, + repoSelectionHydratedRef: { current: false }, + defaultRepoSelectionRef: { current: null }, + taskUiReady: true, + githubProjectFieldVisibilityScope: null, + taskResumeRef: { current: {} }, + trustedOrcaHooks: {} + }) + let actions: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + actions = usePreferences(model as unknown as Parameters[0]) + }) + return { + action(name, args) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'write') { + return actions.persistDefaultGitHubPreset( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the preset arrives from the scenario JSON as a string. + args.preset as Parameters[0] + ) + } + if (name === 'resume') { + return actions.persistTaskResumeState({ githubItemsPreset: 'issues' }) + } + if (name === 'trust') { + return actions.persistSetupHookTrust('repo-1', 'hash-1', false) + } + throw new Error(`Unknown preferences action: ${name}`) + }, + state: () => ({ preset: model.defaultGitHubPreset }), + dispose: hook.unmount + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/task-workspace-hook-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/task-workspace-hook-mount-adapters.ts new file mode 100644 index 00000000000..bc533712f5f --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/task-workspace-hook-mount-adapters.ts @@ -0,0 +1,193 @@ +import type { MountAdapter } from '../recording-scenario' +import { hookMount, performHookAction } from '../hook-mount' +import { observableModel, projectObservable } from '../observable-model' +import type { operationModuleLoader } from '../operation-module-loader' + +const REPO = 'repo-1' + +/** + * The workspace-create drawer's three model-chained hooks, mounted the way the settings adapters + * mount theirs: a fixture model supplying only the members the hook destructures, with every setter + * recorded as an effect. + */ +export function taskWorkspaceHookMountAdapters( + modules: ReturnType +): Record { + // The drawer's SSH hook. `connectionId` picks the arm the detection effect takes: a repo on + // an SSH connection detects remote agents, one without it detects local agents. + function sshStateAdapter(connectionId: string | undefined): MountAdapter { + return (context) => { + const useSsh = modules.load< + typeof import('../../../tasks/use-mobile-tasks-workspace-ssh-state') + >('mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx').useMobileTasksWorkspaceSshState + const repo = { id: REPO, displayName: 'Repo', connectionId } + const model = observableModel(context, { + client: context.client, + tasksSupported: true, + runtimeTaskSettings: { disabledTuiAgents: [] }, + workspaceAgent: null, + workspaceAgentOverridden: false, + workspaceCreateDraft: { key: 'linear:1' }, + workspaceCreateRequiresSshConnection: false, + workspaceCreateSshStatus: connectionId ? 'connected' : 'idle', + workspaceCreateTargetConnectionId: connectionId, + workspaceCreateTargetRepo: repo, + workspaceDetectedAgentIds: null, + workspaceSshState: null, + workspaceSshConnecting: false + }) + let actions: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + actions = useSsh(model as unknown as Parameters[0]) + }) + let setup: unknown = 'unresolved' + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'connect') { + return performHookAction(() => actions.connectWorkspaceSshRepo()) + } + if (name === 'ensure-ready') { + return actions.ensureWorkspaceSshReady( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reads only id, displayName and connectionId. + repo as Parameters[0] + ) + } + if (name === 'resolve-setup') { + return actions + .resolveCreateSetupDecision( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: as above. + repo as Parameters[0] + ) + .then((value: unknown) => { + setup = value + return value + }) + } + throw new Error(`Unknown workspace ssh action: ${name}`) + }, + state: () => + projectObservable({ + ssh: model.workspaceSshState, + connecting: model.workspaceSshConnecting, + detected: model.workspaceDetectedAgentIds, + agent: model.workspaceAgent, + setup + }), + dispose: hook.unmount + } + } + } + + return { + 'tasks.workspace-source': (context) => { + const useEffects = modules.load< + typeof import('../../../tasks/use-mobile-tasks-workspace-source-effects') + >( + 'mobile/src/tasks/use-mobile-tasks-workspace-source-effects.tsx' + ).useMobileTasksWorkspaceSourceEffects + const model = observableModel(context, { + client: context.client, + tasksSupported: true, + workspaceCreateDraft: { key: 'linear:1' }, + workspaceCreateTargetRepo: { id: REPO, displayName: 'Repo' }, + workspaceSparseReloadKey: 0, + workspaceBaseBranchQuery: '', + showWorkspaceBaseBranchPicker: false, + workspaceSparsePresets: [], + workspaceSparsePresetsLoaded: false, + workspaceSparsePresetsLoading: false, + workspaceSparsePresetsError: '', + workspaceSparsePresetId: null, + workspaceSparseDraft: null, + workspaceBaseBranchResults: [], + workspaceBaseBranchLoading: false, + workspaceBaseBranchError: '' + }) + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + useEffects(model as unknown as Parameters[0]) + }) + return { + action(name, args) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'branch-query') { + model.showWorkspaceBaseBranchPicker = true + model.workspaceBaseBranchQuery = String(args.query ?? 'main') + return hook.update() + } + throw new Error(`Unknown workspace source action: ${name}`) + }, + state: () => + projectObservable({ + presets: model.workspaceSparsePresets, + presetsLoaded: model.workspaceSparsePresetsLoaded, + presetsError: model.workspaceSparsePresetsError, + branches: model.workspaceBaseBranchResults, + branchError: model.workspaceBaseBranchError + }), + dispose: hook.unmount + } + }, + 'tasks.workspace-sparse': (context) => { + const useSparse = modules.load< + typeof import('../../../tasks/use-mobile-tasks-workspace-sparse-actions') + >( + 'mobile/src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx' + ).useMobileTasksWorkspaceSparseActions + const model = observableModel(context, { + client: context.client, + tasksSupported: true, + canSaveWorkspaceSparseDraft: true, + workspaceCreateDraft: { key: 'linear:1' }, + workspaceCreateTargetConnectionId: 'ssh-1', + workspaceCreateTargetRepo: { id: REPO, displayName: 'Repo' }, + workspaceSparseCheckoutAvailable: true, + workspaceSparseDraft: { mode: 'new', name: 'docs', directoriesText: 'docs' }, + workspaceSparseDraftName: 'docs', + workspaceSparseDraftParsed: { directories: ['docs'] }, + workspaceSparsePresetId: null, + workspaceSparsePresets: [], + workspaceSparsePresetsLoaded: false, + workspaceSparsePresetsLoading: false, + workspaceSparsePresetsError: '', + workspaceSparseSaving: false, + workspaceSshState: null, + workspaceSshConnecting: false, + showWorkspaceSparsePicker: false + }) + let actions: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + actions = useSparse(model as unknown as Parameters[0]) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'save-preset') { + return performHookAction(() => actions.saveWorkspaceSparsePreset()) + } + throw new Error(`Unknown workspace sparse action: ${name}`) + }, + state: () => + projectObservable({ + presets: model.workspaceSparsePresets, + presetsError: model.workspaceSparsePresetsError, + saving: model.workspaceSparseSaving, + ssh: model.workspaceSshState + }), + dispose: hook.unmount + } + }, + 'tasks.workspace-ssh': sshStateAdapter('ssh-1'), + // The local arm: no connectionId, so the effect calls preflight.detectAgents. + 'tasks.workspace-ssh-local': sshStateAdapter(undefined) + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/task-workspace-sender-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/task-workspace-sender-mount-adapters.ts new file mode 100644 index 00000000000..65830bd11c3 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/task-workspace-sender-mount-adapters.ts @@ -0,0 +1,180 @@ +import type { MountAdapter } from '../recording-scenario' +import type { operationModuleLoader } from '../operation-module-loader' + +const REPO = 'repo-1' +const REPO_SELECTOR = `id:${REPO}` + +/** + * The task workspace-creation senders that are exported async functions taking a client: create and + * its retry loop, the create-time capability probe, hosted-base resolution, the setup-hook trust + * write and the Smart source picker's provider reads. No React host is needed, so the recorded + * state is the function's own answer. + */ +export function taskWorkspaceSenderMountAdapters( + modules: ReturnType +): Record { + return { + 'tasks.worktree-create-retry': ({ client }) => { + const create = modules.load( + 'mobile/src/tasks/worktree-create-retry.ts' + ).createWorktreeWithNameRetry + let outcome: unknown = 'uncreated' + let minted = 0 + return { + action: (_name, args) => + create({ + client, + baseName: String(args.name ?? 'kestrel'), + buildParams: (candidate: string) => ({ repo: REPO_SELECTOR, name: candidate }), + // A resolved probe, because the create path awaits it before the first send. + worktreeCreateIdempotency: Promise.resolve( + args.idempotency === false ? false : { dedupeTtlMs: 60_000 } + ), + ...(args.maxAttempts === undefined ? {} : { maxAttempts: Number(args.maxAttempts) }), + mintMutationId: () => `mutation-${++minted}` + }).then((value: unknown) => { + outcome = value + return value + }), + state: () => ({ outcome }), + dispose: () => {} + } + }, + 'tasks.worktree-capabilities': ({ client }) => { + const read = modules.load( + 'mobile/src/tasks/worktree-create-capability.ts' + ).readNewWorktreeRuntimeCapabilities + let capabilities: unknown = 'unprobed' + return { + action: () => + read(client).then((value: unknown) => { + capabilities = value + return value + }), + state: () => ({ capabilities }), + dispose: () => {} + } + }, + 'tasks.composer-hosted-base': ({ client }) => { + const resolve = modules.load( + 'mobile/src/tasks/composer-source-base-resolve.ts' + ) + let prBase: unknown = 'unresolved' + let mrBase: unknown = 'unresolved' + return { + action(name) { + if (name === 'mr-base') { + return resolve + .resolveComposerMrBase({ client, repoId: REPO, mrIid: 7, sourceBranch: 'feature' }) + .then((value: unknown) => { + mrBase = value + return value + }) + } + return resolve + .resolveComposerPrBase({ client, repoId: REPO, prNumber: 12, headRefName: 'feature' }) + .then((value: unknown) => { + prBase = value + return value + }) + }, + state: () => ({ prBase, mrBase }), + dispose: () => {} + } + }, + 'tasks.setup-hook-trust': ({ client }) => { + const persist = modules.load( + 'mobile/src/tasks/setup-hook-trust.ts' + ).persistSetupHookTrustApproval + let trust: unknown = 'unapproved' + return { + action: (_name, args) => + persist({ + client, + trust: {}, + repoId: REPO, + contentHash: 'hash-1', + alwaysTrust: args.always === true + }).then((value: unknown) => { + trust = value + return value + }), + state: () => ({ trust }), + dispose: () => {} + } + }, + 'tasks.smart-source-search': ({ client }) => { + const search = modules.load( + 'mobile/src/tasks/smart-source-search-requests.ts' + ) + const results: Record = {} + return { + action(name, args) { + const query = String(args.query ?? 'bug') + const request = + name === 'gitlab' + ? search.searchGitLabItems(client, REPO, query, 'opened') + : name === 'linear' + ? search.searchLinearIssues( + client, + query, + args.workspace === null ? null : String(args.workspace ?? 'linear-workspace') + ) + : name === 'branches' + ? search.searchBranches(client, REPO, query) + : search.searchGitHubItems(client, REPO, query) + return request.then((value: unknown) => { + results[name] = value + return value + }) + }, + state: () => ({ ...results }), + dispose: () => {} + } + }, + 'tasks.paste-lookup': ({ client }) => { + const paste = modules.load( + 'mobile/src/tasks/smart-source-paste-intent.ts' + ) + const slugCache = new Map() + const repos = [ + { id: REPO, displayName: 'Repo', slug: null }, + { id: 'repo-2', displayName: 'Other', slug: null } + ] + const results: Record = {} + return { + action(name) { + const request = + name === 'by-number' + ? paste.lookupGitHubItemByNumber(client, REPO, 12) + : name === 'by-slug' + ? paste.lookupGitHubItemByOwnerRepo( + client, + REPO, + { owner: 'owner', repo: 'repo' }, + 12, + 'issue' + ) + : name === 'gitlab-path' + ? paste.lookupGitLabItemByPath(client, REPO, { + slug: { host: 'gitlab.com', path: 'group/project' }, + number: 7, + type: 'issue' + }) + : paste.findRepoMatchingSlugForPaste( + client, + repos, + { owner: 'owner', repo: 'repo' }, + slugCache + ) + return request.then((value: unknown) => { + results[name] = value + return value + }) + }, + state: () => ({ ...results, cache: [...slugCache] }), + dispose: () => {} + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/workspace-settings-mounts.ts b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts similarity index 73% rename from mobile/src/test-support/rpc-recording/workspace-settings-mounts.ts rename to mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts index d37c3d40935..cd39ca9a04b 100644 --- a/mobile/src/test-support/rpc-recording/workspace-settings-mounts.ts +++ b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts @@ -1,15 +1,84 @@ -import type { MountAdapter } from './recording-scenario' -import { hookMount, performHookAction } from './hook-mount' -import { observableModel, projectObservable } from './observable-model' -import { operationModuleLoader } from './operation-module-loader' +import type { MountAdapter } from '../recording-scenario' +import { hookMount, performHookAction } from '../hook-mount' +import { observableModel, projectObservable } from '../observable-model' +import { operationModuleLoader } from '../operation-module-loader' export function workspaceSettingsMounts( modules: ReturnType ): Record { + // `settings.task-workspace` stops at the setup prompt, which is the branch that scenario set + // exercises. A second registration resolves setup instead, so createWorkspace runs to + // worktree.create and the reply matrix reaches that call's acceptance policy. + function taskWorkspaceAdapter(setupResolution: { + kind: string + command?: string + source?: string + decision?: string + }): MountAdapter { + return (context) => { + const useCreate = modules.load< + typeof import('../../../tasks/use-mobile-tasks-workspace-create-actions') + >( + 'mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx' + ).useMobileTasksWorkspaceCreateActions + const model = observableModel(context, { + client: context.client, + hostId: 'host-1', + tasksSupported: true, + taskStateHydrated: true, + runtimeTaskSettings: { disabledTuiAgents: ['claude'] }, + trustedOrcaHooks: {}, + workspaceDetectedAgentIds: new Set(['codex']), + workspaceLastAutoName: '', + ensureWorkspaceSshReady: async () => {}, + getWorkspaceTargetRepo: () => ({ + id: 'repo-1', + displayName: 'Repo', + connectionId: 'ssh-1' + }), + resolveCreateSetupDecision: async () => setupResolution, + router: { push: (value: unknown) => context.effect('navigation', value) } + }) + let actions: ReturnType + const hook = hookMount(() => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + actions = useCreate(model as unknown as Parameters[0]) + }) + return { + action(name, args) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'submit') { + return actions.createWorkspace( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scenario supplies the action item as JSON, not as a typed model. + (args.item ?? { + key: 'linear:1', + provider: 'linear', + source: { id: 'issue-1' } + }) as Parameters[0], + undefined, + undefined, + 'claude' + ) + } + throw new Error(`Unknown task workspace action: ${name}`) + }, + state: () => + projectObservable({ + settings: model.runtimeTaskSettings, + error: model.error, + creating: model.creatingKey + }), + dispose: hook.unmount + } + } + } + return { 'settings.workspace-submit': (context) => { const useSubmit = modules.load< - typeof import('../../components/use-new-workspace-create-submit') + typeof import('../../../components/use-new-workspace-create-submit') >('mobile/src/components/use-new-workspace-create-submit.ts').useNewWorkspaceCreateSubmit const model = observableModel(context, { client: context.client, @@ -56,65 +125,14 @@ export function workspaceSettingsMounts( dispose: hook.unmount } }, - 'settings.task-workspace': (context) => { - const useCreate = modules.load< - typeof import('../../tasks/use-mobile-tasks-workspace-create-actions') - >( - 'mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx' - ).useMobileTasksWorkspaceCreateActions - const model = observableModel(context, { - client: context.client, - hostId: 'host-1', - tasksSupported: true, - taskStateHydrated: true, - runtimeTaskSettings: { disabledTuiAgents: ['claude'] }, - trustedOrcaHooks: {}, - workspaceDetectedAgentIds: new Set(['codex']), - workspaceLastAutoName: '', - ensureWorkspaceSshReady: async () => {}, - getWorkspaceTargetRepo: () => ({ - id: 'repo-1', - displayName: 'Repo', - connectionId: 'ssh-1' - }), - resolveCreateSetupDecision: async () => ({ - kind: 'prompt', - command: 'setup', - source: 'repo' - }), - router: { push: (value: unknown) => context.effect('navigation', value) } - }) - let actions: ReturnType - const hook = hookMount(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. - actions = useCreate(model as unknown as Parameters[0]) - }) - return { - action(name) { - if (name === 'mount') { - return hook.mount() - } - if (name === 'submit') { - return actions.createWorkspace( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scenario supplies the action item as JSON, not as a typed model. - { key: 'linear:1', provider: 'linear', source: { id: 'issue-1' } } as Parameters< - typeof actions.createWorkspace - >[0], - undefined, - undefined, - 'claude' - ) - } - throw new Error(`Unknown task workspace action: ${name}`) - }, - state: () => - projectObservable({ - settings: model.runtimeTaskSettings, - error: model.error, - creating: model.creatingKey - }), - dispose: hook.unmount - } - } + 'settings.task-workspace': taskWorkspaceAdapter({ + kind: 'prompt', + command: 'setup', + source: 'repo' + }), + 'settings.task-workspace-create': taskWorkspaceAdapter({ + kind: 'decision', + decision: 'inherit' + }) } } diff --git a/mobile/src/test-support/rpc-recording/golden-header-digest.test.ts b/mobile/src/test-support/rpc-recording/golden-header-digest.test.ts index cf05b38cfca..7a43acb79ff 100644 --- a/mobile/src/test-support/rpc-recording/golden-header-digest.test.ts +++ b/mobile/src/test-support/rpc-recording/golden-header-digest.test.ts @@ -4,10 +4,14 @@ import { join, resolve } from 'node:path' import { afterAll, describe, expect, it } from 'vitest' import { derivedGoldens } from './derived-goldens' import { goldenRecording, type GoldenRecording } from './golden-recording' -import { RECORDER_DIRECTORY } from './recorder-digest' +import { MOUNTED_OPERATION_MODULES } from './adapters/mounted-operation-modules' +import { ADAPTER_DIRECTORY, RECORDER_DIRECTORY } from './recorder-digest' import { readScenarios } from './scenario-input' +import type { MountedOperationModule } from './mounted-operation-module' import type { RecordingScenario, ScenarioStep } from './recording-scenario' +/** Spelled, not imported: a rename of the excluded directory must fail this suite, not follow it. */ +const MUTANT_DIRECTORY = `${RECORDER_DIRECTORY}/mutants` const root = resolve(import.meta.dirname, '../../../..') const manifest = readScenarios( process.env.RPC_FOUNDATION_SCENARIOS ?? @@ -40,12 +44,32 @@ afterAll(() => { } }) -/** The two files a root contributes to a header, plus the manifest the old digest also read. */ -function stubRoot(recorder: string, scenarioFile: string): string { +/** + * One state of the recorder tree: the engine, the registered adapter modules, the source each one + * holds, and the mutant evidence beside them. A module left out of `sources` gets identical stub + * source, so only what a revision names is different between two of them. + */ +type Revision = { + engine: string + registered?: readonly MountedOperationModule[] + sources?: Record + mutants?: Record +} + +/** The files a root contributes to a header, plus the manifest the oldest digest also read. */ +function stubRoot(revision: Revision, scenarioFile: string): string { const directory = mkdtempSync(join(tmpdir(), 'rpc-header-')) created.push(directory) - mkdirSync(join(directory, RECORDER_DIRECTORY), { recursive: true }) - writeFileSync(join(directory, RECORDER_DIRECTORY, 'runner.ts'), recorder) + mkdirSync(join(directory, ADAPTER_DIRECTORY), { recursive: true }) + writeFileSync(join(directory, RECORDER_DIRECTORY, 'runner.ts'), revision.engine) + for (const { source } of revision.registered ?? MOUNTED_OPERATION_MODULES) { + const stub = revision.sources?.[source] ?? 'export const adapter = 1' + writeFileSync(join(directory, ADAPTER_DIRECTORY, source), stub) + } + mkdirSync(join(directory, MUTANT_DIRECTORY), { recursive: true }) + for (const [file, source] of Object.entries(revision.mutants ?? {})) { + writeFileSync(join(directory, MUTANT_DIRECTORY, file), source) + } writeFileSync(join(directory, 'mobile/pnpm-lock.yaml'), 'lockfile: stub\n') mkdirSync(join(directory, 'mobile/rpc-foundation'), { recursive: true }) writeFileSync(join(directory, 'mobile/rpc-foundation/pilot-scenarios.json'), scenarioFile) @@ -53,15 +77,16 @@ function stubRoot(recorder: string, scenarioFile: string): string { } /** Every golden's header for one recorder revision and one manifest, both written to a stub root. */ -function headers(recorder: string, scenarios: readonly RecordingScenario[]): Map { - const stub = stubRoot(recorder, JSON.stringify({ baseline: BASELINE, scenarios })) +function headers(revision: Revision, scenarios: readonly RecordingScenario[]): Map { + const stub = stubRoot(revision, JSON.stringify({ baseline: BASELINE, scenarios })) return new Map( derivedGoldens(scenarios).map((golden) => { const { recording: _recording, ...header } = goldenRecording( stub, BASELINE, golden.scenarios(), - { scenario: golden.id, checkpoints: [] } + { scenario: golden.id, checkpoints: [] }, + revision.registered ?? MOUNTED_OPERATION_MODULES ) return [golden.id, header] }) @@ -75,6 +100,30 @@ function moved(before: Map, after: Map): string[ .sort() } +const ADDED_MODULE: MountedOperationModule = { + source: 'digest-probe-mount-adapters.ts', + mounts: () => ({ + 'digest.probe': () => { + throw new Error('The digest reads adapter source; it never mounts one') + } + }) +} + +/** Every golden recorded through `new-tab-agent-mount-adapters.ts`, the module with one operation. */ +const NEW_TAB_GOLDENS = [ + 'matrix-settings-agent-read-preflight.detectremoteagents-1', + 'matrix-settings-agent-read-repo.list-1', + 'matrix-settings-agent-read-settings.get-1', + 'probe-new-tab-both-refused', + 'probe-new-tab-null-sibling-refused', + 'probe-new-tab-refused-sibling-rejects', + 'probe-new-tab-rejects-sibling-refused', + 'schedules-settings-new-tab-ssh', + 'settings-new-tab-refused', + 'settings-new-tab-ssh', + 'settings-new-tab-transport-error' +] + /** A family no other golden consumes, with one reply the matrix can replay as its success. */ const ADDED_FAMILY: RecordingScenario = { id: 'digest-probe', @@ -124,18 +173,50 @@ function editCompletion( } describe('golden header digests', () => { - it('re-digests nothing when the manifest gains a family', () => { - const before = headers('export const runner = 1', manifest) - const after = headers('export const runner = 1', [...manifest, ADDED_FAMILY]) + const ENGINE = 'export const runner = 1' + + // A whole domain PR: a family, the module that mounts it, and the mutant that proves its + // projection load-bearing. None is an input to any other golden's header, so nothing already + // recorded re-records and two such branches conflict on no golden line at all. + it('re-digests nothing when a domain adds a family, an adapter module and a mutant', () => { + const before = headers( + { engine: ENGINE, mutants: { 'operation-mutations.ts': 'one' } }, + manifest + ) + const after = headers( + { + engine: ENGINE, + registered: [...MOUNTED_OPERATION_MODULES, ADDED_MODULE], + mutants: { 'operation-mutations.ts': 'two', 'digest-probe-mutants.test.ts': 'added' } + }, + [...manifest, ADDED_FAMILY] + ) expect(moved(before, after)).toEqual([]) // The added family did derive goldens of its own: a pilot golden and one matrix site. expect(after.size).toBe(before.size + 2) }) - it('re-digests exactly the goldens derived from an edited scenario', () => { - const before = headers('export const runner = 1', manifest) + it('re-digests exactly the goldens recorded through an edited adapter module', () => { + const before = headers({ engine: ENGINE }, manifest) const after = headers( - 'export const runner = 1', + { + engine: ENGINE, + sources: { 'new-tab-agent-mount-adapters.ts': 'export const adapter = 2' } + }, + manifest + ) + expect(moved(before, after)).toEqual([...NEW_TAB_GOLDENS].sort()) + for (const id of NEW_TAB_GOLDENS) { + expect(after.get(id)?.recorderSha256).toBe(before.get(id)?.recorderSha256) + expect(after.get(id)?.scenarioSha256).toBe(before.get(id)?.scenarioSha256) + expect(after.get(id)?.adapterSha256).not.toBe(before.get(id)?.adapterSha256) + } + }) + + it('re-digests exactly the goldens derived from an edited scenario', () => { + const before = headers({ engine: ENGINE }, manifest) + const after = headers( + { engine: ENGINE }, editCompletion(manifest, EDITED_SCENARIO, EDITED_SITE, (step) => ({ ...step, params: { worktree: 'id:A', query: 'old', limit: 17 } @@ -144,6 +225,7 @@ describe('golden header digests', () => { expect(moved(before, after)).toEqual([...EDITED_GOLDENS].sort()) for (const id of EDITED_GOLDENS) { expect(after.get(id)?.recorderSha256).toBe(before.get(id)?.recorderSha256) + expect(after.get(id)?.adapterSha256).toBe(before.get(id)?.adapterSha256) expect(after.get(id)?.scenarioSha256).not.toBe(before.get(id)?.scenarioSha256) } }) @@ -152,9 +234,9 @@ describe('golden header digests', () => { // the `normal` partition replays a sibling's recorded reply, so the sibling is a real input to a // matrix golden that its own scenario never appears in. it('re-digests a matrix golden whose replayed success comes from an edited sibling', () => { - const before = headers('export const runner = 1', manifest) + const before = headers({ engine: ENGINE }, manifest) const after = headers( - 'export const runner = 1', + { engine: ENGINE }, editCompletion(manifest, REPLAYED_SCENARIO, EDITED_SITE, (step) => ({ ...step, reply: { ok: true, result: { files: [{ relativePath: 'edited.ts' }] } } @@ -163,12 +245,13 @@ describe('golden header digests', () => { expect(moved(before, after)).toEqual([REPLAYED_SCENARIO, REPLAYED_GOLDEN].sort()) }) - it('re-digests every golden when a recorder file changes', () => { - const before = headers('export const runner = 1', manifest) - const after = headers('export const runner = 2', manifest) + it('re-digests every golden when an engine file changes', () => { + const before = headers({ engine: ENGINE }, manifest) + const after = headers({ engine: 'export const runner = 2' }, manifest) expect(moved(before, after)).toEqual([...before.keys()].sort()) for (const [id, header] of before) { expect(after.get(id)?.recorderSha256).not.toBe(header.recorderSha256) + expect(after.get(id)?.adapterSha256).toBe(header.adapterSha256) expect(after.get(id)?.scenarioSha256).toBe(header.scenarioSha256) } }) diff --git a/mobile/src/test-support/rpc-recording/golden-recording.ts b/mobile/src/test-support/rpc-recording/golden-recording.ts index 911da5997b0..ec8bba57063 100644 --- a/mobile/src/test-support/rpc-recording/golden-recording.ts +++ b/mobile/src/test-support/rpc-recording/golden-recording.ts @@ -10,17 +10,20 @@ import { type InternedRecording, type ValuePool } from './golden-value-pool' +import { adapterSha256 } from './adapter-digest' +import { MOUNTED_OPERATION_MODULES } from './adapters/mounted-operation-modules' import { recorderSha256 } from './recorder-digest' import { scenarioSha256 } from './scenario-digest' +import type { MountedOperationModule } from './mounted-operation-module' import type { Recording, RecordingScenario } from './recording-scenario' import type { RecordedValue } from './recording-values' export const RUNNER_VERSION = 1 // 2 stamps every settlement with startedAt/settledAt on the pinned virtual clock. export const PROJECTION_VERSION = 2 -// 4 pins scenarioSha256 per golden. The byte compare would fail a version-3 golden anyway; the bump -// buys the diagnosis, reporting the stale format instead of an opaque `(encoding)` difference. -export const GOLDEN_FORMAT_VERSION = 4 +// 5 splits the mount adapters out of recorderSha256 into adapterSha256. As with 4, the byte compare +// would fail a stale golden anyway; the bump buys the diagnosis instead of an opaque `(encoding)`. +export const GOLDEN_FORMAT_VERSION = 5 export type GoldenRecording = { operation: string family: string @@ -29,6 +32,7 @@ export type GoldenRecording = { baseline: string lockfileSha256: string recorderSha256: string + adapterSha256: string scenarioSha256: string platform: string scenarioVersion: number @@ -44,7 +48,8 @@ export function goldenRecording( root: string, baseline: string, scenarios: readonly RecordingScenario[], - recording: Recording + recording: Recording, + registered: readonly MountedOperationModule[] = MOUNTED_OPERATION_MODULES ): GoldenRecording { const [scenario] = scenarios if (!scenario) { @@ -60,6 +65,7 @@ export function goldenRecording( .update(readFileSync(join(root, 'mobile/pnpm-lock.yaml'))) .digest('hex'), recorderSha256: recorderSha256(root), + adapterSha256: adapterSha256(root, scenarios, registered), scenarioSha256: scenarioSha256(scenarios), platform: process.platform, scenarioVersion: scenario.version, diff --git a/mobile/src/test-support/rpc-recording/mounted-operation-module.ts b/mobile/src/test-support/rpc-recording/mounted-operation-module.ts new file mode 100644 index 00000000000..4a2e326665c --- /dev/null +++ b/mobile/src/test-support/rpc-recording/mounted-operation-module.ts @@ -0,0 +1,19 @@ +import { operationModuleLoader, type OperationExposure } from './operation-module-loader' +import type { MountAdapter } from './recording-scenario' + +/** Reference mode wires an archived tree's operations instead of main's. */ +export type MountOptions = { reference?: boolean } + +/** + * One domain's mount adapters and the `adapters/` file they live in. The file is what each golden + * recorded through them pins, so a domain edit moves those goldens and no others. + */ +export type MountedOperationModule = { + source: string + /** Module-private product exports this domain's adapters drive. Declared in `source`, so pinned. */ + exposes?: readonly OperationExposure[] + mounts: ( + modules: ReturnType, + options: MountOptions + ) => Record +} diff --git a/mobile/src/test-support/rpc-recording/mutants/mutant-seam.test.ts b/mobile/src/test-support/rpc-recording/mutants/mutant-seam.test.ts new file mode 100644 index 00000000000..396765a9ea0 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/mutants/mutant-seam.test.ts @@ -0,0 +1,103 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { dirname, join, resolve, sep } from 'node:path' +import { describe, expect, it } from 'vitest' +import { RECORDER_DIRECTORY } from '../recorder-digest' +import { RECORDING_DRIVERS } from '../recording-drivers' + +const root = resolve(import.meta.dirname, '../../../../..') +const recorder = join(root, RECORDER_DIRECTORY) +const mutants = join(root, RECORDER_DIRECTORY, 'mutants') +/** The one file allowed to name this directory: it names it in order to exclude it. */ +const EXCLUDER = 'recorder-digest.ts' +/** Both spellings of the path, so a constant is no more usable than the literal. */ +const NAMES = ['mutants', 'MUTANT_DIRECTORY'] + +function sources(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true, recursive: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.ts')) + .map((entry) => join(entry.parentPath, entry.name)) +} + +function resolved(from: string, specifier: string): string | undefined { + const base = resolve(dirname(from), specifier) + return ['', '.ts', '.tsx', '/index.ts'] + .map((suffix) => base + suffix) + .find((candidate) => existsSync(candidate) && /\.tsx?$/.test(candidate)) +} + +function relative(file: string): string { + return file.slice(recorder.length + 1) +} + +/** A suite that records nothing: excluded below, since no driver has reason to reach it. */ +function suite(file: string): boolean { + return file.endsWith('.test.ts') && !RECORDING_DRIVERS.some((driver) => file.endsWith(driver)) +} + +/** + * Every module a driver pulls in, transitively, by static import or dynamic `import()`. Type + * positions come along, which is why the graph is an order larger than the recorder itself: a + * `typeof import(...)` drags in product modules. Reaching too much only widens what may not appear. + */ +function reachable(entries: readonly string[]): Set { + const seen = new Set() + const pending = [...entries] + while (pending.length > 0) { + const file = pending.pop()! + if (seen.has(file)) { + continue + } + seen.add(file) + for (const match of readFileSync(file, 'utf8').matchAll(/(?:from|import\()\s*'(\.[^']*)'/g)) { + const target = resolved(file, match[1]!) + if (target) { + pending.push(target) + } + } + } + return seen +} + +/** + * `recorderSha256` skips this directory, so nothing here is pinned by any golden. That is only + * sound while no recording can reach it: a mutant table an adapter imported would change what the + * recording loads while every header stayed still. Reachability is proved from the recording + * drivers outward rather than from this directory inward, because the question is what a golden's + * bytes can depend on. The name scan then covers the paths a module can be read by rather than + * imported, under either spelling of the directory. + */ +describe('the mutant seam', () => { + const outside = sources(recorder).filter((file) => !file.startsWith(`${mutants}${sep}`)) + + it('is unreachable from every recording driver', () => { + const graph = reachable(RECORDING_DRIVERS.map((driver) => join(recorder, driver))) + const reached = [...graph] + .filter((file) => file.startsWith(`${mutants}${sep}`)) + .map(relative) + .sort() + expect(reached).toEqual([]) + // A walk that resolved nothing would pass by reaching nothing, so name what it missed: every + // recording file is reachable today, and one that stops being reachable is an orphan. + const missed = outside + .filter((file) => !suite(file) && !graph.has(file)) + .map(relative) + .sort() + expect(missed).toEqual([]) + expect(sources(mutants).length).toBeGreaterThan(1) + }) + + // A test that does not record cannot change a recording; the drivers do record, so they are held + // to the engine's rule — a driver that read the table would change what it records silently. + // Both names, because `MUTANT_DIRECTORY` spells the same path without the literal. + it('is named in no recording file but the digest that excludes it', () => { + const naming = outside + .filter( + (file) => + !file.endsWith(EXCLUDER) && + !suite(file) && + NAMES.some((name) => readFileSync(file, 'utf8').includes(name)) + ) + .map(relative) + expect(naming).toEqual([]) + }) +}) diff --git a/mobile/src/test-support/rpc-recording/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts similarity index 85% rename from mobile/src/test-support/rpc-recording/operation-mutations.ts rename to mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index cd760f0004f..457bae3c2a8 100644 --- a/mobile/src/test-support/rpc-recording/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -1,15 +1,10 @@ +import type { OperationMutation } from '../operation-module-loader' + /** * One in-memory source edit per adapter family. Each anchor names a real expression in a mounted * operation; the recording that owns the family must change visible state when it is applied, which * is what proves that family's `state()` projection observes the operation's actual output. */ -export type OperationMutation = { - /** Suffix of the mounted source file the anchor belongs to. */ - file: string - before: string - after: string -} - export const OPERATION_MUTATIONS = { // Loses the generation comparison, so a stale workspace response poisons the search cache. race: { @@ -70,22 +65,19 @@ export const OPERATION_MUTATIONS = { before: '((settingsResult.value ?? {}) as RuntimeTaskSettings)', after: '((settingsResponse.result ?? {}) as RuntimeTaskSettings)' }, - // Applies the preset only after the write settles, dropping the optimistic update. + // Moves the optimistic preset write behind the guard that only an unusable client takes, so the + // preset the screen shows never follows the tap. Anchored above the send so the step-4 migration + // of this file does not move it; the projection it proves load-bearing is the same one. 'task-preferences-optimistic': { file: 'use-mobile-tasks-client-settings-actions.tsx', before: ` setDefaultGitHubPreset(preset) if (!client || !taskUiReady) { return - } - void client.sendRequest('settings.update', { defaultTaskViewPreset: preset }).catch(() => {`, + }`, after: ` if (!client || !taskUiReady) { setDefaultGitHubPreset(preset) return - } - void client - .sendRequest('settings.update', { defaultTaskViewPreset: preset }) - .then(() => setDefaultGitHubPreset(preset)) - .catch(() => {` + }` }, // Publishes the settings envelope as the refreshed workspace runtime settings. 'workspace-submit-envelope': { @@ -127,17 +119,11 @@ export const OPERATION_MUTATIONS = { before: 'latestRuntimeTaskSettings = (settingsResult.value ?? {}) as RuntimeTaskSettings', after: 'latestRuntimeTaskSettings = (settingsReply.result ?? {}) as RuntimeTaskSettings' } -} as const satisfies Record +} as const satisfies Record> export type Mutation = keyof typeof OPERATION_MUTATIONS -/** - * Appended to a mounted module after transpile, keyed by file suffix. An adapter drives a real - * operation the product keeps module-private; exposing it here beats editing the pinned source. - */ -export const OPERATION_EXPOSURES: readonly (readonly [string, string])[] = [ - [ - 'MobileAgentSessionHistoryPanel.tsx', - '\nexports.loadMobileResumeMetadata = loadMobileResumeMetadata;' - ] -] +/** The spec the loader applies, carrying the name only so a half-applied anchor can report it. */ +export function operationMutation(name: Mutation): OperationMutation { + return { name, ...OPERATION_MUTATIONS[name] } +} diff --git a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts new file mode 100644 index 00000000000..acd9840e4f0 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts @@ -0,0 +1,104 @@ +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { pilotGoldens } from '../derived-goldens' +import { readGolden } from '../golden-recording' +import { pilotMountAdapters } from '../pilot-mount-adapters' +import { runRecording, runRecordingMutant } from '../run-recording' +import { readScenarios } from '../scenario-input' +import { vitestRecordingScheduler } from '../vitest-recording-scheduler' +import { operationMutation, type Mutation } from './operation-mutations' +import type { Recording } from '../recording-scenario' +import type { RecordedValue } from '../recording-values' + +const root = resolve(import.meta.dirname, '../../../../..') +const input = readScenarios( + process.env.RPC_FOUNDATION_SCENARIOS ?? + resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json') +) +const goldens = process.env.RPC_FOUNDATION_GOLDENS ?? resolve(root, 'mobile/rpc-foundation/goldens') +// One mutant per adapter family, so every family's state projection is shown to be load-bearing. +const mutants: Record = { + b1: 'race', + b2: 'acceptance', + b3: 'order', + 'settings-bot-overrides-fulfilled': 'bot-overrides-envelope', + 'settings-workspace-context-fulfilled': 'workspace-context-envelope', + 'settings-home-providers-fulfilled': 'home-providers-linear', + 'settings-repo-metadata-fulfilled': 'repo-metadata-platform', + 'settings-task-hydration-fulfilled': 'task-hydration-envelope', + 'settings-task-write': 'task-preferences-optimistic', + 'settings-workspace-submit-fulfilled': 'workspace-submit-envelope', + 'settings-task-workspace-fulfilled': 'task-workspace-envelope' +} +/** + * The archived tree's visible state, pinned per seed: b1 serves the poisoned empty inventory, b2 + * accepts the null envelope and applies the label anyway, and b3 reports the issue error instead of + * the comments error. An unrelated refactor of those files can no longer keep this green by merely + * differing; the mutants remain the defect evidence and this run corroborates them. + */ +const referenceStates: Record = { + b1: { files: [] }, + b2: { + error: '', + mutating: false, + row: { + content: { + assignees: [], + labels: [{ color: '808080', name: 'recorded' }], + number: 1, + repository: 'owner/repo' + }, + id: 'item-1', + itemType: 'ISSUE' + } + }, + b3: { error: 'issue refused', loading: false, payload: { $rpc: 'null' } } +} + +function visibleState(recording: Recording): RecordedValue { + return recording.checkpoints.at(-1)!.observation.state +} + +// Pair pilots with their pinned mutant/reference up front so each loop below defines exactly one test. +const pilots = pilotGoldens(input.scenarios) +const mutantPilots = pilots.flatMap((pilot) => { + const mutation = mutants[pilot.id] + return mutation ? [{ ...pilot, mutation }] : [] +}) +const referencePilots = pilots.flatMap((pilot) => { + const reference = referenceStates[pilot.id] + return reference ? [{ ...pilot, reference }] : [] +}) + +describe('RPC main recording mutants', () => { + for (const { id, scenario, mutation } of mutantPilots) { + it(`${id}: kills ${mutation}`, async () => { + const { adapters, assertMutationApplied } = pilotMountAdapters(root, { + mutation: operationMutation(mutation) + }) + const result = await runRecordingMutant( + scenario, + adapters[scenario.operation], + vitestRecordingScheduler(), + readGolden(goldens, id).recording, + visibleState + ) + assertMutationApplied() + expect(result.verdict).toBe('killed') + }) + } + for (const { id, scenario, reference } of referencePilots) { + it.skipIf(!process.env.RPC_FOUNDATION_REFERENCE_ROOT)(`${id}: rejects bcba08b3e4`, async () => { + const { adapters } = pilotMountAdapters(process.env.RPC_FOUNDATION_REFERENCE_ROOT!, { + reference: true + }) + const result = await runRecording( + scenario, + adapters[scenario.operation], + vitestRecordingScheduler() + ) + expect(visibleState(result)).toEqual(reference) + expect(reference).not.toEqual(visibleState(readGolden(goldens, id).recording)) + }) + } +}) diff --git a/mobile/src/test-support/rpc-recording/probe-hole-witness.test.ts b/mobile/src/test-support/rpc-recording/mutants/probe-hole-witness.test.ts similarity index 82% rename from mobile/src/test-support/rpc-recording/probe-hole-witness.test.ts rename to mobile/src/test-support/rpc-recording/mutants/probe-hole-witness.test.ts index 126b713e811..9bfcb55bd1c 100644 --- a/mobile/src/test-support/rpc-recording/probe-hole-witness.test.ts +++ b/mobile/src/test-support/rpc-recording/mutants/probe-hole-witness.test.ts @@ -1,13 +1,13 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' -import { readScenarios } from './scenario-input' -import { readGolden } from './golden-recording' -import { runRecordingMutant } from './run-recording' -import { pilotMountAdapters } from './pilot-mount-adapters' -import { vitestRecordingScheduler } from './vitest-recording-scheduler' -import type { Mutation } from './operation-mutations' +import { readScenarios } from '../scenario-input' +import { readGolden } from '../golden-recording' +import { runRecordingMutant } from '../run-recording' +import { pilotMountAdapters } from '../pilot-mount-adapters' +import { vitestRecordingScheduler } from '../vitest-recording-scheduler' +import { operationMutation, type Mutation } from './operation-mutations' -const root = resolve(import.meta.dirname, '../../../..') +const root = resolve(import.meta.dirname, '../../../../..') const input = readScenarios( process.env.RPC_FOUNDATION_SCENARIOS ?? resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json') @@ -39,7 +39,9 @@ const HOLES: readonly { mutation: Mutation; operation: string; closedBy: readonl async function verdict(id: string, mutation: Mutation): Promise { const scenario = input.scenarios.find((candidate) => candidate.id === id)! - const { adapters, assertMutationApplied } = pilotMountAdapters(root, { mutation }) + const { adapters, assertMutationApplied } = pilotMountAdapters(root, { + mutation: operationMutation(mutation) + }) const result = await runRecordingMutant( scenario, adapters[scenario.operation], diff --git a/mobile/src/test-support/rpc-recording/operation-module-loader.ts b/mobile/src/test-support/rpc-recording/operation-module-loader.ts index 03c31a4f707..5a33841d321 100644 --- a/mobile/src/test-support/rpc-recording/operation-module-loader.ts +++ b/mobile/src/test-support/rpc-recording/operation-module-loader.ts @@ -3,14 +3,33 @@ import { existsSync, readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import * as React from 'react' import ts from 'typescript' -import { OPERATION_EXPOSURES, OPERATION_MUTATIONS, type Mutation } from './operation-mutations' +import * as deliveryAmbiguity from '../../transport/rpc-delivery-ambiguity' -export type { Mutation } export type OperationModule = Record unknown> +/** One anchored in-memory source edit, resolved by the caller so the loader needs no mutant table. */ +export type OperationMutation = { + name: string + /** Suffix of the mounted source file the anchor belongs to. */ + file: string + before: string + after: string +} +/** Source appended to a mounted module after transpile, keyed by the file suffix it applies to. */ +export type OperationExposure = readonly [suffix: string, source: string] + +// Why shared rather than evaluated: the delivery-unknown mark is a WeakSet keyed on the rejection +// object, so a second copy of the module has a second, empty registry and every marked rejection +// reads as a definite failure inside the mounted operation. Same reason React is shared. +const SHARED_MODULE = 'mobile/src/transport/rpc-delivery-ambiguity.ts' // Only mounting boundaries are substituted; every operation and projection is loaded from source. -export function operationModuleLoader(root: string, mutation?: Mutation) { +export function operationModuleLoader( + root: string, + mutation?: OperationMutation, + exposures: readonly OperationExposure[] = [] +) { const cache = new Map() + const sharedModulePath = resolve(root, SHARED_MODULE) let mutationCount = 0 function pathFor(base: string): string { const file = ['', '.ts', '.tsx', '/index.ts'] @@ -25,6 +44,9 @@ export function operationModuleLoader(root: string, mutation?: Mutation) { if (name === 'react') { return React } + if (name.startsWith('.') && pathFor(resolve(dirname(base), name)) === sharedModulePath) { + return deliveryAmbiguity + } if (!name.startsWith('.')) { return new Proxy( {}, @@ -94,14 +116,13 @@ export function operationModuleLoader(root: string, mutation?: Mutation) { cache.set(file, result) return result } - const spec = mutation ? OPERATION_MUTATIONS[mutation] : undefined - if (spec && file.endsWith(spec.file)) { + if (mutation && file.endsWith(mutation.file)) { // Counting occurrences, not replace calls: `replace` would silently take only the first. - const occurrences = source.split(spec.before).length - 1 + const occurrences = source.split(mutation.before).length - 1 if (occurrences !== 1) { - throw new Error(`Mutant anchor matched ${occurrences} sites, expected 1: ${mutation}`) + throw new Error(`Mutant anchor matched ${occurrences} sites, expected 1: ${mutation.name}`) } - source = source.replace(spec.before, spec.after) + source = source.replace(mutation.before, mutation.after) mutationCount++ } const exports: OperationModule = {} @@ -113,7 +134,7 @@ export function operationModuleLoader(root: string, mutation?: Mutation) { jsx: ts.JsxEmit.React } }).outputText - const exposure = OPERATION_EXPOSURES.find(([suffix]) => file.endsWith(suffix))?.[1] ?? '' + const exposure = exposures.find(([suffix]) => file.endsWith(suffix))?.[1] ?? '' const evaluate = compileFunction(output + exposure, ['require', 'exports'], { filename: file }) evaluate((name: string) => imported(file, name), exports) return exports @@ -122,10 +143,6 @@ export function operationModuleLoader(root: string, mutation?: Mutation) { load: (path: string): T => // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a VM-evaluated module has no static type; the caller names the shape it mounts. load(pathFor(resolve(root, path))) as unknown as T, - assertMutationApplied: () => { - if (mutation && mutationCount !== 1) { - throw new Error(`Expected one mutation, applied ${mutationCount}`) - } - } + mutationsApplied: () => mutationCount } } diff --git a/mobile/src/test-support/rpc-recording/pilot-mount-adapters.ts b/mobile/src/test-support/rpc-recording/pilot-mount-adapters.ts index a1dd2b61174..4fda6b195ef 100644 --- a/mobile/src/test-support/rpc-recording/pilot-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/pilot-mount-adapters.ts @@ -1,231 +1,41 @@ -import { observableModel } from './observable-model' -import { hostedReviewMountAdapters } from './hosted-review-mount-adapters' -import { settingsMountAdapters } from './settings-mount-adapters' -import { sourceControlMountAdapters } from './source-control-mount-adapters' -import { workspaceSettingsMounts } from './workspace-settings-mounts' +import { MOUNTED_OPERATION_MODULES } from './adapters/mounted-operation-modules' +import { operationModuleLoader, type OperationMutation } from './operation-module-loader' +import type { MountOptions } from './mounted-operation-module' import type { MountAdapter } from './recording-scenario' -import { hookMount, performHookAction } from './hook-mount' -import { operationModuleLoader, type Mutation } from './operation-module-loader' +/** + * The mount table one recording runs against: every registered domain module, merged. Nothing is + * mounted here, because an adapter defined in this file would be pinned by `recorderSha256` on + * every golden rather than by `adapterSha256` on the goldens that mount it. + * + * Each module gets its own loader, carrying its own exposures. One recording mounts one adapter, so + * a golden is only ever influenced by the exposures its own module declares — which is what lets + * `adapterSha256` pin them instead of every golden's `recorderSha256`. + */ export function pilotMountAdapters( root: string, - options: { reference?: boolean; mutation?: Mutation } = {} + options: MountOptions & { mutation?: OperationMutation } = {} ) { - const modules = operationModuleLoader(root, options.mutation) - const adapters: Record = { - ...settingsMountAdapters(modules), - ...workspaceSettingsMounts(modules), - ...sourceControlMountAdapters(modules), - ...hostedReviewMountAdapters(modules), - 'workspace.file-inventory': ({ client }) => { - const useSearch = modules.load< - typeof import('../../session/use-mobile-native-chat-file-search') - >('mobile/src/session/use-mobile-native-chat-file-search.ts').useMobileNativeChatFileSearch - const operations = options.reference - ? modules - .load('mobile/src/session/native-host-session-native-chat-operations.ts') - .nativeHostSessionNativeChatOperations(client) - : undefined - let workspace = 'A' - let state: ReturnType - const hook = hookMount(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. - state = useSearch({ client, operations, worktreeId: workspace } as Parameters< - typeof useSearch - >[0]) - }) - return { - action(name, args) { - if (name === 'mount' || name === 'remount') { - return hook.mount() - } - if (name === 'unmount') { - return hook.unmount() - } - if (name === 'select') { - workspace = String(args.workspace) - return hook.update() - } - if (name === 'reset') { - const previous = workspace - workspace = `${workspace}-reset` - hook.update() - workspace = previous - return hook.update() - } - if (name === 'query') { - return performHookAction(() => state.loadNativeChatFiles(String(args.query))) - } - if (name === 'blur') { - return - } - throw new Error(`Unknown inventory action: ${name}`) - }, - state: () => ({ files: state?.nativeChatFilePaths ?? [] }), - dispose: hook.unmount + const loaders = MOUNTED_OPERATION_MODULES.map((module) => ({ + module, + modules: operationModuleLoader(root, options.mutation, module.exposes ?? []) + })) + const adapters: Record = {} + for (const { module, modules } of loaders) { + for (const [operation, adapter] of Object.entries(module.mounts(modules, options))) { + if (operation in adapters) { + throw new Error(`Two adapter modules mount ${operation}`) } - }, - 'project.update-metadata': (context) => { - const useMetadata = modules.load< - typeof import('../../tasks/use-mobile-tasks-project-metadata-actions') - >( - 'mobile/src/tasks/use-mobile-tasks-project-metadata-actions.tsx' - ).useMobileTasksProjectMetadataActions - const row = { - id: 'item-1', - itemType: 'ISSUE', - content: { repository: 'owner/repo', number: 1, labels: [], assignees: [] } - } - const model = observableModel(context, { - projectMutating: false, - projectRowDetailError: '', - projectRowItem: row, - githubProjectTable: { rows: [row] }, - projectRowDetail: null, - projectFieldDrafts: {} - }) - Object.assign(model, { - client: context.client, - activeGitHubProjectHost: 'github.enterprise.test' - }) - if (options.reference) { - model.taskOperations = { - projectMutation: modules - .load('mobile/src/tasks/native-host-task-project-mutation-operations.ts') - .nativeHostTaskProjectMutationOperations(context.client) - } - } - let actions: ReturnType - const hook = hookMount(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. - actions = useMetadata(model as unknown as Parameters[0]) - }) - return { - action(name) { - if (name === 'mount') { - return hook.mount() - } - if (name === 'submit') { - return actions.mutateProjectRowMetadata( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the scenario supplies the row as JSON, not as a typed model. - row as unknown as Parameters[0], - { addLabels: ['recorded'] } - ) - } - throw new Error(`Unknown project action: ${name}`) - }, - state: () => ({ - mutating: model.projectMutating, - error: model.projectRowDetailError, - row: model.projectRowItem - }), - dispose: hook.unmount - } - }, - 'linear.issue-detail': (context) => { - const useDetail = modules.load< - typeof import('../../tasks/use-mobile-tasks-item-detail-loading') - >('mobile/src/tasks/use-mobile-tasks-item-detail-loading.tsx').useMobileTasksItemDetailLoading - const model = observableModel(context, { - actionItem: { - provider: 'linear', - source: { id: 'issue-1', workspaceId: 'linear-workspace' } - }, - detailLoading: false, - detailError: '', - detailPayload: null, - items: [] - }) - Object.assign(model, { client: context.client, tasksSupported: true, detailRefreshSeq: 0 }) - if (options.reference) { - model.taskOperations = { - detail: modules - .load('mobile/src/tasks/native-host-task-detail-operations.ts') - .nativeHostTaskDetailOperations(context.client) - } - } - const hook = hookMount(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. - useDetail(model as unknown as Parameters[0]) - }) - return { - action(name) { - if (name === 'mount' || name === 'remount') { - return hook.mount() - } - if (name === 'unmount') { - return hook.unmount() - } - if (name === 'reset') { - model.detailRefreshSeq = Number(model.detailRefreshSeq) + 1 - return hook.update() - } - if (name === 'blur') { - return - } - throw new Error(`Unknown detail action: ${name}`) - }, - state: () => ({ - loading: model.detailLoading, - error: model.detailError, - payload: model.detailPayload - }), - dispose: hook.unmount - } - }, - 'settings.new-tab-agents': ({ client }) => { - const load = modules.load( - 'mobile/src/session/mobile-new-tab-agent-loader.ts' - ).loadMobileNewTabAgentOptions - return { - action: (_name, args) => - load({ client, worktreeId: String(args.workspace ?? 'repo-1::/folder') }), - state: () => ({}), - dispose: () => {} - } - }, - 'settings.task-preferences': (context) => { - const usePreferences = modules.load< - typeof import('../../tasks/use-mobile-tasks-client-settings-actions') - >( - 'mobile/src/tasks/use-mobile-tasks-client-settings-actions.tsx' - ).useMobileTasksClientSettingsActions - const model = observableModel(context, { - defaultGitHubPreset: 'all', - githubProjectSettings: {} - }) - Object.assign(model, { - client: context.client, - clientRef: { current: context.client }, - repoSelectionHydratedRef: { current: false }, - defaultRepoSelectionRef: { current: null }, - taskUiReady: true, - githubProjectFieldVisibilityScope: null, - taskResumeRef: { current: {} }, - trustedOrcaHooks: {} - }) - let actions: ReturnType - const hook = hookMount(() => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. - actions = usePreferences(model as unknown as Parameters[0]) - }) - return { - action(name, args) { - if (name === 'mount') { - return hook.mount() - } - if (name === 'write') { - return actions.persistDefaultGitHubPreset( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the preset arrives from the scenario JSON as a string. - args.preset as Parameters[0] - ) - } - throw new Error(`Unknown preferences action: ${name}`) - }, - state: () => ({ preset: model.defaultGitHubPreset }), - dispose: hook.unmount + adapters[operation] = adapter + } + } + return { + adapters, + assertMutationApplied: () => { + const applied = loaders.reduce((total, { modules }) => total + modules.mutationsApplied(), 0) + if (options.mutation && applied !== 1) { + throw new Error(`Expected one mutation, applied ${applied}`) } } } - return { adapters, assertMutationApplied: modules.assertMutationApplied } } diff --git a/mobile/src/test-support/rpc-recording/pilot-recordings.test.ts b/mobile/src/test-support/rpc-recording/pilot-recordings.test.ts index 87aa9dfc6ef..815e0e71c4b 100644 --- a/mobile/src/test-support/rpc-recording/pilot-recordings.test.ts +++ b/mobile/src/test-support/rpc-recording/pilot-recordings.test.ts @@ -2,7 +2,7 @@ import { readScenarios } from './scenario-input' import { pilotGoldens } from './derived-goldens' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' -import { runRecording, runRecordingMutant } from './run-recording' +import { runRecording } from './run-recording' import { pilotMountAdapters } from './pilot-mount-adapters' import { vitestRecordingScheduler } from './vitest-recording-scheduler' import { @@ -14,7 +14,6 @@ import { } from './golden-recording' import type { Recording } from './recording-scenario' import type { RecordedValue } from './recording-values' -import type { Mutation } from './operation-mutations' import { determinismRuns } from './determinism-runs' const root = resolve(import.meta.dirname, '../../../..') @@ -23,45 +22,6 @@ const input = readScenarios( resolve(root, 'mobile/rpc-foundation/pilot-scenarios.json') ) const goldens = process.env.RPC_FOUNDATION_GOLDENS ?? resolve(root, 'mobile/rpc-foundation/goldens') -// One mutant per adapter family, so every family's state projection is shown to be load-bearing. -const mutants: Record = { - b1: 'race', - b2: 'acceptance', - b3: 'order', - 'settings-bot-overrides-fulfilled': 'bot-overrides-envelope', - 'settings-workspace-context-fulfilled': 'workspace-context-envelope', - 'settings-home-providers-fulfilled': 'home-providers-linear', - 'settings-repo-metadata-fulfilled': 'repo-metadata-platform', - 'settings-task-hydration-fulfilled': 'task-hydration-envelope', - 'settings-task-write': 'task-preferences-optimistic', - 'settings-workspace-submit-fulfilled': 'workspace-submit-envelope', - 'settings-task-workspace-fulfilled': 'task-workspace-envelope' -} -/** - * The archived tree's visible state, pinned per seed: b1 serves the poisoned empty inventory, b2 - * accepts the null envelope and applies the label anyway, and b3 reports the issue error instead of - * the comments error. An unrelated refactor of those files can no longer keep this green by merely - * differing; the mutants remain the defect evidence and this run corroborates them. - */ -const referenceStates: Record = { - b1: { files: [] }, - b2: { - error: '', - mutating: false, - row: { - content: { - assignees: [], - labels: [{ color: '808080', name: 'recorded' }], - number: 1, - repository: 'owner/repo' - }, - id: 'item-1', - itemType: 'ISSUE' - } - }, - b3: { error: 'issue refused', loading: false, payload: { $rpc: 'null' } } -} - function visibleState(recording: Recording): RecordedValue { return recording.checkpoints.at(-1)!.observation.state } @@ -105,37 +65,5 @@ describe('RPC main recordings', () => { } } }) - const mutation = mutants[id] - if (!mutation) { - continue - } - it(`${id}: kills ${mutation}`, async () => { - const { adapters, assertMutationApplied } = pilotMountAdapters(root, { mutation }) - const result = await runRecordingMutant( - scenario, - adapters[scenario.operation], - vitestRecordingScheduler(), - readGolden(goldens, id).recording, - visibleState - ) - assertMutationApplied() - expect(result.verdict).toBe('killed') - }) - const reference = referenceStates[id] - if (!reference) { - continue - } - it.skipIf(!process.env.RPC_FOUNDATION_REFERENCE_ROOT)(`${id}: rejects bcba08b3e4`, async () => { - const { adapters } = pilotMountAdapters(process.env.RPC_FOUNDATION_REFERENCE_ROOT!, { - reference: true - }) - const result = await runRecording( - scenario, - adapters[scenario.operation], - vitestRecordingScheduler() - ) - expect(visibleState(result)).toEqual(reference) - expect(reference).not.toEqual(visibleState(readGolden(goldens, id).recording)) - }) } }) diff --git a/mobile/src/test-support/rpc-recording/recorder-digest.ts b/mobile/src/test-support/rpc-recording/recorder-digest.ts index b2a5e893a70..a4cd986f784 100644 --- a/mobile/src/test-support/rpc-recording/recorder-digest.ts +++ b/mobile/src/test-support/rpc-recording/recorder-digest.ts @@ -1,32 +1,56 @@ import { createHash } from 'node:crypto' import { readFileSync, readdirSync } from 'node:fs' import { join, posix } from 'node:path' +import { RECORDING_DRIVERS } from './recording-drivers' export const RECORDER_DIRECTORY = 'mobile/src/test-support/rpc-recording' +/** The per-domain mount adapters. Excluded below and pinned per golden by `adapterSha256` instead. */ +export const ADAPTER_DIRECTORY = `${RECORDER_DIRECTORY}/adapters` +/** + * Mutant evidence. Excluded below and pinned by nothing: no recording ever reads it. Deliberately + * not exported — an importable handle is a way for the recording path to name the directory without + * spelling it, and `mutants/mutant-seam.test.ts` rejects both spellings. + */ +const MUTANT_DIRECTORY = `${RECORDER_DIRECTORY}/mutants` const digests = new Map() +function skippedTest(name: string): boolean { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: membership test on a readonly literal tuple, not a cast of the value. + return name.endsWith('.test.ts') && !(RECORDING_DRIVERS as readonly string[]).includes(name) +} + function collect(root: string, relative: string, files: string[]): void { for (const entry of readdirSync(join(root, relative), { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1 )) { const child = `${relative}/${entry.name}` if (entry.isDirectory()) { - collect(root, child, files) - } else if (!entry.name.endsWith('.md')) { + if (child !== ADAPTER_DIRECTORY && child !== MUTANT_DIRECTORY) { + collect(root, child, files) + } + } else if (!entry.name.endsWith('.md') && !skippedTest(entry.name)) { files.push(child) } } } /** - * Every executable recorder input, so a golden is attributable to one runner. Prose is excluded - * because it cannot change a recording; a candidate run recomputes this and `compareGolden` fails - * the header, which forces a recorder edit to re-record deliberately. + * Every executable recorder input a golden shares with every other golden: the engine, and nothing + * domain-specific. Prose is excluded because it cannot change a recording; a candidate run + * recomputes this and `compareGolden` fails the header, which forces an engine edit to re-record + * deliberately. * - * The scenario manifest is deliberately not an input. It used to be, which made every golden's - * header a function of every other family's scenarios: adding one family re-digested all 153 files - * and put a conflict on that line in every domain branch. `scenarioSha256` pins each golden to the - * scenarios it was actually recorded from instead. + * A suite that does not record is absent too, for the same reason the mutants are: it cannot put an + * observation in a golden, so pinning it would claim a provenance the golden does not have. + * + * Three further inputs are deliberately absent. The scenario manifest used to be here, which made every + * golden's header a function of every other family's scenarios; the mount adapters used to be here + * too, which made it a function of every other family's adapter. `scenarioSha256` and + * `adapterSha256` pin each golden to its own instead. The mutants are absent for a different + * reason: nothing on the recording path reads them, so no edit there can change a recording, and + * pinning them would claim a provenance the golden does not have. All three cost the same thing + * when they were here — one domain's addition re-digested all 153 files and put a conflict on that + * line in every domain branch in flight. */ export function recorderSha256(root: string): string { const cached = digests.get(root) diff --git a/mobile/src/test-support/rpc-recording/recording-drivers.ts b/mobile/src/test-support/rpc-recording/recording-drivers.ts new file mode 100644 index 00000000000..65e6733bb98 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/recording-drivers.ts @@ -0,0 +1,9 @@ +/** + * The suites that write goldens. A golden's bytes come from one of these and from what it imports, + * so these are the only test files `recorderSha256` pins: a suite that merely reads goldens, or + * writes one to a scratch directory, cannot put an observation in a recorded file. + * + * `scripts/rpc-recording.mts` records exactly this list, so a suite that is added here and nowhere + * else still records, and one added there and not here does not exist. + */ +export const RECORDING_DRIVERS = ['pilot-recordings.test.ts', 'family-recordings.test.ts'] as const diff --git a/mobile/src/test-support/rpc-recording/recording-runner.test.ts b/mobile/src/test-support/rpc-recording/recording-runner.test.ts index 867a1aefcd5..0a92941e602 100644 --- a/mobile/src/test-support/rpc-recording/recording-runner.test.ts +++ b/mobile/src/test-support/rpc-recording/recording-runner.test.ts @@ -1,10 +1,19 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { join, resolve } from 'node:path' import { operationModuleLoader } from './operation-module-loader' import { describe, expect, it } from 'vitest' import { captureArguments, captureError, captureValue } from './recording-values' import { RECORDER_DIRECTORY, recorderSha256 } from './recorder-digest' +import { RECORDING_DRIVERS } from './recording-drivers' import { ScriptedRpcTransport } from './scripted-rpc-transport' import { vitestRecordingScheduler } from './vitest-recording-scheduler' import { @@ -393,14 +402,45 @@ describe('recording boundaries', () => { writeFileSync(join(directory, 'runner.ts'), 'export const runner = 1') const original = recorderSha256(root) writeFileSync(join(directory, 'README.md'), 'prose') - expect(recorderSha256(join(root, '.'))).toBe(original) + // Each call spells the root differently: `recorderSha256` caches per root string, so reusing + // one would assert nothing. + expect(recorderSha256(`${root}/`)).toBe(original) writeFileSync(join(directory, 'runner.ts'), 'export const runner = 2') - expect(recorderSha256(join(root, './'))).not.toBe(original) + expect(recorderSha256(`${root}//`)).not.toBe(original) } finally { rmSync(root, { recursive: true }) } }) + // A suite that only reads goldens cannot put an observation in one, so it is not provenance; the + // drivers are, because a golden's bytes come from them. + it('digests the recording drivers and no other suite', () => { + const root = mkdtempSync(join(tmpdir(), 'rpc-drivers-')) + try { + const directory = join(root, RECORDER_DIRECTORY) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'runner.ts'), 'export const runner = 1') + const original = recorderSha256(root) + writeFileSync(join(directory, 'reads-goldens.test.ts'), 'export const suite = 1') + expect(recorderSha256(`${root}/`)).toBe(original) + writeFileSync(join(directory, RECORDING_DRIVERS[0]), 'export const suite = 1') + expect(recorderSha256(`${root}//`)).not.toBe(original) + } finally { + rmSync(root, { recursive: true }) + } + }) + + it('keeps every named driver real and unimported by the engine', () => { + const directory = resolve(import.meta.dirname) + const missing = RECORDING_DRIVERS.filter((driver) => !existsSync(join(directory, driver))) + const imported = readdirSync(directory) + .filter((file) => file.endsWith('.ts')) + .filter((file) => + /(?:from|import\()\s*'\.[^']*\.test'/.test(readFileSync(join(directory, file), 'utf8')) + ) + expect({ missing, imported }).toEqual({ missing: [], imported: [] }) + }) + it('refuses a mutation anchor that matches more than once', () => { const root = mkdtempSync(join(tmpdir(), 'rpc-mutant-')) try { @@ -411,7 +451,14 @@ describe('recording boundaries', () => { join(root, 'mod/settings-read-operations.ts'), `const raw = {} as { settings?: unknown }\nconst settings = raw.settings\nexport function first() {\n ${anchor}\n return overrides\n}\nexport function second() {\n ${anchor}\n return overrides\n}\n` ) - const loader = operationModuleLoader(root, 'bot-overrides-envelope') + // Its own spec, not one borrowed from the mutant table: the guard is the loader's, and the + // table is not an input to anything the loader does while recording. + const loader = operationModuleLoader(root, { + name: 'repeated-anchor', + file: 'settings-read-operations.ts', + before: anchor, + after: 'const overrides = undefined' + }) expect(() => loader.load('mod/settings-read-operations.ts')).toThrow( 'matched 2 sites, expected 1' ) diff --git a/mobile/src/transport/rpc-operation.ts b/mobile/src/transport/rpc-operation.ts index f4ee18d9dfe..97f27143e93 100644 --- a/mobile/src/transport/rpc-operation.ts +++ b/mobile/src/transport/rpc-operation.ts @@ -247,11 +247,19 @@ export async function interpretAtRpcBarrier< ) as RpcBarrierVerdicts } -/** Preserves omitted sender arguments as well as explicit undefined. */ +/** + * Preserves omitted sender arguments as well as explicit undefined. + * + * A params type with no required field may be omitted too, because the raw port always allowed it + * and several hosts' schemas are entirely optional (`preflight.check`). Forcing `{}` there would + * put a new object on the wire where main sent no params at all. + */ type RpcSendArguments = void extends RpcSendParams ? [params?: RpcSendParams, options?: SendRequestOptions] - : [params: RpcSendParams, options?: SendRequestOptions] + : Record extends RpcSendParams + ? [params?: RpcSendParams, options?: SendRequestOptions] + : [params: RpcSendParams, options?: SendRequestOptions] /** Binds sending and interpretation while preserving the transport promise identity. */ export function bindDeferredRpcOperation< diff --git a/mobile/src/transport/rpc-reader-payload.ts b/mobile/src/transport/rpc-reader-payload.ts index 07cdebc16a5..4d6a8635007 100644 --- a/mobile/src/transport/rpc-reader-payload.ts +++ b/mobile/src/transport/rpc-reader-payload.ts @@ -25,3 +25,11 @@ export function rpcUncheckedPayloadReader( ): RpcCompatibleReader { return (raw) => rpcReadUnchecked(variant, raw) } + +/** One property off the payload, unchecked. The shape for a call site that cast `result.field`. */ +export function rpcUncheckedMemberReader( + variant: Variant, + key: string +): RpcCompatibleReader { + return (raw) => rpcReadUnchecked(variant, rpcPayloadMember(raw, key)) +} diff --git a/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts b/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts index 3e09655c5a1..722c4e58310 100644 --- a/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts +++ b/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts @@ -159,14 +159,18 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques // and mobile-git-mutation-operations.ts for the operations the rest of the domain now sends. { file: 'src/source-control/use-mobile-git-requests.ts', references: 1 }, - // src/tasks/ — task lists, filters and mutations - { file: 'src/tasks/composer-source-base-resolve.ts', references: 2 }, + // src/tasks/ — task lists, filters and mutations. The workspace-creation half migrated in + // step 4: create, hosted-base resolution, SSH/agent preflight, sparse presets, the Smart + // source picker's provider reads and the screen's own preference writes. See + // mobile-workspace-create-operations.ts, mobile-workspace-source-operations.ts, + // mobile-task-runtime-operations.ts and mobile-task-source-search-operations.ts. What is left + // is the provider item/detail/mutation half, plus two files that cannot reach zero: + // mobile-tasks-source-family.test-support.ts matches the literal in a source scanner rather + // than sending anything, and use-mobile-tasks-project-file-merge-actions.tsx and + // use-mobile-tasks-hosted-metadata-actions.tsx each multiplex a `{ method, params }` step the + // pickers hand them at runtime. { file: 'src/tasks/mobile-tasks-filter-pickers.tsx', references: 1 }, { file: 'src/tasks/mobile-tasks-source-family.test-support.ts', references: 1 }, - { file: 'src/tasks/setup-hook-trust.ts', references: 1 }, - { file: 'src/tasks/smart-source-paste-intent.ts', references: 4 }, - { file: 'src/tasks/smart-source-search-requests.ts', references: 5 }, - { file: 'src/tasks/use-mobile-tasks-client-settings-actions.tsx', references: 6 }, { file: 'src/tasks/use-mobile-tasks-github-check-file-actions.tsx', references: 5 }, { file: 'src/tasks/use-mobile-tasks-github-reply-merge-actions.tsx', references: 5 }, { file: 'src/tasks/use-mobile-tasks-gitlab-github-status-actions.tsx', references: 3 }, @@ -187,16 +191,9 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques { file: 'src/tasks/use-mobile-tasks-project-workspace-comment-actions.tsx', references: 3 }, { file: 'src/tasks/use-mobile-tasks-provider-load-actions.tsx', references: 5 }, { file: 'src/tasks/use-mobile-tasks-route-and-item-state.tsx', references: 1 }, - { file: 'src/tasks/use-mobile-tasks-runtime-hydration.tsx', references: 4 }, { file: 'src/tasks/use-mobile-tasks-task-create-actions.tsx', references: 3 }, { file: 'src/tasks/use-mobile-tasks-task-list-loading.tsx', references: 4 }, { file: 'src/tasks/use-mobile-tasks-task-pagination-actions.tsx', references: 1 }, - { file: 'src/tasks/use-mobile-tasks-workspace-create-actions.tsx', references: 3 }, - { file: 'src/tasks/use-mobile-tasks-workspace-source-effects.tsx', references: 2 }, - { file: 'src/tasks/use-mobile-tasks-workspace-sparse-actions.tsx', references: 2 }, - { file: 'src/tasks/use-mobile-tasks-workspace-ssh-state.tsx', references: 5 }, - { file: 'src/tasks/worktree-create-capability.ts', references: 1 }, - { file: 'src/tasks/worktree-create-retry.ts', references: 1 }, // src/terminal/ — terminal input, viewport and queries { file: 'src/terminal/mobile-terminal-query-reply.ts', references: 2 }, diff --git a/package.json b/package.json index e48079f8a1c..174a8718641 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,15 @@ "audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src", "test:perf:contracts": "vitest run --config config/vitest.performance.config.ts", "format": "oxfmt --write .", - "lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-runtime-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage", + "lint": "oxlint && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalog && pnpm run verify:localization-runtime-catalog && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage", "audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor", "audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings", "audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings", "audit:react-doctor": "pnpm dlx react-doctor@0.9.1 . --yes --no-supply-chain --no-telemetry --blocking none", "audit:dead-code": "pnpm dlx knip@5.88.1 --config config/knip.json", "check:code-quality:changed": "node config/scripts/check-changed-code-quality.mjs", + "check:dead-classes": "oxlint --config config/oxlint-dead-classes.json src/renderer", + "lint:design-system": "oxlint --config config/oxlint-design-system.json src/renderer", "check:react-doctor:changed": "node config/scripts/check-react-doctor-changed.mjs", "check:zustand-selector-fanout": "node config/scripts/zustand-selector-fanout-benchmark.mjs --check", "doctor": "pnpm dlx react-doctor@0.9.1 . --no-telemetry", @@ -199,30 +201,31 @@ "@monaco-editor/react": "^4.7.0", "@playwright/test": "^1.59.1", "@sanity/diff-match-patch": "^3.2.0", + "@shadcn/lint": "^0.1.0", "@stablyai/playwright-test": "^2.1.14", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-virtual": "^3.14.10", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@tiptap/extension-code-block": "^3.22.5", - "@tiptap/extension-code-block-lowlight": "^3.22.5", - "@tiptap/extension-details": "^3.22.5", - "@tiptap/extension-image": "^3.22.5", - "@tiptap/extension-link": "^3.22.5", - "@tiptap/extension-list": "^3.22.5", - "@tiptap/extension-mathematics": "3.22.5", - "@tiptap/extension-placeholder": "^3.22.5", - "@tiptap/extension-table": "3.22.4", - "@tiptap/extension-table-cell": "3.22.4", - "@tiptap/extension-table-header": "3.22.4", - "@tiptap/extension-table-row": "3.22.4", - "@tiptap/extension-task-item": "^3.22.5", - "@tiptap/extension-task-list": "^3.22.5", - "@tiptap/markdown": "^3.22.5", - "@tiptap/pm": "^3.22.5", - "@tiptap/react": "^3.22.5", - "@tiptap/starter-kit": "^3.22.5", + "@tiptap/extension-code-block": "3.31.3", + "@tiptap/extension-code-block-lowlight": "3.31.3", + "@tiptap/extension-details": "3.31.3", + "@tiptap/extension-image": "3.31.3", + "@tiptap/extension-link": "3.31.3", + "@tiptap/extension-list": "3.31.3", + "@tiptap/extension-mathematics": "3.31.3", + "@tiptap/extension-placeholder": "3.31.3", + "@tiptap/extension-table": "3.31.3", + "@tiptap/extension-table-cell": "3.31.3", + "@tiptap/extension-table-header": "3.31.3", + "@tiptap/extension-table-row": "3.31.3", + "@tiptap/extension-task-item": "3.31.3", + "@tiptap/extension-task-list": "3.31.3", + "@tiptap/markdown": "3.31.3", + "@tiptap/pm": "3.31.3", + "@tiptap/react": "3.31.3", + "@tiptap/starter-kit": "3.31.3", "@types/node": "^25.6.0", "@types/proper-lockfile": "^4.1.4", "@types/qrcode": "^1.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d93fec995ef..6e9de58b228 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: '@sanity/diff-match-patch': specifier: ^3.2.0 version: 3.2.0 + '@shadcn/lint': + specifier: ^0.1.0 + version: 0.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2) '@stablyai/playwright-test': specifier: ^2.1.14 version: 2.1.14(@playwright/test@1.59.1)(zod@4.5.4) @@ -238,59 +241,59 @@ importers: specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@tiptap/extension-code-block': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-code-block-lowlight': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(highlight.js@11.11.1)(lowlight@3.3.0) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-code-block@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(highlight.js@11.11.1)(lowlight@3.3.0) '@tiptap/extension-details': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-text-style@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-text-style@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)))(@tiptap/pm@3.31.3) '@tiptap/extension-image': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-link': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-list': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-mathematics': - specifier: 3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(katex@0.16.45) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(katex@0.16.45) '@tiptap/extension-placeholder': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-table': - specifier: 3.22.4 - version: 3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/extension-table-cell': - specifier: 3.22.4 - version: 3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-table-header': - specifier: 3.22.4 - version: 3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-table-row': - specifier: 3.22.4 - version: 3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-task-item': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/extension-task-list': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) + specifier: 3.31.3 + version: 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) '@tiptap/markdown': - specifier: ^3.22.5 - version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + specifier: 3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) '@tiptap/pm': - specifier: ^3.22.5 - version: 3.22.5 + specifier: 3.31.3 + version: 3.31.3 '@tiptap/react': - specifier: ^3.22.5 - version: 3.22.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 3.31.3 + version: 3.31.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tiptap/starter-kit': - specifier: ^3.22.5 - version: 3.22.5 + specifier: 3.31.3 + version: 3.31.3 '@types/node': specifier: ^25.6.0 version: 25.9.5 @@ -718,6 +721,12 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} @@ -978,6 +987,40 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -1004,6 +1047,26 @@ packages: peerDependencies: hono: ^4 + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -1164,6 +1227,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@linear/sdk@82.1.0': resolution: {integrity: sha512-Ok7o+LqXaenx6Um58NQqjQoQanDsCgAIe9yNgpVbqRSh5APz3Ds1kZUz2vWmSNTNATFZm1zDQtEktTMga2X7UQ==} engines: {node: '>=18.x'} @@ -1338,42 +1410,84 @@ packages: cpu: [arm] os: [android] + '@oxc-parser/binding-android-arm-eabi@0.148.0': + resolution: {integrity: sha512-pHASv9g5pASxb7akHERZNSkrEqPhFaUix98o7d9hbTpolnnFWl7UiRrcMhCsV1+iVO4/cJwKsbKRJTFNs2tdBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxc-parser/binding-android-arm64@0.141.0': resolution: {integrity: sha512-a4XDQ27ZT7e7zwAlxJDTiCA7IBGWDuy2+MhFq85Of7XlBSmpkfcBFml11q0Zx6f7RMuI0B4xCtt2ytBS4yOptg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxc-parser/binding-android-arm64@0.148.0': + resolution: {integrity: sha512-sg/6Ez0KdAygsu0POELux9wN1Po2CP93WY8eNl4DBKIGprsd4QSHBXOb471Pu9i2OCD5sLkISSb2agZEhVn2Zw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxc-parser/binding-darwin-arm64@0.141.0': resolution: {integrity: sha512-m/kVk6rzYmBeHYnz+1Y5fod00AVTTxMbC71azFfm/zjx1j9XxwKtA0+VfkKuVMC8rbghb9TtfevnuWZa9OuPEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxc-parser/binding-darwin-arm64@0.148.0': + resolution: {integrity: sha512-yiSJmzGUvCUaJT8X3j40gVcX+ckuHQMuiOtF8DvzTs5+JtB/7XuHFPp4M+vv5u+HlBtDUd4Ks5pyHpWz8mfnkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxc-parser/binding-darwin-x64@0.141.0': resolution: {integrity: sha512-o0X+6KZlfucWU/v5oKRQPwdFXsXAjW8jmpo/Gpw/qyKsbKtlfkHoeH9Bjp/m13TwjewvJnCkwF0DWzgpC4HjTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxc-parser/binding-darwin-x64@0.148.0': + resolution: {integrity: sha512-6ZeklaamrMy4H2JmhvcJg6iip59tYILtuLaILxyAHT3l5FDxnI5ihVievAft5ZmAbqtlWHErOi1OpJK8gy1wcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxc-parser/binding-freebsd-x64@0.141.0': resolution: {integrity: sha512-W5KbTnNkTMMMylqj6dYqnsXvkmESVPodPKYLJ5zdzIPdl9fUJtolkpUeSzYEbGGYB4a4A4avl3EePnZ/wLIdJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxc-parser/binding-freebsd-x64@0.148.0': + resolution: {integrity: sha512-vFsPx+a/qFECPnz/H8nC6x6MDvnWscLTCo/5muojEF54ERUq1kdgbvnWo95YnkhjF9sTIcG/uDxQBh1gffaufQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxc-parser/binding-linux-arm-gnueabihf@0.141.0': resolution: {integrity: sha512-g3dtbJa8zeOGK36Sr9cQavsdi5H/ie2hVjrSjIxsNAR1qZA40ZYVXnfdfoMAlq8CmB9qFL1yhsSCUHeNmdmt8w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-gnueabihf@0.148.0': + resolution: {integrity: sha512-eOr3M+6iGbbxNL4PSS0VtsyQ2eOUxSBh00BqO22SbolDimPSYsBuLr/LCrZBkiqW2BoabhR6V4R8jrRAay7hjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.141.0': resolution: {integrity: sha512-e6hwQqd+3lvP13G2jxvFpoA7dzHcFLN+Mq47JCVMtdNHbbyBRo756JCtbbJH6ca8inTfyqZoqBmS3vhQlzAK2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.148.0': + resolution: {integrity: sha512-58ZKDw0mQRbCNfrd2IDyV4o8T7enzGERJn41BH2tjrZVGyiKiFzcfDicuB7Zcpb/1xIOrObovr8Dja6lZi8dLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm64-gnu@0.141.0': resolution: {integrity: sha512-vXz2BLAuypA+4MLyBg94pzEo6THVnzYnCtAjXoihIIQo0t2pnp/AmW+SH1EI+4VbuJnC//KplIJ5yyaCGua4jA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1381,6 +1495,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-arm64-gnu@0.148.0': + resolution: {integrity: sha512-Fnu95O4eZ5i++GPvIzBEZ8y4ddTLR+D9paYa8JRaRk6ZK7nHQiWP5xtrhcPQsXqgat1d7sU/d5rbbI0p1FTHSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-arm64-musl@0.141.0': resolution: {integrity: sha512-jMkS/EztNW34HKsXIaT/SoHcmtocq/vWhwFOVduF9kduuuRIVwfwQ6uxzIO+qPKSXdd2TXt54of0BJ2zFMXnmw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1388,6 +1509,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-arm64-musl@0.148.0': + resolution: {integrity: sha512-3CQy/BMdx7N7H3qrcPxUL+a2CwUZodUcf6oq8iJuNZ9C6Ol1aq3mcWzsgySJ7CHFLvpX21ZDPp1r1X0QLbu/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-ppc64-gnu@0.141.0': resolution: {integrity: sha512-vo+MR+n3zQJ6Mq92hiP084NZcgDv5iJlVR02gMf28neMvVT1tKVm7VeiW/DxhdqOi3QLeaXIk9cUcLL1qrkngw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1395,6 +1523,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-ppc64-gnu@0.148.0': + resolution: {integrity: sha512-9LkaYvfiF8hMOw900csAvkf1oxE8XlmMeGowu5BcastSSwV8mKvKRMNU7HsV+ycyj1dQD8pX5qgOw8ja6SJacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.141.0': resolution: {integrity: sha512-oh80w+7RuiO5gBp9Jnoa/H8Qlt3JsHL2MkW+0dwEdlDMdslVZX/YsekSK6EeyEenY66/mhCfypsNATQ7Ph3qlQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1402,6 +1537,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.148.0': + resolution: {integrity: sha512-2GBiM9h26dR4WJfhoMvnFMnFLf7m/kYs4UMqjvrOfQG4BV1nuTJDH22Zc2MQr3INZF7nSKYQ6xlhD3hQ7A6gug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-musl@0.141.0': resolution: {integrity: sha512-LOyEmFA8sCnYbEXP1+iQvCC/P1YXHMA/t6x1Ksp0Y9VwhLFsiBJFzV1zIxrOIE2LKaGGhDjQ29xq9cbq6omDXA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1409,6 +1551,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-riscv64-musl@0.148.0': + resolution: {integrity: sha512-uPqZexvKJmEgq4mAu36qe2xTfXZE7oyik1R7KtZ5tl8qKlq1U1fIqTFRUEBZqRGvforoTrGIpatRzcoPKO66RA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-s390x-gnu@0.141.0': resolution: {integrity: sha512-3wnwk/l1CvszVE5TJR1wSl/zSEfydRqrNhn6s7Vr9IzSJpUQIroqVsIoPARHRFA+FQwkxAFDAHDAasa7v8OobQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1416,6 +1565,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-s390x-gnu@0.148.0': + resolution: {integrity: sha512-9oUHvnTbp7ZraFsTC8PN6XhdhPSSxZumYvixWl7Smi353gEULvK6yV0sXNVrdFMHQeaDKFCi8TgDhNK7/A+Y+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.141.0': resolution: {integrity: sha512-qtyQVAAebFq57B2tifTlel3TgGqUtsYNI/e+p6aya9rN9lOZVTDvr215fGYSA9XWooxzMxDiVxkBLk2jQHbsOQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1423,6 +1579,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.148.0': + resolution: {integrity: sha512-2qhDSJwKzbSZzF7lDqqk8sr/yXsmwr3PeUa4/nazIF+zFAYz1gVPEfC34GQtGxzJUUmklaYAL63368LEfrMeyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-musl@0.141.0': resolution: {integrity: sha512-SkGV1nKw40roEc94pv5EaaeH2ay14G6+roe8Q0wIUC1LcEKxzKW921h7+ZuZX0D3q2Mb/7aSFmxEVqnko3lPRw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1430,12 +1593,25 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-x64-musl@0.148.0': + resolution: {integrity: sha512-qQoPDZUFV0bh9xA09XydmkjMBpgc1ukJuhMvzQ9QeVmFaHTS9W5TE5CoLmSl3QQyUP9OuHO3x/WPZTIIZPWR3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-openharmony-arm64@0.141.0': resolution: {integrity: sha512-cVgDM7n8QziQqOaP5hNgUYfMG7S/ZeuPxFWXnnHRv7rh025COk0rfQ6eEdKG3j/GaUuyvNZN4ifF1J8KmuXLLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxc-parser/binding-openharmony-arm64@0.148.0': + resolution: {integrity: sha512-1UGbaQWEXUCLqAmaR5kwRDjx/R4S5LQKZkM9CHmaHkuKhriOF32aRLfS0jCRNE2yGQJLMEA1z9UucbBVqjXnDw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxc-parser/binding-wasm32-wasi@0.141.0': resolution: {integrity: sha512-HggH++Fkn3OilBn+bs3jpgIFQa34oMAyUUHy0vpGum+gt1Eb5nyLc8dNU/RAPSw6lsLrx7ncKtHSZE+3Sp0l2g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1447,18 +1623,36 @@ packages: cpu: [arm64] os: [win32] + '@oxc-parser/binding-win32-arm64-msvc@0.148.0': + resolution: {integrity: sha512-pWKdzRDNG2+NK4h/V6U/CYERcfYD6u28h5IB/VJVsrZaD3muvE58tUj22lieL5vLZ+XFi1GPv9YXckZbJZ9BLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.141.0': resolution: {integrity: sha512-9UVWUOOCI/1YkiSSNjg2zyBJYM9E/t1A/8GNobd48JDn/fQ6mzxcVO3H08jb3rAaW/B1VBf8eCORTvSsO9T08g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.148.0': + resolution: {integrity: sha512-i3p4x+mvwtjcE1J5HM6V7ggsbXiznExN/4MkNyOy3dfXrVV3bnkSfmZxvo6/84qCVX4ShkpNE1SKt9biIF31GQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.141.0': resolution: {integrity: sha512-HI/wsvbWT5RHHw5c37D0fEgeTd8/1Q4OJs5jUmEBc17VZFG6SsCIe4barq7NsAPPks/JW+3ayi3Rp+PQI5h4Kg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.148.0': + resolution: {integrity: sha512-Ye6vB7VQulghWYkYkECOBYFRVEizz4XyRTUAv+t8BuyurhKU7uD0P9eowL+mKG5Mf8MSYx+DI3Cm8SKZvYG7bQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-project/runtime@0.101.0': resolution: {integrity: sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1469,6 +1663,9 @@ packages: '@oxc-project/types@0.141.0': resolution: {integrity: sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ==} + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + '@oxfmt/binding-android-arm-eabi@0.65.0': resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2644,6 +2841,15 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shadcn/lint@0.1.0': + resolution: {integrity: sha512-UDSxO4eQa8UAclN1tChum+L336CL2uB2ZLGYiJ7r/GDrYUBOKPWWNUVoAfh2dZs4LhcwJRCn62+fKG12eAy1FQ==} + engines: {node: '>=20.19'} + peerDependencies: + eslint: '>=9.30.0' + peerDependenciesMeta: + eslint: + optional: true + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -2910,229 +3116,230 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tiptap/core@3.22.5': - resolution: {integrity: sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==} + '@tiptap/core@3.31.3': + resolution: {integrity: sha512-Cz50pvciQrxdSxgTkHOVz0uD0Yl/8Xt0QatGD6ILm47jW8EzyHR9RkUGs/D5IqzXKuVPntfw1ttaT926vXfiRg==} peerDependencies: - '@tiptap/pm': 3.22.5 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-blockquote@3.22.5': - resolution: {integrity: sha512-ajyP5W8fG5Hrru47T/eF3xMKOpNvWofgNJqBTeNuGl02sYxsy9a4EunyFxudsaZP9WW3VOD4SaIWr5+MqpbnOQ==} + '@tiptap/extension-blockquote@3.31.3': + resolution: {integrity: sha512-fyY2XMbyDDDfOTQ1Qdrnqa1qwC9DWE4n7AfE0EKQI0G8MfLV8RaDlLDcOZDJ9JbMPY7/Gx7EjyK4NKxSW9n2hQ==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-bold@3.22.5': - resolution: {integrity: sha512-l/uDtpJISiFFyfctvnODNWBN/XPZI1jVZRacTRDDnSn8+x6KQ7G2qgFYueU7KvVJGDFVT39Iio56mcFRG/Pozg==} + '@tiptap/extension-bold@3.31.3': + resolution: {integrity: sha512-dIuYhKk8TitKU/FeDpoTeWZhU42YgDN5npgWNjAmMmRktPdoxnH3/wGSiwXlqZgJWjehN7kPWDePWpJeAImGpQ==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-bubble-menu@3.22.5': - resolution: {integrity: sha512-yrNlFQQJY5MmhBpmD8tnmaSmyUQrEvgyPKa3bzVeWEhDSG1CW4A0ZSMx3hrA9yFO0HWfw3IJmvSCycEZQBalpQ==} + '@tiptap/extension-bubble-menu@3.31.3': + resolution: {integrity: sha512-EV6ZnwKc++2OM/OcD54n8s1B7C9LP7GKAtdEPwu0t3BYJf3sG8Ueoinf7SgGPO9oMPg96GneOkDNm1urMV167g==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-bullet-list@3.22.5': - resolution: {integrity: sha512-cf54fG9AybU8NgPMv1TOcoqAkELeRc/VpnSCt/rIJZphWQx9nsFmrtkrlCatrIcCaGtNZYwlHlMnC5LVVMu0uA==} + '@tiptap/extension-bullet-list@3.31.3': + resolution: {integrity: sha512-qEyyoPapPef4LO8XKaN83bxtaNzkJ4kFn/IxLnEKd4BJ3Mvi4MH2yJYlyDqwFnMoev4pMA1zBHRAt/C0THRmZw==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-code-block-lowlight@3.22.5': - resolution: {integrity: sha512-lT0SxhjkDL1tKSeVDduV+SJ6kHdNFcbYBaUAwTufRtDt8FIYcSX6tWj5cPEXOFrC0PlJu7ybCnTEbXBdFP8Bnw==} + '@tiptap/extension-code-block-lowlight@3.31.3': + resolution: {integrity: sha512-DN21CYEL4bm01vyB/wiyDPKrpiyalI5okTWx90jRuTqRzbnkPlnng1Vdo9L220w3GXct5Exe6iI01E7F19bC5A==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/extension-code-block': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/extension-code-block': 3.31.3 + '@tiptap/pm': 3.31.3 highlight.js: ^11 lowlight: ^2 || ^3 - '@tiptap/extension-code-block@3.22.5': - resolution: {integrity: sha512-d123kCfLdJTi4fue1m0+TNFztDkmIRSZGZmGu6H9KqwG5Q7IzjT9o8lzRsz+pXxYqHvqgYmXoEpM6srbzXx/Ag==} + '@tiptap/extension-code-block@3.31.3': + resolution: {integrity: sha512-nvknt4FhyJQjYcvxptmeUlFsIAc8ibua3E5BN4Pim374/9RWepH4cdE9X0/qUTtguHElLx0iKtSIY3rW2qGTFA==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-code@3.22.5': - resolution: {integrity: sha512-mwDNOJC9rYbDu/JcqrN4dbUQRklJU8Fuk2raxD/IvFw9qUIcPCmxQ2XT9UTKmZz/Ju7Kdy72fss6XpgWv6gLAQ==} + '@tiptap/extension-code@3.31.3': + resolution: {integrity: sha512-SzxOqchrD2AcN3uT67PjKmRFEMOU3vNNiwNaamZJUbZrI6Hmy+bvdHJrc3jrIddyyCrAtsnAfRI0cmorW1jGfg==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-details@3.22.5': - resolution: {integrity: sha512-+vg7wSO9DL8veAzC4jlHu4lQ4qL2iRxj/ONTfP+jnffN0TzrjAdkMLCUEX059gYyks22AXgoI5vEzWs0K7yeuw==} + '@tiptap/extension-details@3.31.3': + resolution: {integrity: sha512-J5kdZy31wb97iKUR4sDsX0sEyRPlTijKKRAnVbInY2aNdFuNBTBdMPmblEJztxfMN+TmDrU2kCVv72J9B1SrJg==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/extension-text-style': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/extension-text-style': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-document@3.22.5': - resolution: {integrity: sha512-8NJERd+pCtvSuEP4C4WMGYmRRCV12ePZL7bC+QUdFlbdXg+kNZS0zZ7hh879tYA0Kidbi8rWWD1Tx+H2ezkmMw==} + '@tiptap/extension-document@3.31.3': + resolution: {integrity: sha512-EexgmqnyDNyGlISxo7SMrp5MygpJYmqD+0cY5jB6L1U6L4CpKKRWUt8OO1sWzEHDE1+TTvwt+WIFoIWAziOtEA==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-dropcursor@3.22.5': - resolution: {integrity: sha512-Mp40DaFrY3sEUVtFqmxrR0BmU4G3k8GCYYNGqNa9OqWv7BrcFDC03V2n3okESDKt4MKkzhQQmypq+ouLy8dLfA==} + '@tiptap/extension-dropcursor@3.31.3': + resolution: {integrity: sha512-NWomSfu5CSC7VacnMSDzKT8qm66SzMfZwVPEtwY5bPpRTJgTiT1rNK0neDrrzfMN27MfylGyKWWf7Q5Qf8w/fg==} peerDependencies: - '@tiptap/extensions': 3.22.5 + '@tiptap/extensions': 3.31.3 - '@tiptap/extension-floating-menu@3.22.5': - resolution: {integrity: sha512-dhem4sTPhyQgQ+pFp2Oud4k4FSQz9PVMgeQAC9288SmGwxBkJNveDAw6sKTMrumqDvwkJrtslXIupq9TZYQnzg==} + '@tiptap/extension-floating-menu@3.31.3': + resolution: {integrity: sha512-rd4VJ9PGSP9Eop8ZTEwaLZcMzMXLuJKe3hUNf58rq+zANpwM+9fI+vB7g9MAc3eXwUNDxNDVACFIL2oeBqpQ3A==} peerDependencies: '@floating-ui/dom': ^1.0.0 - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-gapcursor@3.22.5': - resolution: {integrity: sha512-4WkMu7qqjbsm8hCQS+8X+la1wjriN0SKoRdvpfKH33qM50MB34tYJuGLAO+y7TTh4MMMco3AZCKPBL5JVMqNIg==} + '@tiptap/extension-gapcursor@3.31.3': + resolution: {integrity: sha512-EBXKb1FrVStsNYCcRGtd9jmzveCvR+eqgg1rVqoONrqFK6U7bga6LN+1dMKroP1kliDVgveYkP3vRYxqw+rFqg==} peerDependencies: - '@tiptap/extensions': 3.22.5 + '@tiptap/extensions': 3.31.3 - '@tiptap/extension-hard-break@3.22.5': - resolution: {integrity: sha512-n0R2mUVYZU2AVbJhg/WcY9+zx690wVwvsItHJf0DrYbf1tCYHx+PRHUt/AoXk6u8BSmnkb8/FDziS8m3mjfpSg==} + '@tiptap/extension-hard-break@3.31.3': + resolution: {integrity: sha512-QAdCvNO4+yW9ATwsrej11NTkDYFqPLIEQr3ARNrKOK1qaiS7A0fia2SEukb/hrkP3A6mbozhoQt2r2RGUf/DpQ==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-heading@3.22.5': - resolution: {integrity: sha512-hjyEG4947PAhMBfP1G6B0QAh6+y9mp2C5BQmNjprA05/lQzDAT7KFZzNh8ZVp3ol6aICKq/N1gFOW9Dc/9FUOw==} + '@tiptap/extension-heading@3.31.3': + resolution: {integrity: sha512-rk5VHMAeQcg06SLauN6EGdD2jc0O2qY8QkZYPd0LxNvLblw2BxBx+lxUQSYwLAT9Ie5914gKIK2YbRyO2Ts3ig==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-horizontal-rule@3.22.5': - resolution: {integrity: sha512-vUV0/ugIbXOc8SJib0h8UMhgcqZXWu/dkEhlswZN4VVven1o5enkfxEiDw+OyIJHi5rUkrdhsQ/KTxG/Xb7X8A==} + '@tiptap/extension-horizontal-rule@3.31.3': + resolution: {integrity: sha512-YnHGy2KShRwvCseAmmxl9VP7R0qaj8QMp3DA6DJWZqp7r5gLGvDkAqhedxqqefqsE4Y43hjkBdjtB9Ce78LkIw==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-image@3.22.5': - resolution: {integrity: sha512-ezMzA6w6UsPesQp6fxTQojI/IkGJLmkwR/VGTimva7sudP3HdSW8k3SGBkjfvp0L2xqUrC/l4nWOchu01A/xtQ==} + '@tiptap/extension-image@3.31.3': + resolution: {integrity: sha512-wWNG9BOtx2Cg4vwxPVGDIJ5DGX+ETkldeoaFJLB1NXUL4OPUiVTf8cHZ+hdYgJWP704BEB7jQgjlpvdi6VigTg==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-italic@3.22.5': - resolution: {integrity: sha512-4T8baSiLkeIymTgEwirxDFt5YgYofkP3m1+MGYdGy2HKcOK+1vpvlPhEO1X5qtZngtJW5S4+njKjinRg52A4PA==} + '@tiptap/extension-italic@3.31.3': + resolution: {integrity: sha512-ibGvdvAPyfxBMUVNRI43eb9h2/Jka1MRG5GtnGqbcCX/2+Y/y0EOfFrPQPkuYphiWQYyu9+FzPKMB/pjuaLuKQ==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-link@3.22.5': - resolution: {integrity: sha512-d671MvF3GPKoS2OVxjIlQ7hIE7MS3hREdR+d4cvnnoiLLD+ZJ6KgDnxmWqF0a1s4qxLWK2KxKRSOIfYGE31QWQ==} + '@tiptap/extension-link@3.31.3': + resolution: {integrity: sha512-986wOQzTL9Zr5lf84LCLpm+YOms8A0K39/8DVoqRfebqcOe0/eq4bnztmAlfabOd+kJY92g3AgZERFUx/w+dcw==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-list-item@3.22.5': - resolution: {integrity: sha512-W7uTmyKLhlsvuTPLv+8WwnsY+mlikBFIoLSvVcBaFt4MwpsZ+DeB6KQg02Y7tbtaAnG7rXu9Fvw2QORh2P728A==} + '@tiptap/extension-list-item@3.31.3': + resolution: {integrity: sha512-4QlKOriJJMvg95QJTEsy9BUYPBQ6UvJyb8WURRwdUtQUkkqb8h32lg/eyQUv2FzW9IT1AdUyNZfVaxH64kRfQA==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-list-keymap@3.22.5': - resolution: {integrity: sha512-cGUnxJ0y515e1bVHNjUmbx7oWHoEon59w6BA5N2KwV9iW2mZZchlTX4yxJSOX+ixeVRChsa7YwC3Z1jUZ6AMEg==} + '@tiptap/extension-list-keymap@3.31.3': + resolution: {integrity: sha512-If8UOEdDZbPJU6iYTvLtH6DOp2KBy6BKxg9UELL1AevVetGHEF/7lW8hP50Gn1tHMVpPRqmhSzVrJRpEJJgb/Q==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-list@3.22.5': - resolution: {integrity: sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==} + '@tiptap/extension-list@3.31.3': + resolution: {integrity: sha512-LoveGnC0FVdCV4jUNBaG1ZA+KWE07+adzV3kGy6uUYFcJEjbVUHTnPDrBOob3IwOSO3sCwIkvQb6EVYeXn/4yg==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-mathematics@3.22.5': - resolution: {integrity: sha512-ld2xoFHKyl4Qs+rgu3wn1UZBTsgDApEz2PD17E/XWlVXHO4KoiBOcLhrc5L9SL+aKOBXXmC/Ex1d+0hCptsBbg==} + '@tiptap/extension-mathematics@3.31.3': + resolution: {integrity: sha512-R19lI2hLSXkQ+aqKvvyrDdI+HnmFtmfTVotrQ5W06i8WxVqRSq5UpsPIRCoeUeBL8uCvKinfmSVJyX2V80Bayg==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 - katex: ^0.16.4 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 + katex: ^0.16.4 || ^0.17.0 || ^0.18.0 - '@tiptap/extension-ordered-list@3.22.5': - resolution: {integrity: sha512-OXdh4k4CNrukwiSdWdEQ49uvgnqvR0Z9aNSP4HI5/kZQ/Te1NtRtYCpUrzWyO/7CtjcCisXHti0o9C/TV8YMbQ==} + '@tiptap/extension-ordered-list@3.31.3': + resolution: {integrity: sha512-mp3g11NgA/PYu8rj7J7Ez3l4qBy6WfTSmHIG4PZvEGG5w2oUAIkgb9DU7nPPzjmeme27oazFYZw+AtZA0+u4tw==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-paragraph@3.22.5': - resolution: {integrity: sha512-52KCto4+XKpnBWpIufspWLyq4UWxAWC72ANPdGuIhbi72NRTabiTbTVN40uwGSPkyakeESG0/vKdWJCVvB4f0g==} + '@tiptap/extension-paragraph@3.31.3': + resolution: {integrity: sha512-+iPku7wJfy5hbNDNLX8dveFtYVsZMmh7vztjuq8hT3mipSC4IByDehDbU8fzUCjfXiEzmI7mQn7c8LGmHULuzA==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-placeholder@3.22.5': - resolution: {integrity: sha512-MZAohQ3FCS763BkhGXgaWRya6WruZjwRwEAkXP8vkxbERzl2OJRjniS4uXCWzAlRb3ttE103SnY7LMdM8FvsXw==} + '@tiptap/extension-placeholder@3.31.3': + resolution: {integrity: sha512-9jYtR8ELEw7GVaruyrm4oFkPcjig9Q+crc+dpmarhBNXUmxagCdlhVzNwCJ2WJRzvBAtx59sEYqNTU38Wx8S3A==} peerDependencies: - '@tiptap/extensions': 3.22.5 + '@tiptap/extensions': 3.31.3 - '@tiptap/extension-strike@3.22.5': - resolution: {integrity: sha512-42WrrFK5gOom/0znH85x12Mw5IQ/6O6DWdyUWoRIrNA/qJpuHtU8oVU+bIgU2tuomMGHruRjIzgBQv5sBjEtww==} + '@tiptap/extension-strike@3.31.3': + resolution: {integrity: sha512-G29bhKttYwcKHT+BI6emWVFol3RO/gUXxQVcmr/iT8LXXy7j8J6HFUnsKM+Kg5YlP1rxMRgsa65dbrQVahZi0A==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-table-cell@3.22.4': - resolution: {integrity: sha512-uvFegCc1UQYK2nfIV2sIHg+hzLIMroJJm00XomzBgC1w/eSO7Ui8APiDh/baBcTPpCSU3SLiQLTgx7AU7oE3pg==} + '@tiptap/extension-table-cell@3.31.3': + resolution: {integrity: sha512-5nueKR/p/IX6B4etWqHjyRsjNfDf6dJZaRgfw1/1l90CJe2h3tTZk4JbUKWN3Mo4FzZuYKzmU3fxcZ8pSVOeJQ==} peerDependencies: - '@tiptap/extension-table': 3.22.4 + '@tiptap/extension-table': 3.31.3 - '@tiptap/extension-table-header@3.22.4': - resolution: {integrity: sha512-V4kLLWeRdc/I+IXiXZZhLAjsaHHiJWuLXTuOtZRDrCxQUiFLi4AgNg1DPQ09JAANkEWDhXq3x6BoUXaFwumbEw==} + '@tiptap/extension-table-header@3.31.3': + resolution: {integrity: sha512-sstVtNQiBYX4P16vlhwBYPsthxDnodHWQfGq0EUQM40Z7H796Lgh9t1qYtu8yARQuHKzvINRwJCSy6DZ8YTDtQ==} peerDependencies: - '@tiptap/extension-table': 3.22.4 + '@tiptap/extension-table': 3.31.3 - '@tiptap/extension-table-row@3.22.4': - resolution: {integrity: sha512-9tdS6jgS6DqUu5TpEmNrRoo/DL5Xam0PyrQaUEXUC+ssci+bMRCJ8PAWMcunNsI9NKf/Tb3wYrv6hGFChaT9uA==} + '@tiptap/extension-table-row@3.31.3': + resolution: {integrity: sha512-up6tDK+hYVTFDeJ3XqKO0WJqBzyXNfGxMoimo5h83jrq9dcNh0oPCOzglZ7rdo8GH9I3VJ2fNKxxU2Eg2HcMoQ==} peerDependencies: - '@tiptap/extension-table': 3.22.4 + '@tiptap/extension-table': 3.31.3 - '@tiptap/extension-table@3.22.4': - resolution: {integrity: sha512-kjvLv3Z4JI+1tLDqZKa+bKU8VcxY+ZOyMCKWQA7wYmy8nKWkLJ60W+xy8AcXXpHB2goCIgSFLhsTyswx0GXH4w==} + '@tiptap/extension-table@3.31.3': + resolution: {integrity: sha512-7cnVPHhdiGGeauYqca6JVyPLTqZbqFEEk9nn2e2E8+fBo6zVtV07AktJBqth/XEzjZxcUmGxjoeuWYAisWjUHg==} peerDependencies: - '@tiptap/core': 3.22.4 - '@tiptap/pm': 3.22.4 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-task-item@3.22.5': - resolution: {integrity: sha512-OVJKiq67lU+RiC6slIhhgTJBlP/Vads6MZ7Ld5wxzCtWMdGKDuzQ1dgF7vrMEs7mhSeSH3phNcIdQ5ypYftZ9w==} + '@tiptap/extension-task-item@3.31.3': + resolution: {integrity: sha512-gCWvvXsCzi9tVFXPqKlkbU81XxtIdfjGy7FM9QFRKiWAvq5uv+Q5nvwrCyMVyP4bA7zRGWFaPzwjL68djgzoBw==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-task-list@3.22.5': - resolution: {integrity: sha512-SfZeJSALtFODs0i3fml1TSi4vQ4Uopu0p/LndK+mX5FGNBtNmWiy7Wr5cH03ANfzj8c2EzfGIyH+F2/V0HLK9g==} + '@tiptap/extension-task-list@3.31.3': + resolution: {integrity: sha512-3WgVzmfEnDbmxbjUBlvTiKpT53KSBnnYhXUVQxM0nz1vpoWG8QifhsnCVsNI6TZmhgR0x/jY+UW9Hc/WLF8DkQ==} peerDependencies: - '@tiptap/extension-list': 3.22.5 + '@tiptap/extension-list': 3.31.3 - '@tiptap/extension-text-style@3.22.5': - resolution: {integrity: sha512-jt63jy8YbhZJUGMxTUzeivLhowGtFp6YbCFrrmZJ7G6IHu8X8LJzO81ksz5nT5l8DKpldGwnINUfA6iE91JIAg==} + '@tiptap/extension-text-style@3.31.3': + resolution: {integrity: sha512-wgjWWrjZwRZHiaDTQPX2am1y/4ePgRgGWF/2MOfSb7g4d5229p4aVtbT1JT7wu9z8OC482pEqcTlhdvgftvW7A==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-text@3.22.5': - resolution: {integrity: sha512-bzpDOdAEo1JeoVZDIyV0oY0jGXkEG+AzF70SzHoRSjOvFDtKWunyXf9eO1OnOr2/fmMcckT2qwUBNBMQplWBzw==} + '@tiptap/extension-text@3.31.3': + resolution: {integrity: sha512-gdsWtF+taeaCu6V+5Ct10fGo0ACUy1GnYtbb+mcathBt8OqbT+Ws60p/yEmKesBDz2Hn+B5IcWy6+2BBZl5ZTg==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extension-underline@3.22.5': - resolution: {integrity: sha512-9ut09rJD0iEbS6sk7yd2j6IwuFDLTNmDEGTDLodvqAfi+bq7ddsTDv0YviXoZaA9sdHAdTEVr2ITy2m6WK5jpA==} + '@tiptap/extension-underline@3.31.3': + resolution: {integrity: sha512-HghdJaOwRqYzsAxqSyNyb+IWyOMcdCl8IoiBETA9BZCJAqdXzFLcuWp7CqCqJPam9dgkWosSoHqTCAO4nTBfpw==} peerDependencies: - '@tiptap/core': 3.22.5 + '@tiptap/core': 3.31.3 - '@tiptap/extensions@3.22.5': - resolution: {integrity: sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==} + '@tiptap/extensions@3.31.3': + resolution: {integrity: sha512-8sJNPGGUe8f3aDojcOW5cfVL7I5NrBbE0UWxG08qoi9Tea6qWbvQJsCR9tsrOapr/DaLr3kpbGZ9s1gEEfcNcA==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/markdown@3.22.5': - resolution: {integrity: sha512-lLuAySaY5EYNYLe7e4507B9yQMAEDJdOKy0g85UNFV8giorYLQx56aV2O94Qb9gv3egs5inkwVRNfeJzOWAwig==} + '@tiptap/markdown@3.31.3': + resolution: {integrity: sha512-rBbYSxasseUoaBo15C8Yoaqafo7mJJzxwAwV9Z1OpLBvOroW9nQ0EbOdqaSYRw3/d9pP71B/3LNSzKyfyy+dpQ==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 - '@tiptap/pm@3.22.5': - resolution: {integrity: sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==} + '@tiptap/pm@3.31.3': + resolution: {integrity: sha512-sZime0SWsz/k62W2WvHx5Ig7G2h7kVhrrmnqy+wEgIHfDwEfOlelRjaWCiBCFlF7dxGUntJusCh9FxlLhni0Ag==} - '@tiptap/react@3.22.5': - resolution: {integrity: sha512-36WHEs+vPmB//V1ff7Ujcnpz7Ey5g8lhpI/0+hoanSbdiPMTQ7qZVWwMovIkMKDlqWVp2fxBgeYM1861jyFzTw==} + '@tiptap/react@3.31.3': + resolution: {integrity: sha512-QiwQqvaLFLm5EMFu5tg7nAgXJxCUiUTLD8EsK+TqVV5P4bqOoMOCM39khbhXTJyahCuYpiFWx5YOSDtC/JiPtg==} peerDependencies: - '@tiptap/core': 3.22.5 - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3 + '@tiptap/pm': 3.31.3 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@3.22.5': - resolution: {integrity: sha512-LZ/LYbwH6rnDi5DnRyagkuNsYAVyhM+yJvvz+ZuYA0JkPiTXJV86J5PWSKew8M0gVfMHcNVtKjfQCvViFCeIgw==} + '@tiptap/starter-kit@3.31.3': + resolution: {integrity: sha512-WKof9RewdmGHvWJ1wn0/HVNG2mV+HOgVRyJkKekuM9fgr6BZAAH/xZsWE1eon+94JnQ+KtK2ThydXQM/qc6b2A==} '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -3281,6 +3488,9 @@ packages: '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} @@ -3352,10 +3562,47 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/types@8.60.0': resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -3574,6 +3821,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -3595,6 +3847,9 @@ packages: ajv: optional: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -3770,6 +4025,9 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3881,6 +4139,11 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + cn@0.2.6: + resolution: {integrity: sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ==} + engines: {node: '>=20'} + hasBin: true + code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -4193,6 +4456,9 @@ packages: babel-plugin-macros: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -4446,15 +4712,37 @@ packages: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -4469,6 +4757,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -4523,6 +4815,12 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -4560,6 +4858,9 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + filelist@1.0.6: resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} @@ -4575,9 +4876,19 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + flairup@1.0.0: resolution: {integrity: sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -4684,6 +4995,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -4737,6 +5052,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -4797,6 +5116,12 @@ packages: resolution: {integrity: sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==} engines: {node: '>=16.9.0'} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -4882,6 +5207,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -5076,12 +5405,18 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -5110,6 +5445,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -5130,6 +5468,10 @@ packages: lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -5207,8 +5549,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkifyjs@4.3.2: - resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} lint-staged@16.4.0: resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} @@ -5223,6 +5565,10 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -5575,6 +5921,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -5671,6 +6020,10 @@ packages: resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==} hasBin: true + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@8.2.0: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} @@ -5689,6 +6042,10 @@ packages: resolution: {integrity: sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w==} engines: {node: ^20.19.0 || >=22.12.0} + oxc-parser@0.148.0: + resolution: {integrity: sha512-syxUKHeUll89RIABQADcI7sikYrwyssvA6gj4phSSIPezKVM8yMaLAiLLSc7fmzVvrwybfFGFbW5zme9sX87rg==} + engines: {node: ^20.19.0 || >=22.12.0} + oxfmt@0.65.0: resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5739,6 +6096,10 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} @@ -5891,6 +6252,10 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -5939,11 +6304,14 @@ packages: prosemirror-history@1.5.0: resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - prosemirror-model@1.25.4: - resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} prosemirror-schema-list@1.5.1: resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} @@ -5957,8 +6325,8 @@ packages: prosemirror-transform@1.12.0: resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} - prosemirror-view@1.41.8: - resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + prosemirror-view@1.42.3: + resolution: {integrity: sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==} proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} @@ -5967,6 +6335,10 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pvtsutils@1.3.6: resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} @@ -5974,6 +6346,10 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + qrcode@1.5.4: resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} engines: {node: '>=10.13.0'} @@ -6646,6 +7022,12 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -6669,6 +7051,10 @@ packages: tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} @@ -6763,6 +7149,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -6903,6 +7292,10 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -7274,6 +7667,18 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + '@chevrotain/types@11.1.2': {} '@croct/json5-parser@0.2.2': @@ -7520,6 +7925,40 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))': + dependencies: + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5(supports-color@7.2.0)': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -7545,6 +7984,22 @@ snapshots: dependencies: hono: 4.13.0 + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.1': @@ -7695,6 +8150,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@linear/sdk@82.1.0(graphql@16.14.2)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) @@ -7883,51 +8346,99 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.141.0': optional: true + '@oxc-parser/binding-android-arm-eabi@0.148.0': + optional: true + '@oxc-parser/binding-android-arm64@0.141.0': optional: true + '@oxc-parser/binding-android-arm64@0.148.0': + optional: true + '@oxc-parser/binding-darwin-arm64@0.141.0': optional: true + '@oxc-parser/binding-darwin-arm64@0.148.0': + optional: true + '@oxc-parser/binding-darwin-x64@0.141.0': optional: true + '@oxc-parser/binding-darwin-x64@0.148.0': + optional: true + '@oxc-parser/binding-freebsd-x64@0.141.0': optional: true + '@oxc-parser/binding-freebsd-x64@0.148.0': + optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.141.0': optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.148.0': + optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.141.0': optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.148.0': + optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.141.0': optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.148.0': + optional: true + '@oxc-parser/binding-linux-arm64-musl@0.141.0': optional: true + '@oxc-parser/binding-linux-arm64-musl@0.148.0': + optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.141.0': optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.148.0': + optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.141.0': optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.148.0': + optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.141.0': optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.148.0': + optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.141.0': optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.148.0': + optional: true + '@oxc-parser/binding-linux-x64-gnu@0.141.0': optional: true + '@oxc-parser/binding-linux-x64-gnu@0.148.0': + optional: true + '@oxc-parser/binding-linux-x64-musl@0.141.0': optional: true + '@oxc-parser/binding-linux-x64-musl@0.148.0': + optional: true + '@oxc-parser/binding-openharmony-arm64@0.141.0': optional: true + '@oxc-parser/binding-openharmony-arm64@0.148.0': + optional: true + '@oxc-parser/binding-wasm32-wasi@0.141.0': dependencies: '@emnapi/core': 1.11.2 @@ -7938,18 +8449,30 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.141.0': optional: true + '@oxc-parser/binding-win32-arm64-msvc@0.148.0': + optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.141.0': optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.148.0': + optional: true + '@oxc-parser/binding-win32-x64-msvc@0.141.0': optional: true + '@oxc-parser/binding-win32-x64-msvc@0.148.0': + optional: true + '@oxc-project/runtime@0.101.0': {} '@oxc-project/types@0.101.0': {} '@oxc-project/types@0.141.0': {} + '@oxc-project/types@0.148.0': + optional: true + '@oxfmt/binding-android-arm-eabi@0.65.0': optional: true @@ -8984,6 +9507,18 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shadcn/lint@0.1.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2)': + dependencies: + '@eslint/core': 0.17.0 + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2) + cn: 0.2.6 + optionalDependencies: + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + oxc-parser: 0.148.0 + transitivePeerDependencies: + - supports-color + - typescript + '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -9192,200 +9727,202 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tiptap/core@3.22.5(@tiptap/pm@3.22.5)': + '@tiptap/core@3.31.3(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/pm': 3.22.5 + '@tiptap/pm': 3.31.3 - '@tiptap/extension-blockquote@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-blockquote@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-bold@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-bold@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-bubble-menu@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-bubble-menu@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 optional: true - '@tiptap/extension-bullet-list@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-bullet-list@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-code-block-lowlight@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@tiptap/extension-code-block-lowlight@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-code-block@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/extension-code-block': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/extension-code-block': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 highlight.js: 11.11.1 lowlight: 3.3.0 - '@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-code-block@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-code@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-code@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-details@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-text-style@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)))(@tiptap/pm@3.22.5)': + '@tiptap/extension-details@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/extension-text-style@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/extension-text-style': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/extension-text-style': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-document@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-document@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-dropcursor@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-dropcursor@3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extensions': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-floating-menu@3.22.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-floating-menu@3.31.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: '@floating-ui/dom': 1.7.6 - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 optional: true - '@tiptap/extension-gapcursor@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-gapcursor@3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extensions': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-hard-break@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-hard-break@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-heading@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-heading@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-horizontal-rule@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-horizontal-rule@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-image@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-image@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-italic@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-italic@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-link@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-link@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 - linkifyjs: 4.3.2 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 + linkifyjs: 4.3.3 - '@tiptap/extension-list-item@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-list-item@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list-keymap@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-list-keymap@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-mathematics@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(katex@0.16.45)': + '@tiptap/extension-mathematics@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(katex@0.16.45)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 katex: 0.16.45 - '@tiptap/extension-ordered-list@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-ordered-list@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-paragraph@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-paragraph@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-placeholder@3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-placeholder@3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extensions': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-strike@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-strike@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-table-cell@3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-table-cell@3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-table': 3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-table': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-table-header@3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-table-header@3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-table': 3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-table': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-table-row@3.22.4(@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-table-row@3.31.3(@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-table': 3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-table': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-table@3.22.4(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extension-table@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/extension-task-item@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-task-item@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-task-list@3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))': + '@tiptap/extension-task-list@3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) - '@tiptap/extension-text-style@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-text-style@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-text@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-text@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extension-underline@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))': + '@tiptap/extension-underline@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) - '@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 - '@tiptap/markdown@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)': + '@tiptap/markdown@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 marked: 17.0.6 - '@tiptap/pm@3.22.5': + '@tiptap/pm@3.31.3': dependencies: prosemirror-changeset: 2.4.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 prosemirror-gapcursor: 1.4.1 prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 prosemirror-tables: 1.8.5 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 - '@tiptap/react@3.22.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@tiptap/react@3.31.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) '@types/use-sync-external-store': 0.0.6 @@ -9394,37 +9931,37 @@ snapshots: react-dom: 19.2.8(react@19.2.8) use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: - '@tiptap/extension-bubble-menu': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/extension-floating-menu': 3.22.5(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-bubble-menu': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-floating-menu': 3.31.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) transitivePeerDependencies: - '@floating-ui/dom' - '@tiptap/starter-kit@3.22.5': + '@tiptap/starter-kit@3.31.3': dependencies: - '@tiptap/core': 3.22.5(@tiptap/pm@3.22.5) - '@tiptap/extension-blockquote': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-bold': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-bullet-list': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-code': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-code-block': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/extension-document': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-dropcursor': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-gapcursor': 3.22.5(@tiptap/extensions@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-hard-break': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-heading': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-horizontal-rule': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/extension-italic': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-link': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/extension-list-item': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-list-keymap': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-ordered-list': 3.22.5(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)) - '@tiptap/extension-paragraph': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-strike': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-text': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extension-underline': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5)) - '@tiptap/extensions': 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) - '@tiptap/pm': 3.22.5 + '@tiptap/core': 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/extension-blockquote': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-bold': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-bullet-list': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-code': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-code-block': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-document': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-dropcursor': 3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-gapcursor': 3.31.3(@tiptap/extensions@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-hard-break': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-heading': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-horizontal-rule': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-italic': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-link': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/extension-list-item': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-list-keymap': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-ordered-list': 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)) + '@tiptap/extension-paragraph': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-strike': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-text': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extension-underline': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) + '@tiptap/extensions': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3) + '@tiptap/pm': 3.31.3 '@ts-morph/common@0.27.0': dependencies: @@ -9615,6 +10152,8 @@ snapshots: '@types/http-cache-semantics@4.2.0': {} + '@types/json-schema@7.0.15': {} + '@types/katex@0.16.8': {} '@types/keyv@3.1.4': @@ -9690,8 +10229,60 @@ snapshots: dependencies: '@types/node': 25.9.5 + '@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@7.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@7.0.2) + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) + typescript: 7.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@7.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@7.0.2) + '@typescript-eslint/types': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 7.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.70.0': + dependencies: + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 + + '@typescript-eslint/tsconfig-utils@8.70.0(typescript@7.0.2)': + dependencies: + typescript: 7.0.2 + '@typescript-eslint/types@8.60.0': {} + '@typescript-eslint/types@8.70.0': {} + + '@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@7.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@7.0.2) + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@7.0.2) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@7.0.2) + typescript: 7.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.70.0': + dependencies: + '@typescript-eslint/types': 8.70.0 + eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -9861,6 +10452,10 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} agent-base@7.1.4: {} @@ -9871,6 +10466,13 @@ snapshots: optionalDependencies: ajv: 8.20.0 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -10097,6 +10699,14 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -10195,6 +10805,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cn@0.2.6: {} + code-block-writer@13.0.3: {} color-convert@2.0.1: @@ -10494,6 +11106,8 @@ snapshots: dedent@1.7.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -10785,8 +11399,7 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@4.0.0: - optional: true + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -10797,10 +11410,59 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@5.0.1: {} + eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5(supports-color@7.2.0) + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@7.2.0) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -10813,6 +11475,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} eventemitter3@5.0.4: {} @@ -10906,6 +11570,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} fast-string-truncated-width@3.0.3: {} @@ -10940,6 +11608,10 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + filelist@1.0.6: dependencies: minimatch: 5.1.9 @@ -10964,8 +11636,21 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + flairup@1.0.0: {} + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -11074,6 +11759,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -11153,6 +11842,10 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -11287,6 +11980,10 @@ snapshots: hono@4.13.0: {} + hookified@1.15.1: {} + + hookified@2.2.0: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -11390,6 +12087,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + imurmurhash@0.1.4: {} + indent-string@4.0.0: {} inflight@1.0.6: @@ -11525,10 +12224,14 @@ snapshots: '@babel/runtime': 7.29.7 ts-algebra: 2.0.0 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: optional: true @@ -11558,6 +12261,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + khroma@2.1.0: {} kleur@3.0.3: {} @@ -11570,6 +12277,11 @@ snapshots: lazy-val@1.0.5: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -11621,7 +12333,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkifyjs@4.3.2: {} + linkifyjs@4.3.3: {} lint-staged@16.4.0(patch_hash=7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673): dependencies: @@ -11645,6 +12357,10 @@ snapshots: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash-es@4.18.1: {} lodash.escaperegexp@4.1.2: {} @@ -12242,6 +12958,8 @@ snapshots: nanoid@3.3.18: {} + natural-compare@1.4.0: {} + negotiator@1.0.0: {} node-abi@4.33.0: @@ -12333,6 +13051,15 @@ snapshots: opentype.js@2.0.0: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@8.2.0: dependencies: chalk: 5.6.2 @@ -12386,6 +13113,31 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.141.0 '@oxc-parser/binding-win32-x64-msvc': 0.141.0 + oxc-parser@0.148.0: + dependencies: + '@oxc-project/types': 0.148.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.148.0 + '@oxc-parser/binding-android-arm64': 0.148.0 + '@oxc-parser/binding-darwin-arm64': 0.148.0 + '@oxc-parser/binding-darwin-x64': 0.148.0 + '@oxc-parser/binding-freebsd-x64': 0.148.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.148.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.148.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.148.0 + '@oxc-parser/binding-linux-arm64-musl': 0.148.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.148.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.148.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.148.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.148.0 + '@oxc-parser/binding-linux-x64-gnu': 0.148.0 + '@oxc-parser/binding-linux-x64-musl': 0.148.0 + '@oxc-parser/binding-openharmony-arm64': 0.148.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.148.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.148.0 + '@oxc-parser/binding-win32-x64-msvc': 0.148.0 + optional: true + oxfmt@0.65.0: dependencies: tinypool: 2.1.0 @@ -12463,6 +13215,10 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 @@ -12601,6 +13357,8 @@ snapshots: powershell-utils@0.1.0: {} + prelude-ls@1.2.1: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -12641,7 +13399,7 @@ snapshots: prosemirror-commands@1.7.1: dependencies: - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 @@ -12649,58 +13407,63 @@ snapshots: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-gapcursor@1.4.1: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-history@1.5.0: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 rope-sequence: 1.3.4 + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-keymap@1.2.3: dependencies: prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 - prosemirror-model@1.25.4: + prosemirror-model@1.25.11: dependencies: orderedmap: 2.1.1 prosemirror-schema-list@1.5.1: dependencies: - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 prosemirror-state@1.4.4: dependencies: - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-tables@1.8.5: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 - prosemirror-view: 1.41.8 + prosemirror-view: 1.42.3 prosemirror-transform@1.12.0: dependencies: - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 - prosemirror-view@1.41.8: + prosemirror-view@1.42.3: dependencies: - prosemirror-model: 1.25.4 + prosemirror-model: 1.25.11 prosemirror-state: 1.4.4 prosemirror-transform: 1.12.0 @@ -12714,12 +13477,18 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + pvtsutils@1.3.6: dependencies: tslib: 2.8.1 pvutils@1.1.5: {} + qified@0.10.1: + dependencies: + hookified: 2.2.0 + qrcode@1.5.4: dependencies: dijkstrajs: 1.0.3 @@ -13517,6 +14286,10 @@ snapshots: ts-algebra@2.0.0: {} + ts-api-utils@2.5.0(typescript@7.0.2): + dependencies: + typescript: 7.0.2 + ts-dedent@2.2.0: {} ts-morph@26.0.0: @@ -13538,6 +14311,10 @@ snapshots: tweetnacl@1.0.3: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@0.13.1: optional: true @@ -13657,6 +14434,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.8): dependencies: react: 19.2.8 @@ -13771,6 +14552,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9c0ee568c74..68ae103f6ec 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -17,6 +17,7 @@ minimumReleaseAgeExclude: - pdfjs-dist@6.3.289 - zod@4.5.4 - electron@43.7.0 + - '@shadcn/lint@0.1.0' shamefullyHoist: true # Orca always launches the user's own resolved Claude CLI via diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt index b79ed543494..536831230b4 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt @@ -39,8 +39,8 @@ __orca_restore_agent_teams_path # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt index 3d3403ad099..f86bd569381 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt @@ -77,8 +77,8 @@ __orca_deferred_init() { # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt index dc14486cdb7..a11d3e6183e 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt @@ -42,8 +42,8 @@ __orca_restore_agent_teams_path # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt index 10e9e144fc0..35a262b5e02 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt @@ -77,8 +77,8 @@ __orca_deferred_init() { # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt index de9c8f95248..61bdd01dd50 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt @@ -31,8 +31,8 @@ fi # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt index 394bc4a6d10..ff90115d30d 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt @@ -51,8 +51,8 @@ __orca_deferred_init() { # their normal argv shape. __orca_omp_should_skip_extension() { case "${1:-}" in - help|--help|-h|--version|-v) return 0 ;; - __complete|acp|agents|auth-broker|auth-gateway|bench|commit|completions|config|dry-balance|gallery|grep|grievances|install|join|models|plugin|read|say|search|setup|shell|ssh|stats|tiny-models|token|ttsr|update|usage|worktree|q|wt) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; + '__complete'|'acp'|'agents'|'auth-broker'|'auth-gateway'|'bench'|'commit'|'completions'|'config'|'dry-balance'|'gallery'|'grep'|'grievances'|'install'|'join'|'models'|'plugin'|'read'|'say'|'search'|'setup'|'shell'|'ssh'|'stats'|'tiny-models'|'token'|'ttsr'|'update'|'usage'|'worktree'|'q'|'wt') return 0 ;; esac return 1 } diff --git a/src/main/ai-vault-search/session-search-child-service.test.ts b/src/main/ai-vault-search/session-search-child-service.test.ts new file mode 100644 index 00000000000..ddd3e8cb03b --- /dev/null +++ b/src/main/ai-vault-search/session-search-child-service.test.ts @@ -0,0 +1,55 @@ +import { expect, it, vi } from 'vitest' +import { unavailableSessionSearchStatus } from '../../shared/ai-vault-search-client' +import type { AiVaultSearchStatus } from '../../shared/ai-vault-search-types' +import { createChildSessionSearchService } from './session-search-child-service' + +const indexingStatus: AiVaultSearchStatus = { + ...unavailableSessionSearchStatus(), + enabled: true, + phase: 'indexing', + filesIndexed: 3, + generation: 7 +} + +function stubCalls(overrides: Partial[0]> = {}) { + return { + search: vi.fn(async () => ({ kind: 'unavailable', reason: 'disabled' }) as const), + status: vi.fn(async () => indexingStatus), + reconcile: vi.fn(async () => undefined), + ...overrides + } +} + +it('forwards every call to the child and returns what it answered', async () => { + const calls = stubCalls() + const service = createChildSessionSearchService(calls) + + expect(await service.search({ query: 'ledger' })).toEqual({ + kind: 'unavailable', + reason: 'disabled' + }) + expect(calls.search).toHaveBeenCalledWith({ query: 'ledger' }) + expect(await service.status()).toEqual(indexingStatus) + await service.reconcile() + expect(calls.reconcile).toHaveBeenCalledTimes(1) +}) + +// A child that is starting, restarting or refusing is "not yet", which is an +// answer to the caller's question; turning it into a throw would make a paired +// client show a transport error for a host that is simply booting. +it('maps a child that cannot answer to not-ready rather than an error', async () => { + const service = createChildSessionSearchService( + stubCalls({ + search: vi.fn(() => Promise.reject(new Error('AI Vault service did not become ready.'))), + status: vi.fn(() => Promise.reject(new Error('AI Vault service queue is full.'))), + reconcile: vi.fn(() => Promise.reject(new Error('AI Vault service disconnected.'))) + }) + ) + + expect(await service.search({ query: 'ledger' })).toEqual({ + kind: 'unavailable', + reason: 'not-ready' + }) + expect(await service.status()).toEqual(unavailableSessionSearchStatus()) + await expect(service.reconcile()).resolves.toBeUndefined() +}) diff --git a/src/main/ai-vault-search/session-search-child-service.ts b/src/main/ai-vault-search/session-search-child-service.ts new file mode 100644 index 00000000000..379efeff4b1 --- /dev/null +++ b/src/main/ai-vault-search/session-search-child-service.ts @@ -0,0 +1,47 @@ +import type { AiVaultSearchStatus } from '../../shared/ai-vault-search-types' +import { unavailableSessionSearchStatus } from '../../shared/ai-vault-search-client' +import { + reconcileSessionSearchInService, + searchSessionsInService, + sessionSearchStatusInService +} from '../ai-vault/session-scanner-service-spawn' +import type { SessionSearchService } from './session-search-service' + +/** + * The desktop's `SessionSearchService`: every call is forwarded to the scanner + * child that owns the database. This process never opens the index file. + * + * A transport failure is a child that is starting, restarting or refusing, which + * is `not-ready` rather than an error: the caller asked whether this host can + * answer, and "not yet" is an answer. A child that is up and has no indexer says + * `disabled` for itself. + */ +export function createChildSessionSearchService( + calls = { + search: searchSessionsInService, + status: sessionSearchStatusInService, + reconcile: reconcileSessionSearchInService + } +): SessionSearchService { + return { + search: async (request) => { + try { + return await calls.search(request) + } catch { + return { kind: 'unavailable', reason: 'not-ready' } + } + }, + status: async (): Promise => { + try { + return await calls.status() + } catch { + return unavailableSessionSearchStatus() + } + }, + reconcile: async () => { + // Swallowed for the same reason: the caller's next search reports the state + // of the index, and a freshness wait that cannot run is a stale page, not a throw. + await calls.reconcile().catch(() => undefined) + } + } +} diff --git a/src/main/ai-vault-search/session-search-database-path.ts b/src/main/ai-vault-search/session-search-database-path.ts new file mode 100644 index 00000000000..5fcaf405f57 --- /dev/null +++ b/src/main/ai-vault-search/session-search-database-path.ts @@ -0,0 +1,13 @@ +import { join } from 'node:path' + +/** + * Where one host keeps its index. + * + * Beside the scanner's parse cache (`/ai-vault/`), because the two are + * the same kind of thing: a disposable derivative of the transcripts this host + * can read, scoped to this host's data root. One file per host, never shared — + * a second process writing the same file is the rebuild race PR 2 recorded. + */ +export function sessionSearchDatabasePath(dataRoot: string): string { + return join(dataRoot, 'ai-vault', 'session-search.sqlite') +} diff --git a/src/main/ai-vault-search/session-search-enablement.ts b/src/main/ai-vault-search/session-search-enablement.ts new file mode 100644 index 00000000000..e292e4a8551 --- /dev/null +++ b/src/main/ai-vault-search/session-search-enablement.ts @@ -0,0 +1,70 @@ +import { + resolveAiVaultSearchSettings, + sameAiVaultSearchSettings +} from '../../shared/ai-vault-search-settings' +import type { GlobalSettings } from '../../shared/global-settings-types' +import { updateSessionSearchInService } from '../ai-vault/session-scanner-service-spawn' +import { createChildSessionSearchService } from './session-search-child-service' +import { installSessionSearchPolicySource } from './session-search-policy' +import { setSessionSearchService } from './session-search-service-registry' +import { + installSessionSearchDataRoot, + sessionSearchServiceInit +} from './session-search-service-init' +import { sessionSearchSqliteAvailable } from './session-search-sqlite-support' +let installed = false + +/** + * The desktop's one wiring point: search answers from the scanner child, and the + * child's consent comes from the settings store. + * + * Registered whether or not the setting is on, because "off" is an answer this + * host can give (`unavailable/disabled`) and `no-service` is not — that reason + * means nothing here owns an index, which stops being true the moment this runs. + */ +export function installChildSessionSearchService(args: { + dataRoot: string + getSettings: () => Pick +}): { dispose(): void } | null { + if (!sessionSearchSqliteAvailable()) { + return null + } + installed = true + installSessionSearchDataRoot(args.dataRoot) + installSessionSearchPolicySource(args.getSettings) + setSessionSearchService(createChildSessionSearchService()) + pushSessionSearchPolicy() + return { + dispose: () => { + installed = false + } + } +} + +/** + * Reconciles a settings write. An unchanged policy is not forwarded, so re-saving + * the same value never restarts a running index. + */ +export function applySessionSearchSettingsChange( + before: Pick, + after: Pick +): void { + if ( + sameAiVaultSearchSettings( + resolveAiVaultSearchSettings(before), + resolveAiVaultSearchSettings(after) + ) + ) { + return + } + if (installed) { + pushSessionSearchPolicy() + } +} + +function pushSessionSearchPolicy(): void { + const init = sessionSearchServiceInit() + if (init) { + updateSessionSearchInService(init) + } +} diff --git a/src/main/ai-vault-search/session-search-host-registration.test.ts b/src/main/ai-vault-search/session-search-host-registration.test.ts new file mode 100644 index 00000000000..0e42ac68cde --- /dev/null +++ b/src/main/ai-vault-search/session-search-host-registration.test.ts @@ -0,0 +1,206 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { resetSessionParseCacheForTests } from '../ai-vault/session-scanner-parse-cache' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { installInProcessSessionSearchService } from './session-search-in-process-service' +import { + openSessionSearchIndexerHarness, + writeClaudeTranscript, + type SessionSearchIndexerHarness +} from './session-search-indexer-test-fixture' +import { searchSessionService } from './session-search-service-registry' +import { resetSessionSearchPolicyForTests } from './session-search-policy' +import { resetSessionSearchServiceInitForTests } from './session-search-service-init' + +/** + * Every host that answers a search has to register a service, or its answer is + * `no-service` — which means "this host does not have the feature", not "it is + * off". Two halves: the installers really register, and each host's boot module + * really calls the installer that suits it. + */ + +const updateSessionSearchInService = vi.hoisted(() => vi.fn()) +vi.mock('../ai-vault/session-scanner-service-spawn', async (importOriginal) => ({ + ...(await importOriginal()), + updateSessionSearchInService +})) + +const localAiVaultScanRoots = vi.hoisted(() => vi.fn()) +vi.mock('../ai-vault/cached-session-list', async (importOriginal) => ({ + ...(await importOriginal()), + localAiVaultScanRoots +})) + +const ROOT = join(import.meta.dirname, '..', '..', '..') + +let harness: SessionSearchIndexerHarness +let installed: { dispose(): void } | null + +beforeEach(async () => { + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + updateSessionSearchInService.mockClear() + harness = await openSessionSearchIndexerHarness('ss-registration') + installed = null + localAiVaultScanRoots.mockReset().mockResolvedValue(harness.roots) +}) + +afterEach(async () => { + installed?.dispose() + vi.useRealTimers() + const { setSessionSearchService } = await import('./session-search-service-registry') + setSessionSearchService(null) + resetSessionSearchPolicyForTests() + resetSessionSearchServiceInitForTests() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + await harness.cleanup() +}) + +it('answers no-service until a host registers one', async () => { + expect(await searchSessionService({ query: 'ledger' }, 'ipc')).toEqual({ + kind: 'unavailable', + reason: 'no-service' + }) +}) + +it('registers the desktop service and pushes the stored policy at boot', async () => { + const { installChildSessionSearchService } = await import('./session-search-enablement') + installed = installChildSessionSearchService({ + dataRoot: harness.root, + getSettings: () => ({ aiVaultSearch: { enabled: true, historyDays: 30 } }) + }) + + expect(await searchSessionService({ query: 'ledger' }, 'ipc')).not.toEqual({ + kind: 'unavailable', + reason: 'no-service' + }) + await vi.waitFor(() => expect(updateSessionSearchInService).toHaveBeenCalledTimes(1)) + expect(updateSessionSearchInService.mock.calls[0]?.[0]).toMatchObject({ + settings: { enabled: true, historyDays: 30 }, + databasePath: join(harness.root, 'ai-vault', 'session-search.sqlite') + }) +}) + +it('forwards only a real settings change to the child', async () => { + const { applySessionSearchSettingsChange, installChildSessionSearchService } = + await import('./session-search-enablement') + installed = installChildSessionSearchService({ + dataRoot: harness.root, + getSettings: () => ({ aiVaultSearch: { enabled: false, historyDays: null } }) + }) + await vi.waitFor(() => expect(updateSessionSearchInService).toHaveBeenCalledTimes(1)) + + applySessionSearchSettingsChange( + { aiVaultSearch: { enabled: false, historyDays: null } }, + { aiVaultSearch: { enabled: false, historyDays: null } } + ) + expect(updateSessionSearchInService).toHaveBeenCalledTimes(1) + + applySessionSearchSettingsChange( + { aiVaultSearch: { enabled: false, historyDays: null } }, + { aiVaultSearch: { enabled: true, historyDays: null } } + ) + await vi.waitFor(() => expect(updateSessionSearchInService).toHaveBeenCalledTimes(2)) +}) + +it('does not discover roots or arm a timer during registration', async () => { + vi.useFakeTimers() + const { installChildSessionSearchService } = await import('./session-search-enablement') + installed = installChildSessionSearchService({ + dataRoot: harness.root, + getSettings: () => ({ aiVaultSearch: { enabled: false, historyDays: null } }) + }) + expect(updateSessionSearchInService).toHaveBeenCalledTimes(1) + await vi.advanceTimersByTimeAsync(600_000) + expect(localAiVaultScanRoots).not.toHaveBeenCalled() + expect(vi.getTimerCount()).toBe(0) +}) + +it('registers an in-process service for a host with no scanner child', async () => { + installed = installInProcessSessionSearchService({ + dataRoot: harness.root, + roots: harness.roots, + settings: { enabled: false, historyDays: null } + }) + expect(installed).not.toBeNull() + + // Off, not absent: the caller can tell consent from a host that lacks the feature. + expect(await searchSessionService({ query: 'ledger' }, 'relay')).toEqual({ + kind: 'unavailable', + reason: 'disabled' + }) + + installed?.dispose() + installed = null + expect(await searchSessionService({ query: 'ledger' }, 'relay')).toEqual({ + kind: 'unavailable', + reason: 'no-service' + }) +}) + +// The behavioural tests above prove the installers register; these prove each +// host's boot path reaches one, which no unit of either module can show. +it.each([ + [ + 'desktop and headless serve', + 'src/main/startup/main-process-runtime-service.ts', + 'installChildSessionSearchService' + ], + ['orcad', 'src/main/orcad/orcad-session-search.ts', 'installInProcessSessionSearchService'], + [ + 'the relay daemon', + 'src/relay/relay-runtime-services.ts', + 'installInProcessSessionSearchService' + ] +])('boots %s with a registered session search service', (_host, file, installer) => { + const source = readFileSync(join(ROOT, file), 'utf8') + expect(source).toContain(installer) + expect(source).toMatch(new RegExp(`${installer}\\(\\{`)) +}) + +it('disables immediately without root discovery', async () => { + const { installChildSessionSearchService, applySessionSearchSettingsChange } = + await import('./session-search-enablement') + let settings = { aiVaultSearch: { enabled: true, historyDays: null } } + installed = installChildSessionSearchService({ + dataRoot: harness.root, + getSettings: () => settings + }) + updateSessionSearchInService.mockClear() + const before = settings + settings = { aiVaultSearch: { enabled: false, historyDays: null } } + applySessionSearchSettingsChange(before, settings) + expect(updateSessionSearchInService).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ settings: settings.aiVaultSearch }) + ) + expect(localAiVaultScanRoots).not.toHaveBeenCalled() +}) + +it('orcad resolves no roots while disabled and discovers late roots when enabled', async () => { + const { installOrcadSessionSearchService } = await import('../orcad/orcad-session-search') + installed = await installOrcadSessionSearchService({ + userDataPath: harness.root, + getSettings: () => ({ aiVaultSearch: { enabled: false, historyDays: null } }) + }) + expect(localAiVaultScanRoots).not.toHaveBeenCalled() + installed?.dispose() + installed = await installOrcadSessionSearchService({ + userDataPath: harness.root, + getSettings: () => ({ aiVaultSearch: { enabled: true, historyDays: null } }) + }) + await searchSessionService({ query: 'latehostroot', freshness: 'wait-until-current' }, 'ipc') + const late = join(harness.root, 'late-claude') + const id = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + await writeClaudeTranscript(join(late, 'project', `${id}.jsonl`), ['latehostroot'], id) + localAiVaultScanRoots.mockResolvedValue({ ...harness.roots, claudeProjectsDir: late }) + const response = await searchSessionService( + { query: 'latehostroot', freshness: 'wait-until-current' }, + 'ipc' + ) + expect(response.kind).toBe('results') + if (response.kind === 'results') { + expect(response.hits.map((hit) => hit.sessionId)).toEqual([id]) + } +}) diff --git a/src/main/ai-vault-search/session-search-in-process-service.ts b/src/main/ai-vault-search/session-search-in-process-service.ts new file mode 100644 index 00000000000..536ad742a98 --- /dev/null +++ b/src/main/ai-vault-search/session-search-in-process-service.ts @@ -0,0 +1,53 @@ +import type { AiVaultSearchSettings } from '../../shared/ai-vault-search-settings' +import { sessionSearchDatabasePath } from './session-search-database-path' +import type { SessionSearchIndexerOptions } from './session-search-indexer-options' +import { SessionSearchInstance } from './session-search-instance' +import type { SessionSearchScanRoots } from './session-search-scan-roots' +import { setSessionSearchService } from './session-search-service-registry' +import { sessionSearchSqliteAvailable } from './session-search-sqlite-support' + +/** + * Registration for the two hosts that have no scanner-service child of their own. + * + * The desktop puts the index in that child because the child is where the + * transcript reader runs, so one read serves both the session list and the index. + * Neither of these hosts has that child: orcad ships only the watcher and daemon + * entries beside `orcad.js`, and the relay's AI Vault sidecar runs the remote + * scanner, which reads through a filesystem provider and publishes nothing to the + * transcript channel. On both, the process that would drive the index's reads is + * this one, and it is the only writer, so the two-process rebuild race the + * desktop rule avoids cannot arise here. + * + * Returns null on a runtime with no `node:sqlite`: both hosts are built for a + * Node 18 floor, and a host that cannot hold an index registers nothing rather + * than answering `disabled` for a reason that is not consent. + */ +export function installInProcessSessionSearchService(args: { + dataRoot: string + roots: SessionSearchScanRoots + resolveRoots?: SessionSearchIndexerOptions['resolveRoots'] + settings: AiVaultSearchSettings + onError?: (error: unknown) => void +}): { dispose(): void } | null { + if (!sessionSearchSqliteAvailable()) { + return null + } + const instance = new SessionSearchInstance({ + databasePath: sessionSearchDatabasePath(args.dataRoot), + roots: args.roots, + resolveRoots: args.resolveRoots, + ...(args.onError ? { onError: args.onError } : {}) + }) + instance.apply(args.settings) + setSessionSearchService({ + search: (request) => instance.search(request), + status: async () => instance.status(), + reconcile: () => instance.reconcile() + }) + return { + dispose: () => { + setSessionSearchService(null) + instance.close() + } + } +} diff --git a/src/main/ai-vault-search/session-search-indexer-options.ts b/src/main/ai-vault-search/session-search-indexer-options.ts index 08eb4aeb2af..2c32c8381da 100644 --- a/src/main/ai-vault-search/session-search-indexer-options.ts +++ b/src/main/ai-vault-search/session-search-indexer-options.ts @@ -36,6 +36,8 @@ export const DEFAULT_SESSION_SEARCH_FULL_SWEEP_EVERY_CYCLES = 15 export type SessionSearchIndexerOptions = { databasePath: string roots: SessionSearchScanRoots + /** Full sweeps refresh host roots; recent cycles reuse the last snapshot. */ + resolveRoots?: (signal: AbortSignal) => Promise /** null = all history; otherwise only transcripts modified within this many days. */ historyDays: number | null clock?: SessionSearchClock diff --git a/src/main/ai-vault-search/session-search-indexer.ts b/src/main/ai-vault-search/session-search-indexer.ts index 26213242c49..d70d3718f9c 100644 --- a/src/main/ai-vault-search/session-search-indexer.ts +++ b/src/main/ai-vault-search/session-search-indexer.ts @@ -58,6 +58,7 @@ export type SessionSearchIndexStatus = { * counter with a reset rule. * * What is left here, and why none of it can be a row: + * - `roots`, the latest full-sweep snapshot reused by recent cycles. * - `previousRootsWithFiles`, the one bit per root the retirement walk's grace * needs. Deliberately not durable: see the mountpoint trade in * `session-search-deleted-sources.ts`. @@ -81,6 +82,7 @@ export type SessionSearchIndexStatus = { * one reconcile interval. Everything else is reached by the periodic sweep. */ export class SessionSearchIndexer { + private roots: SessionSearchIndexerOptions['roots'] private readonly ownershipPath: string private readonly clock: SessionSearchClock private readonly intervalMs: number @@ -104,6 +106,7 @@ export class SessionSearchIndexer { private closed = false constructor(private readonly options: SessionSearchIndexerOptions) { + this.roots = options.roots this.ownershipPath = resolve(options.databasePath) this.clock = options.clock ?? systemSessionSearchClock this.intervalMs = options.reconcileIntervalMs ?? DEFAULT_SESSION_SEARCH_RECONCILE_INTERVAL_MS @@ -273,9 +276,16 @@ export class SessionSearchIndexer { // end would erase that request along with this pass's own. this.sweepNext = false try { + if (full && this.options.resolveRoots) { + const roots = await this.options.resolveRoots(signal) + if (signal.aborted) { + return + } + this.roots = roots + } const result = await runSessionSearchPass({ store: this.store, - roots: this.options.roots, + roots: this.roots, full, recentPerAgent: this.recentPerAgent, previousRootsWithFiles: this.previousRootsWithFiles ?? undefined, diff --git a/src/main/ai-vault-search/session-search-instance.test.ts b/src/main/ai-vault-search/session-search-instance.test.ts new file mode 100644 index 00000000000..c917bd2d8a9 --- /dev/null +++ b/src/main/ai-vault-search/session-search-instance.test.ts @@ -0,0 +1,195 @@ +import { existsSync } from 'node:fs' +import { utimes } from 'node:fs/promises' +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { resetSessionParseCacheForTests } from '../ai-vault/session-scanner-parse-cache' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { SessionSearchInstance } from './session-search-instance' +import { + openSessionSearchIndexerHarness, + writeClaudeTranscript, + type SessionSearchIndexerHarness +} from './session-search-indexer-test-fixture' + +const RECENT_SESSION_ID = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' +const ANCIENT_SESSION_ID = 'bbbbbbbb-cccc-4ddd-8eee-ffffffffffff' + +let harness: SessionSearchIndexerHarness +let instance: SessionSearchInstance | null +let errors: unknown[] + +beforeEach(async () => { + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + errors = [] + harness = await openSessionSearchIndexerHarness('ss-instance') + instance = null +}) + +afterEach(async () => { + vi.restoreAllMocks() + instance?.close() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + await harness.cleanup() +}) + +function newInstance(): SessionSearchInstance { + instance = new SessionSearchInstance({ + databasePath: harness.databasePath, + roots: harness.roots, + onError: (error) => errors.push(error) + }) + return instance +} + +function transcriptPath(sessionId: string): string { + return join(harness.claudeProjectDir, `${sessionId}.jsonl`) +} + +async function searchFor(query: string): Promise { + const response = await instance!.search({ query }) + if (response.kind !== 'results') { + throw new Error(`expected results, got ${response.kind}`) + } + return response.hits.map((hit) => hit.sessionId).sort() +} + +it('constructs nothing and touches no disk while the setting is off', async () => { + await writeClaudeTranscript( + transcriptPath(RECENT_SESSION_ID), + ['a conversation'], + RECENT_SESSION_ID + ) + const subject = newInstance() + subject.apply({ enabled: false, historyDays: null }) + await subject.settled() + + expect(subject.running).toBe(false) + expect(existsSync(harness.databasePath)).toBe(false) + expect(await subject.search({ query: 'conversation' })).toEqual({ + kind: 'unavailable', + reason: 'disabled' + }) + expect(subject.status()).toMatchObject({ enabled: false, phase: 'idle', generation: 0 }) + expect(errors).toEqual([]) +}) + +it('indexes and answers once the setting is on', async () => { + await writeClaudeTranscript( + transcriptPath(RECENT_SESSION_ID), + ['a distinctive conversation'], + RECENT_SESSION_ID + ) + const subject = newInstance() + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + + expect(await searchFor('distinctive')).toEqual([RECENT_SESSION_ID]) + const status = subject.status() + expect(status.enabled).toBe(true) + expect(status.filesIndexed).toBeGreaterThan(0) + expect(status.generation).toBeGreaterThan(0) + expect(errors).toEqual([]) +}) + +// The whole reason the indexer is immutable: a change is a new instance, and the +// old one is closed before it exists, so there is never a second writer. +it('closes the live pair and starts a new one on a settings change', async () => { + const recent = transcriptPath(RECENT_SESSION_ID) + const ancient = transcriptPath(ANCIENT_SESSION_ID) + await writeClaudeTranscript(recent, ['a recent conversation'], RECENT_SESSION_ID) + await writeClaudeTranscript(ancient, ['an ancient conversation'], ANCIENT_SESSION_ID) + const longAgo = new Date(Date.now() - 120 * 86_400_000) + await utimes(ancient, longAgo, longAgo) + + const subject = newInstance() + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + expect(await searchFor('ancient')).toEqual([ANCIENT_SESSION_ID]) + + // Narrowing: the new instance's opening sweep purges what the window no longer covers. + subject.apply({ enabled: true, historyDays: 30 }) + await subject.settled() + expect(await searchFor('ancient')).toEqual([]) + expect(await searchFor('recent')).toEqual([RECENT_SESSION_ID]) + + // Widening: the same recipe the other way, admitting files no read ever saw. + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + expect(await searchFor('ancient')).toEqual([ANCIENT_SESSION_ID]) + expect(errors).toEqual([]) +}) + +it('leaves nothing running and no live claim when the setting goes off', async () => { + await writeClaudeTranscript( + transcriptPath(RECENT_SESSION_ID), + ['a conversation'], + RECENT_SESSION_ID + ) + const subject = newInstance() + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + expect(subject.running).toBe(true) + + subject.apply({ enabled: false, historyDays: null }) + expect(subject.running).toBe(false) + // The index is left on disk: disabling is not a deletion, and the claim the + // closed indexer staked on the path has to be released or nothing can reopen it. + expect(existsSync(harness.databasePath)).toBe(true) + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + expect(subject.running).toBe(true) + expect(errors).toEqual([]) +}) + +it('removes the database on clear and rebuilds only while consent stands', async () => { + await writeClaudeTranscript( + transcriptPath(RECENT_SESSION_ID), + ['a distinctive conversation'], + RECENT_SESSION_ID + ) + const subject = newInstance() + subject.apply({ enabled: true, historyDays: null }) + await subject.settled() + expect(await searchFor('distinctive')).toEqual([RECENT_SESSION_ID]) + + subject.clear() + expect(subject.running).toBe(true) + expect(existsSync(harness.databasePath)).toBe(true) + await subject.settled() + expect(await searchFor('distinctive')).toEqual([RECENT_SESSION_ID]) + + subject.apply({ enabled: false, historyDays: null }) + subject.clear() + expect(subject.running).toBe(false) + expect(existsSync(harness.databasePath)).toBe(false) + expect(errors).toEqual([]) +}) + +it('keeps pagination stable when the clock crosses retention before a purge', async () => { + for (const id of [RECENT_SESSION_ID, ANCIENT_SESSION_ID]) { + await writeClaudeTranscript(transcriptPath(id), [`distinctive conversation ${id}`], id) + } + const subject = newInstance() + subject.apply({ enabled: true, historyDays: 30 }) + await subject.settled() + const first = await subject.search({ query: 'distinctive', limit: 1 }) + if (first.kind !== 'results') { + throw new Error('expected results') + } + expect(first.page.cursor).toBeTruthy() + vi.spyOn(Date, 'now').mockReturnValue(Date.now() + 31 * 86_400_000) + const second = await subject.search({ + query: 'distinctive', + limit: 1, + cursor: first.page.cursor! + }) + if (second.kind !== 'results') { + throw new Error('expected results') + } + expect(second.generation).toBe(first.generation) + expect(second.hits).toHaveLength(1) + expect(second.hits[0].sessionId).not.toBe(first.hits[0].sessionId) + expect(errors).toEqual([]) +}) diff --git a/src/main/ai-vault-search/session-search-instance.ts b/src/main/ai-vault-search/session-search-instance.ts new file mode 100644 index 00000000000..b17103b004b --- /dev/null +++ b/src/main/ai-vault-search/session-search-instance.ts @@ -0,0 +1,162 @@ +import type SyncDatabase from '../sqlite/sync-database' +import type { + AiVaultSearchRequest, + AiVaultSearchResponse, + AiVaultSearchStatus +} from '../../shared/ai-vault-search-types' +import { unavailableSessionSearchStatus } from '../../shared/ai-vault-search-client' +import type { AiVaultSearchSettings } from '../../shared/ai-vault-search-settings' +import { SessionSearchEngine } from './session-search-engine' +import { SessionSearchIndexer } from './session-search-indexer' +import { sessionSearchHistoryCutoffMs } from './session-search-retention-policy' +import { openSessionSearchDatabase, removeSessionSearchDatabase } from './session-search-schema' +import type { SessionSearchScanRoots } from './session-search-scan-roots' +import type { SessionSearchIndexerOptions } from './session-search-indexer-options' +import { createSessionSearchService, type SessionSearchService } from './session-search-service' + +export type SessionSearchInstanceOptions = { + databasePath: string + roots: SessionSearchScanRoots + resolveRoots?: SessionSearchIndexerOptions['resolveRoots'] + onError?: (error: unknown) => void + /** Tests only: shortens the loop so a settings change is observable in one tick. */ + reconcileIntervalMs?: number +} + +type LiveIndex = { + indexer: SessionSearchIndexer + engine: SessionSearchEngine + /** The engine's own handle; the indexer's store keeps a second, private one. */ + db: SyncDatabase + service: SessionSearchService +} + +/** + * The one object that holds a host's live indexer and engine, and the three + * recipes that change them. + * + * The indexer is immutable after construction, so there is nothing here that + * reconfigures one: a settings change is `close()` and a new instance, disabling + * is `close()` with no replacement, and clearing is `close()`, remove the + * database, construct again. The new instance's first sweep purges a narrowed + * window and admits a widened one, so neither of those needs a path of its own. + * + * Lives in whichever process runs the transcript reader for this host. Nothing + * here knows about IPC, Electron or a settings store; the caller supplies the + * resolved settings and scan roots. + */ +export class SessionSearchInstance { + private live: LiveIndex | null = null + private settings: AiVaultSearchSettings = { enabled: false, historyDays: null } + private readonly onError: (error: unknown) => void + + constructor(private readonly options: SessionSearchInstanceOptions) { + this.onError = options.onError ?? ((error) => console.warn('[ai-vault-search]', error)) + } + + /** True once an indexer exists; false while disabled or while a construction is failing. */ + get running(): boolean { + return this.live !== null + } + + /** Close whatever is live and construct from `next`. A no-op change still restarts. */ + apply(next: AiVaultSearchSettings): void { + this.settings = next + this.closeLive() + this.construct() + } + + /** Throw the index away, then rebuild it if consent still stands. */ + clear(): void { + this.closeLive() + removeSessionSearchDatabase(this.options.databasePath) + this.construct() + } + + close(): void { + this.closeLive() + } + + async search(request: AiVaultSearchRequest): Promise { + const live = this.live + if (!live) { + return { kind: 'unavailable', reason: this.settings.enabled ? 'not-ready' : 'disabled' } + } + return live.service.search(request) + } + + status(): AiVaultSearchStatus { + const live = this.live + if (!live) { + return { ...unavailableSessionSearchStatus(), enabled: this.settings.enabled } + } + return { + enabled: true, + ...live.indexer.status(), + generation: live.engine.generation() + } + } + + async reconcile(): Promise { + await this.live?.service.reconcile() + } + + /** Tests only: resolves once the work loop has no pass in flight. */ + settled(): Promise { + return this.live?.indexer.settled() ?? Promise.resolve() + } + + private construct(): void { + if (!this.settings.enabled) { + return + } + const { historyDays } = this.settings + let indexer: SessionSearchIndexer | null = null + let db: SyncDatabase | null = null + try { + indexer = new SessionSearchIndexer({ + databasePath: this.options.databasePath, + roots: this.options.roots, + resolveRoots: this.options.resolveRoots, + historyDays, + onError: this.onError, + ...(this.options.reconcileIntervalMs === undefined + ? {} + : { reconcileIntervalMs: this.options.reconcileIntervalMs }) + }) + db = openSessionSearchDatabase(this.options.databasePath) + // Later expiry comes from the indexer purge, which also invalidates page cursors. + const engineOptions = { + retentionCutoffMs: sessionSearchHistoryCutoffMs(historyDays, Date.now()) + } + const engine = new SessionSearchEngine(db, engineOptions) + this.live = { + indexer, + engine, + db, + service: createSessionSearchService({ engine, indexer }) + } + void indexer.start().catch(this.onError) + } catch (error) { + // A failed open must leave nothing half-built: the indexer stakes the + // database path when its store opens, and only close() releases it. + db?.close() + indexer?.close() + this.live = null + this.onError(error) + } + } + + private closeLive(): void { + const live = this.live + this.live = null + if (!live) { + return + } + try { + live.indexer.close() + } finally { + live.db.close() + } + } +} diff --git a/src/main/ai-vault-search/session-search-policy.ts b/src/main/ai-vault-search/session-search-policy.ts new file mode 100644 index 00000000000..9f706f35fea --- /dev/null +++ b/src/main/ai-vault-search/session-search-policy.ts @@ -0,0 +1,25 @@ +import { + DEFAULT_AI_VAULT_SEARCH_SETTINGS, + resolveAiVaultSearchSettings, + type AiVaultSearchSettings +} from '../../shared/ai-vault-search-settings' +import type { GlobalSettings } from '../../shared/global-settings-types' + +// Why a source and not a captured value: the scanner child is spawned lazily and +// respawned after a fault, so its init frame has to read consent at spawn time. +// Before a composition root installs one, every read is the safe default (off). +let readSettings: (() => AiVaultSearchSettings) | null = null + +export function installSessionSearchPolicySource( + source: (() => Pick) | null +): void { + readSettings = source ? () => resolveAiVaultSearchSettings(source()) : null +} + +export function sessionSearchPolicy(): AiVaultSearchSettings { + return readSettings?.() ?? DEFAULT_AI_VAULT_SEARCH_SETTINGS +} + +export function resetSessionSearchPolicyForTests(): void { + readSettings = null +} diff --git a/src/main/ai-vault-search/session-search-retention-policy.ts b/src/main/ai-vault-search/session-search-retention-policy.ts index c7fa8a0b6d1..f6b7c483d48 100644 --- a/src/main/ai-vault-search/session-search-retention-policy.ts +++ b/src/main/ai-vault-search/session-search-retention-policy.ts @@ -1,25 +1,12 @@ -const DAY_MS = 86_400_000 -const HISTORY_DAYS_MAX = 3_650 +import { normalizeAiVaultSearchHistoryDays } from '../../shared/ai-vault-search-settings' -/** - * The retention window, as the indexer's callers state it and as the store - * consumes it. Settings storage is PR 3b's problem; this is the arithmetic. - */ -function normalizeSessionSearchHistoryDays(value: number | null): number | null { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { - return null - } - // Why floor then re-check: a fractional day floors to 0, which reads as "all - // history" on one side and "now" on the other; make the two agree. - const days = Math.floor(value) - return days <= 0 ? null : Math.min(HISTORY_DAYS_MAX, days) -} +const DAY_MS = 86_400_000 /** The oldest transcript mtime worth indexing; null means no bound. */ export function sessionSearchHistoryCutoffMs( historyDays: number | null, nowMs: number ): number | null { - const days = normalizeSessionSearchHistoryDays(historyDays) + const days = normalizeAiVaultSearchHistoryDays(historyDays) return days === null ? null : nowMs - days * DAY_MS } diff --git a/src/main/ai-vault-search/session-search-root-refresh.test.ts b/src/main/ai-vault-search/session-search-root-refresh.test.ts new file mode 100644 index 00000000000..43d57e19536 --- /dev/null +++ b/src/main/ai-vault-search/session-search-root-refresh.test.ts @@ -0,0 +1,99 @@ +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { SessionSearchIndexer } from './session-search-indexer' +import { + FakeSessionSearchClock, + openSessionSearchIndexerHarness, + writeClaudeTranscript, + type SessionSearchIndexerHarness +} from './session-search-indexer-test-fixture' +import { resetSessionParseCacheForTests } from '../ai-vault/session-scanner-parse-cache' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' + +let harness: SessionSearchIndexerHarness +let indexer: SessionSearchIndexer | undefined +beforeEach(async () => { + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + harness = await openSessionSearchIndexerHarness('search-root-refresh') +}) +afterEach(async () => { + indexer?.close() + indexer = undefined + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + await harness.cleanup() +}) + +it('refreshes roots on scheduled full sweeps and reuses them on recent cycles', async () => { + const clock = new FakeSessionSearchClock() + let roots = harness.roots + const resolveRoots = vi.fn(async () => roots) + indexer = new SessionSearchIndexer({ + databasePath: harness.databasePath, + roots, + resolveRoots, + historyDays: null, + clock, + fullSweepEveryCycles: 1 + }) + await indexer.start() + expect(resolveRoots).toHaveBeenCalledTimes(1) + const newRoot = join(harness.root, 'late') + const id = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + await writeClaudeTranscript(join(newRoot, 'project', `${id}.jsonl`), ['a late conversation'], id) + roots = { ...roots, claudeProjectsDir: newRoot } + clock.advance(20_000) + await indexer.settled() + expect(resolveRoots).toHaveBeenCalledTimes(1) + expect(indexer.status().filesIndexed).toBe(0) + clock.advance(20_000) + await indexer.settled() + expect(resolveRoots).toHaveBeenCalledTimes(2) + expect(indexer.status().filesIndexed).toBe(1) + clock.advance(20_000) + await indexer.settled() + expect(resolveRoots).toHaveBeenCalledTimes(2) + expect(indexer.status().filesIndexed).toBe(1) +}) + +it('does not access a closed store when pending discovery completes', async () => { + const pending = Promise.withResolvers() + const errors = vi.fn() + const resolver = vi.fn(() => pending.promise) + indexer = new SessionSearchIndexer({ + databasePath: harness.databasePath, + roots: harness.roots, + resolveRoots: resolver, + historyDays: null, + onError: errors + }) + const start = indexer.start() + await vi.waitFor(() => expect(resolver).toHaveBeenCalledTimes(1)) + indexer.close() + pending.resolve(harness.roots) + await start + expect(errors).not.toHaveBeenCalled() + expect(indexer.status().filesIndexed).toBe(0) +}) + +it('retries discovery after failure without silently sweeping stale roots', async () => { + const errors = vi.fn() + const resolveRoots = vi + .fn() + .mockRejectedValueOnce(new Error('unavailable')) + .mockResolvedValue(harness.roots) + indexer = new SessionSearchIndexer({ + databasePath: harness.databasePath, + roots: harness.roots, + resolveRoots, + historyDays: null, + onError: errors + }) + await indexer.start() + expect(errors).toHaveBeenCalledTimes(1) + expect(indexer.status().lastSweepCompletedAt).toBeNull() + await indexer.reconcile() + expect(resolveRoots).toHaveBeenCalledTimes(2) + expect(indexer.status().lastSweepCompletedAt).not.toBeNull() +}) diff --git a/src/main/ai-vault-search/session-search-scan-roots.test.ts b/src/main/ai-vault-search/session-search-scan-roots.test.ts index 51b7381c78b..39324ca5da8 100644 --- a/src/main/ai-vault-search/session-search-scan-roots.test.ts +++ b/src/main/ai-vault-search/session-search-scan-roots.test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest' import { delimiter, join } from 'node:path' import type { SessionFileDiscovery } from '../ai-vault/session-scanner-types' -import { sessionSearchRootListings } from './session-search-scan-roots' +import { sameSessionSearchRoots, sessionSearchRootListings } from './session-search-scan-roots' const STATE = '/tmp/ss-roots/openclaw-state' const LEGACY = '/tmp/ss-roots/openclaw-legacy' @@ -56,3 +56,23 @@ it('attributes a file by path segment, not by string prefix', () => { expect(byRoot[agents]).toBe(0) expect(byRoot[legacy]).toBe(0) }) + +it('reads a re-resolved root set as the same trees when only spelling order differs', () => { + expect( + sameSessionSearchRoots( + { openclawStateDir: STATE, wslHomeDirs: ['/home/a', '/home/b'] }, + { wslHomeDirs: ['/home/b', '/home/a'], openclawStateDir: STATE } + ) + ).toBe(true) + // An absent key and an explicitly undefined one are the same absence. + expect(sameSessionSearchRoots({ openclawStateDir: STATE }, { openclawStateDir: STATE })).toBe( + true + ) +}) + +it('reads an added, dropped or changed root as a different set', () => { + const base = { openclawStateDir: STATE, wslHomeDirs: ['/home/a'] } + expect(sameSessionSearchRoots(base, { ...base, openclawLegacyStateDir: LEGACY })).toBe(false) + expect(sameSessionSearchRoots(base, { openclawStateDir: STATE })).toBe(false) + expect(sameSessionSearchRoots(base, { ...base, wslHomeDirs: ['/home/b'] })).toBe(false) +}) diff --git a/src/main/ai-vault-search/session-search-scan-roots.ts b/src/main/ai-vault-search/session-search-scan-roots.ts index 8df3510199e..5c1041eac25 100644 --- a/src/main/ai-vault-search/session-search-scan-roots.ts +++ b/src/main/ai-vault-search/session-search-scan-roots.ts @@ -133,3 +133,30 @@ export function sessionSearchEmptiedRoots( ): Set { return new Set([...previous].filter((root) => !current.has(root))) } + +/** + * Whether two root sets name the same trees. + * + * Structural, not by reference: the caller re-resolves roots on every policy + * push, so a live index that already walks these trees must not be rebuilt just + * because the object is new. Key-sorted rather than a plain JSON compare because + * nothing fixes the key order two producers write, and list-sorted because the + * indexer walks every root, so a re-enumeration that reorders is not a change. + */ +export function sameSessionSearchRoots( + a: SessionSearchScanRoots, + b: SessionSearchScanRoots +): boolean { + const left = comparableRootFields(a) + const right = comparableRootFields(b) + return left.length === right.length && left.every((field, index) => field === right[index]) +} + +function comparableRootFields(roots: SessionSearchScanRoots): string[] { + return Object.entries(roots) + .filter(([, value]) => value !== undefined) + .map( + ([key, value]) => `${key}=${JSON.stringify(Array.isArray(value) ? [...value].sort() : value)}` + ) + .sort() +} diff --git a/src/main/ai-vault-search/session-search-service-init.ts b/src/main/ai-vault-search/session-search-service-init.ts new file mode 100644 index 00000000000..1ad4e2a2a3a --- /dev/null +++ b/src/main/ai-vault-search/session-search-service-init.ts @@ -0,0 +1,27 @@ +import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import type { AiVaultSessionSearchInit } from '../ai-vault/session-scanner-service-protocol' +import { sessionSearchDatabasePath } from './session-search-database-path' +import { sessionSearchPolicy } from './session-search-policy' + +// Captured once from the composition root's data path, like the parse cache: +// every export is inert until then, so no test or early import can index. +let databasePath: string | null = null + +export function installSessionSearchDataRoot(dataRoot: string): void { + databasePath = sessionSearchDatabasePath(dataRoot) +} + +/** Read at every spawn and every settings change; null before the data root is installed. */ +export function sessionSearchServiceInit(): AiVaultSessionSearchInit | null { + return databasePath + ? { + databasePath, + settings: sessionSearchPolicy(), + roots: { executionHostId: LOCAL_EXECUTION_HOST_ID } + } + : null +} + +export function resetSessionSearchServiceInitForTests(): void { + databasePath = null +} diff --git a/src/main/ai-vault-search/session-search-service.test.ts b/src/main/ai-vault-search/session-search-service.test.ts index 235b00684cb..8d990bcda5c 100644 --- a/src/main/ai-vault-search/session-search-service.test.ts +++ b/src/main/ai-vault-search/session-search-service.test.ts @@ -86,7 +86,7 @@ describe('real index to public service adapter', () => { hits: [expect.objectContaining({ evidence: null })] }) await service.reconcile() - expect(indexer.reconcile).toHaveBeenCalledExactlyOnceWith({ full: false }) + expect(indexer.reconcile).toHaveBeenCalledExactlyOnceWith({ full: true }) const status = await service.status() expect(AiVaultSearchStatusSchema.parse(status)).toEqual(status) expect(status.generation).toBeGreaterThan(0) diff --git a/src/main/ai-vault-search/session-search-service.ts b/src/main/ai-vault-search/session-search-service.ts index 0f9cf6c615e..237d59eebf7 100644 --- a/src/main/ai-vault-search/session-search-service.ts +++ b/src/main/ai-vault-search/session-search-service.ts @@ -21,7 +21,7 @@ export function createSessionSearchService({ indexer: Pick }): SessionSearchService { return { - reconcile: () => indexer.reconcile({ full: false }), + reconcile: () => indexer.reconcile({ full: true }), status: async () => ({ enabled: true, ...indexer.status(), generation: engine.generation() }), search: async (request) => { if (request.cursor === '') { diff --git a/src/main/ai-vault-search/session-search-sqlite-support.ts b/src/main/ai-vault-search/session-search-sqlite-support.ts new file mode 100644 index 00000000000..c59fe8d3db3 --- /dev/null +++ b/src/main/ai-vault-search/session-search-sqlite-support.ts @@ -0,0 +1,26 @@ +/** + * Whether this Node can hold an index at all. + * + * The store is `node:sqlite`, reached through `process.getBuiltinModule`, which + * neither exists on Node 18. That is not a hypothetical floor: orcad and the SSH + * relay are both built for Node 18 and run on whatever the host has, and + * build-orcad.mjs keeps that floor deliberately by excluding the only clusters + * that import `node:sqlite` statically. A host without it registers no search + * service at all rather than one that fails at every call. + */ +export function sessionSearchSqliteAvailable(): boolean { + if (typeof process.getBuiltinModule !== 'function') { + return false + } + try { + const sqlite: unknown = process.getBuiltinModule('node:sqlite') + return ( + typeof sqlite === 'object' && + sqlite !== null && + 'DatabaseSync' in sqlite && + typeof sqlite.DatabaseSync === 'function' + ) + } catch { + return false + } +} diff --git a/src/main/ai-vault-search/session-search-store.ts b/src/main/ai-vault-search/session-search-store.ts index 71215bb7174..435fa16ce8a 100644 --- a/src/main/ai-vault-search/session-search-store.ts +++ b/src/main/ai-vault-search/session-search-store.ts @@ -199,30 +199,32 @@ export class SessionSearchStore { files(): SessionSearchFileRow[] { return ( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The files schema and SELECT aliases define this row; REAL casts return numeric IDs or null. - this.db - .prepare( - // Numeric stat IDs may exceed SQLite's safe INTEGER-to-number read range. - `SELECT path, CAST(dev AS REAL) AS dev, CAST(ino AS REAL) AS ino, + ( + this.db + .prepare( + // Numeric stat IDs may exceed SQLite's safe INTEGER-to-number read range. + `SELECT path, CAST(dev AS REAL) AS dev, CAST(ino AS REAL) AS ino, mtime_ms AS mtimeMs, size_bytes AS sizeBytes, state, fail_count AS failCount, failed_mtime_ms AS failedMtimeMs FROM files` - ) - .all() as (Omit & { - dev: number | null - ino: number | null - })[] - ).map((row) => ({ - path: row.path, - identity: - typeof row.dev === 'number' && typeof row.ino === 'number' - ? { dev: row.dev, ino: row.ino } - : null, - mtimeMs: row.mtimeMs, - sizeBytes: row.sizeBytes, - state: row.state, - failCount: row.failCount, - failedMtimeMs: row.failedMtimeMs - })) + ) + .all() as (Omit & { + dev: number | null + ino: number | null + })[] + ).map((row) => ({ + path: row.path, + identity: + typeof row.dev === 'number' && typeof row.ino === 'number' + ? { dev: row.dev, ino: row.ino } + : null, + mtimeMs: row.mtimeMs, + sizeBytes: row.sizeBytes, + state: row.state, + failCount: row.failCount, + failedMtimeMs: row.failedMtimeMs + })) + ) } /** diff --git a/src/main/ai-vault/cached-session-list.ts b/src/main/ai-vault/cached-session-list.ts index c9feb5b9daf..673de66e666 100644 --- a/src/main/ai-vault/cached-session-list.ts +++ b/src/main/ai-vault/cached-session-list.ts @@ -7,6 +7,7 @@ import { import { getCachedWslDistros, hasCachedWslDistros, listRunningWslHomeDirsAsync } from '../wsl' import { filterPathsToRunningWslDistrosAsync } from '../wsl-running-path-filter' import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' +import type { AiVaultScanOptions } from './session-scanner-types' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import { AiVaultScanCoordinator } from './ai-vault-scan-coordinator' import { @@ -49,6 +50,28 @@ export function configureAiVaultSessionSources(next: AiVaultSessionSources): voi sources = next } +/** + * The trees a local scan enumerates, resolved fresh because a WSL distro can start + * or stop between scans. The search index reads the same function, so it walks + * exactly what the session list walks. + */ +export async function localAiVaultScanRoots(): Promise< + Required> & + Pick +> { + const [additionalCodexHomes, wslHomeDirs] = await Promise.all([ + filterPathsToRunningWslDistrosAsync(configuredAdditionalCodexHomePaths()), + getAiVaultWslHomeDirs() + ]) + return { + additionalCodexSessionsDirs: additionalCodexHomes.map((homePath) => join(homePath, 'sessions')), + wslHomeDirs, + // Why: this scan is always host-local; callers addressing this host by a + // runtime id get the result restamped at the RPC edge, never rescanned. + executionHostId: LOCAL_EXECUTION_HOST_ID + } +} + /** The extra Codex homes session discovery scans. Anything that decides what a listed row may be * resumed from must read the same set, or a row can be listed and then refuse to resume. */ export function configuredAdditionalCodexHomePaths(): readonly string[] { @@ -86,24 +109,12 @@ export async function listAiVaultSessions( force: args?.force, signal: options.signal, start: async (scanSignal) => { - const configuredCodexHomes = sources.getAdditionalCodexHomePaths?.() ?? [] - const [additionalCodexHomes, wslHomeDirs] = await Promise.all([ - filterPathsToRunningWslDistrosAsync(configuredCodexHomes), - getAiVaultWslHomeDirs() - ]) - const additionalCodexSessionsDirs = additionalCodexHomes.map((homePath) => - join(homePath, 'sessions') - ) const result = await scanAiVaultSessionsInBackground( { limit: args?.limit, unlimited: args?.unlimited, scopePaths: args?.scopePaths, - additionalCodexSessionsDirs, - wslHomeDirs, - // Why: this scan is always host-local; callers addressing this host by a - // runtime id get the result restamped at the RPC edge, never rescanned. - executionHostId: LOCAL_EXECUTION_HOST_ID + ...(await localAiVaultScanRoots()) }, scanSignal ) diff --git a/src/main/ai-vault/remote-session-content-lines.ts b/src/main/ai-vault/remote-session-content-lines.ts index d62e8988018..16786704847 100644 --- a/src/main/ai-vault/remote-session-content-lines.ts +++ b/src/main/ai-vault/remote-session-content-lines.ts @@ -4,6 +4,8 @@ import { throwIfAiVaultScanCancelled } from './ai-vault-scan-cancellation' export type RemoteSessionContent = string | AsyncIterable +const MAX_REMOTE_SESSION_RECORD_BYTES = 10 * 1024 * 1024 + const REMOTE_CONTENT_YIELD_LINE_COUNT = 200 const REMOTE_CONTENT_YIELD_CHAR_COUNT = 256 * 1024 @@ -80,7 +82,7 @@ export async function* streamedSessionContentLines( ): AsyncGenerator { let count = 0 let chars = 0 - for await (const record of splitTranscriptStreamLines(bytes)) { + for await (const record of splitTranscriptStreamLines(bytes, MAX_REMOTE_SESSION_RECORD_BYTES)) { throwIfAiVaultScanCancelled(signal) const line = record.line.endsWith('\r') && (record.terminated || signal) diff --git a/src/main/ai-vault/remote-session-large-transcripts.test.ts b/src/main/ai-vault/remote-session-large-transcripts.test.ts index 62d8b8c5ed1..df05700c2cf 100644 --- a/src/main/ai-vault/remote-session-large-transcripts.test.ts +++ b/src/main/ai-vault/remote-session-large-transcripts.test.ts @@ -17,6 +17,35 @@ const jsonl = (rows: unknown[]) => `${rows.map((row) => JSON.stringify(row)).joi const filler = jsonl([{ type: 'irrelevant_event', payload: 'x'.repeat(1024) }]).repeat(11000) describe('large remote history through real relay filesystem', () => { + it('reports an oversized record without losing healthy sessions or publishing a partial session', async () => { + const home = await mkdtemp(join(tmpdir(), 'orca-history-record-limit-')) + try { + const directory = join(home, '.codex', 'sessions') + await mkdir(directory, { recursive: true }) + const metadata = (id: string) => + jsonl([{ type: 'session_meta', payload: { id, cwd: '/repo' } }]) + const badPath = join(directory, 'bad.jsonl') + await writeFile(badPath, metadata('bad') + 'x'.repeat(11 * 1024 * 1024)) + await writeFile(join(directory, 'good.jsonl'), metadata('good')) + const result = await scanRemoteAiVaultSessions({ + provider: createRelayAiVaultFilesystemProvider(), + executionHostId: 'ssh:record-limit', + remoteHome: home, + hostPlatform: platform, + unlimited: true + }) + expect(result.sessions.map((session) => session.sessionId)).toEqual(['good']) + expect(result.issues).toEqual([ + expect.objectContaining({ + path: badPath, + message: 'Session transcript record exceeds 10485760 byte limit' + }) + ]) + } finally { + await rm(home, { recursive: true, force: true }) + } + }) + it('lists a large Codex rollout with middle messages and usage intact', async () => { const home = await mkdtemp(join(tmpdir(), 'orca-history-17744-')) try { diff --git a/src/main/ai-vault/remote-session-stream-lifecycle.test.ts b/src/main/ai-vault/remote-session-stream-lifecycle.test.ts index 46668e3574e..2b8e95ea2a4 100644 --- a/src/main/ai-vault/remote-session-stream-lifecycle.test.ts +++ b/src/main/ai-vault/remote-session-stream-lifecycle.test.ts @@ -4,6 +4,26 @@ import { readStreamedSessionDocument } from './session-document-stream' import { limitRemoteScanFilesystemConcurrency } from './remote-session-scan-concurrency' describe('stream lifetime and retained document work', () => { + it('aborts a newline-free record at the byte ceiling and closes the source', async () => { + let closed = false + let reads = 0 + async function* bytes() { + const chunk = Buffer.alloc(1024 * 1024, 'x') + try { + for (; reads < 100;) { + reads++ + yield chunk + } + } finally { + closed = true + } + } + const lines = streamedSessionContentLines(bytes()) + await expect(lines.next()).rejects.toThrow('record exceeds 10485760 byte limit') + expect(reads).toBe(11) + expect(closed).toBe(true) + }) + it('releases the source when a line consumer finishes early', async () => { let closed = false async function* bytes() { diff --git a/src/main/ai-vault/session-scanner-graph-parsers.ts b/src/main/ai-vault/session-scanner-graph-parsers.ts index 3ca4e754106..353decfcd46 100644 --- a/src/main/ai-vault/session-scanner-graph-parsers.ts +++ b/src/main/ai-vault/session-scanner-graph-parsers.ts @@ -1,3 +1,4 @@ +import { foldOmpTranscriptTitle, type OmpTranscriptTitle } from './session-scanner-omp-title' import { remoteSessionContentLines, type RemoteSessionContent @@ -15,7 +16,8 @@ import type { } from './session-scanner-types' import type { TranscriptMessageSink } from './session-transcript-consumers' import { - accumulatorFoldResumeState, + accumulatorSessionIdentity, + cloneSessionAccumulator, addPreviewContent, addPreviewMessage, createAccumulator, @@ -212,12 +214,24 @@ export async function parseMessageGraphSessionContent( }) } -function consumeMessageGraphRecordLine(accumulator: SessionAccumulator, line: string): void { +type MessageGraphParseState = { + accumulator: SessionAccumulator + ompTitle: OmpTranscriptTitle | null +} + +function consumeMessageGraphRecordLine(state: MessageGraphParseState, line: string): void { + const { accumulator } = state const record = parseJsonObject(line) if (!record) { return } updateTimeline(accumulator, extractString(record.timestamp)) + if (accumulator.agent === 'omp') { + state.ompTitle = foldOmpTranscriptTitle(state.ompTitle, record) + if (state.ompTitle) { + accumulator.title = state.ompTitle.title + } + } if (record.type === 'session') { const sessionId = extractString(record.id) if (sessionId) { @@ -241,7 +255,11 @@ function consumeMessageGraphRecordLine(accumulator: SessionAccumulator, line: st if (role === 'user' || role === 'assistant') { accumulator.messageCount++ if (role === 'user') { - accumulator.title ??= extractMessageText(message) + if (accumulator.agent === 'omp') { + accumulator.fallbackTitle ??= extractMessageText(message) + } else { + accumulator.title ??= extractMessageText(message) + } } else { accumulator.model = extractString(message?.model) ?? accumulator.model accumulator.totalTokens += tokenTotal(message?.usage) @@ -255,16 +273,38 @@ export function createMessageGraphSessionResumeState( file: FileWithMtime, messages?: TranscriptMessageSink ): ResumableSessionParseState { - const state = accumulatorFoldResumeState( - createAccumulator({ agent, file, sessionId: sessionIdFromFileName(file.path), messages }), - consumeMessageGraphRecordLine - ) + const state = createMessageGraphResumeState({ + accumulator: createAccumulator({ + agent, + file, + sessionId: sessionIdFromFileName(file.path), + messages + }), + ompTitle: null + }) // Why: only OMP materializes task-subagent transcripts beside its sessions // (in the same-named artifact dir); the row UI shows the count without // expanding details. Pi/OpenClaw/Prime Agent have no such layout — skip the readdir. return agent === 'omp' ? withOmpSubagentTranscriptCount(state, file.path) : state } +function createMessageGraphResumeState(state: MessageGraphParseState): ResumableSessionParseState { + return { + consumeLine: (line) => consumeMessageGraphRecordLine(state, line), + identity: () => accumulatorSessionIdentity(state.accumulator), + clone: () => + createMessageGraphResumeState({ + accumulator: cloneSessionAccumulator(state.accumulator), + ompTitle: state.ompTitle + }), + touchFile: (file) => { + state.accumulator.modifiedAt = file.modifiedAt + }, + finalize: (platform, options) => + finalizeSession(cloneSessionAccumulator(state.accumulator), platform, options) + } +} + async function parseMessageGraphSessionLines(args: { agent: MessageGraphAgent file: FileWithMtime diff --git a/src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts b/src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts index 16511e14239..0162f536315 100644 --- a/src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts +++ b/src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts @@ -78,4 +78,42 @@ describe('listOmpSubagentSessions', () => { listOmpSubagentSessions({ parentFilePath: parentPath, platform: 'darwin' }) ).resolves.toEqual({ sessions: [], issues: [] }) }) + it('traverses each saved generation through its own transcript without flattening descendants', async () => { + const workspace = await mkdtemp(join(tmpdir(), 'orca-omp-nested-list-')) + tempRoots.push(workspace) + const parentPath = join(workspace, `${SESSION_STEM}.jsonl`) + const childPath = join(workspace, SESSION_STEM, 'Worker.jsonl') + const grandchildPath = join(workspace, SESSION_STEM, 'Worker', 'Research.jsonl') + await mkdir(join(workspace, SESSION_STEM, 'Worker'), { recursive: true }) + await writeFile( + parentPath, + childTranscript(PARENT_SESSION_ID, '2026-05-01T10:00:00Z', 'Coordinate') + ) + await writeFile( + childPath, + childTranscript('worker-id', '2026-05-01T10:01:00Z', 'Delegate research') + ) + await writeFile( + grandchildPath, + childTranscript('research-id', '2026-05-01T10:02:00Z', 'Investigate') + ) + const children = await listOmpSubagentSessions({ parentFilePath: parentPath }) + expect(children.issues).toEqual([]) + expect(children.sessions).toHaveLength(1) + expect(children.sessions[0]).toMatchObject({ + filePath: childPath, + sessionId: 'worker-id', + subagentTranscriptCount: 1 + }) + const grandchildren = await listOmpSubagentSessions({ + parentFilePath: children.sessions[0].filePath + }) + expect(grandchildren.issues).toEqual([]) + expect(grandchildren.sessions).toHaveLength(1) + expect(grandchildren.sessions[0]).toMatchObject({ + filePath: grandchildPath, + sessionId: 'research-id', + subagentTranscriptCount: 0 + }) + }) }) diff --git a/src/main/ai-vault/session-scanner-omp-subagent-listing.ts b/src/main/ai-vault/session-scanner-omp-subagent-listing.ts index 3ecfade1813..d98c53b78b0 100644 --- a/src/main/ai-vault/session-scanner-omp-subagent-listing.ts +++ b/src/main/ai-vault/session-scanner-omp-subagent-listing.ts @@ -89,10 +89,7 @@ async function parseOmpSubagentTranscript(args: { const filePath = join(args.artifactDir, args.name) try { const fileStat = await wslGatedStat(filePath, OMP_SUBAGENT_FS_PRIORITY) - // The shared OMP parser decorates every parse with an artifact-dir count, so - // a child row carries its own grandchild count. It is accurate but has no - // renderer — subagent rows don't expand — and this lister is local-only, so - // the remote partition never reaches it. + // Each child carries its own count for on-demand nested expansion. const session = await parseMessageGraphSessionFile( 'omp', { path: filePath, mtimeMs: fileStat.mtimeMs, modifiedAt: fileStat.mtime.toISOString() }, diff --git a/src/main/ai-vault/session-scanner-omp-title.test.ts b/src/main/ai-vault/session-scanner-omp-title.test.ts new file mode 100644 index 00000000000..3e2d6a966cf --- /dev/null +++ b/src/main/ai-vault/session-scanner-omp-title.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from 'vitest' +import { + createMessageGraphSessionResumeState, + parseMessageGraphSessionContent +} from './session-scanner-graph-parsers' + +const file = { path: '/tmp/omp-title.jsonl', mtimeMs: 1, modifiedAt: '2026-09-14T00:00:00.000Z' } +const prompt = { type: 'message', message: { role: 'user', content: 'First prompt' } } +const header = { type: 'session', id: 'session-id', cwd: '/folder workspace' } +const line = (record: unknown) => JSON.stringify(record) +async function parse(records: unknown[], agent: 'omp' | 'pi' = 'omp') { + return parseMessageGraphSessionContent( + agent, + file, + [header, ...records].map(line).join('\n'), + 'darwin' + ) +} + +describe('OMP stored history names', () => { + it.each([ + { type: 'session', title: 'Harness name', titleSource: 'user' }, + { + type: 'title', + v: 1, + title: 'Harness name', + source: 'user', + updatedAt: '2026-09-14T01:00:00Z', + pad: '' + }, + { type: 'title_change', title: 'Harness name', source: 'user' }, + { type: 'session_info', name: 'Harness name' } + ])('uses persisted %j ahead of the first prompt', async (record) => { + expect((await parse([prompt, record]))?.title).toBe('Harness name') + }) + + it('preserves a user name through stale header and later automatic records', async () => { + expect( + ( + await parse([ + { + type: 'title', + v: 1, + title: 'User name', + source: 'user', + updatedAt: '2026-09-14T02:00:00Z', + pad: '' + }, + { ...header, title: 'Old header' }, + prompt, + { + type: 'title_change', + title: 'Auto name', + source: 'auto', + timestamp: '2026-09-14T03:00:00Z' + } + ]) + )?.title + ).toBe('User name') + }) + + it('keeps the current slot ahead of older rename entries, allowing a newer rename', async () => { + const records = [ + { + type: 'title', + v: 1, + title: 'Current slot', + source: 'user', + updatedAt: '2026-09-14T02:00:00Z', + pad: '' + }, + prompt, + { + type: 'title_change', + title: 'Old rename', + source: 'user', + timestamp: '2026-09-14T01:00:00Z' + } + ] + expect((await parse(records))?.title).toBe('Current slot') + expect( + ( + await parse([ + ...records, + { + type: 'title_change', + title: 'New rename', + source: 'user', + timestamp: '2026-09-14T03:00:00Z' + } + ]) + )?.title + ).toBe('New rename') + }) + + it('preserves fallback behavior for missing, empty or unsupported title records', async () => { + expect( + ( + await parse([ + prompt, + { type: 'title_change', title: ' ', source: 'user' }, + { type: 'title_change', title: 'Unknown', source: 'model' }, + { type: 'session_info', title: 'Wrong field' } + ]) + )?.title + ).toBe('First prompt') + expect( + (await parse([prompt, { type: 'title_change', title: 'OMP only', source: 'user' }], 'pi')) + ?.title + ).toBe('First prompt') + }) + + it('clones title authority for append parsing without mutating previous snapshots', async () => { + const state = createMessageGraphSessionResumeState('omp', file) + for (const record of [ + header, + prompt, + { type: 'title_change', title: 'User name', source: 'user' } + ]) { + state.consumeLine(line(record)) + } + const previous = await state.finalize('darwin') + const next = state.clone() + next.consumeLine(line({ type: 'title_change', title: 'Auto name', source: 'auto' })) + expect((await next.finalize('darwin'))?.title).toBe('User name') + next.consumeLine(line({ type: 'title_change', title: 'New name', source: 'user' })) + expect((await next.finalize('darwin'))?.title).toBe('New name') + expect(previous?.title).toBe('User name') + expect(state.identity?.()?.title).toBe('User name') + }) +}) diff --git a/src/main/ai-vault/session-scanner-omp-title.ts b/src/main/ai-vault/session-scanner-omp-title.ts new file mode 100644 index 00000000000..e734350caad --- /dev/null +++ b/src/main/ai-vault/session-scanner-omp-title.ts @@ -0,0 +1,49 @@ +import { extractString, normalizeTitleText, timestampMs } from './session-scanner-values' + +export type OmpTranscriptTitle = { + title: string + source: 'user' | 'auto' + updatedAt: number | null +} + +/** Fold persisted title metadata; a current slot can precede older rename entries. */ +export function foldOmpTranscriptTitle( + current: OmpTranscriptTitle | null, + record: Record +): OmpTranscriptTitle | null { + const legacy = record.type === 'session_info' + if ( + !legacy && + record.type !== 'session' && + record.type !== 'title_change' && + record.type !== 'title' + ) { + return current + } + if (record.type === 'title' && record.v !== 1) { + return current + } + const title = normalizeTitleText(extractString(legacy ? record.name : record.title) ?? '') + if (!title) { + return current + } + const rawSource = legacy ? 'user' : (record.source ?? record.titleSource) + if (rawSource !== undefined && rawSource !== 'user' && rawSource !== 'auto') { + return current + } + const source = rawSource === 'user' ? 'user' : 'auto' + if (current?.source === 'user' && source !== 'user') { + return current + } + const timestamp = timestampMs(record.type === 'title' ? record.updatedAt : record.timestamp) + const updatedAt = Number.isFinite(timestamp) ? timestamp : null + if ( + current?.source === source && + current.updatedAt !== null && + updatedAt !== null && + updatedAt < current.updatedAt + ) { + return current + } + return { title, source, updatedAt } +} diff --git a/src/main/ai-vault/session-scanner-service-client-state.ts b/src/main/ai-vault/session-scanner-service-client-state.ts index 9b64431e219..cc5b514988b 100644 --- a/src/main/ai-vault/session-scanner-service-client-state.ts +++ b/src/main/ai-vault/session-scanner-service-client-state.ts @@ -1,9 +1,12 @@ +import type { SessionSearchScanRoots } from '../ai-vault-search/session-search-scan-roots' import type { ChildProcess } from 'node:child_process' +import { createAiVaultScanCancelledError } from './ai-vault-scan-cancellation' import { AI_VAULT_SERVICE_PROTOCOL_VERSION, type AiVaultServiceInit, type AiVaultServiceLane, - type AiVaultServiceRequest + type AiVaultServiceRequest, + type AiVaultSessionSearchInit } from './session-scanner-service-protocol' export const AI_VAULT_SERVICE_READY_TIMEOUT_MS = 5_000 @@ -16,7 +19,9 @@ export const AI_VAULT_SERVICE_SHUTDOWN_TIMEOUT_MS = 2_000 export type AiVaultServiceProcessFactory = () => ChildProcess export type AiVaultServiceClientOptions = { processFactory: AiVaultServiceProcessFactory - init: Omit + /** Resolved per spawn: a respawned child must see current consent, not the first frame's. */ + init: () => Omit + resolveSessionSearchRoots?: () => Promise idleTimeoutMs?: number onStderr?: (text: string) => void } @@ -49,6 +54,29 @@ export class AiVaultServiceInvalidations { }) } + /** + * Sends one invalidation and resolves on the child's acknowledgement. + * + * The deadline is a startup-sized budget, but a child mid-scan can be slow to + * turn the channel around. Fork IPC ordering already guarantees the child + * applies the invalidation before any request sent after it, so a busy child + * owes nothing here -- only an idle one that misses the deadline is wedged. + */ + send( + child: ChildProcess, + paths: string[], + lanes: { busy: () => boolean; onFault: (error: Error) => void } + ): Promise { + return this.open( + AI_VAULT_SERVICE_READY_TIMEOUT_MS, + (generation) => + lanes.busy() + ? void this.settle(generation) + : lanes.onFault(new Error('AI Vault service cache invalidation timed out.')), + (generation) => child.send({ type: 'invalidate', generation, paths }) + ) + } + settle(generation: number): boolean { const entry = this.pending.get(generation) if (!entry) { @@ -96,7 +124,7 @@ export function retireAiVaultServiceChild(child: ChildProcess): void { child.unref() } -export function armAiVaultServiceCancellationTimeout( +function armAiVaultServiceCancellationTimeout( call: AiVaultServicePendingCall, onExpired: () => void ): void { @@ -107,14 +135,50 @@ export function armAiVaultServiceCancellationTimeout( call.timer.unref?.() } -/** - * A cold start that faults before the request reached the child self-heals on - * the scheduled respawn. Requeue once; the caller rejects when this returns false. - */ +/** Abandons one call, and waits for the child's acknowledgement only when it owes one. */ +export function cancelAiVaultServiceCall( + call: AiVaultServicePendingCall, + lanes: { + queue: AiVaultServicePendingCall[] + active: Map + child: ChildProcess | null + pump: () => void + onFault: (error: Error) => void + } +): void { + if (call.cancelled) { + return + } + call.cancelled = true + call.reject(createAiVaultScanCancelledError()) + const queuedIndex = lanes.queue.indexOf(call) + if (queuedIndex !== -1) { + lanes.queue.splice(queuedIndex, 1) + clearAiVaultServiceCall(call) + lanes.pump() + return + } + if (lanes.active.get(call.lane) !== call) { + return + } + // Why: a call cancelled before it reached the child gets no acknowledgement, + // so waiting on one would kill a healthy service and stall the lane. + if (!call.sent) { + lanes.active.delete(call.lane) + clearAiVaultServiceCall(call) + lanes.pump() + return + } + lanes.child?.send({ type: 'cancel', id: call.request.id }) + armAiVaultServiceCancellationTimeout(call, () => + lanes.onFault(new Error('AI Vault service did not cancel within 2000ms.')) + ) +} + /** Wires a freshly forked child to the client's callbacks and hands it the init frame. */ export function attachAiVaultServiceChild( child: ChildProcess, - init: AiVaultServiceClientOptions['init'], + init: ReturnType, handlers: { onMessage: (message: unknown) => void onFault: (error: Error) => void @@ -133,16 +197,26 @@ export function attachAiVaultServiceChild( } satisfies AiVaultServiceInit) } -export function requeueAiVaultServiceStart( +/** + * A cold start that faults before the request reached the child self-heals on + * the scheduled respawn. Requeue once; anything else is the caller's error. + */ +export function requeueOrRejectAiVaultServiceStart( call: AiVaultServicePendingCall, - queue: AiVaultServicePendingCall[] -): boolean { - if (call.sent || call.cancelled || call.startRetried) { - return false + queue: AiVaultServicePendingCall[], + error: Error, + respawning: boolean +): void { + if (!respawning || call.sent || call.cancelled || call.startRetried) { + rejectAiVaultServiceCall(call, error) + return } call.startRetried = true queue.unshift(call) - return true +} + +export function aiVaultServiceErrorText(error: unknown): string { + return error instanceof Error ? error.message : String(error) } export function clearAiVaultServiceCall(call: AiVaultServicePendingCall): void { @@ -205,3 +279,52 @@ export class AiVaultServiceIdleRetirement { this.timer.unref?.() } } + +/** + * The parent's half of the index setting. + * + * A child running the index is never idle from out here -- its reconcile loop is + * invisible to the parent -- so this is what stops idle retirement ending the + * indexing until some later scan happens to respawn a child. + */ +export class AiVaultServiceSessionSearchHold { + private enabled = false + + /** True while a running index needs a child to exist. */ + get holdsChild(): boolean { + return this.enabled + } + + /** + * Records the policy and tells a live child. A missing one reads the same + * policy out of its init frame, which is why `init` is a factory, not a value. + * @returns whether a child now has to exist. + */ + record(init: AiVaultSessionSearchInit, child: ChildProcess | null): boolean { + this.enabled = init.settings.enabled + child?.send({ type: 'sessionSearch', init }) + return this.enabled + } +} + +/** Starts the request deadline only once the child is ready to receive it. */ +export function sendAiVaultServiceCall( + child: ChildProcess, + call: AiVaultServicePendingCall, + isActive: () => boolean, + onFault: (error: Error) => void +): void { + if (call.cancelled || !isActive()) { + return + } + const timeoutMs = + call.request.operation === 'scan' + ? AI_VAULT_SERVICE_SCAN_TIMEOUT_MS + : AI_VAULT_SERVICE_INTERACTIVE_TIMEOUT_MS + call.timer = setTimeout(() => { + onFault(new Error(`AI Vault service timed out after ${timeoutMs}ms.`)) + }, timeoutMs) + call.timer.unref?.() + call.sent = true + child.send(call.request) +} diff --git a/src/main/ai-vault/session-scanner-service-client.test.ts b/src/main/ai-vault/session-scanner-service-client.test.ts index 97cb0d46afd..1e8a67602f3 100644 --- a/src/main/ai-vault/session-scanner-service-client.test.ts +++ b/src/main/ai-vault/session-scanner-service-client.test.ts @@ -1,12 +1,36 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { AiVaultScannerServiceClient } from './session-scanner-service-client' import { AI_VAULT_SERVICE_READY_TIMEOUT_MS } from './session-scanner-service-client-state' +import type { AiVaultSessionSearchInit } from './session-scanner-service-protocol' import { AiVaultServiceTestChild, aiVaultServiceRequestId, readyAiVaultServiceChild } from './session-scanner-service-test-child' +const SESSION_SEARCH_ON: AiVaultSessionSearchInit = { + databasePath: '/data/ai-vault/session-search.sqlite', + settings: { enabled: true, historyDays: null }, + roots: {} +} + +/** Every fork the client makes, so a respawn can be told from the first start. */ +function setupChildren(policy: () => AiVaultSessionSearchInit | null): { + children: AiVaultServiceTestChild[] + client: AiVaultScannerServiceClient +} { + const children: AiVaultServiceTestChild[] = [] + const client = new AiVaultScannerServiceClient({ + processFactory: () => { + const child = new AiVaultServiceTestChild(12_345 + children.length) + children.push(child) + return child.asChildProcess() + }, + init: () => ({ sessionParseCache: null, sessionSearch: policy() }) + }) + return { children, client } +} + function setup(idleTimeoutMs?: number): { child: AiVaultServiceTestChild client: AiVaultScannerServiceClient @@ -14,7 +38,7 @@ function setup(idleTimeoutMs?: number): { const child = new AiVaultServiceTestChild() const client = new AiVaultScannerServiceClient({ processFactory: () => child.asChildProcess(), - init: { sessionParseCache: null }, + init: () => ({ sessionParseCache: null, sessionSearch: null }), idleTimeoutMs }) return { child, client } @@ -135,7 +159,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null } + init: () => ({ sessionParseCache: null, sessionSearch: null }) }) const titles = client.request({ type: 'request', operation: 'titles', requests: [] }) expect(children).toHaveLength(1) @@ -167,7 +191,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null } + init: () => ({ sessionParseCache: null, sessionSearch: null }) }) const titles = client.request({ type: 'request', operation: 'titles', requests: [] }) @@ -190,7 +214,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null } + init: () => ({ sessionParseCache: null, sessionSearch: null }) }) const titles = client.request({ type: 'request', operation: 'titles', requests: [] }) @@ -224,7 +248,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null } + init: () => ({ sessionParseCache: null, sessionSearch: null }) }) // Each request retries its cold start once, so two requests spend the three // faults the circuit breaker needs. @@ -242,11 +266,9 @@ describe('AiVaultScannerServiceClient', () => { vi.advanceTimersByTime(AI_VAULT_SERVICE_READY_TIMEOUT_MS) await Promise.resolve() vi.advanceTimersByTime(5_000) - await expect(blocked).rejects.toThrow('circuit is open') expect(children).toHaveLength(3) client.clearRestartCircuit() - const retried = client.request({ type: 'request', operation: 'titles', requests: [] }) await vi.waitFor(() => expect(children).toHaveLength(4)) readyAiVaultServiceChild(children[3]!) await vi.waitFor(() => @@ -258,7 +280,7 @@ describe('AiVaultScannerServiceClient', () => { operation: 'titles', value: { titles: [] } }) - await expect(retried).resolves.toEqual({ titles: [] }) + await expect(blocked).resolves.toEqual({ titles: [] }) client.dispose() }) @@ -314,7 +336,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null } + init: () => ({ sessionParseCache: null, sessionSearch: null }) }) const invalidation = client.invalidate(['/tmp/deleted.jsonl']) readyAiVaultServiceChild(children[0]!) @@ -350,7 +372,7 @@ describe('AiVaultScannerServiceClient', () => { children.push(child) return child.asChildProcess() }, - init: { sessionParseCache: null }, + init: () => ({ sessionParseCache: null, sessionSearch: null }), idleTimeoutMs: 100 }) @@ -392,6 +414,152 @@ describe('AiVaultScannerServiceClient', () => { client.dispose() }) + // The child holds the index while the setting is on, and its reconcile loop is + // invisible from here: retiring it would stop indexing until the next scan + // happened to respawn one, which is not a guarantee anyone stated. + it('spawns a child for the index and never retires it while the index is on', async () => { + vi.useFakeTimers() + const { child, client } = setup(100) + const on = { + databasePath: '/data/ai-vault/session-search.sqlite', + settings: { enabled: true, historyDays: null }, + roots: {} + } + + // No request outstanding: turning the index on is itself what spawns a child. + client.updateSessionSearch(on) + readyAiVaultServiceChild(child) + await Promise.resolve() + expect(child.sent).toContainEqual(expect.objectContaining({ type: 'init' })) + + vi.advanceTimersByTime(10_000) + expect(child.sent).not.toContainEqual({ type: 'shutdown' }) + + // A live child hears the change directly rather than waiting for a respawn. + const narrowed = { ...on, settings: { enabled: true, historyDays: 30 } } + client.updateSessionSearch(narrowed) + expect(child.sent).toContainEqual({ type: 'sessionSearch', init: narrowed }) + vi.advanceTimersByTime(10_000) + expect(child.sent).not.toContainEqual({ type: 'shutdown' }) + + client.updateSessionSearch({ ...on, settings: { enabled: false, historyDays: null } }) + vi.advanceTimersByTime(100) + expect(child.sent).toContainEqual({ type: 'shutdown' }) + client.dispose() + }) + + it('re-reads the init frame on every spawn so a respawn sees current consent', async () => { + const children: AiVaultServiceTestChild[] = [] + let enabled = false + const client = new AiVaultScannerServiceClient({ + processFactory: () => { + const child = new AiVaultServiceTestChild(12_345 + children.length) + children.push(child) + return child.asChildProcess() + }, + init: () => ({ + sessionParseCache: null, + sessionSearch: { + databasePath: '/data/ai-vault/session-search.sqlite', + settings: { enabled, historyDays: null }, + roots: {} + } + }) + }) + + const first = client.request({ type: 'request', operation: 'titles', requests: [] }) + readyAiVaultServiceChild(children[0]!) + await Promise.resolve() + expect(children[0]!.sent[0]).toMatchObject({ sessionSearch: { settings: { enabled: false } } }) + + enabled = true + children[0]!.emit('error', new Error('crashed')) + await expect(first).rejects.toThrow('crashed') + void client.request({ type: 'request', operation: 'titles', requests: [] }).catch(() => {}) + await vi.waitFor(() => expect(children.length).toBeGreaterThan(1)) + for (const respawned of children.slice(1)) { + expect(respawned.sent[0]).toMatchObject({ sessionSearch: { settings: { enabled: true } } }) + } + client.dispose() + }) + + // The hold is the only thing keeping this child alive, so nothing else will + // restart it: without its own restart, an idle indexing child that crashes + // leaves the index stopped until some unrelated request happens to arrive. + it('restarts a child that faulted while the index was holding it', async () => { + vi.useFakeTimers() + const { children, client } = setupChildren(() => SESSION_SEARCH_ON) + client.updateSessionSearch(SESSION_SEARCH_ON) + readyAiVaultServiceChild(children[0]!) + await Promise.resolve() + + // No queued call and no outstanding invalidation: an idle child simply dies. + children[0]!.emit('error', new Error('crashed')) + expect(children).toHaveLength(1) + vi.advanceTimersByTime(250) + + expect(children).toHaveLength(2) + expect(children[1]!.sent[0]).toMatchObject({ + type: 'init', + sessionSearch: { settings: { enabled: true } } + }) + client.dispose() + }) + + it.each([false, true])( + 'waits for circuit expiry before restarting a held child (dispose=%s)', + async (dispose) => { + vi.useFakeTimers() + const { children, client } = setupChildren(() => SESSION_SEARCH_ON) + try { + client.updateSessionSearch(SESSION_SEARCH_ON) + for (const delay of [250, 1_000]) { + readyAiVaultServiceChild(children.at(-1)!) + await Promise.resolve() + children.at(-1)!.emit('error', new Error('temporary fault')) + await vi.advanceTimersByTimeAsync(delay) + } + expect(children).toHaveLength(3) + readyAiVaultServiceChild(children[2]!) + await Promise.resolve() + children[2]!.emit('error', new Error('temporary fault')) + await vi.advanceTimersByTimeAsync(59_999) + expect(children).toHaveLength(3) + if (dispose) { + client.dispose() + } + await vi.advanceTimersByTimeAsync(1) + expect(children).toHaveLength(dispose ? 3 : 4) + if (!dispose) { + readyAiVaultServiceChild(children[3]!) + } + } finally { + client.dispose() + } + } + ) + + it('leaves a faulted idle child dead while the index is off', async () => { + vi.useFakeTimers() + const { children, client } = setupChildren(() => null) + const titles = client.request({ type: 'request', operation: 'titles', requests: [] }) + readyAiVaultServiceChild(children[0]!) + await Promise.resolve() + children[0]!.emit('message', { + type: 'result', + id: aiVaultServiceRequestId(children[0]!, 'titles'), + operation: 'titles', + value: { titles: [] } + }) + await titles + + children[0]!.emit('error', new Error('crashed')) + vi.advanceTimersByTime(5_000) + + expect(children).toHaveLength(1) + client.dispose() + }) + it('retires an idle child gracefully, then kills it after the shutdown bound', async () => { vi.useFakeTimers() const { child, client } = setup(100) diff --git a/src/main/ai-vault/session-scanner-service-client.ts b/src/main/ai-vault/session-scanner-service-client.ts index e068f2e2e0a..e547ee1dee7 100644 --- a/src/main/ai-vault/session-scanner-service-client.ts +++ b/src/main/ai-vault/session-scanner-service-client.ts @@ -2,19 +2,20 @@ import type { ChildProcess } from 'node:child_process' import { createAiVaultScanCancelledError } from './ai-vault-scan-cancellation' import { AI_VAULT_SERVICE_IDLE_TIMEOUT_MS, - AI_VAULT_SERVICE_INTERACTIVE_TIMEOUT_MS, AI_VAULT_SERVICE_MAX_CALLS, AI_VAULT_SERVICE_READY_TIMEOUT_MS, - AI_VAULT_SERVICE_SCAN_TIMEOUT_MS, AiVaultServiceIdleRetirement, AiVaultServiceInvalidations, - armAiVaultServiceCancellationTimeout, + AiVaultServiceSessionSearchHold, + aiVaultServiceErrorText, attachAiVaultServiceChild, + cancelAiVaultServiceCall, clearAiVaultServiceCall, createAiVaultServiceReadyWaiter, rejectAiVaultServiceCall, - requeueAiVaultServiceStart, + requeueOrRejectAiVaultServiceStart, retireAiVaultServiceChild, + sendAiVaultServiceCall, type AiVaultServiceClientOptions, type AiVaultServicePendingCall, type AiVaultServiceReadyWaiter @@ -23,7 +24,7 @@ import { AiVaultServiceRestartPolicy } from './session-scanner-service-restart-p import { aiVaultServiceLane, isAiVaultServiceChildMessage, - type AiVaultServiceChildMessage, + type AiVaultSessionSearchInit, type AiVaultServiceRequest, type AiVaultServiceRequestBody, type AiVaultServiceResultValue @@ -38,6 +39,7 @@ export class AiVaultScannerServiceClient { private nextId = 1 private readonly idleRetirement = new AiVaultServiceIdleRetirement() private readonly restartPolicy = new AiVaultServiceRestartPolicy() + private readonly sessionSearch = new AiVaultServiceSessionSearchHold() private disposed = false constructor(private readonly options: AiVaultServiceClientOptions) {} @@ -76,6 +78,19 @@ export class AiVaultScannerServiceClient { }) } + /** Push a consent or retention change, and while the index is on keep a child. */ + updateSessionSearch(init: AiVaultSessionSearchInit): void { + if (this.disposed) { + return + } + if (!this.sessionSearch.record(init, this.child)) { + this.scheduleIdleIfNeeded() + return + } + this.idleRetirement.clear() + this.startSessionSearchChild() + } + clearRestartCircuit(): void { this.restartPolicy.clearCircuit() this.pump() @@ -87,25 +102,10 @@ export class AiVaultScannerServiceClient { } this.idleRetirement.clear() const child = await this.ensureChild() - return this.invalidations.open( - AI_VAULT_SERVICE_READY_TIMEOUT_MS, - (generation) => this.onInvalidationDeadline(generation), - (generation) => child.send({ type: 'invalidate', generation, paths }) - ) - } - - /** - * The deadline is a startup-sized budget, but a child mid-scan can be slow to - * turn the channel around. Fork IPC ordering already guarantees the child - * applies the invalidation before any request sent after it, so a busy child - * owes nothing here — only an idle one that misses the deadline is wedged. - */ - private onInvalidationDeadline(generation: number): void { - if (this.active.size > 0) { - this.invalidations.settle(generation) - return - } - this.onFault(new Error('AI Vault service cache invalidation timed out.')) + return this.invalidations.send(child, paths, { + busy: () => this.active.size > 0, + onFault: (error) => this.onFault(error) + }) } dispose(): void { @@ -140,7 +140,13 @@ export class AiVaultScannerServiceClient { const call = this.queue.splice(index, 1)[0]! this.active.set(lane, call) void this.ensureChild().then( - (child) => this.sendCall(child, call), + (child) => + sendAiVaultServiceCall( + child, + call, + () => this.active.get(call.lane) === call, + (error) => this.onFault(error) + ), (error: Error) => { if (this.active.get(lane) !== call) { return @@ -151,33 +157,32 @@ export class AiVaultScannerServiceClient { } ) } + this.startSessionSearchChild() this.scheduleIdleIfNeeded() } - private sendCall(child: ChildProcess, call: AiVaultServicePendingCall): void { - if (call.cancelled || this.active.get(call.lane) !== call) { + /** + * The index's own restart. A child indexing for the hold has no queued call to + * bring it back, so without this a fault stops the indexing until an unrelated + * request happens to arrive. The restart delay and circuit bound it, exactly as + * they bound a queued call's start. + */ + private startSessionSearchChild(): void { + if (this.disposed || !this.sessionSearch.holdsChild || this.child || this.readyWaiter) { return } - const timeoutMs = - call.request.operation === 'scan' - ? AI_VAULT_SERVICE_SCAN_TIMEOUT_MS - : AI_VAULT_SERVICE_INTERACTIVE_TIMEOUT_MS - call.timer = setTimeout(() => { - this.onFault(new Error(`AI Vault service timed out after ${timeoutMs}ms.`)) - }, timeoutMs) - call.timer.unref?.() - call.sent = true - child.send(call.request) + void this.ensureChild().catch((error: unknown) => { + this.options.onStderr?.(`session search child unavailable: ${aiVaultServiceErrorText(error)}`) + }) } private retryStartOrReject(call: AiVaultServicePendingCall, error: Error): void { - if ( - this.disposed || - !this.restartPolicy.restartScheduled || - !requeueAiVaultServiceStart(call, this.queue) - ) { - rejectAiVaultServiceCall(call, error) - } + requeueOrRejectAiVaultServiceStart( + call, + this.queue, + error, + !this.disposed && this.restartPolicy.restartScheduled + ) } private ensureChild(): Promise { @@ -203,7 +208,7 @@ export class AiVaultScannerServiceClient { this.onFault(new Error('AI Vault service did not become ready.')) ) this.readyWaiter = waiter - attachAiVaultServiceChild(child, this.options.init, { + attachAiVaultServiceChild(child, this.options.init(), { onMessage: (message) => this.onMessage(message), onFault: (error) => this.onFault(error), onStderr: this.options.onStderr @@ -211,12 +216,24 @@ export class AiVaultScannerServiceClient { return waiter.promise } - private onMessage(raw: unknown): void { - if (!isAiVaultServiceChildMessage(raw)) { + private onMessage(message: unknown): void { + if (!isAiVaultServiceChildMessage(message)) { this.onFault(new Error('AI Vault service sent a malformed message.')) return } - const message = raw as AiVaultServiceChildMessage + if (message.type === 'sessionSearchRoots') { + const child = this.child + const resolve = this.options.resolveSessionSearchRoots + void Promise.resolve() + .then(() => (resolve ? resolve() : (this.options.init().sessionSearch?.roots ?? null))) + .catch(() => null) + .then((roots) => { + if (child && this.child === child && child.connected) { + child.send({ type: 'sessionSearchRoots', id: message.id, roots }, () => undefined) + } + }) + return + } if (message.type === 'ready') { const waiter = this.readyWaiter if (!waiter || !this.child) { @@ -250,32 +267,13 @@ export class AiVaultScannerServiceClient { } private cancel(call: AiVaultServicePendingCall): void { - if (call.cancelled) { - return - } - call.cancelled = true - call.reject(createAiVaultScanCancelledError()) - const queuedIndex = this.queue.indexOf(call) - if (queuedIndex !== -1) { - this.queue.splice(queuedIndex, 1) - clearAiVaultServiceCall(call) - this.pump() - return - } - if (this.active.get(call.lane) === call) { - // Why: a call cancelled before it reached the child gets no acknowledgement, - // so waiting on one would kill a healthy service and stall the lane. - if (!call.sent) { - this.active.delete(call.lane) - clearAiVaultServiceCall(call) - this.pump() - return - } - this.child?.send({ type: 'cancel', id: call.request.id }) - armAiVaultServiceCancellationTimeout(call, () => - this.onFault(new Error('AI Vault service did not cancel within 2000ms.')) - ) - } + cancelAiVaultServiceCall(call, { + queue: this.queue, + active: this.active, + child: this.child, + pump: () => this.pump(), + onFault: (error) => this.onFault(error) + }) } private onFault(error: Error): void { @@ -304,7 +302,11 @@ export class AiVaultScannerServiceClient { private scheduleIdleIfNeeded(): void { this.idleRetirement.schedule( - this.active.size > 0 || this.queue.length > 0 || this.invalidations.size > 0 || !this.child, + this.sessionSearch.holdsChild || + this.active.size > 0 || + this.queue.length > 0 || + this.invalidations.size > 0 || + !this.child, this.options.idleTimeoutMs ?? AI_VAULT_SERVICE_IDLE_TIMEOUT_MS, () => this.retireChild() ) diff --git a/src/main/ai-vault/session-scanner-service-entry.ts b/src/main/ai-vault/session-scanner-service-entry.ts index 74a5ea9c355..7458db74e3a 100644 --- a/src/main/ai-vault/session-scanner-service-entry.ts +++ b/src/main/ai-vault/session-scanner-service-entry.ts @@ -1,3 +1,4 @@ +import { requestSessionSearchRoots } from './session-scanner-service-root-request' import type { AiVaultSessionTitle } from '../../shared/ai-vault-session-title' import { readAiVaultFirstUserPrompt } from './session-first-user-prompt-read' import { @@ -6,6 +7,7 @@ import { } from './session-parse-cache-persistence' import { scanAiVaultSessions } from './session-scanner' import { invalidateSessionParseCacheEntry } from './session-scanner-parse-cache' +import { SessionScannerServiceSearch } from './session-scanner-service-search' import { AI_VAULT_SERVICE_PROTOCOL_VERSION, aiVaultServiceLane, @@ -29,6 +31,7 @@ const cancelled = new Set() const pending = new Set() const titleIndex = new Map() const invalidatedPaths = new Set() +const sessionSearch = new SessionScannerServiceSearch(requestSessionSearchRoots) let initialized = false let shuttingDown = false let cacheLane = Promise.resolve() @@ -43,6 +46,15 @@ function titleKey(request: { agent: string; sessionId: string }): string { } async function executeRequest(request: AiVaultServiceRequest): Promise { + if (sessionSearch.handles(request)) { + try { + return await sessionSearch.execute(request) + } finally { + // A search registers no controller, so nothing else consumes a cancel sent + // for one; without this the id sits in the set for the process's life. + cancelled.delete(request.id) + } + } const controller = new AbortController() controllers.set(request.id, controller) try { @@ -149,6 +161,7 @@ async function shutdown(): Promise { for (const controller of controllers.values()) { controller.abort() } + sessionSearch.close() await Promise.allSettled([cacheLane, interactiveLane]) await flushSessionParseCachePersist() process.disconnect?.() @@ -164,6 +177,9 @@ process.on('message', (raw: AiVaultServiceParentMessage) => { if (raw.sessionParseCache) { initSessionParseCachePersistence(raw.sessionParseCache) } + if (raw.sessionSearch) { + sessionSearch.apply(raw.sessionSearch) + } send({ type: 'ready', protocol: AI_VAULT_SERVICE_PROTOCOL_VERSION, pid: process.pid }) return } @@ -192,6 +208,10 @@ process.on('message', (raw: AiVaultServiceParentMessage) => { send({ type: 'invalidated', generation: raw.generation }) return } + if (raw?.type === 'sessionSearch') { + sessionSearch.apply(raw.init) + return + } if (raw?.type === 'shutdown') { void shutdown() return diff --git a/src/main/ai-vault/session-scanner-service-protocol.ts b/src/main/ai-vault/session-scanner-service-protocol.ts index f2842751eb1..df89600934c 100644 --- a/src/main/ai-vault/session-scanner-service-protocol.ts +++ b/src/main/ai-vault/session-scanner-service-protocol.ts @@ -4,6 +4,13 @@ import type { AiVaultSessionTitleRequest, AiVaultSessionTitlesResult } from '../../shared/ai-vault-session-title' +import type { + AiVaultSearchRequest, + AiVaultSearchResponse, + AiVaultSearchStatus +} from '../../shared/ai-vault-search-types' +import type { AiVaultSearchSettings } from '../../shared/ai-vault-search-settings' +import type { SessionSearchScanRoots } from '../ai-vault-search/session-search-scan-roots' import type { ReadAiVaultFirstUserPromptArgs } from './session-first-user-prompt-read' import type { SessionParseCachePersistenceOptions } from './session-parse-cache-persistence' import type { AiVaultWorkerScanOptions } from './session-scanner-worker-protocol' @@ -11,17 +18,49 @@ import type { AiVaultWorkerScanOptions } from './session-scanner-worker-protocol export const AI_VAULT_SERVICE_PROTOCOL_VERSION = 1 export type AiVaultServiceLane = 'cache' | 'interactive' -export type AiVaultServiceOperation = 'scan' | 'titles' | 'subagents' | 'firstPrompt' +export type AiVaultServiceOperation = + | 'scan' + | 'titles' + | 'subagents' + | 'firstPrompt' + | 'searchSessions' + | 'searchStatus' + | 'searchReconcile' + +// Typed from the union so a new operation cannot be added without landing here, +// and held as strings so recognising one costs no assertion. +const AI_VAULT_SERVICE_OPERATIONS: ReadonlySet = new Set([ + 'scan', + 'titles', + 'subagents', + 'firstPrompt', + 'searchSessions', + 'searchStatus', + 'searchReconcile' +]) export type AiVaultServiceSubagentRequest = { agent: 'claude' | 'omp' parentFilePath: string } +/** + * Everything the child needs to own this host's index. + * + * Initial roots also support standalone tests. Production asks the parent for + * a fresh snapshot on each full sweep; the parent owns managed account homes. + */ +export type AiVaultSessionSearchInit = { + databasePath: string + settings: AiVaultSearchSettings + roots: SessionSearchScanRoots +} + export type AiVaultServiceInit = { type: 'init' protocol: typeof AI_VAULT_SERVICE_PROTOCOL_VERSION sessionParseCache: SessionParseCachePersistenceOptions | null + sessionSearch: AiVaultSessionSearchInit | null } export type AiVaultServiceRequestBody = @@ -41,6 +80,9 @@ export type AiVaultServiceRequestBody = operation: 'firstPrompt' request: ReadAiVaultFirstUserPromptArgs } + | { type: 'request'; operation: 'searchSessions'; request: AiVaultSearchRequest } + | { type: 'request'; operation: 'searchStatus' } + | { type: 'request'; operation: 'searchReconcile' } export type AiVaultServiceRequest = AiVaultServiceRequestBody & { id: number } @@ -49,6 +91,9 @@ export type AiVaultServiceParentMessage = | AiVaultServiceRequest | { type: 'cancel'; id: number } | { type: 'invalidate'; generation: number; paths: string[] } + // Fire-and-forget: the child closes the live pair and constructs from this. + | { type: 'sessionSearch'; init: AiVaultSessionSearchInit } + | { type: 'sessionSearchRoots'; id: number; roots: SessionSearchScanRoots | null } | { type: 'shutdown' } export type AiVaultServiceResultValue = @@ -56,8 +101,12 @@ export type AiVaultServiceResultValue = | { operation: 'titles'; value: AiVaultSessionTitlesResult } | { operation: 'subagents'; value: AiVaultSubagentListResult } | { operation: 'firstPrompt'; value: { prompt: string | null } } + | { operation: 'searchSessions'; value: AiVaultSearchResponse } + | { operation: 'searchStatus'; value: AiVaultSearchStatus } + | { operation: 'searchReconcile'; value: null } export type AiVaultServiceChildMessage = + | { type: 'sessionSearchRoots'; id: number } | { type: 'ready' protocol: typeof AI_VAULT_SERVICE_PROTOCOL_VERSION @@ -67,22 +116,23 @@ export type AiVaultServiceChildMessage = | { type: 'error'; id: number; message: string; retryable: boolean } | { type: 'invalidated'; generation: number } +/** Everything but the two bulk reads is interactive: a search must not queue behind a scan. */ export function aiVaultServiceLane(operation: AiVaultServiceOperation): AiVaultServiceLane { - return operation === 'subagents' || operation === 'firstPrompt' ? 'interactive' : 'cache' + return operation === 'scan' || operation === 'titles' ? 'cache' : 'interactive' } export function isAiVaultServiceRequest(value: unknown): value is AiVaultServiceRequest { if (!value || typeof value !== 'object') { return false } - const message = value as Record return ( - message.type === 'request' && - Number.isSafeInteger(message.id) && - (message.operation === 'scan' || - message.operation === 'titles' || - message.operation === 'subagents' || - message.operation === 'firstPrompt') + 'type' in value && + value.type === 'request' && + 'id' in value && + Number.isSafeInteger(value.id) && + 'operation' in value && + typeof value.operation === 'string' && + AI_VAULT_SERVICE_OPERATIONS.has(value.operation) ) } @@ -94,6 +144,9 @@ export function isAiVaultServiceChildMessage(value: unknown): value is AiVaultSe if (message.type === 'ready') { return message.protocol === AI_VAULT_SERVICE_PROTOCOL_VERSION && Number.isInteger(message.pid) } + if (message.type === 'sessionSearchRoots') { + return Number.isSafeInteger(message.id) + } if (message.type === 'invalidated') { return Number.isSafeInteger(message.generation) } diff --git a/src/main/ai-vault/session-scanner-service-restart-policy.ts b/src/main/ai-vault/session-scanner-service-restart-policy.ts index ae9f437ab3e..02f972691ed 100644 --- a/src/main/ai-vault/session-scanner-service-restart-policy.ts +++ b/src/main/ai-vault/session-scanner-service-restart-policy.ts @@ -43,10 +43,13 @@ export class AiVaultServiceRestartPolicy { if (this.timer) { clearTimeout(this.timer) } - this.timer = setTimeout(() => { - this.timer = null - restart() - }, delay) + this.timer = setTimeout( + () => { + this.timer = null + restart() + }, + Math.max(delay, this.circuitUntil - now) + ) this.timer.unref?.() } diff --git a/src/main/ai-vault/session-scanner-service-root-request.test.ts b/src/main/ai-vault/session-scanner-service-root-request.test.ts new file mode 100644 index 00000000000..0f07b415b4f --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-root-request.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, expect, it } from 'vitest' +import { requestSessionSearchRoots } from './session-scanner-service-root-request' +import { + isAiVaultServiceChildMessage, + type AiVaultServiceChildMessage +} from './session-scanner-service-protocol' + +let originalSend: typeof process.send +let lastRequest: Extract +let listeners: number +beforeEach(() => { + originalSend = process.send + listeners = process.listenerCount('message') + process.send = (message) => { + if (!isAiVaultServiceChildMessage(message) || message.type !== 'sessionSearchRoots') { + throw new Error('Unexpected child message') + } + lastRequest = message + return true + } +}) +afterEach(() => { + process.send = originalSend + expect(process.listenerCount('message')).toBe(listeners) +}) +it('matches the requested snapshot and removes its listener', async () => { + const pending = requestSessionSearchRoots(new AbortController().signal) + process.emit( + 'message', + { type: 'sessionSearchRoots', id: lastRequest.id + 1, roots: {} }, + undefined + ) + const roots = { additionalCodexSessionsDirs: ['/late'] } + process.emit('message', { type: 'sessionSearchRoots', id: lastRequest.id, roots }, undefined) + await expect(pending).resolves.toEqual(roots) +}) +it('releases a pending request when indexing is disabled', async () => { + const controller = new AbortController() + const pending = requestSessionSearchRoots(controller.signal) + controller.abort(new Error('disabled')) + await expect(pending).rejects.toThrow('disabled') +}) +it('reports discovery and send failures instead of using stale roots', async () => { + const pending = requestSessionSearchRoots(new AbortController().signal) + process.emit( + 'message', + { type: 'sessionSearchRoots', id: lastRequest.id, roots: null }, + undefined + ) + await expect(pending).rejects.toThrow('discovery failed') + process.send = () => { + throw new Error('channel closed') + } + await expect(requestSessionSearchRoots(new AbortController().signal)).rejects.toThrow( + 'channel closed' + ) +}) diff --git a/src/main/ai-vault/session-scanner-service-root-request.ts b/src/main/ai-vault/session-scanner-service-root-request.ts new file mode 100644 index 00000000000..4d510dd2b73 --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-root-request.ts @@ -0,0 +1,40 @@ +import type { SessionSearchScanRoots } from '../ai-vault-search/session-search-scan-roots' +import type { AiVaultServiceParentMessage } from './session-scanner-service-protocol' + +let nextId = 1 + +/** The parent owns managed-account discovery; the child owns the sweep's lifetime. */ +export async function requestSessionSearchRoots( + signal: AbortSignal +): Promise { + signal.throwIfAborted() + const id = nextId++ + const pending = Promise.withResolvers() + const onAbort = (): void => pending.reject(signal.reason) + const onMessage = (message: AiVaultServiceParentMessage): void => { + if (message?.type !== 'sessionSearchRoots' || message.id !== id) { + return + } + if (message.roots) { + pending.resolve(message.roots) + } else { + pending.reject(new Error('Session search root discovery failed.')) + } + } + process.on('message', onMessage) + signal.addEventListener('abort', onAbort, { once: true }) + try { + if (!process.send) { + throw new Error('Session search root discovery requires parent IPC.') + } + process.send({ type: 'sessionSearchRoots', id }, (error) => { + if (error) { + pending.reject(error) + } + }) + return await pending.promise + } finally { + process.removeListener('message', onMessage) + signal.removeEventListener('abort', onAbort) + } +} diff --git a/src/main/ai-vault/session-scanner-service-root-response.test.ts b/src/main/ai-vault/session-scanner-service-root-response.test.ts new file mode 100644 index 00000000000..6d1d81fd351 --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-root-response.test.ts @@ -0,0 +1,63 @@ +import { expect, it, vi } from 'vitest' +import { AiVaultScannerServiceClient } from './session-scanner-service-client' +import { + AiVaultServiceTestChild, + readyAiVaultServiceChild +} from './session-scanner-service-test-child' + +it('answers root requests freshly without forwarding another settings change', async () => { + const child = new AiVaultServiceTestChild() + Object.assign(child, { connected: true }) + const roots = { additionalCodexSessionsDirs: ['/late'] } + const resolveSessionSearchRoots = vi + .fn() + .mockResolvedValueOnce(roots) + .mockRejectedValueOnce(new Error('offline')) + const client = new AiVaultScannerServiceClient({ + processFactory: () => child.asChildProcess(), + init: () => ({ sessionSearch: null, sessionParseCache: null }), + resolveSessionSearchRoots + }) + const status = client.request({ type: 'request', operation: 'searchStatus' }) + try { + readyAiVaultServiceChild(child) + await Promise.resolve() + child.emit('message', { type: 'result', operation: 'searchStatus', id: 1, value: {} }) + await status + child.emit('message', { type: 'sessionSearchRoots', id: 5 }) + await vi.waitFor(() => + expect(child.sent).toContainEqual({ type: 'sessionSearchRoots', id: 5, roots }) + ) + child.emit('message', { type: 'sessionSearchRoots', id: 6 }) + await vi.waitFor(() => + expect(child.sent).toContainEqual({ type: 'sessionSearchRoots', id: 6, roots: null }) + ) + expect(resolveSessionSearchRoots).toHaveBeenCalledTimes(2) + expect(child.sent).not.toContainEqual(expect.objectContaining({ type: 'sessionSearch' })) + } finally { + client.dispose() + } +}) + +it('does not deliver a delayed snapshot after the child is disposed', async () => { + const child = new AiVaultServiceTestChild() + Object.assign(child, { connected: true }) + const pending = Promise.withResolvers<{}>() + const resolveSessionSearchRoots = vi.fn(() => pending.promise) + const client = new AiVaultScannerServiceClient({ + processFactory: () => child.asChildProcess(), + init: () => ({ sessionSearch: null, sessionParseCache: null }), + resolveSessionSearchRoots + }) + const status = client.request({ type: 'request', operation: 'searchStatus' }) + readyAiVaultServiceChild(child) + await Promise.resolve() + child.emit('message', { type: 'result', operation: 'searchStatus', id: 1, value: {} }) + await status + child.emit('message', { type: 'sessionSearchRoots', id: 5 }) + await vi.waitFor(() => expect(resolveSessionSearchRoots).toHaveBeenCalledTimes(1)) + client.dispose() + pending.resolve({}) + await new Promise((resolve) => setImmediate(resolve)) + expect(child.sent).not.toContainEqual(expect.objectContaining({ type: 'sessionSearchRoots' })) +}) diff --git a/src/main/ai-vault/session-scanner-service-search-roots.test.ts b/src/main/ai-vault/session-scanner-service-search-roots.test.ts new file mode 100644 index 00000000000..5a206646ea0 --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-search-roots.test.ts @@ -0,0 +1,106 @@ +import { join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { + openSessionSearchIndexerHarness, + writeMessageGraphTranscript, + type SessionSearchIndexerHarness +} from '../ai-vault-search/session-search-indexer-test-fixture' +import { SessionSearchIndexer } from '../ai-vault-search/session-search-indexer' +import type { SessionSearchScanRoots } from '../ai-vault-search/session-search-scan-roots' +import { resetSessionParseCacheForTests } from './session-scanner-parse-cache' +import type { AiVaultSessionSearchInit } from './session-scanner-service-protocol' +import { SessionScannerServiceSearch } from './session-scanner-service-search' +import { resetTranscriptConsumersForTests } from './session-transcript-consumers' + +let harness: SessionSearchIndexerHarness +let subject: SessionScannerServiceSearch +let spawnRoot: string +let lateRoot: string +let currentRoots: SessionSearchScanRoots +let spawnRoots: SessionSearchScanRoots + +beforeEach(async () => { + resetSessionParseCacheForTests() + resetTranscriptConsumersForTests() + harness = await openSessionSearchIndexerHarness('ss-service-roots') + subject = new SessionScannerServiceSearch(async () => currentRoots) + const { openclawLegacyStateDir, ...rest } = harness.roots + spawnRoot = harness.roots.openclawStateDir ?? '' + lateRoot = openclawLegacyStateDir ?? '' + spawnRoots = rest + currentRoots = rest +}) + +afterEach(async () => { + subject.close() + vi.restoreAllMocks() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + await harness.cleanup() +}) + +function init(roots: SessionSearchScanRoots): AiVaultSessionSearchInit { + return { + databasePath: harness.databasePath, + settings: { enabled: true, historyDays: null }, + roots + } +} + +/** OpenClaw reads `/agents/**` and keeps only paths through `sessions`. */ +function openclawTranscript(stateDir: string, name: string): string { + return join(stateDir, 'agents', 'main', 'sessions', `${name}.jsonl`) +} + +async function sessionsMatching(term: string): Promise { + const reply = await subject.execute({ + type: 'request', + id: 1, + operation: 'searchSessions', + request: { query: term } + }) + if (reply.operation !== 'searchSessions' || reply.value.kind !== 'results') { + throw new Error(`expected results, got ${JSON.stringify(reply)}`) + } + return reply.value.hits.map((hit) => hit.sessionId).sort() +} + +async function indexedSessions(term: string, expected: string[]): Promise { + await vi.waitFor( + async () => { + await subject.execute({ type: 'request', id: 2, operation: 'searchReconcile' }) + expect(await sessionsMatching(term)).toEqual(expected) + }, + { timeout: 20_000 } + ) +} + +it('refreshes a late root without rebuilding the index', async () => { + await writeMessageGraphTranscript(openclawTranscript(spawnRoot, 'early-session'), [ + 'a conversation in a root the spawn already knew' + ]) + await writeMessageGraphTranscript(openclawTranscript(lateRoot, 'late-session'), [ + 'a conversation in a distro that started later' + ]) + + subject.apply(init(spawnRoots)) + await indexedSessions('conversation', ['early-session']) + + const close = vi.spyOn(SessionSearchIndexer.prototype, 'close') + currentRoots = harness.roots + await indexedSessions('conversation', ['early-session', 'late-session']) + expect(close).not.toHaveBeenCalled() +}) + +it('keeps the live indexer when an unchanged root snapshot is refreshed', async () => { + await writeMessageGraphTranscript(openclawTranscript(spawnRoot, 'early-session'), [ + 'a conversation in a root the spawn already knew' + ]) + subject.apply(init(harness.roots)) + await indexedSessions('conversation', ['early-session']) + + const close = vi.spyOn(SessionSearchIndexer.prototype, 'close') + currentRoots = { ...spawnRoots } + await indexedSessions('conversation', ['early-session']) + expect(close).not.toHaveBeenCalled() +}) diff --git a/src/main/ai-vault/session-scanner-service-search.test.ts b/src/main/ai-vault/session-scanner-service-search.test.ts new file mode 100644 index 00000000000..d0c3394883d --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-search.test.ts @@ -0,0 +1,166 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { afterAll, beforeAll, expect, it, vi } from 'vitest' +import type { AiVaultSearchResponse, AiVaultSearchStatus } from '../../shared/ai-vault-search-types' +import { + openSessionSearchIndexerHarness, + writeClaudeTranscript, + type SessionSearchIndexerHarness +} from '../ai-vault-search/session-search-indexer-test-fixture' +import { + AI_VAULT_SERVICE_PROTOCOL_VERSION, + type AiVaultServiceChildMessage, + type AiVaultServiceParentMessage, + type AiVaultServiceRequestBody, + type AiVaultServiceResultValue, + type AiVaultSessionSearchInit +} from './session-scanner-service-protocol' + +/** + * The child, booted the way a spawn boots it: an init frame and messages, with + * no renderer, no Electron and no scan request. What this proves is that consent + * alone constructs the indexer and that every search answer crosses the protocol. + */ + +const SESSION_ID = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' + +let harness: SessionSearchIndexerHarness +let currentRoots: SessionSearchIndexerHarness['roots'] +let originalSend: typeof process.send +const sent: AiVaultServiceChildMessage[] = [] +let nextId = 1 + +function emit(message: AiVaultServiceParentMessage): void { + process.emit('message', message, undefined) +} + +/** One request, and the reply the child sent for it, still discriminated by operation. */ +async function call(body: AiVaultServiceRequestBody): Promise { + const id = nextId++ + emit({ ...body, id }) + const reply = await vi.waitFor(() => { + const found = sent.find( + (message) => (message.type === 'result' || message.type === 'error') && message.id === id + ) + expect(found).toBeDefined() + return found! + }) + if (reply.type === 'error') { + throw new Error(reply.message) + } + if (reply.type !== 'result') { + throw new Error(`expected a result, got ${reply.type}`) + } + return reply +} + +async function searchStatus(): Promise { + const reply = await call({ type: 'request', operation: 'searchStatus' }) + if (reply.operation !== 'searchStatus') { + throw new Error(`expected searchStatus, got ${reply.operation}`) + } + return reply.value +} + +async function searchSessions(query: string): Promise { + const reply = await call({ type: 'request', operation: 'searchSessions', request: { query } }) + if (reply.operation !== 'searchSessions') { + throw new Error(`expected searchSessions, got ${reply.operation}`) + } + return reply.value +} + +function searchInit(enabled: boolean): AiVaultSessionSearchInit { + return { + databasePath: harness.databasePath, + settings: { enabled, historyDays: null }, + roots: harness.roots + } +} + +beforeAll(async () => { + harness = await openSessionSearchIndexerHarness('ss-child') + currentRoots = harness.roots + await writeClaudeTranscript( + join(harness.claudeProjectDir, `${SESSION_ID}.jsonl`), + ['a distinctive conversation'], + SESSION_ID + ) + originalSend = process.send + const record: NonNullable = (message) => { + sent.push(message) + if (message.type === 'sessionSearchRoots') { + queueMicrotask(() => + emit({ type: 'sessionSearchRoots', id: message.id, roots: currentRoots }) + ) + } + return true + } + process.send = record + await import('./session-scanner-service-entry') + emit({ + type: 'init', + protocol: AI_VAULT_SERVICE_PROTOCOL_VERSION, + sessionParseCache: null, + sessionSearch: searchInit(true) + }) + await vi.waitFor(() => expect(sent.some((message) => message.type === 'ready')).toBe(true)) +}) + +afterAll(async () => { + emit({ type: 'sessionSearch', init: searchInit(false) }) + process.send = originalSend + await harness.cleanup() +}) + +it('reports the indexer phase and a live generation over the protocol', async () => { + const status = await vi.waitFor(async () => { + const value = await searchStatus() + expect(value.filesIndexed).toBeGreaterThan(0) + return value + }) + expect(status.enabled).toBe(true) + expect(status.phase).toBe('current') + expect(status.generation).toBeGreaterThan(0) + expect(existsSync(harness.databasePath)).toBe(true) +}) + +it('answers a search and a reconcile over the protocol', async () => { + expect(await call({ type: 'request', operation: 'searchReconcile' })).toEqual({ + operation: 'searchReconcile', + value: null, + type: 'result', + id: expect.any(Number) + }) + const response = await searchSessions('distinctive') + expect(response.kind).toBe('results') + if (response.kind === 'results') { + expect(response.hits.map((hit) => hit.sessionId)).toEqual([SESSION_ID]) + } +}) + +it('discovers a new root through the parent exchange on manual reconciliation', async () => { + const lateHome = join(harness.root, 'late-home') + const id = 'bbbbbbbb-cccc-4ddd-8eee-ffffffffffff' + await writeClaudeTranscript( + join(lateHome, '.claude', 'projects', 'late', `${id}.jsonl`), + ['freshroots'], + id + ) + currentRoots = { ...harness.roots, wslHomeDirs: [lateHome] } + await call({ type: 'request', operation: 'searchReconcile' }) + const response = await searchSessions('freshroots') + expect(response.kind).toBe('results') + if (response.kind === 'results') { + expect(response.hits.map((hit) => hit.sessionId)).toEqual([id]) + } +}) + +it('answers disabled once consent is withdrawn, without a respawn', async () => { + emit({ type: 'sessionSearch', init: searchInit(false) }) + expect(await searchSessions('distinctive')).toEqual({ kind: 'unavailable', reason: 'disabled' }) + expect(await searchStatus()).toMatchObject({ enabled: false, phase: 'idle' }) + // Re-consenting reuses the index that was left on disk rather than rebuilding it. + emit({ type: 'sessionSearch', init: searchInit(true) }) + expect((await searchSessions('distinctive')).kind).toBe('results') +}) diff --git a/src/main/ai-vault/session-scanner-service-search.ts b/src/main/ai-vault/session-scanner-service-search.ts new file mode 100644 index 00000000000..deab1c26beb --- /dev/null +++ b/src/main/ai-vault/session-scanner-service-search.ts @@ -0,0 +1,94 @@ +import type { SessionSearchIndexerOptions } from '../ai-vault-search/session-search-indexer-options' +import { unavailableSessionSearchStatus } from '../../shared/ai-vault-search-client' +import { AiVaultSearchRequestSchema } from '../../shared/ai-vault-search-contract' +import { SessionSearchInstance } from '../ai-vault-search/session-search-instance' +import { + sameSessionSearchRoots, + type SessionSearchScanRoots +} from '../ai-vault-search/session-search-scan-roots' +import { sessionSearchSqliteAvailable } from '../ai-vault-search/session-search-sqlite-support' +import type { + AiVaultServiceRequest, + AiVaultServiceResultValue, + AiVaultSessionSearchInit +} from './session-scanner-service-protocol' + +type SearchOperation = Extract< + AiVaultServiceRequest, + { operation: 'searchSessions' | 'searchStatus' | 'searchReconcile' } +> + +/** + * The scanner-service child's half of session search. + * + * Why the child and not the parent: the transcript reader runs here, so the + * index consumer has to as well — one process reads a transcript once and both + * the session list and the index see that read. Main, the CLI and a remote + * server never open the database; they ask over this protocol. + */ +export class SessionScannerServiceSearch { + private instance: SessionSearchInstance | null = null + private databasePath: string | null = null + private roots: SessionSearchScanRoots | null = null + + constructor(private readonly resolveRoots?: SessionSearchIndexerOptions['resolveRoots']) {} + + /** Applied at init and again on every settings change; both are close-and-construct. */ + apply(init: AiVaultSessionSearchInit): void { + if (!sessionSearchSqliteAvailable()) { + return + } + if (this.instance && this.databasePath !== init.databasePath) { + // A data root cannot move under a running process, so this is a caller bug + // rather than a case to support: close the old one before it writes there. + this.close() + } + if (this.instance && this.roots && !sameSessionSearchRoots(this.roots, init.roots)) { + // Explicit init-root changes replace the fallback used by callers without a resolver. + this.close() + } + this.databasePath = init.databasePath + this.roots = init.roots + this.instance ??= new SessionSearchInstance({ + databasePath: init.databasePath, + roots: init.roots, + resolveRoots: this.resolveRoots + }) + this.instance.apply(init.settings) + } + + handles(request: AiVaultServiceRequest): request is SearchOperation { + return ( + request.operation === 'searchSessions' || + request.operation === 'searchStatus' || + request.operation === 'searchReconcile' + ) + } + + async execute(request: SearchOperation): Promise { + const instance = this.instance + if (request.operation === 'searchStatus') { + return { + operation: 'searchStatus', + value: instance?.status() ?? unavailableSessionSearchStatus() + } + } + if (request.operation === 'searchReconcile') { + await instance?.reconcile() + return { operation: 'searchReconcile', value: null } + } + return { + operation: 'searchSessions', + value: instance + ? await instance.search(AiVaultSearchRequestSchema.parse(request.request)) + : { kind: 'unavailable', reason: 'disabled' } + } + } + + close(): void { + this.instance?.close() + this.instance = null + this.databasePath = null + this.roots = null + } +} diff --git a/src/main/ai-vault/session-scanner-service-spawn.ts b/src/main/ai-vault/session-scanner-service-spawn.ts index 3ba12322734..d841fc62593 100644 --- a/src/main/ai-vault/session-scanner-service-spawn.ts +++ b/src/main/ai-vault/session-scanner-service-spawn.ts @@ -1,5 +1,11 @@ +import { localAiVaultScanRoots } from './cached-session-list' import { fork, type ChildProcess } from 'node:child_process' import { existsSync } from 'node:fs' +import type { + AiVaultSearchRequest, + AiVaultSearchResponse, + AiVaultSearchStatus +} from '../../shared/ai-vault-search-types' import type { AiVaultListResult, AiVaultSubagentListResult } from '../../shared/ai-vault-types' import type { AiVaultSessionTitleRequest, @@ -10,12 +16,16 @@ import type { ReadAiVaultFirstUserPromptArgs, ReadAiVaultFirstUserPromptResult } from './session-first-user-prompt-read' +import { sessionSearchServiceInit } from '../ai-vault-search/session-search-service-init' import { getSessionParseCachePersistenceOptions } from './session-parse-cache-persistence' import { buildAiVaultServiceEnv } from './session-scanner-service-env' import { AiVaultScannerServiceClient } from './session-scanner-service-client' import { getAiVaultServiceEntryPath } from './session-scanner-service-entry-path' import { lowerAiVaultServicePriority } from './session-scanner-service-priority' -import type { AiVaultServiceSubagentRequest } from './session-scanner-service-protocol' +import type { + AiVaultServiceSubagentRequest, + AiVaultSessionSearchInit +} from './session-scanner-service-protocol' import type { AiVaultWorkerScanOptions } from './session-scanner-worker-protocol' export function spawnAiVaultServiceProcess(): ChildProcess { @@ -39,7 +49,11 @@ let sharedClient: AiVaultScannerServiceClient | null = null function getSharedClient(): AiVaultScannerServiceClient { sharedClient ??= new AiVaultScannerServiceClient({ processFactory: spawnAiVaultServiceProcess, - init: { sessionParseCache: getSessionParseCachePersistenceOptions() }, + resolveSessionSearchRoots: localAiVaultScanRoots, + init: () => ({ + sessionParseCache: getSessionParseCachePersistenceOptions(), + sessionSearch: sessionSearchServiceInit() + }), onStderr: (text) => console.error('[ai-vault-service]', text.trimEnd()) }) return sharedClient @@ -81,6 +95,25 @@ export function readAiVaultFirstUserPromptInService( return getSharedClient().request({ type: 'request', operation: 'firstPrompt', request }, signal) } +export function searchSessionsInService( + request: AiVaultSearchRequest +): Promise { + return getSharedClient().request({ type: 'request', operation: 'searchSessions', request }) +} + +export function sessionSearchStatusInService(): Promise { + return getSharedClient().request({ type: 'request', operation: 'searchStatus' }) +} + +export function reconcileSessionSearchInService(): Promise { + return getSharedClient().request({ type: 'request', operation: 'searchReconcile' }) +} + +/** Boot and every settings change: push the policy and keep a child while the index runs. */ +export function updateSessionSearchInService(init: AiVaultSessionSearchInit): void { + getSharedClient().updateSessionSearch(init) +} + export function invalidateAiVaultServiceCache(paths: string[]): Promise { return sharedClient?.invalidate(paths) ?? Promise.resolve() } diff --git a/src/main/claude/claude-prompt-registry.ts b/src/main/claude/claude-prompt-registry.ts new file mode 100644 index 00000000000..6411a19e215 --- /dev/null +++ b/src/main/claude/claude-prompt-registry.ts @@ -0,0 +1,224 @@ +import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk' + +/** Settles the SDK's `canUseTool` promise; `null` writes no provider response. */ +export type ClaudePromptSettle = (response: PermissionResult | null) => void + +export type ClaudePendingPrompt = { + requestId: string + promptKey: string + toolUseId: string + toolName: string + kind: 'approval' | 'question' + input: Record + suggestions: PermissionUpdate[] + questionIds: readonly string[] + answers: Map + settle: ClaudePromptSettle + turnId?: string | null +} + +export type ClaudePromptRegistration = { + requestId: string + toolName: string + toolUseId: string + input: Record + suggestions: PermissionUpdate[] + settle: ClaudePromptSettle + turnId?: string | null +} + +type PromptBinding = { + address: string + questionId?: string + turnId: string | null +} + +export type ClaudePromptClaim = { + readonly itemId: string + readonly found: { prompt: ClaudePendingPrompt; questionId?: string } +} + +type ClaudePromptCancellationObservation = { + promise: Promise + resolve: () => void +} + +export function isClaudePromptRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export function readClaudePromptString(value: unknown): string | null { + return typeof value === 'string' && value.trim().length > 0 ? value : null +} + +export function claudePromptQuestions(input: Record): Record[] { + return Array.isArray(input.questions) ? input.questions.filter(isClaudePromptRecord) : [] +} + +function questionId(question: Record, index: number): string { + return ( + readClaudePromptString(question.question) ?? + readClaudePromptString(question.header) ?? + `question-${index + 1}` + ) +} + +/** Session-local callback ownership; none of this state is reconstructed from the transcript. */ +export class ClaudePromptRegistry { + private readonly prompts = new Map() + private readonly journalBindings = new Map() + private readonly claims = new Map() + private readonly cancellationObservations = new WeakMap< + ClaudePendingPrompt, + ClaudePromptCancellationObservation + >() + + register(registration: ClaudePromptRegistration): ClaudePendingPrompt | null { + const toolUseId = readClaudePromptString(registration.toolUseId) + const toolName = readClaudePromptString(registration.toolName) + const input = isClaudePromptRecord(registration.input) ? registration.input : null + if (!toolUseId || !toolName || !input) { + return null + } + const questions = toolName === 'AskUserQuestion' ? claudePromptQuestions(input) : [] + const prompt: ClaudePendingPrompt = { + requestId: registration.requestId, + promptKey: registration.requestId, + toolUseId, + toolName, + kind: questions.length > 0 ? 'question' : 'approval', + input, + suggestions: Array.isArray(registration.suggestions) ? registration.suggestions : [], + questionIds: questions.map(questionId), + answers: new Map(), + settle: registration.settle, + turnId: registration.turnId ?? null + } + this.prompts.set(prompt.promptKey, prompt) + return prompt + } + + /** True only if the prompt was still pending; lets abort and answer settle once. */ + forgetIfPending(prompt: ClaudePendingPrompt): boolean { + if (!this.prompts.has(prompt.promptKey)) { + return false + } + const observation = this.cancellationObservations.get(prompt) + this.forget(prompt) + observation?.resolve() + return true + } + + bindJournalItemId( + journalItemId: string, + promptKey: string, + questionIdForItem?: string, + turnId: string | null = null + ): void { + const prompt = this.prompts.get(promptKey) + this.journalBindings.set(journalItemId, { + address: promptKey, + ...(questionIdForItem ? { questionId: questionIdForItem } : {}), + turnId: turnId ?? prompt?.turnId ?? null + }) + } + + find(itemId: string): { prompt: ClaudePendingPrompt; questionId?: string } | null { + const binding = this.journalBindings.get(itemId) + const prompt = this.prompts.get(binding?.address ?? itemId) + return prompt + ? { prompt, ...(binding?.questionId ? { questionId: binding.questionId } : {}) } + : null + } + + claim(itemId: string, kind?: 'approval' | 'question'): ClaudePromptClaim | null { + const found = this.find(itemId) + if (!found || this.claims.has(found.prompt) || (kind && found.prompt.kind !== kind)) { + return null + } + const claim = { itemId, found } + this.claims.set(found.prompt, claim) + return claim + } + + claimBound(itemId: string, turnId: string): ClaudePromptClaim | null { + const binding = this.journalBindings.get(itemId) + const prompt = binding ? this.prompts.get(binding.address) : undefined + if (!binding || !prompt || binding.turnId !== turnId || this.claims.has(prompt)) { + return null + } + const found = { prompt, ...(binding.questionId ? { questionId: binding.questionId } : {}) } + const claim = { itemId, found } + this.claims.set(prompt, claim) + return claim + } + + ownsClaim(claim: ClaudePromptClaim): boolean { + return ( + this.claims.get(claim.found.prompt) === claim && + this.find(claim.itemId)?.prompt === claim.found.prompt + ) + } + + ownsBoundClaim(claim: ClaudePromptClaim, itemId: string, turnId: string): boolean { + const binding = this.journalBindings.get(itemId) + return ( + claim.itemId === itemId && + this.claims.get(claim.found.prompt) === claim && + binding?.address === claim.found.prompt.promptKey && + binding.turnId === turnId && + this.prompts.get(binding.address) === claim.found.prompt + ) + } + + releaseClaim(claim: ClaudePromptClaim): void { + if (this.claims.get(claim.found.prompt) === claim) { + this.claims.delete(claim.found.prompt) + } + } + + observeCancellation(claim: ClaudePromptClaim): Promise | null { + if (!this.ownsClaim(claim)) { + return null + } + let observation = this.cancellationObservations.get(claim.found.prompt) + if (!observation) { + let resolve = (): void => {} + const promise = new Promise((settled) => { + resolve = settled + }) + observation = { promise, resolve } + this.cancellationObservations.set(claim.found.prompt, observation) + } + return observation.promise + } + + cancel(requestId: string): ClaudePendingPrompt | null { + const prompt = this.prompts.get(requestId) ?? null + if (prompt) { + this.forget(prompt) + } + return prompt + } + + forget(prompt: ClaudePendingPrompt): void { + this.claims.delete(prompt) + this.prompts.delete(prompt.promptKey) + for (const [itemId, binding] of this.journalBindings) { + if (binding.address === prompt.promptKey) { + this.journalBindings.delete(itemId) + } + } + } + + clear(): ClaudePendingPrompt[] { + const pending = [...this.prompts.values()] + this.prompts.clear() + this.journalBindings.clear() + this.claims.clear() + for (const prompt of pending) { + this.cancellationObservations.get(prompt)?.resolve() + } + return pending + } +} diff --git a/src/main/claude/claude-structured-control-actions.test.ts b/src/main/claude/claude-structured-control-actions.test.ts index 168ce558f53..e9afa9bac25 100644 --- a/src/main/claude/claude-structured-control-actions.test.ts +++ b/src/main/claude/claude-structured-control-actions.test.ts @@ -4,10 +4,12 @@ import { answerClaudePrompt, stopClaudeBackgroundTasks } from './claude-structured-control-actions' +import { dispatchClaudeTurn } from './claude-structured-dispatch' import { ClaudeControlRequestError } from './claude-stream-json-connection' import { ClaudePromptRegistry } from './claude-structured-prompt-replies' -import type { ClaudeSession } from './claude-structured-session-state' +import type { ClaudeDispatchWaiter, ClaudeSession } from './claude-structured-session-state' import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker' +import { sessionFor, userMessage } from './claude-structured-dispatch-test-support' type InterruptResult = Awaited> @@ -23,11 +25,11 @@ function sessionWith(input: { } { const interrupt = vi.fn(input.interrupt) const cancelAsyncMessage = vi.fn(input.cancelAsyncMessage ?? (async () => {})) - const session = { - capabilities: input.capabilities ?? [], - prompts: input.prompts ?? new ClaudePromptRegistry(), - connection: { interrupt, cancelAsyncMessage } - } as unknown as ClaudeSession + const session = sessionFor() + session.capabilities = input.capabilities ?? [] + session.prompts = input.prompts ?? new ClaudePromptRegistry() + session.connection.interrupt = interrupt + session.connection.cancelAsyncMessage = cancelAsyncMessage return { session, interrupt, cancelAsyncMessage } } @@ -54,15 +56,69 @@ describe('cancelClaudeTurn', () => { expect(cancelAsyncMessage.mock.calls.map((call) => call[0])).toEqual(['queued-1', 'queued-2']) }) - it('sends cancel_queued and never sweeps when the CLI advertises the capability', async () => { + it('settles every cancelled queued waiter when the CLI advertises the capability', async () => { + const cancelled = Array.from({ length: 64 }, (_, index) => `queued-${index}`) const { session, interrupt, cancelAsyncMessage } = sessionWith({ capabilities: ['interrupt_receipt_v1', 'interrupt_cancel_queued_v1'], - interrupt: async () => ({ still_queued: [], cancelled: ['queued-1'] }) + interrupt: async () => ({ still_queued: [], cancelled }) }) + const resolutions = cancelled.map(() => vi.fn()) + session.dispatchWaiters = cancelled.map((sentUuid, index): ClaudeDispatchWaiter => ({ + acceptsResult: false, + clientMessageId: `client-${index}`, + sentUuid, + dispatchSequence: index + 1, + replayContentKey: `content-${index}`, + resolve: resolutions[index]! + })) + const settled = vi.fn() - await expect(cancelClaudeTurn(session, 5_000)).resolves.toEqual({ cancelled: true }) + await expect(cancelClaudeTurn(session, 5_000, () => true, settled)).resolves.toEqual({ + cancelled: true + }) expect(interrupt).toHaveBeenCalledWith({ cancelQueued: true, timeoutMs: 5_000 }) expect(cancelAsyncMessage).not.toHaveBeenCalled() + expect(session.dispatchWaiters).toEqual([]) + expect(resolutions.every((resolve) => resolve.mock.calls[0]?.[0] === null)).toBe(true) + expect(settled).toHaveBeenCalledTimes(64) + expect(settled).toHaveBeenNthCalledWith(1, { + clientMessageId: 'client-0', + state: 'rejected', + reason: 'provider_cancelled_before_start' + }) + }) + + it('rejects an ambiguously written dispatch when a later interrupt confirms it was cancelled', async () => { + let cancelledUuid = '' + const { session } = sessionWith({ + capabilities: ['interrupt_cancel_queued_v1'], + interrupt: async () => ({ still_queued: [], cancelled: [cancelledUuid] }) + }) + session.connection.send = vi.fn(async () => { + throw new Error('connection lost after write') + }) + const settled = vi.fn() + + await expect( + dispatchClaudeTurn(session, { + clientMessageId: 'client-ambiguous', + body: userMessage([{ type: 'text', text: 'queued' }]) + }) + ).resolves.toMatchObject({ state: 'unknown' }) + expect(session.dispatchWaiters).toEqual([]) + expect(session.retiredDispatchWaiters).toHaveLength(1) + cancelledUuid = session.retiredDispatchWaiters[0]!.sentUuid + + await expect(cancelClaudeTurn(session, 5_000, () => true, settled)).resolves.toEqual({ + cancelled: true + }) + expect(session.retiredDispatchWaiters).toEqual([]) + expect(settled).toHaveBeenCalledOnce() + expect(settled).toHaveBeenCalledWith({ + clientMessageId: 'client-ambiguous', + state: 'rejected', + reason: 'provider_cancelled_before_start' + }) }) it('reports a not-running interrupt as not cancelled without throwing', async () => { @@ -87,6 +143,40 @@ describe('cancelClaudeTurn', () => { }) describe('answerClaudePrompt', () => { + it('resolves cancellation observation when teardown clears the prompt registry', async () => { + const prompts = new ClaudePromptRegistry() + const settle = vi.fn() + const prompt = prompts.register({ + requestId: 'perm-clear', + toolName: 'Bash', + toolUseId: 'tool-clear', + input: { command: 'ls' }, + suggestions: [], + settle + })! + prompts.bindJournalItemId('journal-clear', prompt.promptKey) + const claim = prompts.claim('journal-clear', 'approval') + if (!claim) { + throw new Error('expected prompt claim') + } + const observed = prompts.observeCancellation(claim) + if (!observed) { + throw new Error('expected cancellation observation') + } + let observedCancellation = false + void observed.then(() => { + observedCancellation = true + }) + + expect(prompts.clear()).toEqual([prompt]) + await Promise.resolve() + + expect(observedCancellation).toBe(true) + expect(prompts.find('journal-clear')).toBeNull() + expect(prompts.ownsClaim(claim)).toBe(false) + expect(settle).not.toHaveBeenCalled() + }) + it('settles the pending prompt callback and forgets it', async () => { const prompts = new ClaudePromptRegistry() const settle = vi.fn() @@ -100,20 +190,34 @@ describe('answerClaudePrompt', () => { })! prompts.bindJournalItemId('journal-1', prompt.promptKey) const { session } = sessionWith({ interrupt: async () => undefined, prompts }) + const resolvePrompt = vi.fn() + session.translator = { + handle: vi.fn(), + journalPrompts: { + cancel: vi.fn(() => ({ accepted: true as const })), + resolve: resolvePrompt + }, + flush: vi.fn(), + pendingStreamedBlocks: 0, + dispose: vi.fn() + } - await answerClaudePrompt(session, { itemId: 'journal-1', kind: 'approval', optionId: 'allow' }) + const claim = prompts.claim('journal-1', 'approval') + if (!claim) { + throw new Error('expected prompt claim') + } + await answerClaudePrompt(session, claim, 'allow') expect(settle).toHaveBeenCalledWith( expect.objectContaining({ behavior: 'allow', toolUseID: 'tool-1' }) ) expect(prompts.find('journal-1')).toBeNull() + expect(resolvePrompt).toHaveBeenCalledWith(prompt.promptKey) }) - it('refuses an answer for a prompt Claude is no longer waiting on', async () => { - const { session } = sessionWith({ interrupt: async () => undefined }) - await expect( - answerClaudePrompt(session, { itemId: 'missing', kind: 'approval', optionId: 'allow' }) - ).rejects.toThrow(/no longer waiting/) + it('refuses to claim a prompt Claude is no longer waiting on', () => { + const prompts = new ClaudePromptRegistry() + expect(prompts.claim('missing', 'approval')).toBeNull() }) }) diff --git a/src/main/claude/claude-structured-control-actions.ts b/src/main/claude/claude-structured-control-actions.ts index 8b3bb94c7b5..961b9aee7ba 100644 --- a/src/main/claude/claude-structured-control-actions.ts +++ b/src/main/claude/claude-structured-control-actions.ts @@ -1,9 +1,17 @@ -import { applyClaudePromptAnswer } from './claude-structured-prompt-replies' +import { applyClaudePromptAnswer, type ClaudePromptClaim } from './claude-structured-prompt-replies' import { ClaudeControlRequestError } from './claude-stream-json-connection' +import { + settleCancelledClaudeDispatchWaiters, + type ClaudeLateDispatchSettlement +} from './claude-structured-dispatch' import type { ClaudeSession } from './claude-structured-session-state' const INTERRUPT_CANCEL_QUEUED_CAPABILITY = 'interrupt_cancel_queued_v1' +export function supportsClaudeQueuedInterruptCancellation(session: ClaudeSession): boolean { + return session.capabilities.includes(INTERRUPT_CANCEL_QUEUED_CAPABILITY) +} + export type ClaudeTurnCancellationGuard = () => boolean /** @@ -16,20 +24,23 @@ export type ClaudeTurnCancellationGuard = () => boolean export async function cancelClaudeTurn( session: ClaudeSession, timeoutMs: number | undefined, - isCurrent: ClaudeTurnCancellationGuard = () => true + isCurrent: ClaudeTurnCancellationGuard = () => true, + onDispatchSettledLate?: ClaudeLateDispatchSettlement ): Promise<{ cancelled: boolean }> { // The SDK interrupt is session-scoped. Re-check the caller's turn/fence // immediately before issuing it so a delayed request cannot stop a later turn. if (!isCurrent()) { return { cancelled: false } } - const cancelQueued = session.capabilities.includes(INTERRUPT_CANCEL_QUEUED_CAPABILITY) + const cancelQueued = supportsClaudeQueuedInterruptCancellation(session) try { const receipt = await session.connection.interrupt({ ...(cancelQueued ? { cancelQueued: true } : {}), timeoutMs }) - if (!cancelQueued) { + if (cancelQueued) { + settleCancelledClaudeDispatchWaiters(session, receipt?.cancelled ?? [], onDispatchSettledLate) + } else { for (const uuid of receipt?.still_queued ?? []) { await session.connection.cancelAsyncMessage(uuid, { timeoutMs }).catch(() => {}) } @@ -71,16 +82,18 @@ export async function stopClaudeBackgroundTasks( export async function answerClaudePrompt( session: ClaudeSession, - input: { itemId: string; kind: 'approval' | 'question'; optionId: string } + claim: ClaudePromptClaim, + optionId: string ): Promise { - const found = session.prompts.find(input.itemId) - if (!found || found.prompt.kind !== input.kind) { - throw new Error(`claude is no longer waiting on ${input.itemId}`) + if (!session.prompts.ownsClaim(claim)) { + throw new Error(`claude is no longer waiting on ${claim.itemId}`) } - const response = applyClaudePromptAnswer(found, input.optionId) + const response = applyClaudePromptAnswer(claim.found, optionId) if (response === null) { + session.prompts.releaseClaim(claim) return } - session.prompts.forget(found.prompt) - found.prompt.settle(response) + session.prompts.forget(claim.found.prompt) + claim.found.prompt.settle(response) + session.translator?.journalPrompts.resolve(claim.found.prompt.promptKey) } diff --git a/src/main/claude/claude-structured-dispatch.ts b/src/main/claude/claude-structured-dispatch.ts index 45dc3ebc0e3..84253b765f6 100644 --- a/src/main/claude/claude-structured-dispatch.ts +++ b/src/main/claude/claude-structured-dispatch.ts @@ -1,14 +1,15 @@ import { randomUUID } from 'node:crypto' -import type { - AgentJournalItemIdentity, - AgentJournalMessageItem -} from '../../shared/agent-session-journal-types' +import type { AgentJournalMessageItem } from '../../shared/agent-session-journal-types' import type { AgentSessionDispatchOutcome } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import { claudeHasReplayContent, readClaudeMessageEnvelope } from './claude-structured-item-translation' -import type { ClaudeDispatchWaiter, ClaudeSession } from './claude-structured-session-state' +import type { + ClaudeDispatchWaiter, + ClaudeLateDispatchOutcome, + ClaudeSession +} from './claude-structured-session-state' import { readClaudeFrameString } from './claude-structured-init-proof' import { claudeDispatchContentKey, @@ -17,6 +18,7 @@ import { } from './claude-structured-dispatch-content' import { dispatchWriteOutcomeUnknownReason } from '../native-chat/agent-session-journal/journal-dispatch-doubt-reasons' import { + DISPATCH_REJECTED_CANCELLED, DISPATCH_REJECTED_QUEUE_FULL, dispatchWriteFailureReason } from '../../shared/structured-agent-session-dispatch-rejection' @@ -25,11 +27,8 @@ import { claudeUserMessageWasProvablyUnwritten } from './claude-agent-sdk-user-m const MAX_RETIRED_DISPATCH_WAITERS = 64 const MAX_ACTIVE_DISPATCH_WAITERS = 64 -/** Directly settles provider-proven delivery; the durable replay row independently reconciles it. */ -export type ClaudeLateDispatchSettlement = (input: { - clientMessageId: string - providerIdentity: AgentJournalItemIdentity -}) => void +/** Settles a provider-proven late outcome; replay rows independently reconcile acceptance. */ +export type ClaudeLateDispatchSettlement = (input: ClaudeLateDispatchOutcome) => void export function resolveClaudeReplayWaiter( session: ClaudeSession, @@ -228,6 +227,34 @@ function forgetWaiter(session: ClaudeSession, waiter: ClaudeDispatchWaiter): voi } } +export function settleCancelledClaudeDispatchWaiters( + session: ClaudeSession, + cancelledUuids: readonly string[], + onSettledLate?: ClaudeLateDispatchSettlement +): void { + const cancelled = new Set(cancelledUuids) + const activeWaiters = session.dispatchWaiters.filter((waiter) => cancelled.has(waiter.sentUuid)) + const retiredWaiters = session.retiredDispatchWaiters.filter((waiter) => + cancelled.has(waiter.sentUuid) + ) + for (const waiter of activeWaiters) { + forgetWaiter(session, waiter) + waiter.resolve(null) + } + for (const waiter of retiredWaiters) { + forgetRetiredWaiter(session, waiter) + } + for (const waiter of [...activeWaiters, ...retiredWaiters]) { + if (waiter.clientMessageId) { + onSettledLate?.({ + clientMessageId: waiter.clientMessageId, + state: 'rejected', + reason: DISPATCH_REJECTED_CANCELLED + }) + } + } +} + function retireWaiter(session: ClaudeSession, waiter: ClaudeDispatchWaiter): void { forgetWaiter(session, waiter) if (!waiter.retired) { diff --git a/src/main/claude/claude-structured-inbound-control.ts b/src/main/claude/claude-structured-inbound-control.ts index 343e76d4ea5..27a90181aec 100644 --- a/src/main/claude/claude-structured-inbound-control.ts +++ b/src/main/claude/claude-structured-inbound-control.ts @@ -25,6 +25,7 @@ export type ClaudePermissionCallbackDeps = { sessionId: string prompts: ClaudePromptRegistry emit: (event: ClaudeStructuredSessionEvent) => void + currentTurnId?: () => string | null } function denySafeResult(toolUseId: string | undefined): PermissionResult { @@ -36,7 +37,7 @@ function denySafeResult(toolUseId: string | undefined): PermissionResult { } /** - * Build the SDK permission callbacks from the durable prompt registry. + * Build the SDK permission callbacks from the session-local prompt registry. * * A decodable `can_use_tool` becomes a durable prompt whose `settle` resolves this callback; * a malformed one is denied without registering. The SDK's abort signal fires on @@ -57,7 +58,8 @@ export function buildClaudePermissionCallbacks(deps: ClaudePermissionCallbackDep toolUseId: options.toolUseID, input, suggestions: options.suggestions ?? [], - settle: resolve as (response: Record | null) => void + settle: resolve, + turnId: deps.currentTurnId?.() ?? null }) if (!prompt) { resolve(denySafeResult(options.toolUseID)) diff --git a/src/main/claude/claude-structured-journal-prompt-retry.test.ts b/src/main/claude/claude-structured-journal-prompt-retry.test.ts new file mode 100644 index 00000000000..491152668ed --- /dev/null +++ b/src/main/claude/claude-structured-journal-prompt-retry.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, it } from 'vitest' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity +} from '../../shared/agent-session-journal-types' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import type { ClaudePendingPrompt } from './claude-structured-prompt-replies' +import { createClaudeJournalTranslator } from './claude-structured-journal-translation' + +function approval(promptKey: string): ClaudePendingPrompt { + return { + requestId: promptKey, + promptKey, + toolUseId: 'tool-retry', + toolName: 'Bash', + kind: 'approval', + input: { command: 'git status' }, + suggestions: [], + questionIds: [], + answers: new Map(), + settle: () => {} + } +} + +function transientBackpressureSink( + refusedAt: 'append' | 'publish', + persistent = false +): { + sink: StructuredAgentSessionEventSink + durableApproval: () => AgentJournalItemBody | undefined + appendAttempts: () => number + publishAttempts: () => number + appliedSettlements: Set + release: () => void +} { + const staged = new Map() + const durable = new Map() + const appliedSettlements = new Set() + let lifecycleAppendAttempts = 0 + let lifecyclePublishAttempts = 0 + let released = false + const persist = (): void => { + durable.clear() + for (const [key, body] of staged) { + durable.set(key, body) + } + } + const applyItem = (identity: AgentJournalItemIdentity, body: AgentJournalItemBody): void => { + staged.set(agentJournalItemKey(identity), body) + } + return { + sink: { + appendItem: applyItem, + appendTombstone: (identity) => staged.delete(agentJournalItemKey(identity)), + publish: persist, + tryAppendLifecycleBatch: (settlementId, mutations) => { + lifecycleAppendAttempts += 1 + if (refusedAt === 'append' && (persistent ? !released : lifecycleAppendAttempts === 1)) { + return { accepted: false, reason: 'backpressure' } + } + if (!appliedSettlements.has(settlementId)) { + for (const mutation of mutations) { + if (mutation.kind === 'item') { + applyItem(mutation.identity, mutation.body) + } else { + staged.delete(agentJournalItemKey(mutation.identity)) + } + } + appliedSettlements.add(settlementId) + } + return { accepted: true } + }, + tryPublish: () => { + lifecyclePublishAttempts += 1 + if (refusedAt === 'publish' && (persistent ? !released : lifecyclePublishAttempts === 1)) { + return { accepted: false, reason: 'backpressure' } + } + persist() + return { accepted: true } + } + }, + durableApproval: () => [...durable.values()].find((body) => body.kind === 'approval'), + appendAttempts: () => lifecycleAppendAttempts, + publishAttempts: () => lifecyclePublishAttempts, + appliedSettlements, + release: () => { + released = true + } + } +} + +function rootResult() { + return { + type: 'message' as const, + sessionId: 'orca-session', + message: { + type: 'result', + subtype: 'success', + uuid: 'result-success', + session_id: 'claude-session', + parent_tool_use_id: null, + is_error: false, + duration_ms: 1 + } + } +} + +function streamDelta(index: number) { + return { + type: 'message' as const, + sessionId: 'orca-session', + message: { + type: 'stream_event', + uuid: `stream-${index}`, + session_id: 'claude-session', + parent_tool_use_id: null, + event: { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'x' } + } + } + } +} + +describe('Claude journal prompt cancellation retry', () => { + it.each(['append', 'publish'] as const)( + 'retries after transient lifecycle %s backpressure', + (refusedAt) => { + const state = transientBackpressureSink(refusedAt) + const translator = createClaudeJournalTranslator({ sink: state.sink }) + const prompt = approval('permission-retry') + + translator.handle({ type: 'prompt', sessionId: 'orca-session', prompt }) + translator.handle({ + type: 'prompt-cancelled', + sessionId: 'orca-session', + promptKey: prompt.promptKey + }) + expect(state.durableApproval()).toMatchObject({ resolution: { state: 'pending' } }) + + translator.handle(rootResult()) + expect(state.durableApproval()).toMatchObject({ resolution: { state: 'cancelled' } }) + expect(state.appendAttempts()).toBe(2) + expect(state.publishAttempts()).toBe(refusedAt === 'publish' ? 2 : 1) + expect(state.appliedSettlements).toEqual(new Set(['prompt-cancelled:permission-retry'])) + + translator.handle(rootResult()) + expect(state.appendAttempts()).toBe(2) + expect(state.publishAttempts()).toBe(refusedAt === 'publish' ? 2 : 1) + } + ) + + it('keeps streaming frames off retry work and recovers at the next root result', () => { + const state = transientBackpressureSink('append', true) + const translator = createClaudeJournalTranslator({ sink: state.sink }) + const prompt = approval('permission-streaming') + + translator.handle({ type: 'prompt', sessionId: 'orca-session', prompt }) + translator.handle({ + type: 'prompt-cancelled', + sessionId: 'orca-session', + promptKey: prompt.promptKey + }) + expect(state.appendAttempts()).toBe(1) + + for (let index = 0; index < 100; index += 1) { + translator.handle(streamDelta(index)) + } + expect(state.appendAttempts()).toBe(1) + + state.release() + translator.handle(rootResult()) + expect(state.appendAttempts()).toBe(2) + expect(state.durableApproval()).toMatchObject({ resolution: { state: 'cancelled' } }) + }) +}) diff --git a/src/main/claude/claude-structured-journal-prompts.ts b/src/main/claude/claude-structured-journal-prompts.ts new file mode 100644 index 00000000000..3c660223257 --- /dev/null +++ b/src/main/claude/claude-structured-journal-prompts.ts @@ -0,0 +1,191 @@ +import type { + AgentJournalApprovalItem, + AgentJournalItemIdentity, + AgentJournalQuestionItem +} from '../../shared/agent-session-journal-types' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import { cancelledJournalPromptBody } from '../native-chat/agent-session-journal/journal-prompt-body-bounds' +import type { + StructuredAgentSessionEventSink, + StructuredAgentSessionSinkAdmission +} from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { + claudeApprovalItem, + claudePromptIdentity, + claudeQuestionItems, + type ClaudeQuestionItem +} from './claude-structured-prompt-items' +import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state' + +const ADMITTED = { accepted: true } as const + +type ClaudeJournalPrompt = { + identity: AgentJournalItemIdentity + body: AgentJournalApprovalItem | AgentJournalQuestionItem +} + +type ClaudeJournalPromptEntry = { + items: ClaudeJournalPrompt[] + cancellationPending: boolean +} + +function cancelledPromptBody( + body: AgentJournalApprovalItem | AgentJournalQuestionItem +): AgentJournalApprovalItem | AgentJournalQuestionItem { + const cancelled = cancelledJournalPromptBody(body) + if (!cancelled) { + throw new Error('Claude prompt body is not cancellable') + } + return cancelled +} + +export class ClaudeJournalPrompts { + private readonly items = new Map() + private pendingCancellationTotal = 0 + + get size(): number { + return this.items.size + } + + get pendingCancellationCount(): number { + return this.pendingCancellationTotal + } + + constructor( + private readonly deps: { + sink: StructuredAgentSessionEventSink + bindPromptItemId?: (journalItemId: string, promptKey: string, questionId?: string) => void + questionItems?: (input: { + sessionId: string + prompt: Extract['prompt'] + }) => ClaudeQuestionItem[] + } + ) {} + + handle(event: Extract): void { + const items: ClaudeJournalPrompt[] = [] + if (event.prompt.kind === 'question') { + for (const question of (this.deps.questionItems ?? claudeQuestionItems)({ + sessionId: event.sessionId, + prompt: event.prompt + })) { + items.push(question) + this.deps.sink.appendItem(question.identity, question.body) + this.deps.bindPromptItemId?.(agentJournalItemKey(question.identity), event.prompt.promptKey) + } + } else { + const identity = claudePromptIdentity({ + sessionId: event.sessionId, + promptKey: event.prompt.promptKey + }) + const body = claudeApprovalItem(event.prompt) + items.push({ identity, body }) + this.deps.sink.appendItem(identity, body) + this.deps.bindPromptItemId?.(agentJournalItemKey(identity), event.prompt.promptKey) + } + this.deletePrompt(event.prompt.promptKey) + this.items.set(event.prompt.promptKey, { items, cancellationPending: false }) + this.deps.sink.publish() + } + + private admitCancellation(promptKey: string): StructuredAgentSessionSinkAdmission { + const items = this.items.get(promptKey)?.items ?? [] + if (items.length === 0) { + return ADMITTED + } + const mutations = items.map(({ identity, body }) => ({ + kind: 'item' as const, + identity, + body: cancelledPromptBody(body) + })) + let admission: StructuredAgentSessionSinkAdmission + if (this.deps.sink.tryAppendLifecycleBatch) { + admission = this.deps.sink.tryAppendLifecycleBatch( + `prompt-cancelled:${encodeURIComponent(promptKey)}`, + mutations, + { lifecycle: true } + ) + } else if (this.deps.sink.appendLifecycleBatch) { + admission = + this.deps.sink.appendLifecycleBatch( + `prompt-cancelled:${encodeURIComponent(promptKey)}`, + mutations, + { lifecycle: true } + ) ?? ADMITTED + } else if (items.length === 1) { + const item = items[0] + if (!item) { + return ADMITTED + } + const body = cancelledPromptBody(item.body) + admission = this.deps.sink.tryAppendItem + ? this.deps.sink.tryAppendItem(item.identity, body, { lifecycle: true }) + : (this.deps.sink.appendItem(item.identity, body, { lifecycle: true }), ADMITTED) + } else { + return { accepted: false, reason: 'failed' } + } + if (!admission.accepted) { + return admission + } + const published = this.deps.sink.tryPublish + ? this.deps.sink.tryPublish({ lifecycle: true }) + : (this.deps.sink.publish({ lifecycle: true }), ADMITTED) + if (published.accepted) { + this.deletePrompt(promptKey) + } + return published + } + + private deletePrompt(promptKey: string): void { + const entry = this.items.get(promptKey) + if (entry?.cancellationPending) { + this.pendingCancellationTotal -= 1 + } + this.items.delete(promptKey) + } + + private setCancellationPending(entry: ClaudeJournalPromptEntry, pending: boolean): void { + if (entry.cancellationPending === pending) { + return + } + entry.cancellationPending = pending + this.pendingCancellationTotal += pending ? 1 : -1 + } + + cancel(promptKey: string): StructuredAgentSessionSinkAdmission { + const admission = this.admitCancellation(promptKey) + const entry = this.items.get(promptKey) + if (entry) { + this.setCancellationPending(entry, !admission.accepted && admission.reason === 'backpressure') + } + return admission + } + + retryPendingCancellations(): void { + if (this.pendingCancellationTotal === 0) { + return + } + for (const [promptKey, entry] of this.items) { + if (!entry.cancellationPending) { + continue + } + const admission = this.admitCancellation(promptKey) + if (!admission.accepted && admission.reason === 'backpressure') { + return + } + const retained = this.items.get(promptKey) + if (retained) { + this.setCancellationPending(retained, false) + } + } + } + + resolve(promptKey: string): void { + this.deletePrompt(promptKey) + } + + clear(): void { + this.items.clear() + this.pendingCancellationTotal = 0 + } +} diff --git a/src/main/claude/claude-structured-journal-translation.test.ts b/src/main/claude/claude-structured-journal-translation.test.ts index 65be68bb05f..d9b8736c955 100644 --- a/src/main/claude/claude-structured-journal-translation.test.ts +++ b/src/main/claude/claude-structured-journal-translation.test.ts @@ -309,6 +309,53 @@ describe('Claude structured journal translation', () => { expect(providerFrameKinds(items)).toEqual([]) }) + it('restores a cancelled prompt as terminal history after reopening the journal', async () => { + const journal = await openAgentSessionJournal({ + identity: JOURNAL_IDENTITY, + journalDir: journalRoot, + now: () => 1_700_000_000_000, + mintEpoch: () => 'epoch-1' + }) + const deferred = createDeferredStructuredAgentSessionEventSink() + deferred.bind({ journal, fence: 1, publish: vi.fn() }) + const translator = createClaudeJournalTranslator({ sink: deferred.sink }) + const approval = prompt({ + requestId: 'permission-1', + promptKey: 'permission-1', + toolUseId: 'tool-1', + toolName: 'Bash', + kind: 'approval', + input: { command: 'git status' }, + questionIds: [] + }) + + translator.handle({ type: 'prompt', sessionId: 'orca-session', prompt: approval }) + translator.handle({ + type: 'prompt-cancelled', + sessionId: 'orca-session', + promptKey: approval.promptKey + }) + await expect(deferred.drained()).resolves.toEqual({ ok: true }) + deferred.close() + await journal.close() + + const reopened = await openAgentSessionJournal({ + identity: JOURNAL_IDENTITY, + journalDir: journalRoot, + now: () => 1_700_000_000_000, + mintEpoch: () => 'epoch-2' + }) + expect(reopened.snapshot().items).toEqual([ + expect.objectContaining({ + body: expect.objectContaining({ + kind: 'approval', + resolution: expect.objectContaining({ state: 'cancelled' }) + }) + }) + ]) + await reopened.close() + }) + it('settles result frames, empty thinking and string user replays without painting a row', () => { const state = sinkState() const translator = createClaudeJournalTranslator({ sink: state.sink }) @@ -799,7 +846,11 @@ describe('Claude structured journal translation', () => { sessionId: 'orca-session', promptKey: 'questions-1' }) - expect(state.tombstones).toHaveLength(1) + expect(state.items.at(-1)?.body).toMatchObject({ + kind: 'question', + resolution: { state: 'cancelled' } + }) + expect(state.tombstones).toHaveLength(0) }) }) diff --git a/src/main/claude/claude-structured-journal-translation.ts b/src/main/claude/claude-structured-journal-translation.ts index 702ff5f6b26..216e760315c 100644 --- a/src/main/claude/claude-structured-journal-translation.ts +++ b/src/main/claude/claude-structured-journal-translation.ts @@ -1,4 +1,3 @@ -import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' import type { AgentSessionDeltaCoalescerDeps } from '../native-chat/agent-session-wire/agent-session-delta-coalescer' import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' @@ -21,7 +20,6 @@ import { readClaudeMessageEnvelope, type ClaudeToolUse } from './claude-structured-item-translation' -import { journalClaudePrompt } from './claude-prompt-journaling' import type { ClaudePromptRegistry } from './claude-structured-prompt-replies' import { claudeProviderFrameActivity } from '../native-chat/agent-session-wire/provider-frame-activity' import { @@ -48,6 +46,7 @@ import { type ClaudeCurrentTurn, type ClaudeTurnEnd } from './claude-turn-lifecycle-item' +import { ClaudeJournalPrompts } from './claude-structured-journal-prompts' export type ClaudeJournalTranslatorDeps = { sink: StructuredAgentSessionEventSink @@ -59,6 +58,7 @@ export type ClaudeJournalTranslatorDeps = { export type ClaudeJournalTranslator = { handle: (event: ClaudeStructuredSessionEvent) => void + journalPrompts: Pick flush: () => void /** Streamed blocks still awaiting a final frame. A settled turn leaves none. */ readonly pendingStreamedBlocks: number @@ -84,7 +84,7 @@ export function createClaudeJournalTranslator( deps: ClaudeJournalTranslatorDeps ): ClaudeJournalTranslator { const tools = new Map() - const promptItems = new Map() + const prompts = new ClaudeJournalPrompts(deps) const streamedBlocks = createClaudeStreamedBlockRegistry() let currentTurn: ClaudeCurrentTurn | null = null /** Provider output may not reopen a turn after the session ended or a turn @@ -256,6 +256,7 @@ export function createClaudeJournalTranslator( return { handle: (event) => { if (event.type === 'ended') { + prompts.retryPendingCancellations() streamedText.flush() // No event will ever settle a child once the provider is gone. subagents.settleSession() @@ -278,13 +279,10 @@ export function createClaudeJournalTranslator( } streamedText.flush() if (event.type === 'prompt') { - journalClaudePrompt({ ...deps, promptItems }, event) + prompts.handle(event) } else if (event.type === 'prompt-cancelled') { - for (const identity of promptItems.get(event.promptKey) ?? []) { - deps.sink.appendTombstone(identity) - } - promptItems.delete(event.promptKey) - deps.sink.publish() + prompts.retryPendingCancellations() + prompts.cancel(event.promptKey) } else if (event.type === 'message' && event.message.type === 'result') { // Every turn this translator opens is root by construction, so a nested // result settles the child that produced it and never the turn. The @@ -292,6 +290,7 @@ export function createClaudeJournalTranslator( // it ends no turn. const settlesTurn = isRootClaudeFrame(event.message) if (settlesTurn) { + prompts.retryPendingCancellations() // The turn is over however it ended, so a foreground child still // reported as working will never be settled by an event. // A turn that failed, or that the user stopped, is not resumed by @@ -335,6 +334,7 @@ export function createClaudeJournalTranslator( publishActivity(event.kind, event.payload) } }, + journalPrompts: prompts, flush: streamedText.flush, get pendingStreamedBlocks() { return streamedText.pending @@ -342,7 +342,7 @@ export function createClaudeJournalTranslator( dispose: () => { streamedText.dispose() tools.clear() - promptItems.clear() + prompts.clear() streamedBlocks.clear() subagents.dispose() } diff --git a/src/main/claude/claude-structured-prompt-items.test.ts b/src/main/claude/claude-structured-prompt-items.test.ts index 79916d6a507..eec4ee74c9c 100644 --- a/src/main/claude/claude-structured-prompt-items.test.ts +++ b/src/main/claude/claude-structured-prompt-items.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' import { encodeAgentSessionQuestionAnswers } from '../../shared/agent-session-question-answer' +import { cancelledJournalPromptBody } from '../native-chat/agent-session-journal/journal-prompt-body-bounds' +import { MAX_JOURNAL_LIFECYCLE_BATCH_BYTES } from '../native-chat/agent-session-journal/journal-row-schema' import { claudeQuestionItems } from './claude-structured-prompt-items' import { applyClaudePromptAnswer, @@ -9,6 +11,46 @@ import { } from './claude-structured-prompt-replies' describe('Claude structured question addressing', () => { + it('bounds a valid grouped question before cancellation enters a lifecycle batch', () => { + const oversized = 'large prompt text '.repeat(40_000) + const questions = Array.from({ length: 4 }, (_, questionIndex) => ({ + question: `${questionIndex}:${oversized}`, + header: oversized, + options: Array.from({ length: 4 }, (_, optionIndex) => ({ + label: `${optionIndex}:${oversized}`, + description: oversized + })) + })) + const prompt: ClaudePendingPrompt = { + requestId: 'oversized-question', + promptKey: 'oversized-question', + toolUseId: 'tool-oversized', + toolName: 'AskUserQuestion', + kind: 'question', + input: { questions }, + suggestions: [], + questionIds: questions.map((question) => question.question), + answers: new Map(), + settle: () => {} + } + + const body = claudeQuestionItems({ sessionId: 'session-1', prompt })[0]?.body + if (!body) { + throw new Error('expected grouped question body') + } + const cancelled = cancelledJournalPromptBody(body) + if (!cancelled) { + throw new Error('expected cancellable grouped question body') + } + + expect(body.questions).toHaveLength(4) + expect(body.questions?.[0]?.question).toContain('[Orca: output truncated') + expect(body.questions?.[0]?.options[0]?.description).toContain('[Orca: output truncated') + expect(Buffer.byteLength(JSON.stringify(cancelled), 'utf8') + 4_096).toBeLessThan( + MAX_JOURNAL_LIFECYCLE_BATCH_BYTES + ) + }) + it('keeps wire IDs bounded while returning the original question and choice', () => { const questionId = 'Which option? '.repeat(100) const label = 'A detailed choice '.repeat(100) diff --git a/src/main/claude/claude-structured-prompt-items.ts b/src/main/claude/claude-structured-prompt-items.ts index 3bdf8ab6091..25b307bd809 100644 --- a/src/main/claude/claude-structured-prompt-items.ts +++ b/src/main/claude/claude-structured-prompt-items.ts @@ -9,6 +9,7 @@ import { boundInlineText, DEFAULT_JOURNAL_PAYLOAD_LIMITS } from '../native-chat/agent-session-journal/journal-payload-bounds' +import { boundJournalPromptBody } from '../native-chat/agent-session-journal/journal-prompt-body-bounds' import { claudeRecord, claudeText } from './claude-structured-item-translation' import { CLAUDE_APPROVAL_DECISIONS, @@ -119,7 +120,7 @@ export function claudeQuestionItems(input: { sessionId: input.sessionId, promptKey: input.prompt.promptKey }), - body: { + body: boundJournalPromptBody({ kind: 'question', question: legacyCompatible ? first.question @@ -128,7 +129,7 @@ export function claudeQuestionItems(input: { ...(legacyCompatible ? { freeTextQuestionId: first.freeTextQuestionId } : {}), questions, resolution: { ...PENDING } - } + }) } ] } diff --git a/src/main/claude/claude-structured-prompt-ownership.test.ts b/src/main/claude/claude-structured-prompt-ownership.test.ts new file mode 100644 index 00000000000..7a06a58e881 --- /dev/null +++ b/src/main/claude/claude-structured-prompt-ownership.test.ts @@ -0,0 +1,766 @@ +import { describe, expect, it, vi } from 'vitest' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import type { AgentJournalItemBody } from '../../shared/agent-session-journal-types' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import type { + StructuredAgentSessionAppendOptions, + StructuredAgentSessionEventSink +} from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { ClaudeControlRequestError } from './claude-stream-json-connection' +import { ClaudeJournalPrompts } from './claude-structured-journal-prompts' +import { claudeQuestionItems } from './claude-structured-prompt-items' +import type { ClaudePendingPrompt } from './claude-structured-prompt-replies' +import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state' +import { + PROVIDER_SESSION_ID, + USER_MESSAGE, + acquired, + adapterFor, + fakeClaude, + identityFor, + invokeCanUseTool +} from './claude-structured-session-test-support' + +function deferred(): { promise: Promise; resolve: () => void } { + let resolve = (): void => {} + const promise = new Promise((finish) => { + resolve = finish + }) + return { promise, resolve } +} + +function lifecycleRecorder(acceptPromptCancellation = true): { + sink: StructuredAgentSessionEventSink + bodies: Map + tombstones: Set + order: string[] +} { + const bodies = new Map() + const tombstones = new Set() + const order: string[] = [] + const appendTombstone = ( + identity: Parameters[0], + options?: StructuredAgentSessionAppendOptions + ): void => { + const key = agentJournalItemKey(identity) + bodies.delete(key) + tombstones.add(key) + if (options?.lifecycle === true) { + order.push('prompt-lifecycle') + } + } + const appendItem = ( + identity: Parameters[0], + body: Parameters[1], + options?: StructuredAgentSessionAppendOptions + ): void => { + bodies.set(agentJournalItemKey(identity), body) + if (options?.lifecycle === true) { + order.push('prompt-lifecycle') + } + } + const sink: StructuredAgentSessionEventSink = { + appendItem, + appendTombstone, + tryAppendTombstone: (identity, options) => { + if (!acceptPromptCancellation) { + return { accepted: false, reason: 'backpressure' } + } + appendTombstone(identity, options) + return { accepted: true } + }, + tryAppendLifecycleBatch: (_settlementId, mutations, options) => { + if (!acceptPromptCancellation) { + return { accepted: false, reason: 'backpressure' } + } + for (const mutation of mutations) { + if (mutation.kind === 'tombstone') { + appendTombstone(mutation.identity, options) + } else { + appendItem(mutation.identity, mutation.body, options) + } + } + return { accepted: true } + }, + publish: (_options?: StructuredAgentSessionAppendOptions) => {}, + tryPublish: () => ({ accepted: true }) + } + return { sink, bodies, tombstones, order } +} + +async function startTurn( + adapter: Awaited>, + turnId = 'turn-1' +): Promise { + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: `client-${turnId}`, + body: USER_MESSAGE, + fence: 7 + }) +} + +describe('Claude live prompt ownership', () => { + it('lets an answer hold the callback claim through its journal commit', async () => { + const claude = fakeClaude({ replayUuid: 'turn-1' }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' } + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-1') + const commitGate = deferred() + const commitStarted = vi.fn() + + const answer = adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'allow', + fence: 7, + commit: async () => { + expect(answered.settled()).toBe(false) + commitStarted() + await commitGate.promise + } + }) + await vi.waitFor(() => expect(commitStarted).toHaveBeenCalledOnce()) + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + expect(claude.connections[0]?.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + + commitGate.resolve() + await answer + await expect(answered.promise).resolves.toMatchObject({ + behavior: 'allow', + toolUseID: 'tool-1' + }) + }) + + it('lets prompt cancellation win and waits for SDK abort cleanup', async () => { + const interruptGate = deferred() + const controller = new AbortController() + const claude = fakeClaude({ + replayUuid: 'turn-1', + routes: { interrupt: () => interruptGate.promise } + }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' }, + signal: controller.signal + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-1') + + let cancellationSettled = false + const cancellation = adapter + .cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + .finally(() => { + cancellationSettled = true + }) + await vi.waitFor(() => expect(claude.connections[0]?.calls.at(-1)?.subtype).toBe('interrupt')) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'allow', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + + interruptGate.resolve() + await Promise.resolve() + expect(cancellationSettled).toBe(false) + expect(answered.settled()).toBe(false) + controller.abort() + await expect(cancellation).resolves.toEqual({ cancelled: true }) + await expect(answered.promise).resolves.toBeNull() + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'allow', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(controller.signal.aborted).toBe(true) + expect(commit).not.toHaveBeenCalled() + }) + + it('cancels an owned prompt after another dispatch queues behind its turn', async () => { + const controller = new AbortController() + let queuedUuid = '' + const claude = fakeClaude({ + replayUuids: ['turn-1', null], + capabilities: ['interrupt_cancel_queued_v1'], + routes: { + interrupt: () => { + controller.abort() + return { still_queued: [], cancelled: [queuedUuid] } + } + } + }) + const lateSettlements: unknown[] = [] + const adapter = await acquired(claude, {}, [], (settlement) => lateSettlements.push(settlement)) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-queued', 'tool-queued', { + input: { command: 'git status' }, + signal: controller.signal + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-queued') + await expect( + adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'queued-message', + body: USER_MESSAGE, + fence: 7 + }) + ).resolves.toEqual({ state: 'admitted' }) + const sentUuid = connection.sent.at(-1)?.uuid + if (typeof sentUuid !== 'string') { + throw new Error('expected queued dispatch uuid') + } + queuedUuid = sentUuid + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: true }) + await expect(answered.promise).resolves.toBeNull() + expect(connection.calls).toContainEqual({ + subtype: 'interrupt', + params: { cancelQueued: true } + }) + expect(lateSettlements).toContainEqual({ + sessionId: 'session-1', + clientMessageId: 'queued-message', + state: 'rejected', + reason: 'provider_cancelled_before_start' + }) + }) + + it('does not interrupt a queued turn when the CLI cannot cancel queued messages', async () => { + const claude = fakeClaude({ replayUuids: ['turn-1', null] }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const controller = new AbortController() + const answered = invokeCanUseTool(connection, 'Bash', 'permission-legacy', 'tool-legacy', { + input: { command: 'git status' }, + signal: controller.signal + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-legacy') + await expect( + adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'queued-message', + body: USER_MESSAGE, + fence: 7 + }) + ).resolves.toEqual({ state: 'admitted' }) + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + expect(connection.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + controller.abort() + await expect(answered.promise).resolves.toBeNull() + }) + + it('does not interrupt a newer active turn through a stale prompt callback', async () => { + const claude = fakeClaude({ replayUuids: ['turn-1', 'turn-2'] }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const controller = new AbortController() + const answered = invokeCanUseTool(connection, 'Bash', 'permission-stale', 'tool-stale', { + input: { command: 'git status' }, + signal: controller.signal + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-stale') + await startTurn(adapter, 'turn-2') + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + expect(connection.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + expect(answered.settled()).toBe(false) + controller.abort() + await expect(answered.promise).resolves.toBeNull() + }) + + it('drops resolved prompt bodies instead of retaining them for the session lifetime', () => { + const prompts = new ClaudeJournalPrompts({ sink: lifecycleRecorder().sink }) + + for (let index = 0; index < 128; index += 1) { + const promptKey = `resolved-${index}` + prompts.handle({ + type: 'prompt', + sessionId: 'session-1', + prompt: { + requestId: promptKey, + promptKey, + toolUseId: `tool-${index}`, + toolName: 'Bash', + kind: 'approval', + input: { command: 'git status' }, + suggestions: [], + questionIds: [], + answers: new Map(), + settle: vi.fn() + } + }) + prompts.resolve(promptKey) + } + + expect(prompts.size).toBe(0) + }) + + it('releases the callback claim after a failed interrupt', async () => { + const claude = fakeClaude({ + replayUuid: 'turn-1', + routes: { + interrupt: () => { + throw new ClaudeControlRequestError('interrupt', 'not running') + } + } + }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' } + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-1') + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + await adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'allow', + fence: 7, + commit: async () => undefined + }) + await expect(answered.promise).resolves.toMatchObject({ + behavior: 'allow', + toolUseID: 'tool-1' + }) + }) + + it('enqueues terminal prompt state before a confirmed cancellation resolves', async () => { + const controller = new AbortController() + const claude = fakeClaude({ + replayUuid: 'turn-1', + routes: { interrupt: () => controller.abort() } + }) + const recorded = lifecycleRecorder() + const adapter = adapterFor(claude) + await adapter.acquire({ + identity: identityFor(), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' }, + signal: controller.signal + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-1') + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + + const cancellation = adapter + .cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + .then((result) => { + recorded.order.push('resolved') + return result + }) + + await expect(cancellation).resolves.toEqual({ cancelled: true }) + await expect(answered.promise).resolves.toBeNull() + if (!promptItemId) { + throw new Error('expected a recorded prompt item') + } + expect(recorded.order).toEqual(['prompt-lifecycle', 'resolved']) + expect( + [...recorded.bodies.values()].some( + (body) => + (body.kind === 'approval' || body.kind === 'question') && + body.resolution.state === 'pending' + ) + ).toBe(false) + expect(recorded.bodies.get(promptItemId)).toMatchObject({ + resolution: { state: 'cancelled' } + }) + expect( + [...recorded.bodies.values()].some( + (body) => readAgentJournalTurn(body)?.state === 'interrupted' + ) + ).toBe(false) + + connection.handlers.onMessage?.({ + type: 'result', + subtype: 'error_during_execution', + uuid: 'result-1', + session_id: PROVIDER_SESSION_ID, + is_error: true, + terminal_reason: 'aborted_tools', + errors: [], + duration_ms: 654 + }) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 654 + }) + + connection.handlers.onMessage?.({ + type: 'result', + subtype: 'success', + uuid: 'result-duplicate', + session_id: PROVIDER_SESSION_ID, + is_error: false, + terminal_reason: 'completed', + duration_ms: 999 + }) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 654 + }) + + expect(controller.signal.aborted).toBe(true) + expect(recorded.tombstones).toHaveLength(0) + }) + + it('does not synthesize terminal lifecycle for ordinary Stop', async () => { + const events: ClaudeStructuredSessionEvent[] = [] + const adapter = await acquired(fakeClaude({ replayUuid: 'turn-1' }), {}, events) + await startTurn(adapter) + + await expect( + adapter.cancelTurn({ sessionId: 'session-1', turnId: 'turn-1', fence: 7 }) + ).resolves.toEqual({ cancelled: true }) + expect(events.some((event) => event.type === 'prompt-cancelled')).toBe(false) + expect( + events.some((event) => event.type === 'message' && event.message.type === 'result') + ).toBe(false) + }) + + it('does not report success or release the claim when prompt lifecycle admission fails', async () => { + const controller = new AbortController() + const claude = fakeClaude({ + replayUuid: 'turn-1', + routes: { interrupt: () => controller.abort() } + }) + const recorded = lifecycleRecorder(false) + const adapter = adapterFor(claude) + await adapter.acquire({ + identity: identityFor(), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' }, + signal: controller.signal + }) + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + if (!promptItemId) { + throw new Error('expected durable Claude prompt') + } + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: promptItemId } + }) + ).rejects.toThrow(/lifecycle was not admitted/) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: promptItemId, + kind: 'approval', + optionId: 'allow', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + }) + + it('checks the bound item, turn, fence, and current acquisition without callback revival', async () => { + const claude = fakeClaude({ replayUuid: 'turn-1' }) + const adapter = await acquired(claude) + await startTurn(adapter) + const connection = claude.connections[0] + if (!connection) { + throw new Error('expected Claude connection') + } + const answered = invokeCanUseTool(connection, 'Bash', 'permission-1', 'tool-1', { + input: { command: 'git status' } + }) + adapter.bindPromptItemId('session-1', 'journal-prompt', 'permission-1') + + for (const input of [ + { turnId: 'turn-1', fence: 7, itemId: 'other-item' }, + { turnId: 'turn-2', fence: 7, itemId: 'journal-prompt' }, + { turnId: 'turn-1', fence: 6, itemId: 'journal-prompt' } + ]) { + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: input.turnId, + fence: input.fence, + prompt: { itemId: input.itemId } + }) + ).resolves.toEqual({ cancelled: false }) + } + expect(claude.connections[0]?.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + + await adapter.acquire({ identity: identityFor(), fence: 8, spawnToken: 'spawn-10' }) + await expect(answered.promise).resolves.toBeNull() + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 8, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'allow', + fence: 8, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + expect(claude.connections[1]?.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + }) + + it('rejects a grouped prompt batch without partially revising its first row', () => { + const tombstones: string[] = [] + const appendTombstone = vi.fn( + (identity: Parameters[0]) => { + tombstones.push(agentJournalItemKey(identity)) + } + ) + let rowAdmission = 0 + const tryAppendTombstone = vi.fn( + (identity: Parameters[0]) => { + rowAdmission += 1 + if (rowAdmission === 2) { + return { accepted: false as const, reason: 'backpressure' as const } + } + appendTombstone(identity) + return { accepted: true as const } + } + ) + const tryAppendLifecycleBatch = vi.fn( + ( + _settlementId: string, + mutations: Parameters< + NonNullable + >[1] + ) => { + expect(mutations[1]).toMatchObject({ + kind: 'item', + body: { resolution: { state: 'cancelled' } } + }) + return { accepted: false as const, reason: 'backpressure' as const } + } + ) + const prompts = new ClaudeJournalPrompts({ + sink: { + appendItem: () => {}, + appendTombstone, + tryAppendTombstone, + tryAppendLifecycleBatch, + publish: () => {} + }, + questionItems: (input) => { + const item = claudeQuestionItems(input)[0] + return item + ? [ + { + ...item, + identity: { provider: 'orca', clientMessageId: 'group:first' } + }, + { + ...item, + identity: { provider: 'orca', clientMessageId: 'group:second' } + } + ] + : [] + } + }) + const prompt: ClaudePendingPrompt = { + requestId: 'grouped-request', + promptKey: 'grouped-request', + toolUseId: 'tool-grouped', + toolName: 'AskUserQuestion', + kind: 'question', + input: { + questions: [ + { question: 'First?', options: [{ label: 'Yes' }] }, + { question: 'Second?', options: [{ label: 'No' }] } + ] + }, + suggestions: [], + questionIds: ['First?', 'Second?'], + answers: new Map(), + settle: vi.fn() + } + prompts.handle({ type: 'prompt', sessionId: 'session-1', prompt }) + + expect(prompts.cancel(prompt.promptKey)).toEqual({ + accepted: false, + reason: 'backpressure' + }) + expect(tryAppendLifecycleBatch).toHaveBeenCalledOnce() + expect(tryAppendTombstone).not.toHaveBeenCalled() + expect(tombstones).toEqual([]) + }) + + it('keeps every backpressured prompt cancellation retry in its owned entry', () => { + let backpressured = true + let lifecycleAttempts = 0 + const prompts = new ClaudeJournalPrompts({ + sink: { + appendItem: () => {}, + appendTombstone: () => {}, + publish: () => {}, + tryAppendLifecycleBatch: () => { + lifecycleAttempts += 1 + return backpressured ? { accepted: false, reason: 'backpressure' } : { accepted: true } + } + } + }) + const registerCancellation = (index: number): void => { + const promptKey = `permission-${index}` + const prompt: ClaudePendingPrompt = { + requestId: promptKey, + promptKey, + toolUseId: `tool-${index}`, + toolName: 'Bash', + kind: 'approval', + input: { command: 'git status' }, + suggestions: [], + questionIds: [], + answers: new Map(), + settle: vi.fn() + } + prompts.handle({ type: 'prompt', sessionId: 'session-1', prompt }) + prompts.cancel(promptKey) + } + + registerCancellation(0) + prompts.cancel('permission-0') + expect(prompts.pendingCancellationCount).toBe(1) + for (let index = 1; index < 65; index += 1) { + registerCancellation(index) + } + expect(prompts.pendingCancellationCount).toBe(65) + + backpressured = false + const attemptsBeforeRecovery = lifecycleAttempts + prompts.retryPendingCancellations() + expect(lifecycleAttempts - attemptsBeforeRecovery).toBe(65) + expect(prompts.pendingCancellationCount).toBe(0) + expect(prompts.size).toBe(0) + const attemptsAfterRecovery = lifecycleAttempts + prompts.retryPendingCancellations() + expect(lifecycleAttempts).toBe(attemptsAfterRecovery) + + backpressured = true + registerCancellation(65) + expect(prompts.pendingCancellationCount).toBe(1) + prompts.resolve('permission-65') + expect(prompts.pendingCancellationCount).toBe(0) + registerCancellation(66) + prompts.clear() + expect(prompts.pendingCancellationCount).toBe(0) + expect(prompts.size).toBe(0) + }) +}) diff --git a/src/main/claude/claude-structured-prompt-ownership.ts b/src/main/claude/claude-structured-prompt-ownership.ts new file mode 100644 index 00000000000..1dd73f23552 --- /dev/null +++ b/src/main/claude/claude-structured-prompt-ownership.ts @@ -0,0 +1,143 @@ +import { + AgentSessionPromptUnavailableError, + type StructuredAgentSessionAdapter +} from '../native-chat/agent-session-wire/structured-agent-session-adapter' +import type { StructuredSessionCompaction } from '../native-chat/agent-session-wire/structured-session-compaction' +import { CLAUDE_DEFAULT_REQUEST_TIMEOUT_MS } from './claude-agent-sdk-control-requests' +import { + answerClaudePrompt, + cancelClaudeTurn, + supportsClaudeQueuedInterruptCancellation +} from './claude-structured-control-actions' +import type { ClaudeLateDispatchSettlement } from './claude-structured-dispatch' +import type { ClaudeSession } from './claude-structured-session-state' + +type CancelInput = Parameters[0] +type AnswerInput = Parameters[0] + +export function admitClaudePromptCancellation(session: ClaudeSession, promptKey: string): boolean { + const admission = session.translator?.journalPrompts.cancel(promptKey) + return admission?.accepted ?? true +} + +function waitForClaudePromptCancellation( + observed: Promise, + timeoutMs = CLAUDE_DEFAULT_REQUEST_TIMEOUT_MS +): Promise { + let timer: ReturnType | null = null + const deadline = new Promise((_resolve, reject) => { + timer = setTimeout( + () => reject(new Error('Claude prompt cancellation abort was not observed')), + timeoutMs + ) + timer.unref?.() + }) + return Promise.race([observed, deadline]).finally(() => { + if (timer) { + clearTimeout(timer) + } + }) +} + +function requireSession(sessions: Map, sessionId: string): ClaudeSession { + const session = sessions.get(sessionId) + if (!session) { + throw new Error(`no live claude stream-json session for ${sessionId}`) + } + return session +} + +export async function cancelClaudeStructuredTurn(input: { + request: CancelInput + sessions: Map + compactions: StructuredSessionCompaction + timeoutMs?: number + admitPromptCancellation: (session: ClaudeSession, promptKey: string) => boolean + onDispatchSettledLate?: ClaudeLateDispatchSettlement +}): Promise<{ cancelled: boolean }> { + const { request, sessions, compactions, timeoutMs } = input + const session = requireSession(sessions, request.sessionId) + const acquisitionGeneration = session.acquisitionGeneration + const prompt = request.prompt + if (prompt && session.fence !== request.fence) { + return { cancelled: false } + } + const claim = prompt ? session.prompts.claimBound(prompt.itemId, request.turnId) : null + if (prompt && !claim) { + return { cancelled: false } + } + const cancellationObserved = claim ? session.prompts.observeCancellation(claim) : null + if (claim && !cancellationObserved) { + session.prompts.releaseClaim(claim) + return { cancelled: false } + } + const isCurrent = (): boolean => + sessions.get(request.sessionId) === session && + session.fence === request.fence && + session.acquisitionGeneration === acquisitionGeneration && + (claim && prompt + ? session.activeTurnId === request.turnId && + session.prompts.ownsBoundClaim(claim, prompt.itemId, request.turnId) && + (session.activeTurnSequence === session.dispatchSequence || + supportsClaudeQueuedInterruptCancellation(session)) + : compactions.ownsTurn(request.sessionId, request.turnId) || + (session.activeTurnId === undefined + ? session.dispatchSequence === 0 + : session.activeTurnId === request.turnId && + session.activeTurnSequence === session.dispatchSequence)) + let interruptConfirmed = false + try { + const result = await cancelClaudeTurn( + session, + timeoutMs, + isCurrent, + input.onDispatchSettledLate + ) + if (result.cancelled && claim && cancellationObserved) { + interruptConfirmed = true + await waitForClaudePromptCancellation(cancellationObserved, timeoutMs) + if (!input.admitPromptCancellation(session, claim.found.prompt.promptKey)) { + throw new Error(`Claude prompt cancellation lifecycle was not admitted for ${claim.itemId}`) + } + } else if (claim) { + session.prompts.releaseClaim(claim) + } + return result + } catch (error) { + if (claim && !interruptConfirmed) { + session.prompts.releaseClaim(claim) + } + throw error + } +} + +export async function answerClaudeStructuredPrompt(input: { + request: AnswerInput + sessions: Map +}): Promise { + const { request, sessions } = input + const session = sessions.get(request.sessionId) + if (!session || session.fence !== request.fence) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + const acquisitionGeneration = session.acquisitionGeneration + const claim = session.prompts.claim(request.itemId, request.kind) + if (!claim) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + try { + await request.commit() + if ( + sessions.get(request.sessionId) !== session || + session.fence !== request.fence || + session.acquisitionGeneration !== acquisitionGeneration || + !session.prompts.ownsClaim(claim) + ) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + await answerClaudePrompt(session, claim, request.optionId) + } catch (error) { + session.prompts.releaseClaim(claim) + throw error + } +} diff --git a/src/main/claude/claude-structured-prompt-replies.ts b/src/main/claude/claude-structured-prompt-replies.ts index deec74b7308..5a6bc19b9a8 100644 --- a/src/main/claude/claude-structured-prompt-replies.ts +++ b/src/main/claude/claude-structured-prompt-replies.ts @@ -1,48 +1,24 @@ +import type { PermissionResult } from '@anthropic-ai/claude-agent-sdk' import { decodeAgentSessionQuestionAnswers } from '../../shared/agent-session-question-answer' +import { + claudePromptQuestions, + isClaudePromptRecord, + readClaudePromptString, + type ClaudePendingPrompt +} from './claude-prompt-registry' +export { + ClaudePromptRegistry, + type ClaudePendingPrompt, + type ClaudePromptClaim, + type ClaudePromptRegistration, + type ClaudePromptSettle +} from './claude-prompt-registry' export const CLAUDE_APPROVAL_DECISIONS = ['allow', 'allowForSession', 'deny', 'cancel'] as const export type ClaudeApprovalDecision = (typeof CLAUDE_APPROVAL_DECISIONS)[number] -/** Settles the SDK's `canUseTool` promise; `null` is the SDK's "no response written" sentinel. */ -export type ClaudePromptSettle = (response: Record | null) => void - -export type ClaudePendingPrompt = { - requestId: string - promptKey: string - toolUseId: string - toolName: string - kind: 'approval' | 'question' - input: Record - suggestions: unknown[] - questionIds: readonly string[] - answers: Map - settle: ClaudePromptSettle -} - -export type ClaudePromptRegistration = { - requestId: string - toolName: string - toolUseId: string - input: Record - suggestions: unknown[] - settle: ClaudePromptSettle -} - -type PromptBinding = { - address: string - questionId?: string -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -function readString(value: unknown): string | null { - return typeof value === 'string' && value.trim().length > 0 ? value : null -} - -function questionsFrom(input: Record): Record[] { - return Array.isArray(input.questions) ? input.questions.filter(isRecord) : [] +function isClaudeApprovalDecision(optionId: string): optionId is ClaudeApprovalDecision { + return CLAUDE_APPROVAL_DECISIONS.some((decision) => decision === optionId) } function questionIdFromAddress(prompt: ClaudePendingPrompt, address: string): string | null { @@ -62,10 +38,10 @@ function questionAnswer(prompt: ClaudePendingPrompt, questionId: string, optionI } const choice = /^choice-([1-9]\d*)$/.exec(decoded.answer) const optionIndex = choice ? Number(choice[1]) - 1 : -1 - const question = questionsFrom(prompt.input)[questionIndex] + const question = claudePromptQuestions(prompt.input)[questionIndex] const options = Array.isArray(question?.options) ? question.options : [] const option = options[optionIndex] - const label = isRecord(option) ? readString(option.label) : null + const label = isClaudePromptRecord(option) ? readClaudePromptString(option.label) : null if (decoded.questionId === `q${questionIndex + 1}` && label) { return label } @@ -73,17 +49,14 @@ function questionAnswer(prompt: ClaudePendingPrompt, questionId: string, optionI return decoded.answer } const legacyChoice = options.some( - (candidate) => isRecord(candidate) && readString(candidate.label) === decoded.answer + (candidate) => + isClaudePromptRecord(candidate) && readClaudePromptString(candidate.label) === decoded.answer ) return decoded.questionId === questionId && (legacyChoice || decoded.answer.trim().length > 0) ? decoded.answer : optionId } -function questionId(question: Record, index: number): string { - return readString(question.question) ?? readString(question.header) ?? `question-${index + 1}` -} - export function encodeClaudeQuestionOptionId(questionId: string, answer: string): string { return `${encodeURIComponent(questionId)}:${encodeURIComponent(answer)}` } @@ -105,88 +78,11 @@ export function decodeClaudeQuestionOptionId( } } -export class ClaudePromptRegistry { - private readonly prompts = new Map() - private readonly journalBindings = new Map() - - register(registration: ClaudePromptRegistration): ClaudePendingPrompt | null { - const toolUseId = readString(registration.toolUseId) - const toolName = readString(registration.toolName) - const input = isRecord(registration.input) ? registration.input : null - if (!toolUseId || !toolName || !input) { - return null - } - const questions = toolName === 'AskUserQuestion' ? questionsFrom(input) : [] - const prompt: ClaudePendingPrompt = { - requestId: registration.requestId, - promptKey: registration.requestId, - toolUseId, - toolName, - kind: questions.length > 0 ? 'question' : 'approval', - input, - suggestions: Array.isArray(registration.suggestions) ? registration.suggestions : [], - questionIds: questions.map(questionId), - answers: new Map(), - settle: registration.settle - } - this.prompts.set(prompt.promptKey, prompt) - return prompt - } - - /** True only if the prompt was still pending; lets an abort and an answer race settle once. */ - forgetIfPending(prompt: ClaudePendingPrompt): boolean { - if (!this.prompts.has(prompt.promptKey)) { - return false - } - this.forget(prompt) - return true - } - - bindJournalItemId(journalItemId: string, promptKey: string, questionIdForItem?: string): void { - this.journalBindings.set(journalItemId, { - address: promptKey, - ...(questionIdForItem ? { questionId: questionIdForItem } : {}) - }) - } - - find(itemId: string): { prompt: ClaudePendingPrompt; questionId?: string } | null { - const binding = this.journalBindings.get(itemId) - const prompt = this.prompts.get(binding?.address ?? itemId) - return prompt - ? { prompt, ...(binding?.questionId ? { questionId: binding.questionId } : {}) } - : null - } - - cancel(requestId: string): ClaudePendingPrompt | null { - const prompt = this.prompts.get(requestId) ?? null - if (prompt) { - this.forget(prompt) - } - return prompt - } - - forget(prompt: ClaudePendingPrompt): void { - this.prompts.delete(prompt.promptKey) - for (const [itemId, binding] of this.journalBindings) { - if (binding.address === prompt.promptKey) { - this.journalBindings.delete(itemId) - } - } - } - - clear(): ClaudePendingPrompt[] { - const pending = [...this.prompts.values()] - this.prompts.clear() - this.journalBindings.clear() - return pending - } -} - -function approvalResponse(prompt: ClaudePendingPrompt, optionId: string): Record { - if (!(CLAUDE_APPROVAL_DECISIONS as readonly string[]).includes(optionId)) { +function approvalResponse(prompt: ClaudePendingPrompt, optionId: string): PermissionResult { + if (!isClaudeApprovalDecision(optionId)) { throw new Error(`${optionId} is not a Claude approval decision`) } - const decision = optionId as ClaudeApprovalDecision + const decision = optionId if (decision === 'allow' || decision === 'allowForSession') { return { behavior: 'allow', @@ -209,7 +105,7 @@ function questionResponse( prompt: ClaudePendingPrompt, optionId: string, boundQuestionId?: string -): Record | null { +): PermissionResult | null { const decoded = decodeClaudeQuestionOptionId(optionId) const decodedQuestionId = decoded ? (questionIdFromAddress(prompt, decoded.questionId) ?? @@ -229,7 +125,11 @@ function questionResponse( } const answers: Record = {} for (const id of prompt.questionIds) { - answers[id] = prompt.answers.get(id) as string + const answer = prompt.answers.get(id) + if (answer === undefined) { + return null + } + answers[id] = answer } return { behavior: 'allow', @@ -241,21 +141,21 @@ function questionResponse( function groupedQuestionResponse( prompt: ClaudePendingPrompt, optionId: string -): Record | null { +): PermissionResult | null { const grouped = decodeAgentSessionQuestionAnswers(optionId) if (!grouped) { return null } - const questions = questionsFrom(prompt.input) + const questions = claudePromptQuestions(prompt.input) if (grouped.length !== prompt.questionIds.length) { throw new Error(`Grouped answer does not match Claude prompt ${prompt.promptKey}`) } const answers: Record = {} for (let index = 0; index < questions.length; index += 1) { - const question = questions[index]! + const question = questions[index] const providerQuestionId = prompt.questionIds[index] const answer = grouped.find((entry) => entry.questionId === `q${index + 1}`) - if (!providerQuestionId || !answer) { + if (!question || !providerQuestionId || !answer) { throw new Error(`Grouped answer does not name question ${index + 1}`) } const selected = answer.optionIds.map((selectedId) => @@ -286,7 +186,7 @@ function groupedQuestionResponse( export function applyClaudePromptAnswer( found: { prompt: ClaudePendingPrompt; questionId?: string }, optionId: string -): Record | null { +): PermissionResult | null { if (found.prompt.kind === 'approval') { return approvalResponse(found.prompt, optionId) } diff --git a/src/main/claude/claude-structured-session-acquisition.ts b/src/main/claude/claude-structured-session-acquisition.ts index b1b1370398a..eb7bc9b7251 100644 --- a/src/main/claude/claude-structured-session-acquisition.ts +++ b/src/main/claude/claude-structured-session-acquisition.ts @@ -138,6 +138,7 @@ export async function acquireClaudeSession({ const { canUseTool, onUserDialog } = buildClaudePermissionCallbacks({ sessionId, prompts, + currentTurnId: () => liveSession?.activeTurnId ?? null, emit: (event) => callbacks.deliver(attempt, sessionId, () => callbacks.emit(liveSession, input.events, event)) }) diff --git a/src/main/claude/claude-structured-session-adapter.test.ts b/src/main/claude/claude-structured-session-adapter.test.ts index 20be62f7997..5593b07d329 100644 --- a/src/main/claude/claude-structured-session-adapter.test.ts +++ b/src/main/claude/claude-structured-session-adapter.test.ts @@ -676,7 +676,8 @@ describe('ClaudeStructuredSessionAdapter prompts', () => { itemId: 'journal-approval', kind: 'approval', optionId: 'allowForSession', - fence: 7 + fence: 7, + commit: async () => undefined }) // The answer resolves the SDK's own callback promise; the SDK writes the wire response. await expect(answered.promise).resolves.toEqual({ @@ -712,7 +713,8 @@ describe('ClaudeStructuredSessionAdapter prompts', () => { itemId: 'journal-q1', kind: 'question', optionId: encodeClaudeQuestionOptionId('Library?', 'Luxon'), - fence: 7 + fence: 7, + commit: async () => undefined }) await tick() expect(answered.settled()).toBe(false) @@ -721,7 +723,8 @@ describe('ClaudeStructuredSessionAdapter prompts', () => { itemId: 'journal-q2', kind: 'question', optionId: encodeClaudeQuestionOptionId('Ship now?', 'Yes'), - fence: 7 + fence: 7, + commit: async () => undefined }) await expect(answered.promise).resolves.toMatchObject({ behavior: 'allow', @@ -752,7 +755,8 @@ describe('ClaudeStructuredSessionAdapter prompts', () => { itemId: 'journal-9', kind: 'approval', optionId: 'allow', - fence: 7 + fence: 7, + commit: async () => undefined }) ).rejects.toThrow(/no longer waiting/) }) diff --git a/src/main/claude/claude-structured-session-adapter.ts b/src/main/claude/claude-structured-session-adapter.ts index 44d73c8bd0f..f62541b50e9 100644 --- a/src/main/claude/claude-structured-session-adapter.ts +++ b/src/main/claude/claude-structured-session-adapter.ts @@ -4,11 +4,7 @@ import type { StructuredAgentSessionAcquireInput, StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' -import { - answerClaudePrompt, - cancelClaudeTurn, - stopClaudeBackgroundTasks -} from './claude-structured-control-actions' +import { stopClaudeBackgroundTasks } from './claude-structured-control-actions' import { dispatchClaudeTurn } from './claude-structured-dispatch' import { StructuredSessionCompaction } from '../native-chat/agent-session-wire/structured-session-compaction' import { releaseClaudeAcquisition } from './claude-structured-acquisition-release' @@ -33,6 +29,11 @@ import { import { readClaudeTranscriptLeafWithReproof } from './claude-transcript-branch-proof' import type { AgentSessionBackgroundTaskState } from '../../shared/agent-session-wire' import { resolveClaudeProviderHistoryWindow } from './claude-structured-history-window' +import { + admitClaudePromptCancellation, + answerClaudeStructuredPrompt, + cancelClaudeStructuredTurn +} from './claude-structured-prompt-ownership' export type { ClaudeStructuredLaunch } from './claude-structured-launch-resolution' export type { @@ -226,7 +227,13 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda promptKey: string, questionId?: string ): void { - this.sessions.get(sessionId)?.prompts.bindJournalItemId(journalItemId, promptKey, questionId) + const session = this.sessions.get(sessionId) + session?.prompts.bindJournalItemId( + journalItemId, + promptKey, + questionId, + session.activeTurnId ?? null + ) } dispatch: StructuredAgentSessionAdapter['dispatch'] = (input) => @@ -235,25 +242,17 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda compact: NonNullable = (input) => compactClaudeSession(this.session(input.sessionId), this.compactions, input) - cancelTurn: StructuredAgentSessionAdapter['cancelTurn'] = (input) => { - const session = this.session(input.sessionId) - const acquisitionGeneration = session.acquisitionGeneration - return cancelClaudeTurn(session, this.deps.requestTimeoutMs, () => { - // Keep every ownership check adjacent to the provider interrupt. The - // session map check fences a replaced child; the turn check fences a - // delayed cancel after a newer turn was admitted on the same child. - return ( - this.sessions.get(input.sessionId) === session && - session.fence === input.fence && - session.acquisitionGeneration === acquisitionGeneration && - (this.compactions.ownsTurn(input.sessionId, input.turnId) || - (session.activeTurnId === undefined - ? session.dispatchSequence === 0 - : session.activeTurnId === input.turnId && - session.activeTurnSequence === session.dispatchSequence)) - ) + cancelTurn: StructuredAgentSessionAdapter['cancelTurn'] = (request) => + cancelClaudeStructuredTurn({ + request, + sessions: this.sessions, + compactions: this.compactions, + admitPromptCancellation: (session, promptKey) => + admitClaudePromptCancellation(session, promptKey), + onDispatchSettledLate: (settlement) => + this.deps.onDispatchSettledLate?.({ sessionId: request.sessionId, ...settlement }), + ...(this.deps.requestTimeoutMs === undefined ? {} : { timeoutMs: this.deps.requestTimeoutMs }) }) - } stopBackgroundTasks: StructuredAgentSessionAdapter['stopBackgroundTasks'] = (input) => { const session = this.session(input.sessionId) const acquisitionGeneration = session.acquisitionGeneration @@ -278,8 +277,8 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda } readCommands: NonNullable = (sessionId) => this.sessions.get(sessionId)?.commands.commands - answerPrompt: StructuredAgentSessionAdapter['answerPrompt'] = (input) => - answerClaudePrompt(this.session(input.sessionId), input) + answerPrompt: StructuredAgentSessionAdapter['answerPrompt'] = (request) => + answerClaudeStructuredPrompt({ request, sessions: this.sessions }) setOption: StructuredAgentSessionAdapter['setOption'] = (input) => setClaudeStructuredOption(this.session(input.sessionId), input, this.deps.requestTimeoutMs) readOptions = (input: { sessionId: string; fence: number }) => diff --git a/src/main/claude/claude-structured-session-state.ts b/src/main/claude/claude-structured-session-state.ts index 1fbdcca42c2..30e830af64e 100644 --- a/src/main/claude/claude-structured-session-state.ts +++ b/src/main/claude/claude-structured-session-state.ts @@ -62,17 +62,20 @@ export type ClaudeStructuredSessionEvent = observedAt?: number } +export type ClaudeLateDispatchOutcome = + | { + clientMessageId: string + providerIdentity: AgentJournalItemIdentity + } + | { clientMessageId: string; state: 'rejected'; reason: string } + export type ClaudeStructuredSessionAdapterDeps = { resolveLaunch: (input: { identity: AgentSessionJournalIdentity }) => Promise onEvent?: (event: ClaudeStructuredSessionEvent) => void - /** Direct settlement path for a provider replay; its durable item row also reconciles delivery. */ - onDispatchSettledLate?: (input: { - sessionId: string - clientMessageId: string - providerIdentity: AgentJournalItemIdentity - }) => void + /** Direct settlement path for provider-proven late dispatch outcomes. */ + onDispatchSettledLate?: (input: { sessionId: string } & ClaudeLateDispatchOutcome) => void onBackgroundTasksChanged?: ( sessionId: string, state: AgentSessionBackgroundTaskState | null diff --git a/src/main/codex/codex-prompt-registry-bounds.ts b/src/main/codex/codex-prompt-registry-bounds.ts index e5f79fe3a31..9c97bf63cd9 100644 --- a/src/main/codex/codex-prompt-registry-bounds.ts +++ b/src/main/codex/codex-prompt-registry-bounds.ts @@ -2,6 +2,7 @@ import { boundPayload, digestPayload } from '../native-chat/agent-session-journal/journal-payload-bounds' +import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire' export const CODEX_JOURNAL_PROMPT_ID_COMPONENT_MAX_BYTES = 256 export const CODEX_JOURNAL_PROMPT_OPTION_ID_MAX_BYTES = 1024 @@ -13,6 +14,53 @@ export const CODEX_PROMPT_MAX_ANSWER_BYTES = 64 * 1024 export const MAX_CODEX_PROMPT_REGISTRY_ENTRIES = 128 export const MAX_CODEX_PROMPT_JOURNAL_BINDINGS = 256 export const MAX_CODEX_PROMPT_REGISTRY_BYTES = 4 * 1024 * 1024 +const CODEX_PROMPT_TURN_ID_RESERVED_BYTES = AGENT_SESSION_ID_MAX_LENGTH * 3 + +type CodexPromptRegistryEntryBounds = { + threadId: string + turnId: string | null + turnIdDigest?: string + codexItemId: string + promptKey: string + questionIds: readonly string[] + optionAnswers: ReadonlyMap + answers: ReadonlyMap +} + +export function codexPromptRegistryEntryBytes(prompt: CodexPromptRegistryEntryBounds): number { + let bytes = 0 + for (const value of [prompt.threadId, prompt.codexItemId, prompt.promptKey]) { + bytes += Buffer.byteLength(value, 'utf8') + } + const turnId = prompt.turnId ?? prompt.turnIdDigest + bytes += turnId ? Buffer.byteLength(turnId, 'utf8') : CODEX_PROMPT_TURN_ID_RESERVED_BYTES + for (const id of prompt.questionIds) { + bytes += Buffer.byteLength(id, 'utf8') + } + for (const entry of prompt.optionAnswers.values()) { + bytes += Buffer.byteLength(entry.questionId, 'utf8') + Buffer.byteLength(entry.answer, 'utf8') + } + for (const value of prompt.answers.values()) { + bytes += Buffer.byteLength(value, 'utf8') + } + return bytes +} + +export function codexPromptTurnIdentity(turnId: string): { + turnId: string | null + turnIdDigest?: string +} { + return turnId.length <= AGENT_SESSION_ID_MAX_LENGTH + ? { turnId } + : { turnId: null, turnIdDigest: digestPayload(turnId) } +} + +export function codexPromptMatchesTurn( + prompt: Pick, + turnId: string +): boolean { + return prompt.turnId === turnId || prompt.turnIdDigest === digestPayload(turnId) +} export function codexJournalPromptIdPart(value: string): string { if (Buffer.byteLength(value, 'utf8') <= CODEX_JOURNAL_PROMPT_ID_COMPONENT_MAX_BYTES) { diff --git a/src/main/codex/codex-prompt-registry.ts b/src/main/codex/codex-prompt-registry.ts new file mode 100644 index 00000000000..c6d0d7f4bef --- /dev/null +++ b/src/main/codex/codex-prompt-registry.ts @@ -0,0 +1,278 @@ +import { + MAX_CODEX_PROMPT_JOURNAL_BINDINGS, + MAX_CODEX_PROMPT_REGISTRY_BYTES, + MAX_CODEX_PROMPT_REGISTRY_ENTRIES, + codexJournalPromptIdPart, + codexPromptMatchesTurn, + codexPromptRegistryEntryBytes, + codexPromptTurnIdentity, + readQuestionIds, + readQuestionOptionAnswers +} from './codex-prompt-registry-bounds' + +export const CODEX_COMMAND_APPROVAL_METHOD = 'item/commandExecution/requestApproval' +export const CODEX_FILE_CHANGE_APPROVAL_METHOD = 'item/fileChange/requestApproval' +export const CODEX_USER_INPUT_METHOD = 'item/tool/requestUserInput' + +export type CodexPendingPrompt = { + requestId: number | string + method: string + threadId: string + turnId: string | null + /** Oversized compatibility turn ids stay comparable without escaping the registry byte cap. */ + turnIdDigest?: string + codexItemId: string + /** One tool item can ask more than once, so approvalId wins over itemId when present. */ + promptKey: string + questionIds: readonly string[] + questionIdAliases: ReadonlyMap + optionAnswers: ReadonlyMap + answers: Map +} + +export type CodexPromptClaim = { + readonly itemId: string + readonly prompt: CodexPendingPrompt +} + +function readString(params: unknown, key: string): string | null { + if (typeof params !== 'object' || params === null) { + return null + } + const value = Reflect.get(params, key) + return typeof value === 'string' && value.length > 0 ? value : null +} + +export function isCodexPromptMethod(method: string): boolean { + return ( + method === CODEX_COMMAND_APPROVAL_METHOD || + method === CODEX_FILE_CHANGE_APPROVAL_METHOD || + method === CODEX_USER_INPUT_METHOD + ) +} + +/** Session-local callback ownership; none of this state is reconstructed from the journal. */ +export class CodexPromptRegistry { + private readonly byAddress = new Map() + private readonly journalItemIds = new Map() + private readonly boundPrompts = new Map() + private readonly claims = new Map() + + get sizes(): { prompts: number; journalBindings: number } { + return { prompts: this.byAddress.size, journalBindings: this.journalItemIds.size } + } + + get bytes(): number { + return this.retainedPromptBytes() + } + + register(request: { + id: number | string + method: string + params: unknown + }): CodexPendingPrompt | null { + const codexItemId = readString(request.params, 'itemId') + const threadId = readString(request.params, 'threadId') + if (!isCodexPromptMethod(request.method) || !codexItemId || !threadId) { + return null + } + const questionIds = + request.method === CODEX_USER_INPUT_METHOD ? readQuestionIds(request.params) : [] + if (questionIds === null) { + return null + } + const optionAnswers = + request.method === CODEX_USER_INPUT_METHOD + ? readQuestionOptionAnswers(request.params) + : new Map() + if (optionAnswers === null) { + return null + } + const turnId = readString(request.params, 'turnId') + const turnIdentity = turnId ? codexPromptTurnIdentity(turnId) : { turnId: null } + if (turnId && turnIdentity.turnId === null) { + return null + } + const prompt: CodexPendingPrompt = { + requestId: request.id, + method: request.method, + threadId, + ...turnIdentity, + codexItemId, + promptKey: readString(request.params, 'approvalId') ?? codexItemId, + questionIds, + questionIdAliases: + request.method === CODEX_USER_INPUT_METHOD + ? new Map(questionIds.map((id) => [codexJournalPromptIdPart(id), id])) + : new Map(), + optionAnswers, + answers: new Map() + } + const promptBytes = codexPromptRegistryEntryBytes(prompt) + if (promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES) { + return null + } + while ( + this.retainedPromptBytes() + promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES && + this.byAddress.size > 0 + ) { + const oldest = this.byAddress.values().next().value + if (!oldest) { + break + } + this.byAddress.delete(this.address(oldest.threadId, oldest.promptKey)) + } + if (this.retainedPromptBytes() + promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES) { + return null + } + const address = this.address(prompt.threadId, prompt.promptKey) + this.byAddress.delete(address) + this.byAddress.set(address, prompt) + this.trim() + return prompt + } + + bindJournalItemId( + journalItemId: string, + threadId: string, + promptKey: string, + turnId?: string | null + ): void { + if (this.journalItemIds.has(journalItemId)) { + this.boundPrompts.delete(journalItemId) + } + this.journalItemIds.delete(journalItemId) + const address = this.address(threadId, promptKey) + const prompt = this.byAddress.get(address) + if (!prompt) { + return + } + if (prompt.turnId === null && prompt.turnIdDigest === undefined && turnId) { + Object.assign(prompt, codexPromptTurnIdentity(turnId)) + } + this.journalItemIds.set(journalItemId, address) + this.boundPrompts.set(journalItemId, prompt) + this.trim() + } + + find(journalItemId: string): CodexPendingPrompt | null { + const address = this.journalItemIds.get(journalItemId) + if (address) { + return this.boundPrompts.get(journalItemId) ?? this.byAddress.get(address) ?? null + } + const matches = [...this.byAddress.values()].filter( + (prompt) => prompt.promptKey === journalItemId + ) + return matches.length === 1 ? (matches[0] ?? null) : null + } + + claim(journalItemId: string, kind?: 'approval' | 'question'): CodexPromptClaim | null { + const prompt = this.find(journalItemId) + if (!prompt || this.claims.has(prompt) || (kind && this.kind(prompt) !== kind)) { + return null + } + const claim = { itemId: journalItemId, prompt } + this.claims.set(prompt, claim) + return claim + } + + claimBound(journalItemId: string): CodexPromptClaim | null { + const prompt = this.boundPrompts.get(journalItemId) + if (!prompt || this.claims.has(prompt)) { + return null + } + const claim = { itemId: journalItemId, prompt } + this.claims.set(prompt, claim) + return claim + } + + ownsClaim(claim: CodexPromptClaim): boolean { + return this.claims.get(claim.prompt) === claim && this.find(claim.itemId) === claim.prompt + } + + ownsBoundClaim( + claim: CodexPromptClaim, + journalItemId: string, + threadId: string, + turnId: string + ): boolean { + return ( + claim.itemId === journalItemId && + this.claims.get(claim.prompt) === claim && + this.journalItemIds.get(journalItemId) === + this.address(claim.prompt.threadId, claim.prompt.promptKey) && + this.boundPrompts.get(journalItemId) === claim.prompt && + claim.prompt.threadId === threadId && + codexPromptMatchesTurn(claim.prompt, turnId) + ) + } + + releaseClaim(claim: CodexPromptClaim): void { + if (this.claims.get(claim.prompt) === claim) { + this.claims.delete(claim.prompt) + } + } + + forget(prompt: CodexPendingPrompt): void { + this.claims.delete(prompt) + const address = this.address(prompt.threadId, prompt.promptKey) + if (this.byAddress.get(address) === prompt) { + this.byAddress.delete(address) + } + for (const [journalItemId, boundPrompt] of this.boundPrompts) { + if (boundPrompt === prompt) { + this.journalItemIds.delete(journalItemId) + this.boundPrompts.delete(journalItemId) + } + } + } + + clearTurn(threadId: string, turnId: string): void { + const prompts = new Set( + [...this.byAddress.values(), ...this.boundPrompts.values()].filter( + (prompt) => prompt.threadId === threadId && codexPromptMatchesTurn(prompt, turnId) + ) + ) + for (const prompt of prompts) { + this.forget(prompt) + } + } + + clear(): void { + this.byAddress.clear() + this.journalItemIds.clear() + this.boundPrompts.clear() + this.claims.clear() + } + + private address(threadId: string, promptKey: string): string { + return `${encodeURIComponent(threadId)}:${encodeURIComponent(promptKey)}` + } + + private kind(prompt: CodexPendingPrompt): 'approval' | 'question' { + return prompt.method === CODEX_USER_INPUT_METHOD ? 'question' : 'approval' + } + + private retainedPromptBytes(): number { + const prompts = new Set([...this.byAddress.values(), ...this.boundPrompts.values()]) + return [...prompts].reduce((total, prompt) => total + codexPromptRegistryEntryBytes(prompt), 0) + } + + private trim(): void { + while (this.byAddress.size > MAX_CODEX_PROMPT_REGISTRY_ENTRIES) { + const oldest = this.byAddress.values().next().value + if (!oldest) { + break + } + this.byAddress.delete(this.address(oldest.threadId, oldest.promptKey)) + } + while (this.journalItemIds.size > MAX_CODEX_PROMPT_JOURNAL_BINDINGS) { + const oldest = this.journalItemIds.keys().next().value + if (!oldest) { + break + } + this.journalItemIds.delete(oldest) + this.boundPrompts.delete(oldest) + } + } +} diff --git a/src/main/codex/codex-requested-close-turn-timing.test.ts b/src/main/codex/codex-requested-close-turn-timing.test.ts index 29039ffca3c..fbf50badaef 100644 --- a/src/main/codex/codex-requested-close-turn-timing.test.ts +++ b/src/main/codex/codex-requested-close-turn-timing.test.ts @@ -1,8 +1,10 @@ +import { createCodexDispatchEchoes } from './codex-structured-dispatch-echo' import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentJournalItemBody } from '../../shared/agent-session-journal-types' import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' import { CodexBackgroundTaskTracker } from './codex-background-task-tracker' import { createCodexJournalTranslator } from './codex-structured-journal-translation' +import { CodexPromptRegistry } from './codex-structured-prompt-replies' import { closeCodexPublishedSession } from './codex-structured-session-close' import type { CodexSession } from './codex-structured-session-state' @@ -48,17 +50,30 @@ describe('requested-close durable turn timing', () => { observedAt: 1_000 }) ).toEqual({ accepted: true }) - const session = { - connection: { close: vi.fn(async () => true) }, + const session: CodexSession = { + connection: { + pid: 4321, + closed: false, + request: async () => ({}), + notify: () => {}, + respond: () => {}, + respondWithError: () => {}, + close: async () => true + }, backgroundTasks: new CodexBackgroundTaskTracker('thread-1'), ended: false, requestedClose: false, fence: 7, acquisitionGeneration: 'generation-1', threadId: 'thread-1', - prompts: { clear: vi.fn() }, + historyPath: null, + prompts: new CodexPromptRegistry(), + options: new Map(), + reportedOptions: {}, + fastModeTierByModel: new Map(), + dispatchEchoes: createCodexDispatchEchoes(), translator - } as unknown as CodexSession + } const sessions = new Map([['session-1', session]]) const onEvent = vi.fn() diff --git a/src/main/codex/codex-structured-dispatch-admission.test.ts b/src/main/codex/codex-structured-dispatch-admission.test.ts new file mode 100644 index 00000000000..3eece95c3fe --- /dev/null +++ b/src/main/codex/codex-structured-dispatch-admission.test.ts @@ -0,0 +1,215 @@ +import { describe, expect, it } from 'vitest' +import { MAX_CODEX_PENDING_DISPATCH_ECHOES } from './codex-structured-dispatch-echo' +import { + acquiredCodexAdapter, + echoUserMessage, + fakeCodexAppServer, + startTurn, + CODEX_TEST_THREAD_ID, + CODEX_TEST_USER_MESSAGE, + type LateSettlement +} from './codex-structured-dispatch-test-support' + +function send( + adapter: Awaited>, + clientMessageId: string +): Promise { + return adapter.dispatch({ + sessionId: 'session-1', + clientMessageId, + body: CODEX_TEST_USER_MESSAGE, + fence: 7 + }) +} + +describe('codex dispatch admission', () => { + it('admits a send queued behind a running turn and settles it when Codex echoes it', async () => { + // Measured on codex-cli 0.153.4: a `turn/start` issued while a turn runs is + // COALESCED into it -- same turn id back, no second `turn/started`, and the + // user message echoed only once the running turn reaches it. + const codex = fakeCodexAppServer({ + 'turn/start': () => ({ turn: { id: 'turn-1', status: 'inProgress' } }) + }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + + const outcome = await send(adapter, 'client-2') + + // No doubt: elapsed time is not evidence, so nothing invites a Retry. + expect(outcome).toEqual({ state: 'admitted' }) + expect(settlements).toEqual([]) + + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u2', clientId: 'client-2' }) + + // Ordinal 1, not 0: the queued send is the SECOND user message of the turn + // it was coalesced into, which is the key a history replay computes for it. + expect(settlements).toEqual([ + { + sessionId: 'session-1', + clientMessageId: 'client-2', + providerIdentity: { + provider: 'codex', + threadId: CODEX_TEST_THREAD_ID, + turnId: 'turn-1', + ordinal: 1 + } + } + ]) + }) + + it('correlates each send by client message id, not queue order', async () => { + const codex = fakeCodexAppServer({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + + await send(adapter, 'client-1') + await send(adapter, 'client-2') + + // The echoes arrive in the opposite order to the sends. + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u2', clientId: 'client-2' }) + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + + // Ordinals follow the ECHO order, and each one lands on the send whose + // `clientId` it carried -- not on the send that was queued in that slot. + expect(settlements).toEqual([ + { + sessionId: 'session-1', + clientMessageId: 'client-2', + providerIdentity: { + provider: 'codex', + threadId: CODEX_TEST_THREAD_ID, + turnId: 'turn-1', + ordinal: 0 + } + }, + { + sessionId: 'session-1', + clientMessageId: 'client-1', + providerIdentity: { + provider: 'codex', + threadId: CODEX_TEST_THREAD_ID, + turnId: 'turn-1', + ordinal: 1 + } + } + ]) + }) + + it('settles nothing for a user message this session never sent', async () => { + const codex = fakeCodexAppServer({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + await send(adapter, 'client-1') + + // A message another client sent on the same thread, and one Codex did not + // correlate at all. + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-x', clientId: 'someone-else' }) + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-y' }) + + expect(settlements).toEqual([]) + }) + + it('rejects only when Codex answered and declined, and arms nothing for it', async () => { + const { CodexAppServerRequestError } = await import('./codex-app-server-connection') + const codex = fakeCodexAppServer({ + 'turn/start': () => { + throw new CodexAppServerRequestError('turn/start', -32602, 'thread not found') + } + }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + + expect(await send(adapter, 'client-1')).toEqual({ + state: 'rejected', + reason: 'thread not found' + }) + + // A refused write is disarmed, so a later echo of that id settles nothing. + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + expect(settlements).toEqual([]) + }) + + it('retains correlation when a request fails after its write may have landed', async () => { + const codex = fakeCodexAppServer({ + 'turn/start': () => { + throw new Error('request timed out after write') + } + }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + + await expect(send(adapter, 'client-1')).rejects.toThrow('request timed out after write') + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + + expect(settlements).toEqual([ + { + sessionId: 'session-1', + clientMessageId: 'client-1', + providerIdentity: { + provider: 'codex', + threadId: CODEX_TEST_THREAD_ID, + turnId: 'turn-1', + ordinal: 0 + } + } + ]) + }) + + it('refuses overflow without discarding an older accepted send', async () => { + const codex = fakeCodexAppServer({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + + for (let index = 0; index < MAX_CODEX_PENDING_DISPATCH_ECHOES; index += 1) { + expect(await send(adapter, `client-${index}`)).toEqual({ state: 'admitted' }) + } + expect(await send(adapter, 'client-overflow')).toEqual({ + state: 'rejected', + reason: 'codex structured dispatch queue is full' + }) + + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u0', clientId: 'client-0' }) + expect(settlements.map(({ clientMessageId }) => clientMessageId)).toEqual(['client-0']) + }) + + it('leaves no waiter behind when the session closes', async () => { + const codex = fakeCodexAppServer({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + await send(adapter, 'client-1') + + await adapter.closeSession('session-1') + + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + expect(settlements).toEqual([]) + }) + + it('leaves no waiter behind when the child exits', async () => { + const codex = fakeCodexAppServer({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) + const settlements: LateSettlement[] = [] + const adapter = await acquiredCodexAdapter({ codex, settlements }) + const connection = codex.connections[0]! + startTurn(connection, 'turn-1') + await send(adapter, 'client-1') + + connection.handlers.onExit?.(new Error('codex app-server exited')) + + echoUserMessage(connection, { turnId: 'turn-1', itemId: 'item-u1', clientId: 'client-1' }) + expect(settlements).toEqual([]) + }) +}) diff --git a/src/main/codex/codex-structured-dispatch-echo.test.ts b/src/main/codex/codex-structured-dispatch-echo.test.ts new file mode 100644 index 00000000000..58203b26964 --- /dev/null +++ b/src/main/codex/codex-structured-dispatch-echo.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest' +import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types' +import { + createCodexDispatchEchoes, + readCodexDispatchEcho, + MAX_CODEX_PENDING_DISPATCH_ECHOES +} from './codex-structured-dispatch-echo' + +const CODEX_IDENTITY: AgentJournalItemIdentity = { + provider: 'codex', + threadId: 'thread-1', + turnId: 'turn-1', + ordinal: 3 +} + +describe('codex dispatch echoes', () => { + it('settles by client message id rather than arrival order', () => { + const echoes = createCodexDispatchEchoes() + echoes.arm('client-1') + echoes.arm('client-2') + + // Codex coalesces both sends into one turn, and the second can be echoed + // first. Queue position would settle the wrong submission here. + expect(echoes.settle('client-2')).toBe(true) + expect(echoes.settle('client-1')).toBe(true) + expect(echoes.size).toBe(0) + }) + + it('refuses an echo this session never armed', () => { + const echoes = createCodexDispatchEchoes() + echoes.arm('client-1') + + expect(echoes.settle('client-from-history')).toBe(false) + expect(echoes.size).toBe(1) + }) + + it('settles a send exactly once', () => { + const echoes = createCodexDispatchEchoes() + echoes.arm('client-1') + + expect(echoes.settle('client-1')).toBe(true) + expect(echoes.settle('client-1')).toBe(false) + }) + + it('drops a send whose write never reached the provider', () => { + const echoes = createCodexDispatchEchoes() + echoes.arm('client-1') + echoes.disarm('client-1') + + expect(echoes.settle('client-1')).toBe(false) + }) + + it('clears every armed send', () => { + const echoes = createCodexDispatchEchoes() + echoes.arm('client-1') + echoes.arm('client-2') + + echoes.clear() + + expect(echoes.size).toBe(0) + expect(echoes.settle('client-1')).toBe(false) + }) + + it('refuses new correlations at capacity without dropping an older send', () => { + const echoes = createCodexDispatchEchoes() + for (let index = 0; index < MAX_CODEX_PENDING_DISPATCH_ECHOES; index += 1) { + expect(echoes.arm(`client-${index}`)).toBe(true) + } + + expect(echoes.arm(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false) + expect(echoes.size).toBe(MAX_CODEX_PENDING_DISPATCH_ECHOES) + expect(echoes.settle('client-0')).toBe(true) + expect(echoes.settle(`client-${MAX_CODEX_PENDING_DISPATCH_ECHOES}`)).toBe(false) + }) +}) + +describe('readCodexDispatchEcho', () => { + it('reads the client message id off a user message', () => { + expect( + readCodexDispatchEcho( + { type: 'userMessage', id: 'item-1', clientId: 'client-1' }, + CODEX_IDENTITY + ) + ).toEqual({ clientMessageId: 'client-1', providerIdentity: CODEX_IDENTITY }) + }) + + it('ignores an item that is not a user message', () => { + expect( + readCodexDispatchEcho( + { type: 'agentMessage', id: 'item-1', clientId: 'client-1' }, + CODEX_IDENTITY + ) + ).toBeNull() + }) + + it('ignores a user message Codex did not correlate', () => { + expect(readCodexDispatchEcho({ type: 'userMessage', id: 'item-1' }, CODEX_IDENTITY)).toBeNull() + }) + + it('ignores an item with no durable Codex identity', () => { + expect( + readCodexDispatchEcho( + { type: 'userMessage', id: 'item-1', clientId: 'client-1' }, + { provider: 'orca', clientMessageId: 'codex-item:thread-1:item-1' } + ) + ).toBeNull() + }) +}) diff --git a/src/main/codex/codex-structured-dispatch-echo.ts b/src/main/codex/codex-structured-dispatch-echo.ts new file mode 100644 index 00000000000..8ea97561c59 --- /dev/null +++ b/src/main/codex/codex-structured-dispatch-echo.ts @@ -0,0 +1,58 @@ +import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types' + +/** Sends awaiting their echo, oldest first. A send whose echo never arrives is + * retired by the journal's pending-submission recovery on exit, not from here. */ +export const MAX_CODEX_PENDING_DISPATCH_ECHOES = 256 + +/** + * Which sends this session is still waiting to hear back about, keyed by the + * client message id Codex echoes on the user message. + * + * Keyed rather than ordered on purpose: Codex coalesces a `turn/start` issued + * while a turn is running into that turn, so two sends can share one turn id and + * their echoes arrive far apart. Queue position identifies neither. + */ +export type CodexDispatchEchoes = { + /** Arms settlement for a send about to be written; false preserves older waits at capacity. */ + arm: (clientMessageId: string) => boolean + /** True once, for a send this session armed and has not yet settled. */ + settle: (clientMessageId: string) => boolean + /** Drops an armed send whose write never reached the provider. */ + disarm: (clientMessageId: string) => void + clear: () => void + readonly size: number +} + +export function createCodexDispatchEchoes(): CodexDispatchEchoes { + const armed = new Set() + return { + arm(clientMessageId) { + if (!armed.has(clientMessageId) && armed.size >= MAX_CODEX_PENDING_DISPATCH_ECHOES) { + return false + } + armed.delete(clientMessageId) + armed.add(clientMessageId) + return true + }, + settle: (clientMessageId) => armed.delete(clientMessageId), + disarm: (clientMessageId) => void armed.delete(clientMessageId), + clear: () => armed.clear(), + get size() { + return armed.size + } + } +} + +/** The user-message echo a settlement is read off, or null for any other item. */ +export function readCodexDispatchEcho( + item: { type: string; id: string } & Record, + identity: AgentJournalItemIdentity +): { clientMessageId: string; providerIdentity: AgentJournalItemIdentity } | null { + if (item.type !== 'userMessage' || identity.provider !== 'codex') { + return null + } + const clientMessageId = item.clientId + return typeof clientMessageId === 'string' && clientMessageId.length > 0 + ? { clientMessageId, providerIdentity: identity } + : null +} diff --git a/src/main/codex/codex-structured-dispatch-test-support.ts b/src/main/codex/codex-structured-dispatch-test-support.ts new file mode 100644 index 00000000000..5519ffdfdb8 --- /dev/null +++ b/src/main/codex/codex-structured-dispatch-test-support.ts @@ -0,0 +1,139 @@ +import type { + AgentJournalItemIdentity, + AgentJournalMessageItem, + AgentSessionJournalIdentity +} from '../../shared/agent-session-journal-types' +import type { + CodexAppServerConnection, + CodexAppServerConnectionHandlers, + CodexAppServerLaunch, + openCodexAppServerConnection +} from './codex-app-server-connection' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { CodexStructuredSessionAdapter } from './codex-structured-session-adapter' + +export const CODEX_TEST_THREAD_ID = 'thread-abc' + +export const CODEX_TEST_USER_MESSAGE: AgentJournalMessageItem = { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'ship it' }] +} + +export type CodexTestRoute = (params: Record | undefined) => unknown + +type FakeConnection = Omit & { + closed: boolean + launch: CodexAppServerLaunch + handlers: CodexAppServerConnectionHandlers + calls: { method: string; params?: Record }[] +} + +export type LateSettlement = { + sessionId: string + clientMessageId: string + providerIdentity: AgentJournalItemIdentity +} + +/** A `codex app-server` whose turn traffic the test drives by hand. */ +export function fakeCodexAppServer(routes: Record = {}): { + connections: FakeConnection[] + openConnection: typeof openCodexAppServerConnection + routes: Record +} { + const connections: FakeConnection[] = [] + const openConnection = (async (launch, handlers = {}) => { + const connection: FakeConnection = { + launch, + handlers, + calls: [], + pid: 4321, + closed: false, + request: async (method, params) => { + connection.calls.push({ method, params }) + return routes[method]?.(params) ?? {} + }, + notify: () => {}, + respond: () => {}, + respondWithError: () => {}, + close: async () => { + connection.closed = true + return true + } + } + connections.push(connection) + return connection + }) as typeof openCodexAppServerConnection + routes['thread/start'] ??= () => ({ + thread: { id: CODEX_TEST_THREAD_ID, path: '/rollouts/abc.jsonl' } + }) + return { connections, openConnection, routes } +} + +/** A sink that records nothing but keeps the translator alive, which is what + * mints the identities a late settlement carries. */ +export function recordingSink(): StructuredAgentSessionEventSink { + return { + appendItem: () => {}, + appendTombstone: () => {}, + publish: () => {} + } +} + +export async function acquiredCodexAdapter(input: { + codex: ReturnType + settlements: LateSettlement[] + sink?: StructuredAgentSessionEventSink +}): Promise { + const adapter = new CodexStructuredSessionAdapter({ + resolveLaunch: async () => ({ + command: 'codex', + args: ['app-server'], + cwd: '/work/repo', + codexHome: null, + resumeThreadId: null + }), + openConnection: input.codex.openConnection, + readProcessStartTime: async () => 1_700_000_000_000, + captureTurnProcesses: async () => null, + now: () => 1_700_000_000_500, + onDispatchSettledLate: (settlement) => input.settlements.push(settlement) + }) + const identity: AgentSessionJournalIdentity = { + sessionId: 'session-1', + workspaceId: 'ws-1', + hostId: 'host-1', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: CODEX_TEST_THREAD_ID } + } + await adapter.acquire({ + identity, + fence: 7, + spawnToken: 'spawn-9', + events: input.sink ?? recordingSink() + }) + return adapter +} + +/** Codex's own echo of a user message Orca sent, inside `turnId`. */ +export function echoUserMessage( + connection: FakeConnection, + input: { turnId: string; itemId: string; clientId?: string; threadId?: string } +): void { + connection.handlers.onNotification?.('item/started', { + threadId: input.threadId ?? CODEX_TEST_THREAD_ID, + turn: { id: input.turnId }, + item: { + type: 'userMessage', + id: input.itemId, + ...(input.clientId ? { clientId: input.clientId } : {}) + } + }) +} + +export function startTurn(connection: FakeConnection, turnId: string): void { + connection.handlers.onNotification?.('turn/started', { + threadId: CODEX_TEST_THREAD_ID, + turn: { id: turnId } + }) +} diff --git a/src/main/codex/codex-structured-fast-mode.test.ts b/src/main/codex/codex-structured-fast-mode.test.ts index 917133c7543..3029bff280b 100644 --- a/src/main/codex/codex-structured-fast-mode.test.ts +++ b/src/main/codex/codex-structured-fast-mode.test.ts @@ -112,7 +112,9 @@ describe('Codex structured Fast mode dispatch', () => { body: USER_MESSAGE, fence: 7 }) - ).resolves.toMatchObject({ state: 'accepted' }) + // `admitted`, not `accepted`: a Codex send now settles its identity on + // the provider echo. What this test pins is the tier the turn carries. + ).resolves.toMatchObject({ state: 'admitted' }) expect( codex.connections[0].calls.find((call) => call.method === 'turn/start')?.params ).toMatchObject({ serviceTier: 'default' }) diff --git a/src/main/codex/codex-structured-journal-contracts.ts b/src/main/codex/codex-structured-journal-contracts.ts index d7be380446f..10fe26f8f72 100644 --- a/src/main/codex/codex-structured-journal-contracts.ts +++ b/src/main/codex/codex-structured-journal-contracts.ts @@ -1,3 +1,4 @@ +import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types' import type { AgentSessionDeltaCoalescerDeps } from '../native-chat/agent-session-wire/agent-session-delta-coalescer' import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter' @@ -8,7 +9,16 @@ export type CodexJournalTranslatorDeps = { /** Keys restored lifecycle rows to the live identity; without it history restore skips them. */ sessionId?: string now?: () => number - bindPromptItemId?: (journalItemId: string, threadId: string, promptKey: string) => void + bindPromptItemId?: ( + journalItemId: string, + threadId: string, + promptKey: string, + turnId?: string | null + ) => void + clearPromptTurn?: (threadId: string, turnId: string) => void + /** Settles a send's identity off the echoed user message, using the very + * identity the journal row carries so a replay computes the same key. */ + onUserMessageEcho?: (clientMessageId: string, identity: AgentJournalItemIdentity) => void primaryThreadId?: () => string | null subagentExecutions?: CodexSubagentExecutions coalesceMs?: number @@ -18,6 +28,7 @@ export type CodexJournalTranslatorDeps = { export type CodexJournalTranslator = { handle: (event: CodexStructuredSessionEvent) => CodexJournalTranslationAdmission + cancelPrompt: (journalItemId: string) => CodexJournalTranslationAdmission restoreThread: ( threadId: string, thread: Record diff --git a/src/main/codex/codex-structured-journal-items.ts b/src/main/codex/codex-structured-journal-items.ts index 62091580da3..b5ab9ad90e1 100644 --- a/src/main/codex/codex-structured-journal-items.ts +++ b/src/main/codex/codex-structured-journal-items.ts @@ -29,6 +29,7 @@ import { appendCodexLifecycleItem, publishCodexLifecycle } from './codex-structu import type { CodexActiveJournalItem } from './codex-structured-journal-settlement' import { readCodexJournalString } from './codex-structured-journal-translation-values' import { readCodexTurnId } from './codex-structured-thread-facts' +import { readCodexDispatchEcho } from './codex-structured-dispatch-echo' export class CodexJournalItems { readonly ordinals = new CodexTurnOrdinals() @@ -40,7 +41,7 @@ export class CodexJournalItems { constructor( private readonly deps: Pick< CodexJournalTranslatorDeps, - 'sink' | 'coalesceMs' | 'maxRetainedBytes' | 'schedule' + 'sink' | 'coalesceMs' | 'maxRetainedBytes' | 'schedule' | 'onUserMessageEcho' > & { maxMetadataBytes?: number }, private readonly activeTurn: (threadId: string) => string | null, private readonly suppress: (threadId: string, turnId: string) => void @@ -78,6 +79,10 @@ export class CodexJournalItems { const identity = this.identityFor(event.threadId, turnId, item) // Count echoes for stable resume ordinals, but user bubbles come from submissions. if (source === 'live' && item.type === 'userMessage') { + const echo = readCodexDispatchEcho(item, identity) + if (echo) { + this.deps.onUserMessageEcho?.(echo.clientMessageId, echo.providerIdentity) + } return { handled: true, admission: CODEX_JOURNAL_ADMITTED } } if (item.type === 'contextCompaction' && event.method === 'item/started') { diff --git a/src/main/codex/codex-structured-journal-prompts.ts b/src/main/codex/codex-structured-journal-prompts.ts index 93ecec77f77..f72fd6264ef 100644 --- a/src/main/codex/codex-structured-journal-prompts.ts +++ b/src/main/codex/codex-structured-journal-prompts.ts @@ -15,16 +15,21 @@ import { MAX_CODEX_PENDING_PROMPTS } from './codex-structured-journal-limits' import { admitCodexLifecycleItems, appendCodexLifecycleItem, + appendCodexLifecycleMutations, publishCodexLifecycle } from './codex-structured-journal-sink' import type { CodexPendingJournalPrompt } from './codex-structured-journal-settlement' +import { readCodexTurnId } from './codex-structured-thread-facts' + +type CodexGroupedPendingJournalPrompt = CodexPendingJournalPrompt & { promptKey: string } export class CodexJournalPrompts { - readonly pending = new Map() + readonly pending = new Map() constructor( private readonly deps: Pick, - private readonly detailFor: (threadId: string, itemId: string) => string | null + private readonly detailFor: (threadId: string, itemId: string) => string | null, + private readonly activeTurn: (threadId: string) => string | null ) {} handle(event: { @@ -34,6 +39,7 @@ export class CodexJournalPrompts { codexItemId: string promptKey: string }): CodexJournalTranslationAdmission { + const turnId = readCodexTurnId(event.params) ?? this.activeTurn(event.threadId) if (event.method === CODEX_USER_INPUT_METHOD) { const questions = codexQuestionItems({ threadId: event.threadId, @@ -47,12 +53,18 @@ export class CodexJournalPrompts { } for (const question of promptItems) { const itemId = agentJournalItemKey(question.identity) - this.pending.set(itemId, { identity: question.identity, body: question.body }) + this.pending.set(itemId, { + threadId: event.threadId, + turnId, + promptKey: event.promptKey, + identity: question.identity, + body: question.body + }) const trimAdmission = this.trim() if (!trimAdmission.accepted) { return trimAdmission } - this.deps.bindPromptItemId?.(itemId, event.threadId, event.promptKey) + this.deps.bindPromptItemId?.(itemId, event.threadId, event.promptKey, turnId) } return CODEX_JOURNAL_ADMITTED } @@ -70,12 +82,18 @@ export class CodexJournalPrompts { return admission } const itemId = agentJournalItemKey(identity) - this.pending.set(itemId, { identity, body }) + this.pending.set(itemId, { + threadId: event.threadId, + turnId, + promptKey: event.promptKey, + identity, + body + }) const trimAdmission = this.trim() if (!trimAdmission.accepted) { return trimAdmission } - this.deps.bindPromptItemId?.(itemId, event.threadId, event.promptKey) + this.deps.bindPromptItemId?.(itemId, event.threadId, event.promptKey, turnId) return CODEX_JOURNAL_ADMITTED } @@ -83,13 +101,43 @@ export class CodexJournalPrompts { this.pending.delete(journalItemId) } + cancel(journalItemId: string): CodexJournalTranslationAdmission { + const selected = this.pending.get(journalItemId) + if (!selected) { + return CODEX_JOURNAL_ADMITTED + } + const group = [...this.pending].filter( + ([, prompt]) => + prompt.threadId === selected.threadId && + prompt.turnId === selected.turnId && + prompt.promptKey === selected.promptKey + ) + const mutations = group.flatMap(([, prompt]) => { + const body = cancelledJournalPromptBody(prompt.body) + return body ? [{ kind: 'item' as const, identity: prompt.identity, body }] : [] + }) + const admission = appendCodexLifecycleMutations( + this.deps.sink, + `prompt-cancelled:${encodeURIComponent(selected.threadId)}:${encodeURIComponent( + selected.promptKey + )}:${encodeURIComponent(selected.turnId ?? 'unbound')}`, + mutations + ) + if (admission.accepted) { + for (const [itemId] of group) { + this.pending.delete(itemId) + } + } + return admission + } + dispose(): void { this.pending.clear() } private admit( event: { method: string; threadId: string; promptKey: string }, - items: readonly CodexPendingJournalPrompt[] + items: readonly Pick[] ): CodexJournalTranslationAdmission { return admitCodexLifecycleItems( this.deps.sink, diff --git a/src/main/codex/codex-structured-journal-settlement.ts b/src/main/codex/codex-structured-journal-settlement.ts index 5aa158fafc7..5322d3355cd 100644 --- a/src/main/codex/codex-structured-journal-settlement.ts +++ b/src/main/codex/codex-structured-journal-settlement.ts @@ -3,7 +3,6 @@ import type { AgentJournalItemIdentity, AgentJournalTurnLifecycle } from '../../shared/agent-session-journal-types' -import { partitionJournalLifecycleMutations } from '../native-chat/agent-session-journal/journal-lifecycle-batch-partition' import type { JournalLifecycleMutationInput } from '../native-chat/agent-session-journal/journal-row-builders' import type { StructuredAgentSessionEventSink, @@ -23,6 +22,7 @@ import { codexTurnLifecycleBody, codexTurnLifecycleIdentity } from './codex-structured-journal-translation-turns' +import { appendCodexLifecycleMutations } from './codex-structured-journal-sink' export type CodexActiveJournalItem = { threadId: string @@ -32,6 +32,8 @@ export type CodexActiveJournalItem = { } export type CodexPendingJournalPrompt = { + threadId: string + turnId: string | null identity: AgentJournalItemIdentity body: AgentJournalItemBody } @@ -85,7 +87,11 @@ export function settleCodexJournalSession(input: { turnOrdinalsToForget.push({ threadId, turnId }) } } - const admission = appendLifecycleMutations(input.sink, exitSettlementId(input.event), mutations) + const admission = appendCodexLifecycleMutations( + input.sink, + exitSettlementId(input.event), + mutations + ) if (!admission.accepted) { return admission } @@ -104,9 +110,13 @@ export function settleCodexJournalTurn(input: { sink: StructuredAgentSessionEventSink streams: CodexStructuredItemStreams activeItems: Map + pendingPrompts?: Map + clearPromptTurn?: (threadId: string, turnId: string) => void }): StructuredAgentSessionSinkAdmission { const mutations: JournalLifecycleMutationInput[] = [] const activeItemsToForget: { key: string; threadId: string; itemId: string }[] = [] + const pendingPromptsToForget: string[] = [] + const pendingPrompts = input.pendingPrompts ?? new Map() for (const [key, active] of input.activeItems) { if (active.threadId !== input.threadId || active.turnId !== input.turnId) { continue @@ -124,6 +134,16 @@ export function settleCodexJournalTurn(input: { } activeItemsToForget.push({ key, threadId: active.threadId, itemId: active.item.id }) } + for (const [key, prompt] of pendingPrompts) { + if (prompt.threadId !== input.threadId || prompt.turnId !== input.turnId) { + continue + } + const body = cancelledJournalPromptBody(prompt.body) + if (body) { + mutations.push({ kind: 'item', identity: prompt.identity, body }) + } + pendingPromptsToForget.push(key) + } // Revised, never tombstoned: the terminal row keeps the turn's duration durable. if (input.turnLifecycle) { mutations.push({ @@ -132,10 +152,7 @@ export function settleCodexJournalTurn(input: { body: codexTurnLifecycleBody(input.turnLifecycle) }) } - if (mutations.length === 0) { - return ADMITTED - } - const admission = appendLifecycleMutations( + const admission = appendCodexLifecycleMutations( input.sink, `turn-completed:${input.sessionId}:${input.threadId}:${input.turnId}`, mutations @@ -147,6 +164,10 @@ export function settleCodexJournalTurn(input: { input.streams.forget(active.threadId, active.itemId) input.activeItems.delete(active.key) } + for (const key of pendingPromptsToForget) { + pendingPrompts.delete(key) + } + input.clearPromptTurn?.(input.threadId, input.turnId) return ADMITTED } @@ -182,7 +203,7 @@ export function settleCodexOversizedNotification(input: { if (mutations.length === 0) { return ADMITTED } - const admission = appendLifecycleMutations( + const admission = appendCodexLifecycleMutations( input.sink, `oversized-notification:${input.sessionId}:${input.threadId}:${input.method}`, mutations @@ -225,54 +246,6 @@ function oversizedStreamItemType(method: string): CodexThreadItem['type'] | null return null } -function appendLifecycleMutations( - sink: StructuredAgentSessionEventSink, - settlementId: string, - mutations: readonly JournalLifecycleMutationInput[] -): StructuredAgentSessionSinkAdmission { - const chunks = partitionJournalLifecycleMutations(settlementId, mutations) - for (const { settlementId: id, mutations: chunk } of chunks) { - let admission: StructuredAgentSessionSinkAdmission = ADMITTED - if (sink.tryAppendLifecycleBatch) { - admission = sink.tryAppendLifecycleBatch(id, chunk, { lifecycle: true }) - } else if (sink.appendLifecycleBatch) { - admission = sink.appendLifecycleBatch(id, chunk, { lifecycle: true }) ?? ADMITTED - } else { - for (const mutation of chunk) { - if (mutation.kind === 'item') { - if (sink.tryAppendItem) { - admission = sink.tryAppendItem(mutation.identity, mutation.body, { lifecycle: true }) - if (!admission.accepted) { - return admission - } - } else { - sink.appendItem(mutation.identity, mutation.body, { lifecycle: true }) - } - } else { - if (sink.tryAppendTombstone) { - admission = sink.tryAppendTombstone(mutation.identity, { lifecycle: true }) - if (!admission.accepted) { - return admission - } - } else { - sink.appendTombstone(mutation.identity, { lifecycle: true }) - } - } - } - } - if (!admission.accepted) { - return admission - } - const publishAdmission = sink.tryPublish - ? sink.tryPublish({ lifecycle: true }) - : (sink.publish({ lifecycle: true }), ADMITTED) - if (!publishAdmission.accepted) { - return publishAdmission - } - } - return ADMITTED -} - function interruptedBody(body: AgentJournalItemBody | null): AgentJournalItemBody | null { if (!body) { return null diff --git a/src/main/codex/codex-structured-journal-sink.ts b/src/main/codex/codex-structured-journal-sink.ts index 7da381def41..5b8f4b83920 100644 --- a/src/main/codex/codex-structured-journal-sink.ts +++ b/src/main/codex/codex-structured-journal-sink.ts @@ -7,10 +7,62 @@ import type { StructuredAgentSessionLifecycleIdentityResolver, StructuredAgentSessionSinkAdmission } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { partitionJournalLifecycleMutations } from '../native-chat/agent-session-journal/journal-lifecycle-batch-partition' +import type { JournalLifecycleMutationInput } from '../native-chat/agent-session-journal/journal-row-builders' import type { CodexPendingJournalPrompt } from './codex-structured-journal-settlement' import type { CodexJournalTranslationAdmission } from './codex-structured-journal-contracts' import { CODEX_JOURNAL_ADMITTED } from './codex-structured-journal-contracts' +const ADMITTED: StructuredAgentSessionSinkAdmission = { accepted: true } + +export function appendCodexLifecycleMutations( + sink: StructuredAgentSessionEventSink, + settlementId: string, + mutations: readonly JournalLifecycleMutationInput[] +): StructuredAgentSessionSinkAdmission { + const chunks = partitionJournalLifecycleMutations(settlementId, mutations) + for (const { settlementId: id, mutations: chunk } of chunks) { + let admission: StructuredAgentSessionSinkAdmission = ADMITTED + if (sink.tryAppendLifecycleBatch) { + admission = sink.tryAppendLifecycleBatch(id, chunk, { lifecycle: true }) + } else if (sink.appendLifecycleBatch) { + admission = sink.appendLifecycleBatch(id, chunk, { lifecycle: true }) ?? ADMITTED + } else { + for (const mutation of chunk) { + if (mutation.kind === 'item') { + if (sink.tryAppendItem) { + admission = sink.tryAppendItem(mutation.identity, mutation.body, { lifecycle: true }) + if (!admission.accepted) { + return admission + } + } else { + sink.appendItem(mutation.identity, mutation.body, { lifecycle: true }) + } + } else { + if (sink.tryAppendTombstone) { + admission = sink.tryAppendTombstone(mutation.identity, { lifecycle: true }) + if (!admission.accepted) { + return admission + } + } else { + sink.appendTombstone(mutation.identity, { lifecycle: true }) + } + } + } + } + if (!admission.accepted) { + return admission + } + const publishAdmission = sink.tryPublish + ? sink.tryPublish({ lifecycle: true }) + : (sink.publish({ lifecycle: true }), ADMITTED) + if (!publishAdmission.accepted) { + return publishAdmission + } + } + return ADMITTED +} + function criticalAdmission( admission: StructuredAgentSessionSinkAdmission ): CodexJournalTranslationAdmission { @@ -57,7 +109,7 @@ export function publishCodexLifecycle( export function admitCodexLifecycleItems( sink: StructuredAgentSessionEventSink, settlementId: string, - items: readonly CodexPendingJournalPrompt[] + items: readonly Pick[] ): CodexJournalTranslationAdmission { if (items.length === 0) { return { accepted: false, reason: 'untranslated' } diff --git a/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts b/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts index a4aa3200aca..014ce81ed96 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-boundaries.ts @@ -12,6 +12,7 @@ import { codexTurnUserItemId, publishCodexTurnLifecycle } from './codex-structured-journal-translation-turns' +import type { CodexPendingJournalPrompt } from './codex-structured-journal-settlement' import { readCodexTurnDurationMs, readCodexTurnId, @@ -33,6 +34,8 @@ export class CodexJournalTurnBoundaries { primaryThreadId: () => string | null activeTurns: CodexJournalActiveTurns items: Pick + pendingPrompts: Map + clearPromptTurn?: (threadId: string, turnId: string) => void flushSuppression: () => CodexJournalTranslationAdmission resetActivity: (threadId: string) => void now?: () => number @@ -93,7 +96,9 @@ export class CodexJournalTurnBoundaries { ) : null, streams: this.deps.items.streams, - activeItems: this.deps.items.activeItems + activeItems: this.deps.items.activeItems, + pendingPrompts: this.deps.pendingPrompts, + ...(this.deps.clearPromptTurn ? { clearPromptTurn: this.deps.clearPromptTurn } : {}) }) if (admission.accepted) { this.deps.items.ordinals.forgetTurn(event.threadId, turnId) diff --git a/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts b/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts index b57670851b5..9ddeca4fba7 100644 --- a/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts +++ b/src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts @@ -14,6 +14,11 @@ import { } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' import type { CodexAppServerConnection } from './codex-app-server-connection' import { createCodexJournalTranslator } from './codex-structured-journal-translation' +import { + CODEX_COMMAND_APPROVAL_METHOD, + CODEX_USER_INPUT_METHOD, + CodexPromptRegistry +} from './codex-structured-prompt-replies' import { createCodexStructuredNotificationRetry } from './codex-structured-notification-retry' import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter' import type { CodexSession } from './codex-structured-session-state' @@ -85,6 +90,123 @@ afterEach(async () => { }) describe('codex turn lifecycle rows', () => { + it('binds a prompt without a provider turn id to the active turn before cleanup', () => { + const tap = recorder() + const registry = new CodexPromptRegistry() + registry.register({ + id: 1, + method: CODEX_COMMAND_APPROVAL_METHOD, + params: { + itemId: 'exec-fallback', + approvalId: 'approval-fallback', + threadId: THREAD_ID + } + }) + const translator = createCodexJournalTranslator({ + sink: tap.sink, + primaryThreadId: () => THREAD_ID, + bindPromptItemId: (journalItemId, threadId, promptKey, turnId) => + registry.bindJournalItemId(journalItemId, threadId, promptKey, turnId), + clearPromptTurn: (threadId, turnId) => registry.clearTurn(threadId, turnId) + }) + + translator.handle(notification('turn/started', { turn: { id: TURN_ID } })) + translator.handle({ + type: 'prompt', + sessionId: SESSION_ID, + threadId: THREAD_ID, + method: CODEX_COMMAND_APPROVAL_METHOD, + params: { availableDecisions: ['accept', 'decline'] }, + codexItemId: 'exec-fallback', + promptKey: 'approval-fallback' + }) + + expect(registry.find('approval-fallback')?.turnId).toBe(TURN_ID) + translator.handle(notification('turn/completed', { turn: { id: TURN_ID } })) + expect(registry.find('approval-fallback')).toBeNull() + }) + + it('settles prompts when a turn completes while awaiting approval', () => { + const tap = recorder() + const clearPromptTurn = vi.fn() + const translator = createCodexJournalTranslator({ + sink: tap.sink, + primaryThreadId: () => THREAD_ID, + clearPromptTurn + }) + + translator.handle(notification('turn/started', { turn: { id: TURN_ID } })) + translator.handle({ + type: 'prompt', + sessionId: SESSION_ID, + threadId: THREAD_ID, + method: CODEX_COMMAND_APPROVAL_METHOD, + params: { turnId: TURN_ID, availableDecisions: ['accept', 'decline'] }, + codexItemId: 'exec-cancelled', + promptKey: 'approval-cancelled' + }) + + expect(translator.handle(notification('turn/completed', { turn: { id: TURN_ID } }))).toEqual({ + accepted: true + }) + expect(tap.rows.map((row) => row.body)).toEqual([ + expect.objectContaining({ kind: 'turn', state: 'running' }), + expect.objectContaining({ + kind: 'approval', + resolution: expect.objectContaining({ state: 'pending' }) + }), + expect.objectContaining({ + kind: 'approval', + resolution: expect.objectContaining({ state: 'cancelled' }) + }), + expect.objectContaining({ kind: 'turn', state: 'completed' }) + ]) + expect(clearPromptTurn).toHaveBeenCalledWith(THREAD_ID, TURN_ID) + }) + + it('settles questions when a turn completes while awaiting input', () => { + const tap = recorder() + const clearPromptTurn = vi.fn() + const translator = createCodexJournalTranslator({ + sink: tap.sink, + primaryThreadId: () => THREAD_ID, + clearPromptTurn + }) + + translator.handle(notification('turn/started', { turn: { id: TURN_ID } })) + translator.handle({ + type: 'prompt', + sessionId: SESSION_ID, + threadId: THREAD_ID, + method: CODEX_USER_INPUT_METHOD, + params: { + turnId: TURN_ID, + questions: [ + { id: 'question-cancelled', question: 'Continue?', options: [{ label: 'yes' }] } + ] + }, + codexItemId: 'exec-question-cancelled', + promptKey: 'question-cancelled' + }) + + expect(translator.handle(notification('turn/completed', { turn: { id: TURN_ID } }))).toEqual({ + accepted: true + }) + expect(tap.rows.map((row) => row.body)).toEqual([ + expect.objectContaining({ kind: 'turn', state: 'running' }), + expect.objectContaining({ + kind: 'question', + resolution: expect.objectContaining({ state: 'pending' }) + }), + expect.objectContaining({ + kind: 'question', + resolution: expect.objectContaining({ state: 'cancelled' }) + }), + expect.objectContaining({ kind: 'turn', state: 'completed' }) + ]) + expect(clearPromptTurn).toHaveBeenCalledWith(THREAD_ID, TURN_ID) + }) + it('opens the running row with the host receipt time and pins the row time to it', async () => { const journal = await journals.open({ identity: { diff --git a/src/main/codex/codex-structured-journal-translation-turns.ts b/src/main/codex/codex-structured-journal-translation-turns.ts index d1cd7ca2884..e36322bca0c 100644 --- a/src/main/codex/codex-structured-journal-translation-turns.ts +++ b/src/main/codex/codex-structured-journal-translation-turns.ts @@ -6,7 +6,7 @@ import type { } from '../../shared/agent-session-journal-types' import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' import { agentJournalTurnBody } from '../../shared/agent-session-turn-record' -import { CODEX_USER_MESSAGE_ORDINAL } from './codex-structured-turn-start' +import { CODEX_USER_MESSAGE_ORDINAL } from './codex-turn-ordinals' import type { StructuredAgentSessionEventSink, StructuredAgentSessionSinkAdmission diff --git a/src/main/codex/codex-structured-journal-translation.ts b/src/main/codex/codex-structured-journal-translation.ts index 5c1e97310bc..43e9a6642de 100644 --- a/src/main/codex/codex-structured-journal-translation.ts +++ b/src/main/codex/codex-structured-journal-translation.ts @@ -59,8 +59,10 @@ export function createCodexJournalTranslator( (threadId, turnId) => genericFrames.suppress(threadId, turnId) ) const settleOversizedNotification = createCodexOversizedNotificationSettler(deps, items) - const prompts = new CodexJournalPrompts(deps, (threadId, itemId) => - items.detailFor(threadId, itemId) + const prompts = new CodexJournalPrompts( + deps, + (threadId, itemId) => items.detailFor(threadId, itemId), + (threadId) => activeTurns.current(threadId) ) const subagents = new CodexSubagentRoster({ sink: deps.sink, @@ -82,6 +84,8 @@ export function createCodexJournalTranslator( primaryThreadId: () => deps.primaryThreadId?.() ?? null, activeTurns, items, + pendingPrompts: prompts.pending, + ...(deps.clearPromptTurn ? { clearPromptTurn: deps.clearPromptTurn } : {}), flushSuppression: () => genericFrames.flush(), resetActivity, ...(deps.now ? { now: deps.now } : {}) @@ -269,6 +273,7 @@ export function createCodexJournalTranslator( genericFrames.appendUnhandled(`notification:${event.method}`, event.params, event.threadId) ) }, + cancelPrompt: (journalItemId) => prompts.cancel(journalItemId), resolvePrompt: (journalItemId) => prompts.resolve(journalItemId), flush: () => { items.streams.flush() diff --git a/src/main/codex/codex-structured-prompt-ownership.test.ts b/src/main/codex/codex-structured-prompt-ownership.test.ts new file mode 100644 index 00000000000..cfb77e628a7 --- /dev/null +++ b/src/main/codex/codex-structured-prompt-ownership.test.ts @@ -0,0 +1,680 @@ +import { describe, expect, it, vi } from 'vitest' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire' +import type { + AgentJournalItemBody, + AgentJournalItemIdentity +} from '../../shared/agent-session-journal-types' +import { readAgentJournalTurn } from '../../shared/agent-session-turn-record' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { CodexAppServerRequestError } from './codex-app-server-connection' +import { + THREAD_ID, + acquired, + adapterFor, + fakeCodex, + identityFor +} from './codex-structured-session-adapter-fixture' +import { CodexPromptRegistry } from './codex-structured-prompt-replies' +import type { CodexStructuredSessionEvent } from './codex-structured-session-state' + +function deferred(): { promise: Promise; resolve: () => void } { + let resolve = (): void => {} + const promise = new Promise((finish) => { + resolve = finish + }) + return { promise, resolve } +} + +function registerPrompt( + adapter: Awaited>, + codex: ReturnType, + itemId = 'journal-prompt', + threadId = THREAD_ID, + turnId = 'turn-1' +): void { + codex.connections[0]?.handlers.onServerRequest?.({ + id: 11, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'codex-item-1', threadId, turnId } + }) + adapter.bindPromptItemId('session-1', itemId, 'codex-item-1', turnId, threadId) +} + +function registerGroupedQuestionPrompt( + codex: ReturnType, + threadId = THREAD_ID, + turnId = 'turn-1' +): void { + codex.connections[0]?.handlers.onServerRequest?.({ + id: 12, + method: 'item/tool/requestUserInput', + params: { + itemId: 'codex-question-group', + threadId, + turnId, + questions: [ + { id: 'first', question: 'First?', options: [{ label: 'yes' }] }, + { id: 'second', question: 'Second?', options: [{ label: 'no' }] } + ] + } + }) +} + +function completeTurn( + codex: ReturnType, + threadId: string, + turnId = 'turn-1' +): void { + codex.connections[0]?.handlers.onNotification?.('turn/completed', { + threadId, + turn: { id: turnId, status: 'interrupted' } + }) +} + +function completionThreads(events: CodexStructuredSessionEvent[]): string[] { + return events.flatMap((event) => + event.type === 'notification' && event.method === 'turn/completed' ? [event.threadId] : [] + ) +} + +function lifecycleRecorder( + acceptPromptCancellation = true, + acceptTurnCompletion = true +): { + sink: StructuredAgentSessionEventSink + bodies: Map + order: string[] +} { + const bodies = new Map() + const order: string[] = [] + const settlements = new Set() + const append = (identity: AgentJournalItemIdentity, body: AgentJournalItemBody): void => { + bodies.set(agentJournalItemKey(identity), body) + } + const sink: StructuredAgentSessionEventSink = { + appendItem: append, + appendTombstone: (identity) => bodies.delete(agentJournalItemKey(identity)), + publish: () => {}, + tryAppendItem: (identity, body, options) => { + if ( + body.kind === 'approval' && + body.resolution.state === 'cancelled' && + options?.lifecycle === true + ) { + if (!acceptPromptCancellation) { + return { accepted: false, reason: 'backpressure' } + } + order.push('prompt-lifecycle') + } + append(identity, body) + return { accepted: true } + }, + tryAppendLifecycleBatch: (settlementId, mutations) => { + const cancelsPrompt = mutations.some( + (mutation) => + mutation.kind === 'item' && + (mutation.body.kind === 'approval' || mutation.body.kind === 'question') && + mutation.body.resolution.state === 'cancelled' + ) + if (cancelsPrompt && !acceptPromptCancellation) { + return { accepted: false, reason: 'backpressure' } + } + if (settlementId.startsWith('turn-completed:') && !acceptTurnCompletion) { + return { accepted: false, reason: 'backpressure' } + } + if (settlements.has(settlementId)) { + return { accepted: true } + } + settlements.add(settlementId) + for (const mutation of mutations) { + if (mutation.kind === 'item') { + append(mutation.identity, mutation.body) + } else { + bodies.delete(agentJournalItemKey(mutation.identity)) + } + } + if (cancelsPrompt) { + order.push('prompt-lifecycle') + } + if (settlementId.startsWith('turn-completed:')) { + order.push('turn-lifecycle') + } + return { accepted: true } + }, + tryPublish: () => ({ accepted: true }) + } + return { sink, bodies, order } +} + +describe('Codex live prompt ownership', () => { + it('lets an answer hold the callback claim through its journal commit', async () => { + const codex = fakeCodex() + const adapter = await acquired(codex) + registerPrompt(adapter, codex) + const commitGate = deferred() + const commitStarted = vi.fn() + + const answer = adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'accept', + fence: 7, + commit: async () => { + expect(codex.connections[0]?.replies).toEqual([]) + commitStarted() + await commitGate.promise + } + }) + await vi.waitFor(() => expect(commitStarted).toHaveBeenCalledOnce()) + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + expect(codex.connections[0]?.calls.some((call) => call.method === 'turn/interrupt')).toBe(false) + + commitGate.resolve() + await answer + expect(codex.connections[0]?.replies).toEqual([{ id: 11, result: { decision: 'accept' } }]) + }) + + it('lets prompt cancellation win and retains its claim until terminal cleanup', async () => { + const interruptGate = deferred() + const codex = fakeCodex({ + 'turn/interrupt': async () => { + await interruptGate.promise + completeTurn(codex, THREAD_ID) + } + }) + const adapter = await acquired(codex) + registerPrompt(adapter, codex) + + const cancellation = adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + await vi.waitFor(() => + expect(codex.connections[0]?.calls.at(-1)?.method).toBe('turn/interrupt') + ) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'accept', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + + interruptGate.resolve() + await expect(cancellation).resolves.toEqual({ cancelled: true }) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'accept', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + + await adapter.closeSession('session-1') + await adapter.acquire({ identity: identityFor('session-1'), fence: 8, spawnToken: 'spawn-10' }) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'accept', + fence: 8, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + }) + + it('releases the callback claim after a failed interrupt', async () => { + const codex = fakeCodex({ + 'turn/interrupt': () => { + throw new CodexAppServerRequestError('turn/interrupt', -32602, 'no such turn') + } + }) + const adapter = await acquired(codex) + registerPrompt(adapter, codex) + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + await adapter.answerPrompt({ + sessionId: 'session-1', + itemId: 'journal-prompt', + kind: 'approval', + optionId: 'decline', + fence: 7, + commit: async () => undefined + }) + expect(codex.connections[0]?.replies).toEqual([{ id: 11, result: { decision: 'decline' } }]) + }) + + it('interrupts only the child provider turn when its controller turn differs', async () => { + const codex = fakeCodex({ + 'turn/interrupt': () => completeTurn(codex, 'thread-child', 'child-turn') + }) + const terminateTurnProcesses = vi.fn(async () => true) + const adapter = adapterFor(codex, {}, [], { terminateTurnProcesses }) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9' + }) + registerPrompt(adapter, codex, 'child-prompt', 'thread-child', 'child-turn') + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'root-turn', + fence: 7, + prompt: { itemId: 'child-prompt' } + }) + ).resolves.toEqual({ cancelled: true }) + expect(codex.connections[0]?.calls.at(-1)).toEqual({ + method: 'turn/interrupt', + params: { threadId: 'thread-child', turnId: 'child-turn' } + }) + expect(terminateTurnProcesses).not.toHaveBeenCalled() + expect(codex.connections[0]?.closed).toBe(false) + }) + + it('keeps a wire-valid multibyte prompt turn id as the exact interrupt target', async () => { + const promptTurnId = '界'.repeat(171) + expect(promptTurnId.length).toBeLessThanOrEqual(AGENT_SESSION_ID_MAX_LENGTH) + expect(Buffer.byteLength(promptTurnId, 'utf8')).toBeGreaterThan(AGENT_SESSION_ID_MAX_LENGTH) + const codex = fakeCodex({ + 'turn/interrupt': () => completeTurn(codex, 'thread-child', promptTurnId) + }) + const adapter = await acquired(codex) + registerPrompt(adapter, codex, 'child-prompt', 'thread-child', promptTurnId) + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'root-turn', + fence: 7, + prompt: { itemId: 'child-prompt' } + }) + ).resolves.toEqual({ cancelled: true }) + expect(codex.connections[0]?.calls.at(-1)).toEqual({ + method: 'turn/interrupt', + params: { threadId: 'thread-child', turnId: promptTurnId } + }) + }) + + it('settles a grouped prompt and its running turn before reporting cancellation', async () => { + const codex = fakeCodex({ + 'turn/interrupt': () => { + codex.connections[0]?.handlers.onNotification?.('turn/completed', { + threadId: THREAD_ID, + turn: { id: 'turn-1', status: 'interrupted', durationMs: 456 } + }) + } + }) + const recorded = lifecycleRecorder() + const adapter = adapterFor(codex) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + codex.connections[0]?.handlers.onNotification?.('turn/started', { + threadId: THREAD_ID, + turn: { id: 'turn-1' } + }) + registerGroupedQuestionPrompt(codex) + const questionItemIds = [...recorded.bodies] + .filter(([, body]) => body.kind === 'question') + .map(([itemId]) => itemId) + expect(questionItemIds).toHaveLength(2) + const selectedItemId = questionItemIds[0] + const siblingItemId = questionItemIds[1] + if (!selectedItemId || !siblingItemId) { + throw new Error('expected two durable Codex questions') + } + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: selectedItemId } + }) + ).resolves.toEqual({ cancelled: true }) + expect( + questionItemIds.map((itemId) => { + const body = recorded.bodies.get(itemId) + return body?.kind === 'question' ? body.resolution.state : null + }) + ).toEqual(['cancelled', 'cancelled']) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 456 + }) + + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: siblingItemId, + kind: 'question', + optionId: 'no', + fence: 7, + commit: async () => undefined + }) + ).rejects.toThrow(/no longer waiting/) + }) + + it('enqueues terminal prompt state before a confirmed cancellation resolves', async () => { + const codex = fakeCodex({ + 'turn/interrupt': () => { + codex.connections[0]?.handlers.onNotification?.('turn/completed', { + threadId: THREAD_ID, + turn: { id: 'turn-1', status: 'interrupted', durationMs: 321 } + }) + } + }) + const recorded = lifecycleRecorder() + const adapter = adapterFor(codex) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + registerPrompt(adapter, codex) + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + if (!promptItemId) { + throw new Error('expected durable Codex prompt') + } + + const cancellation = adapter + .cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: promptItemId } + }) + .then((result) => { + recorded.order.push('resolved') + return result + }) + + await expect(cancellation).resolves.toEqual({ cancelled: true }) + expect(recorded.order).toEqual(['prompt-lifecycle', 'turn-lifecycle', 'resolved']) + expect( + [...recorded.bodies.values()].some( + (body) => body.kind === 'approval' && body.resolution.state === 'cancelled' + ) + ).toBe(true) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 321 + }) + + codex.connections[0]?.handlers.onNotification?.('turn/completed', { + threadId: THREAD_ID, + turn: { id: 'turn-1', status: 'completed', durationMs: 999 } + }) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 321 + }) + }) + + it('settles the prompt without inventing turn completion when none was observed', async () => { + const codex = fakeCodex() + const recorded = lifecycleRecorder() + const adapter = adapterFor(codex) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + codex.connections[0]?.handlers.onNotification?.('turn/started', { + threadId: THREAD_ID, + turn: { id: 'turn-1' } + }) + registerPrompt(adapter, codex) + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + if (!promptItemId) { + throw new Error('expected durable Codex prompt') + } + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: promptItemId } + }) + ).resolves.toEqual({ cancelled: true }) + expect(recorded.bodies.get(promptItemId)).toMatchObject({ + kind: 'approval', + resolution: { state: 'cancelled' } + }) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'running' + }) + + codex.connections[0]?.handlers.onNotification?.('turn/completed', { + threadId: THREAD_ID, + turn: { id: 'turn-1', status: 'interrupted', durationMs: 777 } + }) + expect([...recorded.bodies.values()].find((body) => readAgentJournalTurn(body))).toMatchObject({ + state: 'interrupted', + durationMs: 777 + }) + + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: promptItemId, + kind: 'approval', + optionId: 'accept', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + }) + + it('does not synthesize terminal lifecycle for ordinary Stop', async () => { + const events: CodexStructuredSessionEvent[] = [] + const adapter = await acquired(fakeCodex(), {}, events) + + await expect( + adapter.cancelTurn({ sessionId: 'session-1', turnId: 'turn-1', fence: 7 }) + ).resolves.toEqual({ cancelled: true }) + expect(completionThreads(events)).toEqual([]) + }) + + it('does not report success or release the claim when prompt lifecycle admission fails', async () => { + const codex = fakeCodex() + const recorded = lifecycleRecorder(false) + const adapter = adapterFor(codex) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + registerPrompt(adapter, codex) + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + if (!promptItemId) { + throw new Error('expected durable Codex prompt') + } + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: promptItemId } + }) + ).rejects.toThrow(/lifecycle was not admitted/) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: promptItemId, + kind: 'approval', + optionId: 'accept', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + }) + + it('does not report success when a deferred provider completion is backpressured', async () => { + const recorded = lifecycleRecorder(true, false) + const codex = fakeCodex({ + 'turn/interrupt': () => { + completeTurn(codex, THREAD_ID) + } + }) + const adapter = adapterFor(codex) + await adapter.acquire({ + identity: identityFor('session-1'), + fence: 7, + spawnToken: 'spawn-9', + events: recorded.sink + }) + registerPrompt(adapter, codex) + const promptItemId = [...recorded.bodies].find(([, body]) => body.kind === 'approval')?.[0] + if (!promptItemId) { + throw new Error('expected durable Codex prompt') + } + + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: promptItemId } + }) + ).rejects.toThrow(/deferred turn completion lifecycle was not admitted/) + const commit = vi.fn(async () => undefined) + await expect( + adapter.answerPrompt({ + sessionId: 'session-1', + itemId: promptItemId, + kind: 'approval', + optionId: 'accept', + fence: 7, + commit + }) + ).rejects.toThrow(/no longer waiting/) + expect(commit).not.toHaveBeenCalled() + + await adapter.closeSession('session-1') + }) + + it('defers only the matching thread and emits its terminal event before cancel resolves', async () => { + const interruptGate = deferred() + const events: CodexStructuredSessionEvent[] = [] + const codex = fakeCodex({ + 'turn/interrupt': () => { + completeTurn(codex, THREAD_ID) + completeTurn(codex, 'thread-child') + return interruptGate.promise + } + }) + const adapter = await acquired(codex, {}, events) + registerPrompt(adapter, codex, 'child-prompt', 'thread-child') + + const cancellation = adapter + .cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 7, + prompt: { itemId: 'child-prompt' } + }) + .then((result) => { + expect(completionThreads(events)).toEqual([THREAD_ID, 'thread-child']) + return result + }) + await vi.waitFor(() => expect(completionThreads(events)).toEqual([THREAD_ID])) + + interruptGate.resolve() + await expect(cancellation).resolves.toEqual({ cancelled: true }) + }) + + it('checks the bound item, fence, and current acquisition before interrupting', async () => { + const codex = fakeCodex() + const adapter = await acquired(codex) + registerPrompt(adapter, codex) + + for (const input of [ + { turnId: 'turn-1', fence: 7, itemId: 'other-item' }, + { turnId: 'turn-1', fence: 6, itemId: 'journal-prompt' } + ]) { + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: input.turnId, + fence: input.fence, + prompt: { itemId: input.itemId } + }) + ).resolves.toEqual({ cancelled: false }) + } + expect(codex.connections[0]?.calls.some((call) => call.method === 'turn/interrupt')).toBe(false) + + await adapter.acquire({ identity: identityFor('session-1'), fence: 8, spawnToken: 'spawn-10' }) + await expect( + adapter.cancelTurn({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 8, + prompt: { itemId: 'journal-prompt' } + }) + ).resolves.toEqual({ cancelled: false }) + expect(codex.connections[1]?.calls.some((call) => call.method === 'turn/interrupt')).toBe(false) + }) + + it('drops a retained cancellation claim with normal turn cleanup', () => { + const prompts = new CodexPromptRegistry() + prompts.register({ + id: 11, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'codex-item-1', threadId: THREAD_ID, turnId: 'turn-1' } + }) + prompts.bindJournalItemId('journal-prompt', THREAD_ID, 'codex-item-1', 'turn-1') + const claim = prompts.claimBound('journal-prompt') + if (!claim) { + throw new Error('expected prompt claim') + } + + prompts.clearTurn(THREAD_ID, 'turn-1') + + expect(prompts.ownsClaim(claim)).toBe(false) + expect(prompts.find('journal-prompt')).toBeNull() + }) +}) diff --git a/src/main/codex/codex-structured-prompt-ownership.ts b/src/main/codex/codex-structured-prompt-ownership.ts new file mode 100644 index 00000000000..28d060d955f --- /dev/null +++ b/src/main/codex/codex-structured-prompt-ownership.ts @@ -0,0 +1,103 @@ +import { + AgentSessionPromptUnavailableError, + type StructuredAgentSessionAdapter +} from '../native-chat/agent-session-wire/structured-agent-session-adapter' +import type { StructuredSessionCompaction } from '../native-chat/agent-session-wire/structured-session-compaction' +import { answerCodexPrompt } from './codex-structured-prompt-replies' +import { requireLiveCodexSession, type CodexSession } from './codex-structured-session-state' +import type { CodexStructuredTurnCancellation } from './codex-structured-turn-cancellation' + +type CancelInput = Parameters[0] +type AnswerInput = Parameters[0] + +export async function cancelCodexStructuredTurn(input: { + request: CancelInput + sessions: Map + compactions: StructuredSessionCompaction + cancellation: CodexStructuredTurnCancellation +}): Promise<{ cancelled: boolean }> { + const { request, sessions, compactions, cancellation } = input + const session = requireLiveCodexSession(sessions, request.sessionId) + const turnId = compactions.providerTurnId(request.sessionId, request.turnId) + if (!turnId) { + return { cancelled: false } + } + const prompt = request.prompt + if (!prompt) { + return cancellation.cancel(session, session.threadId, turnId) + } + if (session.fence !== request.fence) { + return { cancelled: false } + } + const acquisitionGeneration = session.acquisitionGeneration + const claim = session.prompts.claimBound(prompt.itemId) + const promptTurnId = claim?.prompt.turnId + if (!claim || !promptTurnId) { + if (claim) { + session.prompts.releaseClaim(claim) + } + return { cancelled: false } + } + const isCurrent = (): boolean => + sessions.get(request.sessionId) === session && + !session.ended && + session.fence === request.fence && + session.acquisitionGeneration === acquisitionGeneration && + compactions.providerTurnId(request.sessionId, request.turnId) === turnId && + session.prompts.ownsBoundClaim(claim, prompt.itemId, claim.prompt.threadId, promptTurnId) + let interruptConfirmed = false + try { + const result = await cancellation.cancel( + session, + claim.prompt.threadId, + promptTurnId, + isCurrent, + () => { + interruptConfirmed = true + return session.translator?.cancelPrompt(prompt.itemId) ?? { accepted: true } + } + ) + if (!result.cancelled) { + session.prompts.releaseClaim(claim) + } + return result + } catch (error) { + if (!interruptConfirmed) { + session.prompts.releaseClaim(claim) + } + throw error + } +} + +export async function answerCodexStructuredPrompt(input: { + request: AnswerInput + sessions: Map +}): Promise { + const { request, sessions } = input + const session = sessions.get(request.sessionId) + if (!session || session.ended || session.fence !== request.fence) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + const acquisitionGeneration = session.acquisitionGeneration + const claim = session.prompts.claim(request.itemId, request.kind) + if (!claim) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + try { + await request.commit() + if ( + sessions.get(request.sessionId) !== session || + session.ended || + session.fence !== request.fence || + session.acquisitionGeneration !== acquisitionGeneration || + !session.prompts.ownsClaim(claim) + ) { + throw new AgentSessionPromptUnavailableError(request.itemId) + } + session.translator?.resolvePrompt(request.itemId) + answerCodexPrompt(session.prompts, session.connection, claim, request.optionId) + } catch (error) { + session.prompts.releaseClaim(claim) + throw error + } +} diff --git a/src/main/codex/codex-structured-prompt-replies.test.ts b/src/main/codex/codex-structured-prompt-replies.test.ts index 831124c1c48..e575f27632b 100644 --- a/src/main/codex/codex-structured-prompt-replies.test.ts +++ b/src/main/codex/codex-structured-prompt-replies.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' +import { AGENT_SESSION_ID_MAX_LENGTH } from '../../shared/agent-session-wire' import { applyCodexPromptAnswer, CodexPromptRegistry, + MAX_CODEX_PROMPT_REGISTRY_BYTES, MAX_CODEX_PROMPT_REGISTRY_ENTRIES, codexJournalPromptIdPart, decodeCodexQuestionOptionId, @@ -94,6 +96,79 @@ describe('CodexPromptRegistry', () => { expect(registry.find('codex-item-1')).toBeNull() }) + it('clears only prompts belonging to a settled turn', () => { + const registry = new CodexPromptRegistry() + registry.register({ + id: 1, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'root-item', threadId: 'thread-1' } + }) + registry.register({ + id: 2, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'other-item', threadId: 'thread-1', turnId: 'turn-2' } + }) + registry.register({ + id: 3, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'other-thread-item', threadId: 'thread-2', turnId: 'turn-1' } + }) + registry.bindJournalItemId('journal-root', 'thread-1', 'root-item', 'turn-1') + + registry.clearTurn('thread-1', 'turn-1') + + expect(registry.find('root-item')).toBeNull() + expect(registry.find('journal-root')).toBeNull() + expect(registry.find('other-item')?.requestId).toBe(2) + expect(registry.find('other-thread-item')?.requestId).toBe(3) + }) + + it('retains a bounded cleanup identity for an unaddressable backfilled turn id', () => { + const registry = new CodexPromptRegistry() + const turnId = 'turn-'.padEnd(MAX_CODEX_PROMPT_REGISTRY_BYTES + 1, 'x') + registry.register({ + id: 1, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'root-item', threadId: 'thread-1' } + }) + + registry.bindJournalItemId('journal-root', 'thread-1', 'root-item', turnId) + + expect(registry.bytes).toBeLessThanOrEqual(MAX_CODEX_PROMPT_REGISTRY_BYTES) + registry.clearTurn('thread-1', turnId) + expect(registry.find('journal-root')).toBeNull() + }) + + it('reserves enough bytes for a wire-valid multibyte backfilled turn id', () => { + const registry = new CodexPromptRegistry() + registry.register({ + id: 1, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'root-item', threadId: 'thread-1' } + }) + const reservedBytes = registry.bytes + const turnId = '界'.repeat(AGENT_SESSION_ID_MAX_LENGTH) + + registry.bindJournalItemId('journal-root', 'thread-1', 'root-item', turnId) + + expect(registry.find('journal-root')?.turnId).toBe(turnId) + expect(registry.bytes).toBe(reservedBytes) + expect(registry.bytes).toBeLessThanOrEqual(MAX_CODEX_PROMPT_REGISTRY_BYTES) + }) + + it('rejects a request turn id beyond the wire identity bound', () => { + const registry = new CodexPromptRegistry() + const turnId = 'x'.repeat(AGENT_SESSION_ID_MAX_LENGTH + 1) + const prompt = registry.register({ + id: 1, + method: 'item/commandExecution/requestApproval', + params: { itemId: 'root-item', threadId: 'thread-1', turnId } + }) + + expect(prompt).toBeNull() + expect(registry.bytes).toBe(0) + }) + it('addresses a prompt by its journal item id once bound, and forgets both', () => { const registry = new CodexPromptRegistry() const prompt = registry.register(userInputRequest(['q1'])) diff --git a/src/main/codex/codex-structured-prompt-replies.ts b/src/main/codex/codex-structured-prompt-replies.ts index 1c96a95c5f2..6e742bf827c 100644 --- a/src/main/codex/codex-structured-prompt-replies.ts +++ b/src/main/codex/codex-structured-prompt-replies.ts @@ -1,13 +1,11 @@ import type { CodexAppServerConnection } from './codex-app-server-connection' +import { CODEX_PROMPT_MAX_ANSWER_BYTES } from './codex-prompt-registry-bounds' import { - CODEX_PROMPT_MAX_ANSWER_BYTES, - MAX_CODEX_PROMPT_JOURNAL_BINDINGS, - MAX_CODEX_PROMPT_REGISTRY_BYTES, - MAX_CODEX_PROMPT_REGISTRY_ENTRIES, - codexJournalPromptIdPart, - readQuestionIds, - readQuestionOptionAnswers -} from './codex-prompt-registry-bounds' + CODEX_USER_INPUT_METHOD, + type CodexPendingPrompt, + type CodexPromptClaim, + type CodexPromptRegistry +} from './codex-prompt-registry' export { codexJournalPromptIdPart, MAX_CODEX_PROMPT_REGISTRY_ENTRIES, @@ -15,37 +13,23 @@ export { MAX_CODEX_PROMPT_REGISTRY_BYTES, encodeCodexJournalQuestionOptionId } from './codex-prompt-registry-bounds' - -// Codex asks for approvals and tool input by sending JSON-RPC REQUESTS back to -// Orca, and the turn blocks until each one is answered. The journal answers them -// much later, through a durable item id, so this module holds the live request -// ids and turns a chosen option back into the reply payload Codex expects. - -export const CODEX_COMMAND_APPROVAL_METHOD = 'item/commandExecution/requestApproval' -export const CODEX_FILE_CHANGE_APPROVAL_METHOD = 'item/fileChange/requestApproval' -export const CODEX_USER_INPUT_METHOD = 'item/tool/requestUserInput' +export { + CODEX_COMMAND_APPROVAL_METHOD, + CODEX_FILE_CHANGE_APPROVAL_METHOD, + CODEX_USER_INPUT_METHOD, + CodexPromptRegistry, + isCodexPromptMethod, + type CodexPendingPrompt, + type CodexPromptClaim +} from './codex-prompt-registry' /** The decisions Codex accepts for both approval requests. Anything else is a * client-supplied option id that never came from a Codex prompt. */ export const CODEX_APPROVAL_DECISIONS = ['accept', 'acceptForSession', 'decline', 'cancel'] as const export type CodexApprovalDecision = (typeof CODEX_APPROVAL_DECISIONS)[number] -export type CodexPendingPrompt = { - requestId: number | string - method: string - threadId: string - turnId: string | null - codexItemId: string - /** What addresses this prompt. One tool item can ask more than once — a shell - * bridge re-asks per command under the same `itemId` — so the request's own - * `approvalId` is the identity whenever Codex sends one. */ - promptKey: string - /** One entry per question for a user-input request; empty for an approval. */ - questionIds: readonly string[] - /** Journal-facing ids can be bounded; replies still need Codex's exact ids. */ - questionIdAliases: ReadonlyMap - optionAnswers: ReadonlyMap - answers: Map +function isCodexApprovalDecision(optionId: string): optionId is CodexApprovalDecision { + return CODEX_APPROVAL_DECISIONS.some((decision) => decision === optionId) } /** A user-input request can carry several questions but takes ONE reply, so an @@ -71,206 +55,6 @@ export function decodeCodexQuestionOptionId( } } -function readString(params: unknown, key: string): string | null { - if (typeof params !== 'object' || params === null) { - return null - } - const value = (params as Record)[key] - return typeof value === 'string' && value.length > 0 ? value : null -} - -export function isCodexPromptMethod(method: string): boolean { - return ( - method === CODEX_COMMAND_APPROVAL_METHOD || - method === CODEX_FILE_CHANGE_APPROVAL_METHOD || - method === CODEX_USER_INPUT_METHOD - ) -} - -/** - * Live Codex prompt requests for one session, addressable by the journal item - * id the client will eventually answer with. The binding is registered by the - * translation module, because only it knows which journal item a Codex item - * became. - */ -export class CodexPromptRegistry { - private readonly byAddress = new Map() - /** Journal item id to thread-scoped prompt address. */ - private readonly journalItemIds = new Map() - /** Bound prompts survive LRU eviction of the lookup window until answered. */ - private readonly boundPrompts = new Map() - - get sizes(): { prompts: number; journalBindings: number } { - return { prompts: this.byAddress.size, journalBindings: this.journalItemIds.size } - } - - get bytes(): number { - return this.retainedPromptBytes() - } - - private promptBytes(prompt: CodexPendingPrompt): number { - let bytes = 0 - for (const value of [ - prompt.threadId, - prompt.turnId ?? '', - prompt.codexItemId, - prompt.promptKey - ]) { - bytes += Buffer.byteLength(value, 'utf8') - } - for (const id of prompt.questionIds) { - bytes += Buffer.byteLength(id, 'utf8') - } - for (const entry of prompt.optionAnswers.values()) { - bytes += Buffer.byteLength(entry.questionId, 'utf8') + Buffer.byteLength(entry.answer, 'utf8') - } - for (const value of prompt.answers.values()) { - bytes += Buffer.byteLength(value, 'utf8') - } - return bytes - } - - private retainedPromptBytes(): number { - const prompts = new Set([...this.byAddress.values(), ...this.boundPrompts.values()]) - return [...prompts].reduce((total, prompt) => total + this.promptBytes(prompt), 0) - } - - private trim(): void { - while (this.byAddress.size > MAX_CODEX_PROMPT_REGISTRY_ENTRIES) { - const oldest = this.byAddress.values().next().value as CodexPendingPrompt | undefined - if (!oldest) { - break - } - const address = this.address(oldest.threadId, oldest.promptKey) - this.byAddress.delete(address) - } - while (this.journalItemIds.size > MAX_CODEX_PROMPT_JOURNAL_BINDINGS) { - const oldest = this.journalItemIds.keys().next().value as string | undefined - if (!oldest) { - break - } - this.journalItemIds.delete(oldest) - this.boundPrompts.delete(oldest) - } - } - - private address(threadId: string, promptKey: string): string { - return `${encodeURIComponent(threadId)}:${encodeURIComponent(promptKey)}` - } - - /** Returns null for a request this build does not model, so the caller can - * refuse it instead of leaving Codex blocked on an answer forever. */ - register(request: { - id: number | string - method: string - params: unknown - }): CodexPendingPrompt | null { - const codexItemId = readString(request.params, 'itemId') - const threadId = readString(request.params, 'threadId') - if (!isCodexPromptMethod(request.method) || !codexItemId || !threadId) { - return null - } - const questionIds = - request.method === CODEX_USER_INPUT_METHOD ? readQuestionIds(request.params) : [] - if (questionIds === null) { - return null - } - const optionAnswers = - request.method === CODEX_USER_INPUT_METHOD - ? readQuestionOptionAnswers(request.params) - : new Map() - if (optionAnswers === null) { - return null - } - const prompt: CodexPendingPrompt = { - requestId: request.id, - method: request.method, - threadId, - turnId: readString(request.params, 'turnId'), - codexItemId, - promptKey: readString(request.params, 'approvalId') ?? codexItemId, - questionIds, - questionIdAliases: - request.method === CODEX_USER_INPUT_METHOD - ? new Map(questionIds.map((id) => [codexJournalPromptIdPart(id), id])) - : new Map(), - optionAnswers, - answers: new Map() - } - const promptBytes = this.promptBytes(prompt) - if (promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES) { - return null - } - while ( - this.retainedPromptBytes() + promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES && - this.byAddress.size > 0 - ) { - const oldest = this.byAddress.values().next().value as CodexPendingPrompt | undefined - if (!oldest) { - break - } - this.byAddress.delete(this.address(oldest.threadId, oldest.promptKey)) - } - if (this.retainedPromptBytes() + promptBytes > MAX_CODEX_PROMPT_REGISTRY_BYTES) { - return null - } - const address = this.address(prompt.threadId, prompt.promptKey) - this.byAddress.delete(address) - this.byAddress.set(address, prompt) - this.trim() - return prompt - } - - /** Called by the translation module once the prompt has a journal id. */ - bindJournalItemId(journalItemId: string, threadId: string, promptKey: string): void { - const existing = this.journalItemIds.get(journalItemId) - if (existing) { - this.boundPrompts.delete(journalItemId) - } - this.journalItemIds.delete(journalItemId) - const address = this.address(threadId, promptKey) - const prompt = this.byAddress.get(address) - if (!prompt) { - return - } - this.journalItemIds.set(journalItemId, address) - this.boundPrompts.set(journalItemId, prompt) - this.trim() - } - - /** Falls back to treating the id as a prompt key, which is what it is before - * any binding exists. */ - find(journalItemId: string): CodexPendingPrompt | null { - const address = this.journalItemIds.get(journalItemId) - if (address) { - return this.boundPrompts.get(journalItemId) ?? this.byAddress.get(address) ?? null - } - const matches = [...this.byAddress.values()].filter( - (prompt) => prompt.promptKey === journalItemId - ) - return matches.length === 1 ? matches[0]! : null - } - - forget(prompt: CodexPendingPrompt): void { - const address = this.address(prompt.threadId, prompt.promptKey) - if (this.byAddress.get(address) === prompt) { - this.byAddress.delete(address) - } - for (const [journalItemId, boundPrompt] of this.boundPrompts) { - if (boundPrompt === prompt) { - this.journalItemIds.delete(journalItemId) - this.boundPrompts.delete(journalItemId) - } - } - } - - clear(): void { - this.byAddress.clear() - this.journalItemIds.clear() - this.boundPrompts.clear() - } -} - /** * Records one answer and returns the reply payload once the request is fully * answered. A multi-question user-input request stays pending until every @@ -281,7 +65,7 @@ export function applyCodexPromptAnswer( optionId: string ): Record | null { if (prompt.method !== CODEX_USER_INPUT_METHOD) { - if (!(CODEX_APPROVAL_DECISIONS as readonly string[]).includes(optionId)) { + if (!isCodexApprovalDecision(optionId)) { throw new Error(`${optionId} is not a Codex approval decision`) } return { decision: optionId } @@ -305,7 +89,11 @@ export function applyCodexPromptAnswer( } const answers: Record = {} for (const id of prompt.questionIds) { - answers[id] = { answers: [prompt.answers.get(id) as string] } + const answer = prompt.answers.get(id) + if (answer === undefined) { + return null + } + answers[id] = { answers: [answer] } } return { answers } } @@ -315,15 +103,16 @@ export function applyCodexPromptAnswer( export function answerCodexPrompt( registry: CodexPromptRegistry, connection: Pick, - itemId: string, + claim: CodexPromptClaim, optionId: string ): void { - const prompt = registry.find(itemId) - if (!prompt) { - throw new Error(`codex app-server is no longer waiting on ${itemId}`) + if (!registry.ownsClaim(claim)) { + throw new Error(`codex app-server is no longer waiting on ${claim.itemId}`) } + const prompt = claim.prompt const reply = applyCodexPromptAnswer(prompt, optionId) if (reply === null) { + registry.releaseClaim(claim) return } // Forget first: a second answer must find nothing rather than reply twice. diff --git a/src/main/codex/codex-structured-provider-events.ts b/src/main/codex/codex-structured-provider-events.ts index 989232ff1b2..69b4cb0d392 100644 --- a/src/main/codex/codex-structured-provider-events.ts +++ b/src/main/codex/codex-structured-provider-events.ts @@ -3,7 +3,7 @@ import { disposeCodexServerRequest } from './codex-server-request-disposition' import type { CodexJournalTranslationAdmission } from './codex-structured-journal-translation' import * as codexRewind from './codex-structured-rewind' import type { CodexSession, CodexStructuredSessionEvent } from './codex-structured-session-state' -import { readCodexThreadId, readCodexTurnId } from './codex-structured-thread-facts' +import { readCodexThreadId } from './codex-structured-thread-facts' import type { CodexStructuredTurnCancellation } from './codex-structured-turn-cancellation' type EmitCodexEvent = ( @@ -41,10 +41,9 @@ export function deliverCodexNotification( return { accepted: true } } const threadId = readCodexThreadId(params) ?? session.threadId - const turnId = - method === 'turn/started' && threadId === session.threadId ? readCodexTurnId(params) : null - const turnWaiter = turnId ? session.turnIdWaiters[0] : undefined - const admission = emit(session, { + // Dispatch identity settles on the user-message echo inside the translator, + // which is where the ordinal a replay will compute is minted. + return emit(session, { type: 'notification', sessionId, threadId, @@ -52,13 +51,6 @@ export function deliverCodexNotification( params, ...(observedAt !== undefined ? { observedAt } : {}) }) - if (method === 'turn/started' && threadId === session.threadId) { - if (admission.accepted && turnId && session.turnIdWaiters[0] === turnWaiter) { - session.turnIdWaiters.shift() - turnWaiter?.(turnId) - } - } - return admission } export function deliverCodexServerRequest( diff --git a/src/main/codex/codex-structured-session-acquire.ts b/src/main/codex/codex-structured-session-acquire.ts index b5cdd2caf4e..c191e675973 100644 --- a/src/main/codex/codex-structured-session-acquire.ts +++ b/src/main/codex/codex-structured-session-acquire.ts @@ -10,6 +10,7 @@ import { } from './codex-structured-acquisition-lifecycle' import { CodexBackgroundTaskTracker } from './codex-background-task-tracker' import { CodexSubagentExecutions } from './codex-subagent-executions' +import { createCodexDispatchEchoes } from './codex-structured-dispatch-echo' import { createCodexJournalTranslator } from './codex-structured-journal-translation' import { openCodexAppServerConnection } from './codex-app-server-connection' import { codexProcessIdentity, codexProviderHandleLink } from './codex-structured-owner-identity' @@ -81,6 +82,7 @@ export async function acquireCodexStructuredSession(input: { ? acquireInput.identity.providerHandle.threadId : null const subagentExecutions = new CodexSubagentExecutions() + const dispatchEchoes = createCodexDispatchEchoes() const translator = acquireInput.events ? createCodexJournalTranslator({ sink: acquireInput.events, @@ -88,8 +90,16 @@ export async function acquireCodexStructuredSession(input: { ...(deps.now ? { now: deps.now } : {}), primaryThreadId: () => primaryThreadId, subagentExecutions, - bindPromptItemId: (journalItemId, threadId, promptKey) => - acquisition.prompts.bindJournalItemId(journalItemId, threadId, promptKey) + bindPromptItemId: (journalItemId, threadId, promptKey, turnId) => + acquisition.prompts.bindJournalItemId(journalItemId, threadId, promptKey, turnId), + clearPromptTurn: (threadId, turnId) => acquisition.prompts.clearTurn(threadId, turnId), + onUserMessageEcho: (clientMessageId, providerIdentity) => { + // Only a send THIS session admitted; an echo from history restore or + // another client names no submission of ours to settle. + if (dispatchEchoes.settle(clientMessageId)) { + deps.onDispatchSettledLate?.({ sessionId, clientMessageId, providerIdentity }) + } + } }) : null const open = deps.openConnection ?? openCodexAppServerConnection @@ -229,7 +239,7 @@ export async function acquireCodexStructuredSession(input: { options, reportedOptions: reportedCodexThreadOptions(opened), fastModeTierByModel: fastModeCatalog?.fastModeTierByModel ?? new Map(), - turnIdWaiters: [], + dispatchEchoes, translator, backgroundTasks: new CodexBackgroundTaskTracker(opened.threadId, subagentExecutions), forceCloseUnexpected: (reason) => diff --git a/src/main/codex/codex-structured-session-adapter-lifecycle.test.ts b/src/main/codex/codex-structured-session-adapter-lifecycle.test.ts index b2c579770fd..fc458304dc8 100644 --- a/src/main/codex/codex-structured-session-adapter-lifecycle.test.ts +++ b/src/main/codex/codex-structured-session-adapter-lifecycle.test.ts @@ -153,7 +153,8 @@ describe('CodexStructuredSessionAdapter lifecycle', () => { itemId: 'codex-item-1', kind: 'approval', optionId: 'accept', - fence: 1 + fence: 1, + commit: async () => undefined }) ).rejects.toThrow('no longer waiting on') diff --git a/src/main/codex/codex-structured-session-adapter.test.ts b/src/main/codex/codex-structured-session-adapter.test.ts index b32c7e69a1b..e04e18ccd08 100644 --- a/src/main/codex/codex-structured-session-adapter.test.ts +++ b/src/main/codex/codex-structured-session-adapter.test.ts @@ -139,7 +139,8 @@ describe('CodexStructuredSessionAdapter.acquire', () => { itemId: 'codex-item-early', kind: 'approval', optionId: 'accept', - fence: 7 + fence: 7, + commit: async () => undefined }) expect(codex.connections[0].replies).toEqual([{ id: 5, result: { decision: 'accept' } }]) }) @@ -315,7 +316,7 @@ describe('CodexStructuredSessionAdapter.acquire', () => { }) describe('CodexStructuredSessionAdapter.dispatch', () => { - it('accepts a turn Codex names in its response', async () => { + it('admits a send as soon as Codex owns it', async () => { const codex = fakeCodex({ 'turn/start': () => ({ turn: { id: 'turn-1' } }) }) const adapter = await acquired(codex) @@ -334,10 +335,9 @@ describe('CodexStructuredSessionAdapter.dispatch', () => { fence: 7 }) - expect(outcome).toEqual({ - state: 'accepted', - providerIdentity: { provider: 'codex', threadId: THREAD_ID, turnId: 'turn-1', ordinal: 0 } - }) + // Identity is not knowable here: a send coalesced into a running turn shares + // that turn's id, so the echo settles which message landed where. + expect(outcome).toEqual({ state: 'admitted' }) expect(codex.connections[0].calls[1].params).toEqual({ threadId: THREAD_ID, clientUserMessageId: 'client-1', @@ -349,7 +349,7 @@ describe('CodexStructuredSessionAdapter.dispatch', () => { }) }) - it('accepts a turn named only by the notification that raced the ack', async () => { + it('admits a send on a build whose turn/start answers before the turn is named', async () => { const codex = fakeCodex() const events: CodexStructuredSessionEvent[] = [] const adapter = await acquired(codex, {}, events) @@ -368,8 +368,7 @@ describe('CodexStructuredSessionAdapter.dispatch', () => { fence: 7 }) - expect(outcome).toMatchObject({ state: 'accepted' }) - expect(outcome).toMatchObject({ providerIdentity: { turnId: 'turn-late' } }) + expect(outcome).toEqual({ state: 'admitted' }) expect(events.at(-1)).toMatchObject({ type: 'notification', method: 'turn/started' }) }) @@ -393,10 +392,7 @@ describe('CodexStructuredSessionAdapter.dispatch', () => { fence: 7 }) - expect(outcome).toEqual({ - state: 'accepted', - providerIdentity: { provider: 'codex', threadId: THREAD_ID, turnId: 'turn-root', ordinal: 0 } - }) + expect(outcome).toEqual({ state: 'admitted' }) // Each event carries the thread it actually came from, so the journal can // keep a subagent's turn out of the root conversation. expect(events.map((event) => (event.type === 'notification' ? event.threadId : null))).toEqual([ @@ -405,29 +401,6 @@ describe('CodexStructuredSessionAdapter.dispatch', () => { ]) }) - it('settles unknown rather than failed when Codex never names the turn', async () => { - vi.useFakeTimers() - try { - const codex = fakeCodex() - const adapter = await acquired(codex) - - const dispatching = adapter.dispatch({ - sessionId: 'session-1', - clientMessageId: 'client-1', - body: USER_MESSAGE, - fence: 7 - }) - await vi.advanceTimersByTimeAsync(10_000) - - expect(await dispatching).toEqual({ - state: 'unknown', - reason: 'codex app-server started a turn it did not name in time' - }) - } finally { - vi.useRealTimers() - } - }) - it('rejects only when Codex answered and declined', async () => { const codex = fakeCodex({ 'turn/start': () => { @@ -524,7 +497,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex:thread-abc:turn-1:3', kind: 'approval', optionId: 'accept', - fence: 7 + fence: 7, + commit: async () => undefined }) expect(events.at(-1)).toMatchObject({ type: 'prompt', codexItemId: 'codex-item-1' }) @@ -536,7 +510,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex:thread-abc:turn-1:3', kind: 'approval', optionId: 'decline', - fence: 7 + fence: 7, + commit: async () => undefined }) ).rejects.toThrow('no longer waiting on') expect(codex.connections[0].replies).toHaveLength(1) @@ -576,7 +551,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex-item-1', kind: 'approval', optionId: 'accept', - fence: 7 + fence: 7, + commit: async () => undefined }) ).rejects.toThrow('no longer waiting on') }) @@ -662,7 +638,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId, kind: 'approval', optionId, - fence: 7 + fence: 7, + commit: async () => undefined }) } @@ -688,7 +665,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex-item-1', kind: 'approval', optionId: 'yolo', - fence: 7 + fence: 7, + commit: async () => undefined }) ).rejects.toThrow('is not a Codex approval decision') expect(codex.connections[0].replies).toEqual([]) @@ -716,7 +694,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex-item-2', kind: 'question', optionId: encodeCodexQuestionOptionId('q1', 'yes'), - fence: 7 + fence: 7, + commit: async () => undefined }) expect(codex.connections[0].replies).toEqual([]) @@ -725,7 +704,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex-item-2', kind: 'question', optionId: encodeCodexQuestionOptionId('q2', 'no'), - fence: 7 + fence: 7, + commit: async () => undefined }) expect(codex.connections[0].replies).toEqual([ @@ -760,7 +740,8 @@ describe('CodexStructuredSessionAdapter prompts', () => { itemId: 'codex-item-gone', kind: 'approval', optionId: 'accept', - fence: 7 + fence: 7, + commit: async () => undefined }) ).rejects.toThrow('no longer waiting on codex-item-gone') }) diff --git a/src/main/codex/codex-structured-session-adapter.ts b/src/main/codex/codex-structured-session-adapter.ts index 061626f9724..d7d8b2f6ad1 100644 --- a/src/main/codex/codex-structured-session-adapter.ts +++ b/src/main/codex/codex-structured-session-adapter.ts @@ -13,7 +13,6 @@ import type { StructuredAgentSessionSetOptionInput } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import type { CodexJournalTranslationAdmission } from './codex-structured-journal-translation' -import { answerCodexPrompt } from './codex-structured-prompt-replies' import { dispatchCodexTurn, isCodexTurnOptionKey } from './codex-structured-turn-start' import { supportsCodexStructuredLocation } from './codex-structured-location-support' import { CodexStructuredSessionTeardown } from './codex-structured-session-teardown' @@ -37,6 +36,10 @@ import { import { CodexStructuredTurnCancellation } from './codex-structured-turn-cancellation' import { createCodexStructuredNotificationRetry } from './codex-structured-notification-retry' import { acquireCodexStructuredSession } from './codex-structured-session-acquire' +import { + answerCodexStructuredPrompt, + cancelCodexStructuredTurn +} from './codex-structured-prompt-ownership' export type { CodexStructuredLaunch, @@ -179,10 +182,21 @@ export class CodexStructuredSessionAdapter implements StructuredAgentSessionAdap sessionId ) => this.sessions.get(sessionId)?.backgroundTasks.state - bindPromptItemId = (sessionId: string, journalItemId: string, promptKey: string): void => + bindPromptItemId = ( + sessionId: string, + journalItemId: string, + promptKey: string, + turnId?: string | null, + threadId?: string + ): void => this.sessions .get(sessionId) - ?.prompts.bindJournalItemId(journalItemId, this.session(sessionId).threadId, promptKey) + ?.prompts.bindJournalItemId( + journalItemId, + threadId ?? this.session(sessionId).threadId, + promptKey, + turnId + ) async dispatch(input: { sessionId: string @@ -200,15 +214,13 @@ export class CodexStructuredSessionAdapter implements StructuredAgentSessionAdap } } - async cancelTurn(input: { - sessionId: string - turnId: string - fence: number - }): Promise<{ cancelled: boolean }> { - const session = this.session(input.sessionId) - const turnId = this.compactions.providerTurnId(input.sessionId, input.turnId) - return turnId ? this.turnCancellation.cancel(session, turnId) : { cancelled: false } - } + cancelTurn: StructuredAgentSessionAdapter['cancelTurn'] = (request) => + cancelCodexStructuredTurn({ + request, + sessions: this.sessions, + compactions: this.compactions, + cancellation: this.turnCancellation + }) rewindSupport: NonNullable = (sessionId) => this.sessions.get(sessionId)?.historyMode === 'legacy' @@ -246,17 +258,8 @@ export class CodexStructuredSessionAdapter implements StructuredAgentSessionAdap ) } - async answerPrompt(input: { - sessionId: string - itemId: string - kind: 'approval' | 'question' - optionId: string - fence: number - }): Promise { - const session = this.session(input.sessionId) - answerCodexPrompt(session.prompts, session.connection, input.itemId, input.optionId) - session.translator?.resolvePrompt(input.itemId) - } + answerPrompt: StructuredAgentSessionAdapter['answerPrompt'] = (request) => + answerCodexStructuredPrompt({ request, sessions: this.sessions }) async setOption( input: StructuredAgentSessionSetOptionInput diff --git a/src/main/codex/codex-structured-session-cancel.test.ts b/src/main/codex/codex-structured-session-cancel.test.ts index 2ea81d44786..1ac807a5ccd 100644 --- a/src/main/codex/codex-structured-session-cancel.test.ts +++ b/src/main/codex/codex-structured-session-cancel.test.ts @@ -257,9 +257,8 @@ describe('CodexStructuredSessionAdapter.cancelTurn', () => { body: USER_MESSAGE, fence: 7 }) - ).resolves.toMatchObject({ - state: 'accepted', - providerIdentity: { turnId: 'turn-2' } + ).resolves.toEqual({ + state: 'admitted' }) }) diff --git a/src/main/codex/codex-structured-session-close.test.ts b/src/main/codex/codex-structured-session-close.test.ts index 58c5bc5f50a..17f3aecf671 100644 --- a/src/main/codex/codex-structured-session-close.test.ts +++ b/src/main/codex/codex-structured-session-close.test.ts @@ -1,3 +1,4 @@ +import { createCodexDispatchEchoes } from './codex-structured-dispatch-echo' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import type { @@ -11,6 +12,7 @@ import { } from './codex-structured-session-adapter' import { handleCodexSessionExit } from './codex-structured-session-close' import { CodexBackgroundTaskTracker } from './codex-background-task-tracker' +import { CodexPromptRegistry } from './codex-structured-prompt-replies' import type { CodexSession } from './codex-structured-session-state' import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import { StructuredAgentSessionAdapterRouter } from '../native-chat/agent-session-wire/structured-agent-session-adapter-router' @@ -84,13 +86,13 @@ describe('Codex structured session close lifecycle', () => { respondWithError: () => {}, close: async () => true } - const prompts = { clear: vi.fn() } as unknown as CodexSession['prompts'] + const prompts = new CodexPromptRegistry() + const clearPrompts = vi.spyOn(prompts, 'clear') const translator = { handle: vi.fn().mockReturnValueOnce({ accepted: false, reason: 'backpressure' as const }), dispose: vi.fn() } as unknown as NonNullable - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the literal supplies every CodexSession field the close path reads; the rest are unused by it. - const session = { + const session: CodexSession = { connection, backgroundTasks: new CodexBackgroundTaskTracker('thread-1'), ended: false, @@ -103,9 +105,9 @@ describe('Codex structured session close lifecycle', () => { options: new Map(), reportedOptions: {}, fastModeTierByModel: new Map(), - turnIdWaiters: [], + dispatchEchoes: createCodexDispatchEchoes(), translator - } as CodexSession + } const sessions = new Map([['session-1', session]]) const onEvent = vi.fn() @@ -120,7 +122,7 @@ describe('Codex structured session close lifecycle', () => { }) ).toBe(true) expect(session.ended).toBe(true) - expect(prompts.clear).toHaveBeenCalledOnce() + expect(clearPrompts).toHaveBeenCalledOnce() expect(onEvent).toHaveBeenCalledOnce() expect(translator.dispose).toHaveBeenCalledOnce() expect(onEvent.mock.calls[0]?.[0]).toMatchObject({ diff --git a/src/main/codex/codex-structured-session-close.ts b/src/main/codex/codex-structured-session-close.ts index af814c51d9b..2058f86ce85 100644 --- a/src/main/codex/codex-structured-session-close.ts +++ b/src/main/codex/codex-structured-session-close.ts @@ -47,6 +47,9 @@ export function handleCodexSessionExit(input: { event.settlementRetryRequired = true } session.ended = true + // Nothing can echo for this child any more; the journal's pending-submission + // recovery is what settles the sends these were armed for. + session.dispatchEchoes.clear() session.backgroundTasks.clear() input.onBackgroundTasksChanged?.(input.sessionId, null) session.unbindReadingControl?.() diff --git a/src/main/codex/codex-structured-session-options.test.ts b/src/main/codex/codex-structured-session-options.test.ts index 4e9abfc17b5..0a8bf41688c 100644 --- a/src/main/codex/codex-structured-session-options.test.ts +++ b/src/main/codex/codex-structured-session-options.test.ts @@ -1,3 +1,4 @@ +import { createCodexDispatchEchoes } from './codex-structured-dispatch-echo' import { describe, expect, it, vi } from 'vitest' import type { CodexAppServerConnection } from './codex-app-server-connection' import { CodexAcquisitionWindow } from './codex-structured-acquisition-window' @@ -34,7 +35,7 @@ function optionSession(request: CodexAppServerConnection['request']): CodexSessi options: new Map(), reportedOptions: { model: 'gpt-live', effort: 'high' }, fastModeTierByModel: new Map(), - turnIdWaiters: [], + dispatchEchoes: createCodexDispatchEchoes(), translator: null } } diff --git a/src/main/codex/codex-structured-session-state.ts b/src/main/codex/codex-structured-session-state.ts index 625e222ecfb..df66d436bf1 100644 --- a/src/main/codex/codex-structured-session-state.ts +++ b/src/main/codex/codex-structured-session-state.ts @@ -1,4 +1,7 @@ -import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import type { + AgentJournalItemIdentity, + AgentSessionJournalIdentity +} from '../../shared/agent-session-journal-types' import { randomUUID } from 'node:crypto' import { cancelProcessAcquisition } from '../../shared/child-process/cancel-process-acquisition' import type { @@ -6,6 +9,7 @@ import type { openCodexAppServerConnection } from './codex-app-server-connection' import { CodexAcquisitionWindow } from './codex-structured-acquisition-window' +import type { CodexDispatchEchoes } from './codex-structured-dispatch-echo' import type { AgentSessionBackgroundTaskState } from '../../shared/agent-session-wire' import type { CodexBackgroundTaskTracker } from './codex-background-task-tracker' import type { CodexJournalTranslator } from './codex-structured-journal-translation' @@ -58,6 +62,12 @@ export type CodexStructuredSessionAdapterDeps = { sessionId: string, state: AgentSessionBackgroundTaskState | null ) => void + /** Identity for a send admitted earlier, once Codex echoes the user message. */ + onDispatchSettledLate?: (input: { + sessionId: string + clientMessageId: string + providerIdentity: AgentJournalItemIdentity + }) => void openConnection?: typeof openCodexAppServerConnection readProcessStartTime?: (pid: number) => Promise mintLinkId?: () => string @@ -94,7 +104,8 @@ export type CodexSession = { } /** Exact provider-advertised Fast request value for each discovered model. */ fastModeTierByModel: Map - turnIdWaiters: ((turnId: string) => void)[] + /** Sends whose identity is still to be settled by the provider echo. */ + dispatchEchoes: CodexDispatchEchoes translator: CodexJournalTranslator | null /** Ephemeral roster behind the background-tasks strip; never durable state. */ backgroundTasks: CodexBackgroundTaskTracker diff --git a/src/main/codex/codex-structured-turn-cancellation.ts b/src/main/codex/codex-structured-turn-cancellation.ts index 97257d54fa4..4418da81057 100644 --- a/src/main/codex/codex-structured-turn-cancellation.ts +++ b/src/main/codex/codex-structured-turn-cancellation.ts @@ -8,6 +8,7 @@ import type { CodexStructuredSessionAdapterDeps, CodexStructuredSessionEvent } from './codex-structured-session-state' +import type { CodexJournalTranslationAdmission } from './codex-structured-journal-contracts' import { readCodexThreadId, readCodexTurnId } from './codex-structured-thread-facts' import { captureCodexTurnProcesses, @@ -21,13 +22,22 @@ type TurnProcessState = { deferredCompletions: Map } +function turnKey(threadId: string, turnId: string): string { + return JSON.stringify([threadId, turnId]) +} + type TurnCancellationDeps = Pick< CodexStructuredSessionAdapterDeps, 'captureTurnProcesses' | 'requestTimeoutMs' | 'terminateTurnProcesses' > & { - emit: (session: CodexSession, event: CodexStructuredSessionEvent) => void + emit: ( + session: CodexSession, + event: CodexStructuredSessionEvent + ) => CodexJournalTranslationAdmission } +const ADMITTED: CodexJournalTranslationAdmission = { accepted: true } + export class CodexStructuredTurnCancellation { private readonly states = new WeakMap() @@ -54,12 +64,13 @@ export class CodexStructuredTurnCancellation { observedAt?: number ): boolean { const threadId = readCodexThreadId(params) ?? session.threadId - if (method !== 'turn/completed' || threadId !== session.threadId) { + if (method !== 'turn/completed') { return false } const turnId = readCodexTurnId(params) const state = this.state(session) - if (!turnId || !state.blockedCompletions.has(turnId)) { + const key = turnId ? turnKey(threadId, turnId) : null + if (!key || !state.blockedCompletions.has(key)) { return false } const event = { @@ -70,21 +81,29 @@ export class CodexStructuredTurnCancellation { params, ...(observedAt !== undefined ? { observedAt } : {}) } - state.deferredCompletions.set(turnId, event) + state.deferredCompletions.set(key, event) return true } - async cancel(session: CodexSession, turnId: string): Promise<{ cancelled: boolean }> { + async cancel( + session: CodexSession, + threadId: string, + turnId: string, + isCurrent: () => boolean = () => true, + onConfirmed?: () => CodexJournalTranslationAdmission + ): Promise<{ cancelled: boolean }> { const state = this.state(session) - state.blockedCompletions.add(turnId) - const baseline = await state.baseline + const key = turnKey(threadId, turnId) + state.blockedCompletions.add(key) + const targetsPrimaryTurn = threadId === session.threadId + const baseline = targetsPrimaryTurn ? await state.baseline : null + if (!isCurrent()) { + this.releaseCompletion(session, key) + return { cancelled: false } + } let requestError: unknown const interruptReceipt = session.connection - .request( - 'turn/interrupt', - { threadId: session.threadId, turnId }, - { timeoutMs: this.deps.requestTimeoutMs } - ) + .request('turn/interrupt', { threadId, turnId }, { timeoutMs: this.deps.requestTimeoutMs }) .then( () => true, (error: unknown) => { @@ -94,10 +113,31 @@ export class CodexStructuredTurnCancellation { ) const [acknowledged, terminated] = await Promise.all([ interruptReceipt, - this.terminate(session.connection, baseline) + targetsPrimaryTurn ? this.terminate(session.connection, baseline) : Promise.resolve(true) ]) if (terminated && acknowledged) { - this.releaseCompletion(session, turnId) + const completion = state.deferredCompletions.get(key) + let confirmationError: unknown + let promptAdmission = ADMITTED + try { + promptAdmission = onConfirmed?.() ?? ADMITTED + } catch (error) { + confirmationError = error + } + const completionAdmission = this.releaseCompletion(session, key, completion) + if (confirmationError) { + throw confirmationError + } + if (!promptAdmission.accepted) { + throw new Error( + `Codex prompt cancellation lifecycle was not admitted (${promptAdmission.reason})` + ) + } + if (onConfirmed && completion && !completionAdmission.accepted) { + throw new Error( + `Codex deferred turn completion lifecycle was not admitted (${completionAdmission.reason})` + ) + } return { cancelled: true } } if ( @@ -105,12 +145,12 @@ export class CodexStructuredTurnCancellation { !isCodexAppServerRequestError(requestError) && !isCodexAppServerUnsupportedError(requestError) ) { - this.releaseCompletion(session, turnId) + this.releaseCompletion(session, key) throw requestError } // A failed cancellation must not permanently divert the provider's later // completion for this turn. Let the normal completion path settle it. - this.releaseCompletion(session, turnId) + this.releaseCompletion(session, key) return { cancelled: false } } @@ -135,15 +175,13 @@ export class CodexStructuredTurnCancellation { private releaseCompletion( session: CodexSession, - turnId: string, - completion = this.state(session).deferredCompletions.get(turnId) - ): void { + key: string, + completion = this.state(session).deferredCompletions.get(key) + ): CodexJournalTranslationAdmission { const state = this.state(session) - state.blockedCompletions.delete(turnId) - state.deferredCompletions.delete(turnId) - if (completion) { - this.deps.emit(session, completion) - } + state.blockedCompletions.delete(key) + state.deferredCompletions.delete(key) + return completion ? this.deps.emit(session, completion) : ADMITTED } private state(session: CodexSession): TurnProcessState { diff --git a/src/main/codex/codex-structured-turn-start.ts b/src/main/codex/codex-structured-turn-start.ts index e6a53925fc6..c88370adf05 100644 --- a/src/main/codex/codex-structured-turn-start.ts +++ b/src/main/codex/codex-structured-turn-start.ts @@ -6,21 +6,16 @@ import { type CodexAppServerConnection } from './codex-app-server-connection' import { isCodexAppServerUnsupportedError } from './codex-app-server-session' -import { readCodexTurnId } from './codex-structured-thread-facts' -import { DISPATCH_DOUBT_CODEX_TURN_UNNAMED } from '../native-chat/agent-session-journal/journal-dispatch-doubt-reasons' +import type { CodexDispatchEchoes } from './codex-structured-dispatch-echo' +import { DISPATCH_REJECTED_CODEX_QUEUE_FULL } from '../../shared/structured-agent-session-dispatch-rejection' import { decodeStructuredAgentSessionOptionValue } from '../../shared/structured-agent-session-option-codec' -// Starting a Codex turn and learning its id, which are not the same event: -// `turn/start` returns the id on newer builds and acks before it exists on -// older ones, where it arrives as a `turn/started` notification instead. - -/** Codex records the user message first in a turn, so the submission Orca just - * accepted is ordinal 0 of `(threadId, turnId)`. */ -export const CODEX_USER_MESSAGE_ORDINAL = 0 - -/** Past this the turn is real but unnameable, which the journal renders as - * delivery unconfirmed rather than failure. */ -const TURN_ID_WAIT_MS = 10_000 +// Writing a Codex turn and learning which message landed where, which are not +// the same event. `turn/start` answers as soon as Codex owns the message, but a +// message issued while a turn is running is COALESCED into that turn: the same +// turn id comes back, no second `turn/started` fires, and the user message is +// echoed only when the running turn reaches it. So the response proves +// admission and nothing about identity, which the echo settles later. /** Keys Codex accepts as per-turn overrides. An unlisted key would otherwise * become an arbitrary client-controlled `turn/start` parameter. */ @@ -38,16 +33,14 @@ export function isCodexTurnOptionKey(key: string): boolean { return CODEX_TURN_OPTION_KEYS.has(key) } -/** The session state one turn needs. `turnIdWaiters` is shared with the - * notification handler, which resolves the head of the queue — correct because - * Codex runs one turn per thread, so starts and `turn/started` share an order. */ +/** The session state one turn needs. */ export type CodexTurnHost = { connection: Pick threadId: string options: Map reportedOptions?: { model?: string } fastModeTierByModel: ReadonlyMap - turnIdWaiters: ((turnId: string) => void)[] + dispatchEchoes: CodexDispatchEchoes } function turnInputFor(body: AgentJournalMessageItem): Record[] { @@ -92,69 +85,54 @@ function codexTurnOptions(host: CodexTurnHost): Record { } /** - * Resolves the turn id, or null when Codex owns a turn it never named. Throws - * only for outcomes the wire must not read as acceptance. + * Hands one submission to Codex. False means the bounded correlation window + * refused it before the write; otherwise resolves when Codex has taken it. */ export async function startCodexTurn( host: CodexTurnHost, input: { clientMessageId: string; body: AgentJournalMessageItem; timeoutMs?: number } -): Promise { - // Registered BEFORE the call: on builds that ack first, `turn/started` can - // land while the response is still in flight. - let notified: ((turnId: string) => void) | null = null - const fromNotification = new Promise((resolve) => { - notified = resolve - host.turnIdWaiters.push(resolve) - setTimeout(() => resolve(null), TURN_ID_WAIT_MS).unref?.() - }) - try { - const started = await host.connection.request( - 'turn/start', - { - threadId: host.threadId, - clientUserMessageId: input.clientMessageId, - input: turnInputFor(input.body), - ...codexTurnOptions(host) - }, - { timeoutMs: input.timeoutMs } - ) - return readCodexTurnId(started) ?? (await fromNotification) - } finally { - const index = notified ? host.turnIdWaiters.indexOf(notified) : -1 - if (index !== -1) { - host.turnIdWaiters.splice(index, 1) - } +): Promise { + // Armed before the write: the echo can land while the response is in flight. + if (!host.dispatchEchoes.arm(input.clientMessageId)) { + return false } + await host.connection.request( + 'turn/start', + { + threadId: host.threadId, + clientUserMessageId: input.clientMessageId, + input: turnInputFor(input.body), + ...codexTurnOptions(host) + }, + { timeoutMs: input.timeoutMs } + ) + return true } /** - * One submission's outcome as the wire must read it: accepted names the turn, - * rejected is Codex answering and declining, and unknown covers a turn that is - * real but unnameable — never a failure the user is told their message hit. + * One submission's outcome as the wire must read it: admitted means Codex owns + * the message and its identity settles on the echo, rejected is Codex answering + * and declining. Elapsed time is never evidence here, because the wait a + * coalesced send would face is bounded only by the running turn. */ export async function dispatchCodexTurn( session: CodexTurnHost, input: { clientMessageId: string; body: AgentJournalMessageItem }, timeoutMs: number | undefined ): Promise { - let turnId: string | null try { - turnId = await startCodexTurn(session, { ...input, timeoutMs }) + if (!(await startCodexTurn(session, { ...input, timeoutMs }))) { + return { state: 'rejected', reason: DISPATCH_REJECTED_CODEX_QUEUE_FULL } + } } catch (error) { if (isCodexAppServerRequestError(error) || isCodexAppServerUnsupportedError(error)) { + // Codex answered and declined, so no echo for this write can arrive. + session.dispatchEchoes.disarm(input.clientMessageId) return { state: 'rejected', reason: (error as Error).message } } + // A timeout or transport failure can happen after the frame was written. + // Keep the correlation armed so a later echo can prove delivery. throw error } - return turnId === null - ? { state: 'unknown', reason: DISPATCH_DOUBT_CODEX_TURN_UNNAMED } - : { - state: 'accepted', - providerIdentity: { - provider: 'codex', - threadId: session.threadId, - turnId, - ordinal: CODEX_USER_MESSAGE_ORDINAL - } - } + return { state: 'admitted' } } diff --git a/src/main/codex/codex-turn-ordinals.ts b/src/main/codex/codex-turn-ordinals.ts index 89ed72666db..7e21b900d96 100644 --- a/src/main/codex/codex-turn-ordinals.ts +++ b/src/main/codex/codex-turn-ordinals.ts @@ -3,6 +3,10 @@ import { digestPayload } from '../native-chat/agent-session-journal/journal-payload-bounds' +/** Codex records the user message first in a turn, so a restored submission is + * ordinal 0 of `(threadId, turnId)`. */ +export const CODEX_USER_MESSAGE_ORDINAL = 0 + /** Maximum forgotten turn keys retained for late-frame reconciliation. */ export const MAX_CODEX_TURN_ORDINAL_ENTRIES = 256 export const MAX_CODEX_TURN_ORDINAL_BYTES = 512 * 1024 diff --git a/src/main/folder-upgrade-worktree-path.test.ts b/src/main/folder-upgrade-worktree-path.test.ts new file mode 100644 index 00000000000..37d28a32837 --- /dev/null +++ b/src/main/folder-upgrade-worktree-path.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { Repo } from '../shared/repo-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import { preserveFolderUpgradeWorktreePath } from './folder-upgrade-worktree-path' + +const roots: string[] = [] +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }) + } +}) +const repo: Repo = { + id: 'folder', + path: 'C:\\projects\\draft', + displayName: 'draft', + badgeColor: 'blue', + addedAt: 0, + kind: 'git', + folderUpgradeGitRootPath: 'C:/projects/draft' +} +function row(path: string): GitWorktreeInfo { + return { path, branch: 'draft', head: 'abc', isBare: false, isMainWorktree: false } +} + +describe('upgraded folder path projection', () => { + it('leaves existing Git repos and unrelated linked checkouts untouched', () => { + const rows = [row('C:/projects/draft'), row('C:/projects/other')] + expect( + preserveFolderUpgradeWorktreePath({ ...repo, folderUpgradeGitRootPath: undefined }, rows) + ).toBe(rows) + expect(preserveFolderUpgradeWorktreePath(repo, rows)).toEqual([ + { ...rows[0], path: repo.path }, + rows[1] + ]) + expect(rows[0].path).toBe('C:/projects/draft') + }) + + it('is idempotent and does not publish both Windows separator spellings', () => { + const rows = [row(repo.path), row('c:/projects/draft')] + const projected = preserveFolderUpgradeWorktreePath(repo, rows) + expect(projected).toEqual([row(repo.path)]) + expect(preserveFolderUpgradeWorktreePath(repo, projected)).toEqual(projected) + }) + + it('does not equate case-distinct POSIX workspaces', () => { + const owner = { ...repo, path: '/project/draft', folderUpgradeGitRootPath: '/project/draft' } + const rows = [row('/project/draft'), row('/project/Draft')] + expect(preserveFolderUpgradeWorktreePath(owner, rows)).toEqual(rows) + }) + + it('revalidates a symlink locally and refuses to inspect a remote symlink', () => { + const root = realpathSync(mkdtempSync(join(tmpdir(), 'orca-folder-upgrade-path-'))) + roots.push(root) + const target = join(root, 'target') + const other = join(root, 'other') + const alias = join(root, 'alias') + mkdirSync(target) + mkdirSync(other) + symlinkSync(target, alias, 'junction') + const owner = { ...repo, path: alias, folderUpgradeGitRootPath: target } + const rows = [row(target)] + expect(preserveFolderUpgradeWorktreePath(owner, rows)).toEqual([row(alias)]) + expect( + preserveFolderUpgradeWorktreePath({ ...owner, executionHostId: 'ssh:builder' }, rows) + ).toBe(rows) + rmSync(alias) + symlinkSync(other, alias, 'junction') + expect(preserveFolderUpgradeWorktreePath(owner, rows)).toBe(rows) + }) +}) diff --git a/src/main/folder-upgrade-worktree-path.ts b/src/main/folder-upgrade-worktree-path.ts new file mode 100644 index 00000000000..eb70bd65031 --- /dev/null +++ b/src/main/folder-upgrade-worktree-path.ts @@ -0,0 +1,41 @@ +import { realpathSync } from 'node:fs' +import type { Repo } from '../shared/repo-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host' +import { areWorktreePathsEqual, dedupeWorktreesByPath } from './ipc/worktree-path-comparison' + +function stillNamesRegisteredCheckout(repo: Repo, gitRoot: string): boolean { + if (areWorktreePathsEqual(repo.path, gitRoot)) { + return true + } + if (getRepoExecutionHostId(repo) !== LOCAL_EXECUTION_HOST_ID) { + return false + } + try { + // A symlink may have been retargeted since the upgrade. + return areWorktreePathsEqual(realpathSync(repo.path), realpathSync(gitRoot)) + } catch { + return false + } +} + +export function preserveFolderUpgradeWorktreePath( + repo: Repo, + worktrees: GitWorktreeInfo[] +): GitWorktreeInfo[] { + const gitRoot = repo.folderUpgradeGitRootPath + if ( + repo.kind !== 'git' || + typeof gitRoot !== 'string' || + !gitRoot || + !stillNamesRegisteredCheckout(repo, gitRoot) + ) { + return worktrees + } + // Apply after raw Git caches: this repo's locator must not leak into another registration. + return dedupeWorktreesByPath( + worktrees.map((worktree) => + areWorktreePathsEqual(worktree.path, gitRoot) ? { ...worktree, path: repo.path } : worktree + ) + ) +} diff --git a/src/main/git/command-runner/spawned-command-tree-kill.test.ts b/src/main/git/command-runner/spawned-command-tree-kill.test.ts new file mode 100644 index 00000000000..758fb296227 --- /dev/null +++ b/src/main/git/command-runner/spawned-command-tree-kill.test.ts @@ -0,0 +1,122 @@ +import { ChildProcess } from 'node:child_process' +import { once } from 'node:events' +import { spawnProcess } from '../../../shared/child-process/run-process' +import type * as NodeChildProcess from 'node:child_process' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { spawnMock, admitMock } = vi.hoisted(() => ({ + spawnMock: vi.fn(), + admitMock: vi.fn(() => true) +})) + +vi.mock('node:child_process', async (importOriginal) => ({ + ...(await importOriginal()), + spawn: spawnMock +})) +vi.mock('../../own-chromium-tree-kill-guard', () => ({ + admitSelfInitiatedTreeKill: admitMock +})) + +import { killSpawnedCommandTree } from './spawned-command-tree-kill' + +const originalPlatform = process.platform + +function childWithPid(pid: number): ChildProcess { + const child = new ChildProcess() + Object.defineProperty(child, 'pid', { value: pid }) + vi.spyOn(child, 'kill').mockReturnValue(true) + vi.spyOn(child, 'unref').mockImplementation(() => {}) + return child +} + +describe('Git command tree termination', () => { + beforeEach(() => { + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }) + spawnMock.mockReset() + admitMock.mockReset().mockReturnValue(true) + }) + + afterEach(() => { + Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }) + vi.restoreAllMocks() + }) + + it.each([0, 128])( + 'never taskkills a child that exited with code %i before close', + async (code) => { + const child = childWithPid(1234) + Object.defineProperty(child, 'exitCode', { value: code }) + + await killSpawnedCommandTree(child) + + expect(spawnMock).not.toHaveBeenCalled() + expect(admitMock).not.toHaveBeenCalled() + expect(child.kill).toHaveBeenCalledOnce() + } + ) + + it('never taskkills a child that exited by signal before close', async () => { + const child = childWithPid(1234) + Object.defineProperty(child, 'signalCode', { value: 'SIGTERM' }) + + await killSpawnedCommandTree(child) + + expect(spawnMock).not.toHaveBeenCalled() + expect(admitMock).not.toHaveBeenCalled() + }) + + it('still waits for tree termination when the Windows root has not exited', async () => { + const child = childWithPid(1234) + const killer = childWithPid(5678) + spawnMock.mockReturnValue(killer) + let settled = false + const pending = killSpawnedCommandTree(child).then(() => { + settled = true + }) + + await Promise.resolve() + expect(settled).toBe(false) + expect(spawnMock).toHaveBeenCalledWith('taskkill', ['/pid', '1234', '/t', '/f'], { + stdio: 'ignore', + windowsHide: true + }) + killer.emit('close', 0) + await pending + expect(child.kill).not.toHaveBeenCalled() + }) + + it('preserves handle termination on POSIX', async () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + const child = childWithPid(1234) + + await killSpawnedCommandTree(child) + + expect(child.kill).toHaveBeenCalledOnce() + expect(spawnMock).not.toHaveBeenCalled() + }) + it.skipIf(originalPlatform !== 'win32').each([0, 128])( + 'does not taskkill an actual native Windows child after exit %i', + async (exitCode) => { + const original = await vi.importActual('node:child_process') + spawnMock.mockImplementation((program, args, options) => { + if (program !== process.execPath) { + throw new Error('Unexpected external process in native exit probe') + } + return original.spawn(program, args, options) + }) + const child = spawnProcess({ + program: process.execPath, + args: ['-e', `process.exit(${exitCode})`] + }) + const closed = once(child, 'close') + await once(child, 'exit') + expect(child.exitCode).toBe(exitCode) + expect(child.pid).toBeGreaterThan(0) + spawnMock.mockClear() + await killSpawnedCommandTree(child) + expect(spawnMock).not.toHaveBeenCalled() + expect(admitMock).not.toHaveBeenCalled() + await closed + } + ) +}) diff --git a/src/main/git/command-runner/spawned-command-tree-kill.ts b/src/main/git/command-runner/spawned-command-tree-kill.ts index 324e04f1db3..035764d5249 100644 --- a/src/main/git/command-runner/spawned-command-tree-kill.ts +++ b/src/main/git/command-runner/spawned-command-tree-kill.ts @@ -9,6 +9,11 @@ export function killSpawnedCommandTree(child: ChildProcess): Promise { child.kill() return Promise.resolve() } + // Windows may reuse the pid after exit while inherited pipes still delay close. + if ((child.exitCode ?? null) !== null || (child.signalCode ?? null) !== null) { + child.kill() + return Promise.resolve() + } if ( !admitSelfInitiatedTreeKill({ pid, site: 'git-command-tree-kill', scope: 'win-taskkill-tree' }) ) { diff --git a/src/main/git/worktree-deferred-removal-real-git.test.ts b/src/main/git/worktree-deferred-removal-real-git.test.ts index 374be4ecc0f..06b58b4319e 100644 --- a/src/main/git/worktree-deferred-removal-real-git.test.ts +++ b/src/main/git/worktree-deferred-removal-real-git.test.ts @@ -2,12 +2,14 @@ // accepts `worktree remove --force` on a path Orca just renamed away. import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' -import { mkdir, mkdtemp, readdir, realpath, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { removeWorktree } from './worktree' +import { listWorktreesStrict, removeWorktree } from './worktree' +import { isPrunableGitFileWorktree } from '../worktree-prunable-git-file' +import { removeStaleLocalWorktreeRegistration } from '../local-worktree-removal-recovery' import { getWorktreeTrashRoot, isWorktreeTrashEntryName, @@ -96,6 +98,55 @@ describe('deferred worktree removal against the real Git binary', () => { expect(existsSync(getWorktreeTrashRoot(worktreePath))).toBe(false) }) + it('does not rename a malformed registration that points at the checkout git file', async () => { + const markerPath = join(worktreePath, '.git') + const marker = await readFile(markerPath, 'utf8') + const adminPath = marker.trim().replace(/^gitdir: /, '') + await writeFile(join(adminPath, 'gitdir'), `${join(markerPath, '.git')}\n`) + await writeFile(join(worktreePath, 'untracked.txt'), 'keep this work\n') + + await expect( + removeWorktree(repoPath, markerPath, true, { deleteBranch: false }) + ).rejects.toThrow() + await whenWorktreeTrashDeletionsSettled() + + expect(await readFile(markerPath, 'utf8')).toBe(marker) + expect(await readFile(join(worktreePath, 'untracked.txt'), 'utf8')).toBe('keep this work\n') + expect(await git(['branch', '--list', 'feature'], repoPath)).toContain('feature') + expect(existsSync(getWorktreeTrashRoot(markerPath))).toBe(false) + }) + + it('prunes a proven malformed registration while retaining checkout files and its branch', async () => { + const markerPath = join(worktreePath, '.git') + const marker = await readFile(markerPath, 'utf8') + const adminPath = marker.trim().replace(/^gitdir: /, '') + await writeFile(join(adminPath, 'gitdir'), `${join(markerPath, '.git')}\n`) + await writeFile(join(worktreePath, 'untracked.txt'), 'keep this work\n') + const row = (await listWorktreesStrict(repoPath)).find((entry) => entry.path === markerPath) + expect(row).toBeDefined() + if (!row) { + throw new Error('Missing malformed registration') + } + expect(await isPrunableGitFileWorktree(row)).toBe(true) + + const result = await removeStaleLocalWorktreeRegistration({ + canonicalWorktreePath: markerPath, + repoPath, + localWorktreeGitOptions: {}, + registeredWorktree: row, + deleteBranch: true + }) + + expect(result).toEqual({ preservedBranch: { branchName: 'feature', head: row.head } }) + expect(await readFile(markerPath, 'utf8')).toBe(marker) + expect(await readFile(join(worktreePath, 'untracked.txt'), 'utf8')).toBe('keep this work\n') + expect(await git(['rev-parse', 'refs/heads/feature'], repoPath)).toBe(`${row.head}\n`) + expect((await listWorktreesStrict(repoPath)).some((entry) => entry.path === markerPath)).toBe( + false + ) + expect(existsSync(adminPath)).toBe(false) + }) + it('sweeps trash a previous run left behind', async () => { const stalePath = join( workspaceRoot, diff --git a/src/main/hook-archive-termination-safety.test.ts b/src/main/hook-archive-termination-safety.test.ts deleted file mode 100644 index 7627c7e84fc..00000000000 --- a/src/main/hook-archive-termination-safety.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import type { Repo } from '../shared/repo-types' - -vi.mock('./effective-hook-config', () => ({ - getEffectiveHooksFromConfig: (_repo: unknown, hooks: unknown) => hooks -})) - -const REPO: Repo = { id: 'r', path: '/repo', displayName: 'r', badgeColor: '#000', addedAt: 0 } - -/** - * Run a hook past its deadline with `process.kill` intercepted, so the escalation's decisions are - * observed directly instead of raced against the kernel. `groupAlive` answers the signal-0 probe. - */ -async function signalsFromTimedOutHook(groupAlive: boolean): Promise { - const { runHook } = await import('./hooks') - const dir = mkdtempSync(join(tmpdir(), 'orca-hook-signals-')) - writeFileSync(join(dir, 'orca.yaml'), 'scripts:\n archive: |\n sleep 30\n') - const sent: string[] = [] - const fakeKill = (pid: number, signal?: string | number): true => { - if (signal === 0) { - if (!groupAlive) { - throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' }) - } - return true - } - sent.push(`${pid < 0 ? 'group' : 'child'}:${String(signal)}`) - return true - } - const spy = vi.spyOn(process, 'kill').mockImplementation(fakeKill) - try { - await runHook('archive', dir, REPO, dir, undefined, 100) - await new Promise((resolve) => setTimeout(resolve, 2_400)) - return sent - } finally { - spy.mockRestore() - rmSync(dir, { recursive: true, force: true }) - } -} - -// Why (#19334): the escalation exists for descendants that outlive the shell — a setup hook that -// backgrounds a server typically loses its leader to the first SIGTERM while the server keeps -// running. Keying the skip on the CHILD's exit would miss exactly that case; the probe asks the -// GROUP instead. The residual hazard, stated in hooks.ts: a recycled pid answers the probe too. -describe.skipIf(process.platform === 'win32')('archive hook termination', () => { - it('escalates to the group when members survive the first signal', async () => { - await expect(signalsFromTimedOutHook(true)).resolves.toEqual(['group:SIGTERM', 'group:SIGKILL']) - }, 20_000) - - it('sends nothing once the group is provably empty', async () => { - // A group that answers ESRCH has no members left to kill, and its pid may since belong to - // someone else — so neither the SIGTERM nor the escalation is delivered. - await expect(signalsFromTimedOutHook(false)).resolves.toEqual([]) - }, 20_000) -}) - -// The regression the group probe exists for, pinned directly because it cannot be reproduced -// through `runHook` with signals intercepted: with `process.kill` mocked nothing actually dies, so -// the child never reaches the exited state that a child-liveness skip would key on. -describe.skipIf(process.platform === 'win32')('terminateHookTree', () => { - const fakeChild = (exited: boolean) => ({ - pid: 4242, - exitCode: exited ? 0 : null, - signalCode: null, - kill: vi.fn() - }) - - it('signals a surviving group even though the shell leader already exited', async () => { - const { terminateHookTree } = await import('./hooks') - const sent: (string | number | undefined)[][] = [] - const recordKill = (pid: number, signal?: string | number): true => { - if (signal !== 0) { - sent.push([pid, signal]) - } - return true - } - const spy = vi.spyOn(process, 'kill').mockImplementation(recordKill) - try { - // A hook that backgrounds a server loses its leader to the first SIGTERM; the server lives on. - terminateHookTree(fakeChild(true), 'SIGKILL') - expect(sent).toEqual([[-4242, 'SIGKILL']]) - } finally { - spy.mockRestore() - } - }) - - it('sends nothing when the group answers ESRCH', async () => { - const { terminateHookTree } = await import('./hooks') - const child = fakeChild(true) - const emptyGroup = (): true => { - throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' }) - } - const spy = vi.spyOn(process, 'kill').mockImplementation(emptyGroup) - try { - terminateHookTree(child, 'SIGKILL') - expect(child.kill).not.toHaveBeenCalled() - } finally { - spy.mockRestore() - } - }) -}) diff --git a/src/main/hook-termination-real-process.test.ts b/src/main/hook-termination-real-process.test.ts new file mode 100644 index 00000000000..65d1b0910d1 --- /dev/null +++ b/src/main/hook-termination-real-process.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest' +import { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { Repo } from '../shared/repo-types' + +const REPO: Repo = { id: 'r', path: '/repo', displayName: 'r', badgeColor: '#000', addedAt: 0 } + +const alive = (pid: number): boolean => { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +/** Run a hook past its deadline and report which of its real processes survived. */ +async function survivorsAfterDeadline( + script: string +): Promise<{ shell: boolean; child: boolean; output: string; pids: number[] }> { + const { runHook } = await import('./hooks') + const dir = mkdtempSync(join(tmpdir(), 'orca-term-')) + const pidFile = join(dir, 'pids') + writeFileSync( + join(dir, 'orca.yaml'), + `scripts:\n archive: |\n${script.replace(/^/gm, ' ')}\n` + ) + let pids: number[] = [] + try { + const result = await runHook('archive', dir, REPO, dir, undefined, 400) + expect(result.success).toBe(false) + // SIGTERM lands at the deadline, SIGKILL two seconds later. + await new Promise((resolve) => setTimeout(resolve, 3_500)) + expect(existsSync(pidFile)).toBe(true) + pids = readFileSync(pidFile, 'utf8').trim().split(/\s+/).map(Number) + // Without this, a script that recorded only the shell leaves `pids[1]` undefined, `alive` + // throws, and the missing descendant reads as dead — a test that passes on nothing. + expect(pids).toHaveLength(2) + expect(pids.every((pid) => Number.isSafeInteger(pid) && pid > 0)).toBe(true) + return { shell: alive(pids[0]!), child: alive(pids[1]!), output: result.output, pids } + } finally { + for (const pid of pids) { + try { + process.kill(pid, 'SIGKILL') + } catch { + /* already gone */ + } + } + rmSync(dir, { recursive: true, force: true }) + } +} + +// NO `process.kill` mock, deliberately. The defect this file exists for — `exec` silently ignoring +// `detached`, so the shell was never a group leader and the group signal reached nothing — is +// invisible to a mocked `process.kill`, because the mock makes the signal-0 probe succeed whether +// or not a real group exists. That is the precise condition the bug turns on. +describe.skipIf(process.platform === 'win32')('hook termination against real processes', () => { + it('kills the shell and its child when the deadline expires', async () => { + const { shell, child, output } = await survivorsAfterDeadline( + 'echo "archive step 3 of 7"\nsleep 120 &\necho "$$ $!" > "$PWD/pids"\nwait' + ) + expect({ shell, child }).toEqual({ shell: false, child: false }) + // The gate reports this run as `unverifiable`; what the hook printed is the only clue why. + expect(output).toContain('archive step 3 of 7') + }, 30_000) + + it('kills a descendant that ignores SIGTERM', async () => { + // Only the group SIGKILL can end this one; a SIGTERM to the shell alone leaves it running. + const { child } = await survivorsAfterDeadline( + '(trap "" TERM; sleep 120) &\necho "$$ $!" > "$PWD/pids"\nwait' + ) + expect(child).toBe(false) + }, 30_000) +}) diff --git a/src/main/hooks-archive-exit-observation.test.ts b/src/main/hooks-archive-exit-observation.test.ts index 7773894c4c4..f924a5291cf 100644 --- a/src/main/hooks-archive-exit-observation.test.ts +++ b/src/main/hooks-archive-exit-observation.test.ts @@ -1,62 +1,130 @@ import { describe, expect, it, vi } from 'vitest' +import { EventEmitter } from 'node:events' import type { Repo } from '../shared/repo-types' -const { execMock } = vi.hoisted(() => ({ execMock: vi.fn() })) -vi.mock('child_process', () => ({ - exec: execMock, - execFileSync: vi.fn(), - execFile: vi.fn(), - spawn: vi.fn() -})) +const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() })) +vi.mock('child_process', () => ({ spawn: spawnMock, execFileSync: vi.fn() })) vi.mock('./effective-hook-config', () => ({ getEffectiveHooksFromConfig: () => ({ scripts: { archive: 'do-the-archive' } }) })) const REPO: Repo = { id: 'r', path: '/repo', displayName: 'r', badgeColor: '#000', addedAt: 0 } -const execFailure = (code: unknown): Error => Object.assign(new Error('Command failed'), { code }) - -/** Drive runHook once with the error object `exec` hands back for a given failure mode. */ -async function runArchiveWith( - error: Error | null -): Promise<{ success: boolean; exitCode?: number }> { - const { runHook } = await import('./hooks') - execMock.mockImplementationOnce((_script, _opts, cb) => { - cb(error, '', '') - return { pid: 1234, kill: vi.fn() } - }) - const outcome = await runHook('archive', '/repo/wt', REPO) - // Guard against a vacuous pass: if the mock ever stops intercepting, a real shell would run and - // this, rather than the subtle assertions below, is what fails. - expect(execMock).toHaveBeenCalled() - return outcome +/** + * A real EventEmitter, so an `error` with no listener throws exactly as Node's would — which is the + * whole point of the stream-error row below. Replays its chunks to whoever subscribes to `data`. + */ +class FakeStream extends EventEmitter { + constructor(private readonly chunks: string[]) { + super() + } + setEncoding(): void {} + override on(event: string, fn: (chunk: string) => void): this { + super.on(event, fn) + if (event === 'data') { + for (const chunk of this.chunks) { + fn(chunk) + } + } + return this + } } -// Why (#19334): an ABSENT exitCode is what the removal gate reads as `unverifiable`. The guard is -// `typeof code === 'number'`, because `exec` reports a spawn failure with a *string* code — a -// looser null-check would file ENOENT as `exited "ENOENT"`, reading a hook that never ran as one -// that reported an exit. The timeout arm of the same contract is covered against a real shell in -// hook-archive-timeout-observation.test.ts. +/** Minimal ChildProcess stand-in: runHook reads the streams and waits for close/error. */ +function fakeChild( + outcome: { code?: number | null; signal?: NodeJS.Signals | null } | Error, + stdoutChunks: string[] = [], + stdoutError?: Error +) { + const listeners: Record void)[]> = {} + const stdout = new FakeStream(stdoutChunks) + queueMicrotask(() => { + if (stdoutError) { + stdout.emit('error', stdoutError) + } + if (outcome instanceof Error) { + for (const fn of listeners.error ?? []) { + fn(outcome) + } + return + } + for (const fn of listeners.close ?? []) { + fn(outcome.code ?? null, outcome.signal ?? null) + } + }) + return { + pid: 4242, + stdout, + stderr: new FakeStream([]), + exitCode: null, + signalCode: null, + kill: () => true, + on(event: string, fn: (...args: unknown[]) => void) { + ;(listeners[event] ??= []).push(fn) + return this + } + } +} + +async function runArchiveWith( + outcome: { code?: number | null; signal?: NodeJS.Signals | null } | Error, + stdoutChunks?: string[], + stdoutError?: Error +): Promise<{ success: boolean; output: string; exitCode?: number }> { + const { runHook } = await import('./hooks') + spawnMock.mockImplementationOnce(() => fakeChild(outcome, stdoutChunks, stdoutError)) + const result = await runHook('archive', '/repo/wt', REPO) + // Guard against a vacuous pass: if the mock stops intercepting, a real shell would run. + expect(spawnMock).toHaveBeenCalled() + return result +} + +// Why (#19334): an ABSENT exitCode is what the removal gate reads as `unverifiable`. Every row here +// is a way a hook can fail to deliver one. The timeout and termination arms of the same contract +// are covered against REAL processes in hook-termination-real-process.test.ts — deliberately not +// here, because a mocked child cannot show whether a process group exists. describe('archive hook exit observation', () => { it('passes a clean run through without an exit code', async () => { - await expect(runArchiveWith(null)).resolves.toEqual({ success: true, output: '' }) + await expect(runArchiveWith({ code: 0 })).resolves.toEqual({ success: true, output: '' }) }) it.each([ ['a non-zero exit', 23], ['a shell command-not-found', 127] ])('reports %s as the observed exit it is', async (_label, code) => { - await expect(runArchiveWith(execFailure(code))).resolves.toMatchObject({ + await expect(runArchiveWith({ code })).resolves.toMatchObject({ success: false, exitCode: code }) }) + it('caps what it retains from a hook that floods stdout', async () => { + // `exec`'s 1 MiB maxBuffer is gone with `spawn`; without a cap a flooding hook grows the main + // process's heap for the whole 120 s deadline. + const megabyte = 'x'.repeat(1024 * 1024) + const result = await runArchiveWith( + { code: 0 }, + Array.from({ length: 12 }, () => megabyte) + ) + expect(result.output.length).toBeLessThan(11 * 1024 * 1024) + expect(result.output).toContain('output truncated at 10485760 bytes') + }) + + it('survives an error on the output stream', async () => { + // An `error` with no listener is an uncaught exception, and in the main process that is the + // app. `exec` never covered this either — its only `error` listener is on the child. + await expect( + runArchiveWith({ code: 0 }, ['partial'], new Error('EIO: read failed')) + ).resolves.toMatchObject({ success: true }) + }) + it.each([ - ['was killed by a signal', null], - ['never started, so the code is a string', 'ENOENT'] - ])('withholds the exit code when the hook %s', async (_label, code) => { - const result = await runArchiveWith(execFailure(code)) + ['was killed by a signal', { code: null, signal: 'SIGKILL' as const }], + // A real spawn failure carries a STRING code; the guard under test is `typeof code === + // 'number'`, so a bare Error would pass even if that guard regressed. + ['never started', Object.assign(new Error('spawn /bin/bash ENOENT'), { code: 'ENOENT' })] + ])('withholds the exit code when the hook %s', async (_label, outcome) => { + const result = await runArchiveWith(outcome) expect(result.success).toBe(false) expect(result.exitCode).toBeUndefined() }) diff --git a/src/main/hooks.test.ts b/src/main/hooks.test.ts index 12744712543..3b911de99c6 100644 --- a/src/main/hooks.test.ts +++ b/src/main/hooks.test.ts @@ -1,6 +1,6 @@ import type * as GitRunner from './git/runner' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { makeHookTestRepo } from './hooks-test-fixtures' // Mock fs used by loadHooks @@ -13,17 +13,17 @@ vi.mock('fs', () => ({ chmodSync: vi.fn() })) -const { execMock, runWslProcessMock, gitExecFileSyncMock } = vi.hoisted(() => ({ - execMock: vi.fn(), +const { spawnMock, runWslProcessMock, gitExecFileSyncMock } = vi.hoisted(() => ({ + spawnMock: vi.fn(), runWslProcessMock: vi.fn(), gitExecFileSyncMock: vi.fn() })) vi.mock('child_process', () => ({ - exec: execMock, - execFileSync: vi.fn(), - // runner.ts imports spawn from child_process transitively. - spawn: vi.fn() + // One `spawn` for both: hooks.ts runs the script through it, and runner.ts imports it + // transitively. A second key here silently shadowed the first. + spawn: spawnMock, + execFileSync: vi.fn() })) vi.mock('./wsl/wsl-runner', () => ({ @@ -35,6 +35,35 @@ vi.mock('./git/runner', async () => ({ gitExecFileSync: gitExecFileSyncMock })) +/** Minimal ChildProcess stand-in: hooks.ts reads the streams and waits for close/error. */ +function fakeChild(exit: { code?: number | null; signal?: NodeJS.Signals | null } = { code: 0 }) { + const listeners: Record void)[]> = {} + const stream = { setEncoding: () => {}, on: () => {} } + queueMicrotask(() => { + for (const fn of listeners.close ?? []) { + fn(exit.code ?? null, exit.signal ?? null) + } + }) + return { + pid: 4242, + stdout: stream, + stderr: stream, + exitCode: null, + signalCode: null, + kill: () => true, + on(event: string, fn: (...args: unknown[]) => void) { + ;(listeners[event] ??= []).push(fn) + return this + } + } +} + +beforeEach(() => { + // Clear as well as re-arm: these assertions are order-sensitive and calls otherwise accumulate. + spawnMock.mockClear() + spawnMock.mockImplementation(() => fakeChild()) +}) + describe('runHook', () => { const makeRepo = (hookSettings?: { mode?: 'auto' | 'override' @@ -43,10 +72,7 @@ describe('runHook', () => { }) => makeHookTestRepo(hookSettings) it('uses the Windows command shell when running hooks', async () => { - execMock.mockImplementation((_script, _options, callback) => { - callback?.(null, '', '') - return {} as never - }) + spawnMock.mockImplementation(() => fakeChild()) const fs = await import('node:fs') vi.mocked(fs.existsSync).mockReturnValue(true) @@ -66,13 +92,12 @@ describe('runHook', () => { const result = await runHook('setup', 'C:\\repo\\worktree', makeRepo()) expect(result).toEqual({ success: true, output: '' }) - expect(execMock).toHaveBeenCalledWith( + expect(spawnMock).toHaveBeenCalledWith( 'echo hello', expect.objectContaining({ cwd: 'C:\\repo\\worktree', shell: 'C:\\Windows\\System32\\cmd.exe' - }), - expect.any(Function) + }) ) } finally { Object.defineProperty(process, 'platform', { @@ -91,10 +116,9 @@ describe('runHook', () => { // Why: setup scripts source conda exactly like a shell rc does, so the // orphaned CONDA_SHLVL sentinel surfaces as an opaque hook failure (#14195). let capturedEnv: Record | undefined - execMock.mockImplementation((_script, options, callback) => { + spawnMock.mockImplementation((_script, options) => { capturedEnv = (options as { env: Record }).env - callback?.(null, '', '') - return {} as never + return fakeChild() }) const fs = await import('node:fs') @@ -131,10 +155,7 @@ describe('runHook', () => { }) it('keeps bash as the hook shell on non-Windows platforms', async () => { - execMock.mockImplementation((_script, _options, callback) => { - callback?.(null, '', '') - return {} as never - }) + spawnMock.mockImplementation(() => fakeChild()) const fs = await import('node:fs') vi.mocked(fs.existsSync).mockReturnValue(true) @@ -154,7 +175,7 @@ describe('runHook', () => { const result = await runHook('setup', '/repo/worktree', makeRepo()) expect(result).toEqual({ success: true, output: '' }) - expect(execMock).toHaveBeenCalledWith( + expect(spawnMock).toHaveBeenCalledWith( 'echo hello', expect.objectContaining({ cwd: '/repo/worktree', @@ -165,8 +186,7 @@ describe('runHook', () => { GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' }) - }), - expect.any(Function) + }) ) } finally { Object.defineProperty(process, 'platform', { @@ -182,7 +202,8 @@ describe('runHook', () => { }) it('runs WSL hooks through runWslProcess and translates env paths to Linux', async () => { - execMock.mockReset() + spawnMock.mockReset() + spawnMock.mockImplementation(() => fakeChild()) runWslProcessMock.mockReset() runWslProcessMock.mockResolvedValue({ environmentResolved: true, @@ -228,7 +249,7 @@ describe('runHook', () => { }) }) ) - expect(execMock).not.toHaveBeenCalled() + expect(spawnMock).not.toHaveBeenCalled() } finally { Object.defineProperty(process, 'platform', { configurable: true, @@ -238,7 +259,8 @@ describe('runHook', () => { }) it('runs Windows-path hooks through WSL when the project runtime targets WSL', async () => { - execMock.mockReset() + spawnMock.mockReset() + spawnMock.mockImplementation(() => fakeChild()) runWslProcessMock.mockReset() runWslProcessMock.mockResolvedValue({ environmentResolved: true, @@ -286,7 +308,7 @@ describe('runHook', () => { }) }) ) - expect(execMock).not.toHaveBeenCalled() + expect(spawnMock).not.toHaveBeenCalled() } finally { Object.defineProperty(process, 'platform', { configurable: true, @@ -349,7 +371,8 @@ describe('runHook', () => { it('settles WSL hooks when wsl.exe never reports completion', async () => { // Why no fake timers: the timeout is now runProcess's own, internal to the // mocked runWslProcess -- there is nothing left in hooks.ts to advance. - execMock.mockReset() + spawnMock.mockReset() + spawnMock.mockImplementation(() => fakeChild()) runWslProcessMock.mockReset() runWslProcessMock.mockResolvedValue({ environmentResolved: true, diff --git a/src/main/hooks.ts b/src/main/hooks.ts index f9d91f38423..8ea68c8f5fa 100644 --- a/src/main/hooks.ts +++ b/src/main/hooks.ts @@ -14,7 +14,12 @@ import type { HookRuntimeTarget } from './hook-runtime-target' import type { OrcaHooks } from '../shared/orca-yaml-hook-types' import type { Repo } from '../shared/repo-types' import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' -import { exec } from 'node:child_process' +import { spawn } from 'node:child_process' +import { + forceTerminateProcessTree, + signalProcessTree +} from '../shared/child-process/process-tree-termination' +import { createOutputSink } from '../shared/child-process/bounded-output-sink' const HOOK_TIMEOUT = 120_000 // 2 minutes @@ -53,50 +58,21 @@ function classifyHookProcessResult( const SIGTERM_GRACE_MS = 2_000 -/** Signal the hook's whole process group where the platform has one, else just the child. */ -export type TerminableChild = { - pid?: number - exitCode: number | null - signalCode: NodeJS.Signals | null - kill: (signal: NodeJS.Signals) => boolean +/** + * `exec` capped output at 1 MiB and killed the hook on overflow; `spawn` has no cap at all, and a + * hook flooding stdout for the full deadline can take the main process's heap with it. Truncation + * is reported in the output rather than as a failure — a chatty hook that exits 0 did succeed, and + * failing it for being chatty is the `exec` behaviour this is replacing. + */ +const HOOK_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024 + +function readSink(sink: ReturnType): string { + return sink.truncated() + ? `${sink.text()}\n[output truncated at ${HOOK_OUTPUT_LIMIT_BYTES} bytes]` + : sink.text() } -export function terminateHookTree(child: TerminableChild, signal: NodeJS.Signals): void { - // Why probe the GROUP and not the child: the escalation exists for descendants that outlive the - // shell. A hook that backgrounds a server typically loses its leader to the first SIGTERM while - // the server keeps running, so keying this on `child.exitCode` would skip the SIGKILL in exactly - // the case it was added for. - // - // The trade-off it does not solve: signalling by negative pid names whatever group owns that pid - // now. Once the leader is reaped its pid can be recycled, and a probe cannot tell a surviving - // descendant from a stranger that inherited the number. Killing a runaway hook is the likelier - // event and the one the deadline promises, so the group is signalled whenever it answers; the - // residual window is pid wraparound inside the two-second grace. - if (process.platform !== 'win32' && child.pid) { - try { - // Signal 0 tests for members without delivering anything: ESRCH means the group is empty. - process.kill(-child.pid, 0) - } catch { - return - } - try { - process.kill(-child.pid, signal) - return - } catch { - // Raced with the last member exiting; fall through to the direct kill. - } - } - if (child.exitCode !== null || child.signalCode !== null) { - return - } - try { - child.kill(signal) - } catch { - // Already dead. - } -} - -/** An `exec` failure: a string `code` (ENOENT) means it never started, so no exit was observed. */ +/** A spawn failure: the process never started, so no exit was ever observed. */ function hookProcessError( error: Error, stdout: string, @@ -287,8 +263,7 @@ export function runHook( // reports whatever it chose to do, so a hook that traps SIGTERM and exits 0 came back as a // PASS — a hook cut off mid-archive, indistinguishable from one that finished. Settle on the // deadline instead, and settle AT it, so a hook that traps and keeps running cannot hold a - // removal open. `exec` stays because it owns the per-platform shell invocation (`cmd.exe` - // wants `/d /s /c`, not `-c`), which is not this change's to re-derive. + // removal open. let settled = false let deadline: NodeJS.Timeout | undefined const settle = (result: HookProcessOutcome): void => { @@ -301,42 +276,71 @@ export function runHook( } resolve(result) } - const child = exec( - script, - { - cwd, - shell: getHookShell(), - // Why: hooks run unattended; block Git Credential Manager's interactive prompt while keeping cached auth (issue #7652). - env: promptGuardShellEnv(shellHookEnv), - // Signal the whole group on POSIX: the script is a shell, and the work is its children. - ...(process.platform === 'win32' ? {} : { detached: true }) - }, - (error, stdout, stderr) => { - if (error) { - settle(hookProcessError(error, stdout, stderr, { hookName, cwd })) - return - } - settle( - classifyHookProcessResult( - { code: 0, stdout, stderr, timedOut: false }, - { hookName, cwd, timeoutMs } - ) + // Why `spawn` and not `exec` (#19334 follow-up): `detached` is a spawn-only option — `exec` + // accepts and ignores it, so the shell never became a group leader and the group signal below + // had nothing to reach. Passing `shell` as a string keeps Node's own platform invocation, which + // is what `exec` was being kept for: `cmd.exe /d /s /c` on Windows rather than a bare `-c`. + const child = spawn(script, { + cwd, + shell: getHookShell(), + // Why: hooks run unattended; block Git Credential Manager's interactive prompt while keeping cached auth (issue #7652). + env: promptGuardShellEnv(shellHookEnv), + stdio: ['ignore', 'pipe', 'pipe'], + // Pinned, not left to Node's default, for the same reason `runProcess` pins it: a `cmd.exe` + // hook otherwise flashes a console window and takes focus. Pre-existing — `exec` did not set + // it either — but AGENTS.md asks for it pinned on every Windows spawn. + windowsHide: true, + // Make the shell a group leader so its children can be reached. Not on Windows, which has no + // process groups in this sense and where `detached` means a new console instead. + ...(process.platform === 'win32' ? {} : { detached: true }) + }) + const stdout = createOutputSink(HOOK_OUTPUT_LIMIT_BYTES) + const stderr = createOutputSink(HOOK_OUTPUT_LIMIT_BYTES) + child.stdout?.on('data', (chunk: Buffer | string) => stdout.write(chunk)) + child.stderr?.on('data', (chunk: Buffer | string) => stderr.write(chunk)) + // Why listeners that do nothing: an unhandled `error` on a stream is an uncaught exception, and + // in the Electron main process that is the whole app. `exec` never covered this either — its + // only `error` listener is on the child — so this is a pre-existing gap, closed the way + // `runProcess` closes it. Losing output is not worth a crash; the exit code still gets through. + for (const stream of [child.stdin, child.stdout, child.stderr]) { + stream?.on('error', () => {}) + } + child.on('error', (error) => { + settle(hookProcessError(error, readSink(stdout), readSink(stderr), { hookName, cwd })) + }) + child.on('close', (code, signal) => { + settle( + classifyHookProcessResult( + // A signalled exit reports no code, which stays `unverifiable` rather than becoming a 0. + { + code: signal ? null : code, + stdout: readSink(stdout), + stderr: readSink(stderr), + timedOut: false + }, + { hookName, cwd, timeoutMs } ) - } - ) - // Why guarded: `exec`'s callback can fire synchronously (the unit test's mock does), and arming - // a deadline on an already-settled run would later signal a process group whose pid is long - // gone — and may by then belong to something else. + ) + }) + // Why guarded: a spawn failure can settle before the deadline is armed, and arming one on a + // finished run would later signal a pid that is gone — and may by then belong to something else. if (!settled) { deadline = setTimeout(() => { settle( classifyHookProcessResult( - { code: null, stdout: '', stderr: '', timedOut: true }, + // Keep what the hook printed: it is the only clue to why the removal gate says + // `unverifiable`. + { code: null, stdout: readSink(stdout), stderr: readSink(stderr), timedOut: true }, { hookName, cwd, timeoutMs } ) ) - terminateHookTree(child, 'SIGTERM') - setTimeout(() => terminateHookTree(child, 'SIGKILL'), SIGTERM_GRACE_MS).unref?.() + // Orca's own tree terminator: POSIX process groups, `taskkill /t /f` on Windows (where a + // bare `child.kill` reaches only the shell and leaves its descendants running), and the + // recycled-pid guard that hazard needs. SIGTERM first so a well-behaved hook can clean up. + void signalProcessTree(child, 'SIGTERM') + setTimeout(() => { + void forceTerminateProcessTree(child) + }, SIGTERM_GRACE_MS).unref?.() }, timeoutMs) } }) diff --git a/src/main/ipc/filesystem-import-local.ts b/src/main/ipc/filesystem-import-local.ts index 49b910dac5f..1df2b3f2fb3 100644 --- a/src/main/ipc/filesystem-import-local.ts +++ b/src/main/ipc/filesystem-import-local.ts @@ -2,7 +2,7 @@ import { lstat, rm } from 'node:fs/promises' import { basename, join, resolve } from 'node:path' import { authorizeExternalPath } from './filesystem-auth' import { isENOENT } from './filesystem-path-containment' -import type { ImportItemResult } from './filesystem-import-result-types' +import type { ImportItemResult } from '../../shared/filesystem-import-result-types' import { copyLocalFileNoFollow, preScanForSymlinks, diff --git a/src/main/ipc/filesystem-import-result-types.ts b/src/main/ipc/filesystem-import-result-types.ts deleted file mode 100644 index d1d9f446836..00000000000 --- a/src/main/ipc/filesystem-import-result-types.ts +++ /dev/null @@ -1,51 +0,0 @@ -export type ImportSkipReason = 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - -export type ResolveDroppedPathsResult = { - resolvedPaths: string[] - skipped: { sourcePath: string; reason: ImportSkipReason }[] - failed: { sourcePath: string; reason: string }[] -} - -// ─── External Import Types ────────────────────────────────────────── - -export type ImportItemResult = - | { - sourcePath: string - status: 'imported' - destPath: string - kind: 'file' | 'directory' - renamed: boolean - } - | { - sourcePath: string - status: 'skipped' - reason: ImportSkipReason - } - | { - sourcePath: string - status: 'failed' - reason: string - } - -export type StagedExternalImportSource = - | { - sourcePath: string - status: 'staged' - name: string - kind: 'file' | 'directory' - entries: StagedExternalImportEntry[] - } - | { - sourcePath: string - status: 'skipped' - reason: ImportSkipReason - } - | { - sourcePath: string - status: 'failed' - reason: string - } - -export type StagedExternalImportEntry = - | { relativePath: string; kind: 'directory' } - | { relativePath: string; kind: 'file'; contentBase64: string } diff --git a/src/main/ipc/filesystem-import-ssh.ts b/src/main/ipc/filesystem-import-ssh.ts index ae17f3d8e19..9268c6ece24 100644 --- a/src/main/ipc/filesystem-import-ssh.ts +++ b/src/main/ipc/filesystem-import-ssh.ts @@ -5,7 +5,7 @@ import { isENOENT } from './filesystem-path-containment' import { getSshConnectionManager } from './ssh' import { requireSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch' import type { FileUploadSession, IFilesystemProvider } from '../providers/types' -import type { ImportItemResult } from './filesystem-import-result-types' +import type { ImportItemResult } from '../../shared/filesystem-import-result-types' import { assertSafeRemotePathSegment, type RemotePathFlavor } from '../ssh/ssh-remote-platform' import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' import { diff --git a/src/main/ipc/filesystem-import.test.ts b/src/main/ipc/filesystem-import.test.ts index bb143987c96..7703cd8a8b1 100644 --- a/src/main/ipc/filesystem-import.test.ts +++ b/src/main/ipc/filesystem-import.test.ts @@ -73,6 +73,7 @@ describe('fs:importExternalPaths', () => { size: 12, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false @@ -94,6 +95,7 @@ describe('fs:importExternalPaths', () => { size: entry.isDir ? 0 : 12, ino: entry.isDir ? 2 : 3, dev: 1, + mtimeMs: 1700000000000, isFile: () => !entry.isDir, isDirectory: () => entry.isDir, isSymbolicLink: () => false @@ -142,6 +144,7 @@ describe('fs:importExternalPaths', () => { size: content.byteLength, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), createReadStream: () => Readable.from([content]), @@ -216,6 +219,7 @@ describe('fs:importExternalPaths', () => { size: 12, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), createReadStream: () => Readable.from([Buffer.from('file-content')]), @@ -484,6 +488,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false @@ -498,6 +503,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), readFile: readFileHandleMock, @@ -514,11 +520,21 @@ describe('fs:importExternalPaths', () => { status: 'staged', name: 'logo.png', kind: 'file', - entries: [{ relativePath: '', kind: 'file', contentBase64: 'cG5n' }] + entries: [ + { + relativePath: '', + kind: 'file', + byteLength: 4, + inode: 1, + deviceId: 1, + modifiedAtMs: 1700000000000 + } + ] } ]) expect(copyFileMock).not.toHaveBeenCalled() - expect(readFileHandleMock).toHaveBeenCalled() + // Why: bodies stream at upload time, so staging must never read the file. + expect(readFileHandleMock).not.toHaveBeenCalled() expect(closeMock).toHaveBeenCalled() }) @@ -533,6 +549,7 @@ describe('fs:importExternalPaths', () => { size: 0, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false @@ -543,6 +560,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 2, dev: 1, + mtimeMs: 1700000000000, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false @@ -578,6 +596,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 2, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), readFile: vi.fn().mockResolvedValue(Buffer.from('icon')), @@ -597,7 +616,14 @@ describe('fs:importExternalPaths', () => { entries: [ { relativePath: '', kind: 'directory' }, { relativePath: '..assets', kind: 'directory' }, - { relativePath: '..assets/icon.txt', kind: 'file', contentBase64: 'aWNvbg==' } + { + relativePath: '..assets/icon.txt', + kind: 'file', + byteLength: 4, + inode: 2, + deviceId: 1, + modifiedAtMs: 1700000000000 + } ] } ]) @@ -612,6 +638,7 @@ describe('fs:importExternalPaths', () => { size: 0, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false @@ -637,14 +664,15 @@ describe('fs:importExternalPaths', () => { expect(openMock).not.toHaveBeenCalled() }) - it('checks runtime upload directory byte budget before reading a file that exceeds the total cap', async () => { + it('checks runtime upload directory byte budget before opening a file that exceeds the total cap', async () => { const sourcePath = '/tmp/dropped/project' const resolvedPath = path.resolve(sourcePath) const filePaths = ['one.bin', 'two.bin', 'three.bin', 'four.bin', 'overflow.bin'].map((name) => path.join(resolvedPath, name) ) const mib = 1024 * 1024 - const regularSize = 25 * mib + // Four files exactly fill the 8 GB total ceiling; the fifth pushes past it. + const regularSize = 2 * 1024 * mib const overflowSize = Number(mib) const readFileMock = vi.fn().mockResolvedValue(Buffer.from('chunk')) @@ -654,6 +682,7 @@ describe('fs:importExternalPaths', () => { size: 0, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false @@ -666,6 +695,7 @@ describe('fs:importExternalPaths', () => { size, ino: fileIndex + 2, dev: 1, + mtimeMs: 1700000000000, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false @@ -689,6 +719,7 @@ describe('fs:importExternalPaths', () => { size: regularSize, ino: fileIndex + 2, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), readFile: readFileMock, @@ -702,11 +733,9 @@ describe('fs:importExternalPaths', () => { sourcePaths: [sourcePath] })) as { sources: { status: string; reason?: string }[] } - expect(result.sources[0]).toMatchObject({ - status: 'failed', - reason: 'Remote import is too large' - }) - expect(readFileMock).toHaveBeenCalledTimes(4) + expect(result.sources[0]).toMatchObject({ status: 'failed' }) + expect(result.sources[0]?.reason).toContain('total remote import limit') + expect(readFileMock).not.toHaveBeenCalled() expect(openMock).not.toHaveBeenCalledWith(filePaths.at(-1), expect.anything()) }) @@ -719,6 +748,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 1, dev: 1, + mtimeMs: 1700000000000, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false @@ -732,6 +762,7 @@ describe('fs:importExternalPaths', () => { size: 4, ino: 2, dev: 1, + mtimeMs: 1700000000000, isFile: () => true }), readFile: readFileHandleMock, @@ -744,7 +775,7 @@ describe('fs:importExternalPaths', () => { expect(result.sources[0]).toMatchObject({ status: 'failed', - reason: "File changed during upload staging: ''" + reason: "File changed during upload staging: 'logo.png'" }) expect(readFileHandleMock).not.toHaveBeenCalled() }) diff --git a/src/main/ipc/filesystem-mutations-runtime-upload.test.ts b/src/main/ipc/filesystem-mutations-runtime-upload.test.ts new file mode 100644 index 00000000000..fa7edc1d5b7 --- /dev/null +++ b/src/main/ipc/filesystem-mutations-runtime-upload.test.ts @@ -0,0 +1,176 @@ +import { EventEmitter } from 'node:events' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const handlers = new Map Promise>() +const { handleMock, streamMock, sweepMock } = vi.hoisted(() => ({ + handleMock: vi.fn(), + streamMock: vi.fn(), + sweepMock: vi.fn() +})) + +vi.mock('electron', () => ({ + ipcMain: { handle: handleMock }, + app: { getPath: () => '/user-data' } +})) +vi.mock('./runtime-upload-file-stream', () => ({ + streamExternalFileToRuntime: streamMock +})) +vi.mock('./runtime-upload-temp-sweep', () => ({ + sweepAbandonedRuntimeUploadTempPath: sweepMock +})) +vi.mock('../../shared/runtime-environment-store', () => ({ + resolveEnvironment: (_userDataPath: string, selector: string) => ({ + id: selector === 'env-alias' ? 'env-1' : selector + }) +})) + +import { registerFilesystemMutationHandlers } from './filesystem-mutations' +import { RENDERER_GONE_MESSAGE } from './renderer-lifetime-abort' + +const request = { + environmentId: 'env-1', + sourceRootPath: '/drop/file.bin', + entryRelativePath: '', + expected: { byteLength: 1, inode: 1, deviceId: 1, modifiedAtMs: 1 }, + worktree: 'wt-1', + relativePath: '.file.bin.orca-upload-x', + expectedEnvironmentPairingRevision: 3, + expectedEnvironmentRuntimeId: 'rt-1' +} + +function fakeSender(): EventEmitter { + return new EventEmitter() +} + +function listenerCount(sender: EventEmitter): number { + return ['destroyed', 'render-process-gone', 'did-navigate'].reduce( + (total, name) => total + sender.listenerCount(name), + 0 + ) +} + +beforeEach(() => { + handlers.clear() + handleMock.mockReset() + streamMock.mockReset() + sweepMock.mockReset() + sweepMock.mockResolvedValue(undefined) + handleMock.mockImplementation((channel: string, handler: never) => { + handlers.set(channel, handler) + }) + registerFilesystemMutationHandlers( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the upload handler under test never reads the store; registration only needs a Store-shaped value. + { getRepos: () => [], getSettings: () => ({ workspaceDir: '/workspace' }) } as never + ) +}) + +function invoke(sender: EventEmitter): Promise { + return handlers.get('fs:uploadExternalFileToRuntime')!({ sender }, request) +} + +describe('fs:uploadExternalFileToRuntime', () => { + it('streams with the user data path and a live signal, and leaves no listeners behind', async () => { + const sender = fakeSender() + streamMock.mockImplementation(async (args: { userDataPath: string; signal: AbortSignal }) => { + expect(args.userDataPath).toBe('/user-data') + expect(args.signal.aborted).toBe(false) + expect(listenerCount(sender)).toBe(3) + return { byteLength: 42 } + }) + + await expect(invoke(sender)).resolves.toEqual({ byteLength: 42 }) + + expect(streamMock).toHaveBeenCalledWith(expect.objectContaining(request)) + expect(sweepMock).not.toHaveBeenCalled() + expect(listenerCount(sender)).toBe(0) + }) + + it('resolves the selector to the environment id before streaming and sweeping', async () => { + const sender = fakeSender() + streamMock.mockImplementation( + ({ signal }: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + sender.emit('destroyed') + }) + ) + + await expect( + handlers.get('fs:uploadExternalFileToRuntime')!( + { sender }, + { ...request, environmentId: 'env-alias' } + ) + ).rejects.toThrow(RENDERER_GONE_MESSAGE) + + expect(streamMock).toHaveBeenCalledWith(expect.objectContaining({ environmentId: 'env-1' })) + expect(sweepMock).toHaveBeenCalledWith('/user-data', { ...request, environmentId: 'env-1' }) + }) + + it('aborts, sweeps the temp path, and rethrows when the renderer is destroyed mid-stream', async () => { + const sender = fakeSender() + streamMock.mockImplementation( + ({ signal }: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + sender.emit('destroyed') + }) + ) + + await expect(invoke(sender)).rejects.toThrow(RENDERER_GONE_MESSAGE) + + expect(sweepMock).toHaveBeenCalledTimes(1) + expect(sweepMock).toHaveBeenCalledWith('/user-data', request) + expect(listenerCount(sender)).toBe(0) + }) + + it('aborts once a reload commits, not on a blocked navigation or an in-app route change', async () => { + const sender = fakeSender() + let observed: AbortSignal | undefined + streamMock.mockImplementation( + ({ signal }: { signal: AbortSignal }) => + new Promise((resolve, reject) => { + observed = signal + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + sender.emit('did-start-navigation', { isMainFrame: true, isSameDocument: true }) + sender.emit('did-start-navigation', { isMainFrame: true, isSameDocument: false }) + sender.emit('will-navigate', { defaultPrevented: true }, 'https://example.invalid/') + queueMicrotask(() => { + expect(signal.aborted).toBe(false) + sender.emit('did-navigate', 'file:///app/index.html', 200, 'OK') + resolve({ byteLength: 0 }) + }) + }) + ) + + await expect(invoke(sender)).rejects.toThrow(RENDERER_GONE_MESSAGE) + expect(observed?.aborted).toBe(true) + expect(sweepMock).toHaveBeenCalledTimes(1) + }) + + it('does not sweep when the stream fails while the renderer is still alive', async () => { + const sender = fakeSender() + streamMock.mockRejectedValue(new Error("File changed since it was staged: 'file.bin'")) + + await expect(invoke(sender)).rejects.toThrow("File changed since it was staged: 'file.bin'") + + expect(sweepMock).not.toHaveBeenCalled() + expect(listenerCount(sender)).toBe(0) + }) + + it('still rethrows the stream error if the sweep itself throws', async () => { + const sender = fakeSender() + sweepMock.mockRejectedValue(new Error('sweep exploded')) + streamMock.mockImplementation( + ({ signal }: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + sender.emit('render-process-gone') + }) + ) + + // Why: the sweep contract is "never rejects"; if it ever did, this documents + // that the handler would surface the sweep error instead of the upload's. + await expect(invoke(sender)).rejects.toThrow('sweep exploded') + expect(listenerCount(sender)).toBe(0) + }) +}) diff --git a/src/main/ipc/filesystem-mutations.ts b/src/main/ipc/filesystem-mutations.ts index 57ac0c5e197..ad1308f0c30 100644 --- a/src/main/ipc/filesystem-mutations.ts +++ b/src/main/ipc/filesystem-mutations.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { app, ipcMain } from 'electron' import { constants } from 'node:fs' import { copyFile, mkdir, writeFile } from 'node:fs/promises' import { basename, dirname } from 'node:path' @@ -16,9 +16,17 @@ import type { ImportSkipReason, ResolveDroppedPathsResult, StagedExternalImportSource -} from './filesystem-import-result-types' +} from '../../shared/filesystem-import-result-types' import { importOneSource } from './filesystem-import-local' -import { stageOneSourceForRuntimeUpload } from './filesystem-runtime-upload-staging' +import { + stagedRuntimeUploadByteLength, + stageOneSourceForRuntimeUpload +} from './filesystem-runtime-upload-staging' +import { streamExternalFileToRuntime } from './runtime-upload-file-stream' +import { abortWhenRendererGone } from './renderer-lifetime-abort' +import { sweepAbandonedRuntimeUploadTempPath } from './runtime-upload-temp-sweep' +import type { RuntimeUploadFileStreamRequest } from '../../shared/runtime-upload-staging-contract' +import { resolveEnvironment } from '../../shared/runtime-environment-store' /** * IPC handlers for file/folder creation and renaming. @@ -196,13 +204,54 @@ export function registerFilesystemMutationHandlers(store: Store): void { args: { sourcePaths: string[] } ): Promise<{ sources: StagedExternalImportSource[] }> => { const sources: StagedExternalImportSource[] = [] + // Why: one budget for the whole drop — per-source counters would let five + // 2 GB files through a ceiling meant to cap the drop. + let totalBytes = 0 for (const sourcePath of args.sourcePaths) { - sources.push(await stageOneSourceForRuntimeUpload(sourcePath)) + const source = await stageOneSourceForRuntimeUpload(sourcePath, totalBytes) + totalBytes += stagedRuntimeUploadByteLength(source) + sources.push(source) } return { sources } } ) + // Why: the file handle and the runtime socket both live in main, so the byte + // pump runs here. The renderer keeps deconflict/commit/rollback orchestration + // and never sees file contents. + ipcMain.handle( + 'fs:uploadExternalFileToRuntime', + async (event, args: RuntimeUploadFileStreamRequest): Promise<{ byteLength: number }> => { + const userDataPath = app.getPath('userData') + // Why: the streamer's manual-disconnect check keys on the environment id, + // and the renderer may pass any selector the store resolves. + const request = { + ...args, + environmentId: resolveEnvironment(userDataPath, args.environmentId).id + } + // Why: the renderer's own loop died with its window. Now that the bytes + // move in main, a reload or close has to stop the transfer explicitly, + // or a multi-GB upload outlives the window that asked for it. + const lifetime = abortWhenRendererGone(event.sender) + try { + return await streamExternalFileToRuntime({ + ...request, + userDataPath, + signal: lifetime.signal + }) + } catch (error) { + if (lifetime.signal.aborted) { + // Why: the renderer owns temp cleanup, and it is gone — so the + // abandoned temp path is only collectable from here. + await sweepAbandonedRuntimeUploadTempPath(userDataPath, request) + } + throw error + } finally { + lifetime.dispose() + } + } + ) + // Why: terminal drag-and-drop resolver. Local worktrees pass paths through // unchanged (reference-in-place; preserves zero-latency drop). SSH worktrees // upload each path into `${worktreePath}/.orca/drops/` and return remote diff --git a/src/main/ipc/filesystem-runtime-upload-staging.test.ts b/src/main/ipc/filesystem-runtime-upload-staging.test.ts new file mode 100644 index 00000000000..3fe70b4e1f2 --- /dev/null +++ b/src/main/ipc/filesystem-runtime-upload-staging.test.ts @@ -0,0 +1,166 @@ +import { lstat, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as RuntimeImportLimits from './runtime-import-limits' + +type RuntimeImportLimitsModule = typeof RuntimeImportLimits + +vi.mock('./filesystem-auth', () => ({ authorizeExternalPath: () => {} })) +// Why: real ceilings are gigabytes, and truncate() is not sparse on NTFS, so a +// literal over-limit fixture would allocate that much on Windows CI. +vi.mock('./runtime-import-limits', async (importOriginal) => ({ + ...(await importOriginal()), + REMOTE_IMPORT_MAX_FILE_BYTES: 4 * 1024, + REMOTE_IMPORT_MAX_TOTAL_BYTES: 16 * 1024 +})) + +const { stagedRuntimeUploadByteLength, stageOneSourceForRuntimeUpload } = + await import('./filesystem-runtime-upload-staging') + +let workDir: string + +beforeEach(async () => { + workDir = await mkdtemp(join(tmpdir(), 'orca-upload-staging-')) +}) + +afterEach(async () => { + await rm(workDir, { force: true, recursive: true }) +}) + +describe('stageOneSourceForRuntimeUpload', () => { + it('records size instead of file contents so staging never holds the body', async () => { + const filePath = join(workDir, 'note.txt') + await writeFile(filePath, 'hello world') + + const staged = await stageOneSourceForRuntimeUpload(filePath) + + expect(staged).toMatchObject({ + status: 'staged', + kind: 'file', + name: 'note.txt', + entries: [{ relativePath: '', kind: 'file', byteLength: 11 }] + }) + expect(JSON.stringify(staged)).not.toContain('contentBase64') + }) + + it('records the identity the uploader re-checks, not just the size', async () => { + const filePath = join(workDir, 'note.txt') + await writeFile(filePath, 'hello world') + const stat = await lstat(filePath) + + const staged = await stageOneSourceForRuntimeUpload(filePath) + + expect(staged).toMatchObject({ + status: 'staged', + entries: [ + { + byteLength: 11, + inode: stat.ino, + deviceId: stat.dev, + modifiedAtMs: stat.mtimeMs + } + ] + }) + }) + + it('stages a file with no cap error, where the old buffering path refused', async () => { + const filePath = join(workDir, 'big.bin') + await writeFile(filePath, Buffer.alloc(3 * 1024)) + + await expect(stageOneSourceForRuntimeUpload(filePath)).resolves.toMatchObject({ + status: 'staged', + entries: [{ kind: 'file', byteLength: 3 * 1024 }] + }) + }) + + it('names the file, the actual size and the limit when a file is over the ceiling', async () => { + const filePath = join(workDir, 'clip.mp4') + await writeFile(filePath, Buffer.alloc(6 * 1024)) + + const staged = await stageOneSourceForRuntimeUpload(filePath) + + expect(staged).toMatchObject({ status: 'failed' }) + // Why: a dropped file's relative path is '', so this is the regression that + // would otherwise report "'' is 6 KB, over the 4 KB ... limit". + expect(staged.status === 'failed' && staged.reason).toBe( + "'clip.mp4' is 6 KB, over the 4 KB per-file remote import limit" + ) + }) + + it('names the offending entry by its path inside a dropped directory', async () => { + const rootPath = join(workDir, 'media') + await mkdir(join(rootPath, 'clips'), { recursive: true }) + await writeFile(join(rootPath, 'clips', 'big.mp4'), Buffer.alloc(6 * 1024)) + + const staged = await stageOneSourceForRuntimeUpload(rootPath) + + expect(staged.status === 'failed' && staged.reason).toContain("'clips/big.mp4'") + }) + + it('counts earlier sources in the drop against the total ceiling', async () => { + const filePath = join(workDir, 'second.bin') + await writeFile(filePath, Buffer.alloc(3 * 1024)) + + // Alone it fits; after 14 KB of earlier sources the 16 KB drop ceiling is gone. + await expect(stageOneSourceForRuntimeUpload(filePath, 0)).resolves.toMatchObject({ + status: 'staged' + }) + const overBudget = await stageOneSourceForRuntimeUpload(filePath, 14 * 1024) + expect(overBudget).toMatchObject({ status: 'failed' }) + expect(overBudget.status === 'failed' && overBudget.reason).toContain( + 'total remote import limit' + ) + }) + + it('reports the bytes a source contributes to the drop budget', async () => { + const rootPath = join(workDir, 'tree') + await mkdir(join(rootPath, 'nested'), { recursive: true }) + await writeFile(join(rootPath, 'a.txt'), 'aa') + await writeFile(join(rootPath, 'nested', 'b.txt'), 'bbb') + + const staged = await stageOneSourceForRuntimeUpload(rootPath) + + expect(stagedRuntimeUploadByteLength(staged)).toBe(5) + expect( + stagedRuntimeUploadByteLength({ + sourcePath: '/missing', + status: 'skipped', + reason: 'missing' + }) + ).toBe(0) + }) + + // symlink() needs privileges or Developer Mode on Windows. + it.skipIf(process.platform === 'win32')('keeps rejecting symlinked sources', async () => { + const targetPath = join(workDir, 'target.txt') + await writeFile(targetPath, 'data') + const linkPath = join(workDir, 'link.txt') + await symlink(targetPath, linkPath) + + await expect(stageOneSourceForRuntimeUpload(linkPath)).resolves.toMatchObject({ + status: 'skipped', + reason: 'symlink' + }) + }) + + it('stages directory trees as metadata for every entry', async () => { + const rootPath = join(workDir, 'assets') + await mkdir(join(rootPath, 'nested'), { recursive: true }) + await writeFile(join(rootPath, 'a.txt'), 'aa') + await writeFile(join(rootPath, 'nested', 'b.txt'), 'bbb') + + const staged = await stageOneSourceForRuntimeUpload(rootPath) + + expect(staged.status).toBe('staged') + const entries = staged.status === 'staged' ? staged.entries : [] + expect(entries).toEqual( + expect.arrayContaining([ + { relativePath: '', kind: 'directory' }, + expect.objectContaining({ relativePath: 'a.txt', kind: 'file', byteLength: 2 }), + { relativePath: 'nested', kind: 'directory' }, + expect.objectContaining({ relativePath: 'nested/b.txt', kind: 'file', byteLength: 3 }) + ]) + ) + }) +}) diff --git a/src/main/ipc/filesystem-runtime-upload-staging.ts b/src/main/ipc/filesystem-runtime-upload-staging.ts index 5af76029b98..6531f8a499a 100644 --- a/src/main/ipc/filesystem-runtime-upload-staging.ts +++ b/src/main/ipc/filesystem-runtime-upload-staging.ts @@ -1,3 +1,8 @@ +import { + formatByteCeiling, + REMOTE_IMPORT_MAX_FILE_BYTES, + REMOTE_IMPORT_MAX_TOTAL_BYTES +} from './runtime-import-limits' import { constants } from 'node:fs' import { lstat, open, readdir, realpath } from 'node:fs/promises' import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path' @@ -6,20 +11,33 @@ import { isENOENT } from './filesystem-path-containment' import type { StagedExternalImportEntry, StagedExternalImportSource -} from './filesystem-import-result-types' - -const REMOTE_IMPORT_MAX_FILE_BYTES = 25 * 1024 * 1024 -const REMOTE_IMPORT_MAX_TOTAL_BYTES = 100 * 1024 * 1024 +} from '../../shared/filesystem-import-result-types' class RuntimeUploadSymlinkError extends Error {} +/** Bytes this source contributes to the drop budget; 0 unless it staged. */ +export function stagedRuntimeUploadByteLength(source: StagedExternalImportSource): number { + if (source.status !== 'staged') { + return 0 + } + return source.entries.reduce( + (total, entry) => (entry.kind === 'file' ? total + entry.byteLength : total), + 0 + ) +} + +/** + * @param totalBytesBefore Bytes already staged by earlier sources in the same drop, + * so the total ceiling covers the whole drop rather than each source alone. + */ export async function stageOneSourceForRuntimeUpload( - sourcePath: string + sourcePath: string, + totalBytesBefore = 0 ): Promise { const resolvedSource = resolve(sourcePath) // Why: runtime uploads read client-local paths in the client main process; - // authorize before lstat/readFile just like local copy imports. + // authorize before lstat just like local copy imports. authorizeExternalPath(resolvedSource) let sourceStat: Awaited> @@ -52,8 +70,8 @@ export async function stageOneSourceForRuntimeUpload( } try { const entries = sourceStat.isDirectory() - ? await stageDirectoryEntries(resolvedSource) - : [(await stageFileEntry(resolvedSource, '')).entry] + ? await stageDirectoryEntries(resolvedSource, totalBytesBefore) + : [(await stageFileEntry(resolvedSource, '', { totalBytesBefore })).entry] return { sourcePath, status: 'staged', @@ -73,9 +91,12 @@ export async function stageOneSourceForRuntimeUpload( } } -async function stageDirectoryEntries(rootPath: string): Promise { +async function stageDirectoryEntries( + rootPath: string, + totalBytesBefore: number +): Promise { const entries: StagedExternalImportEntry[] = [{ relativePath: '', kind: 'directory' }] - let totalBytes = 0 + let totalBytes = totalBytesBefore const rootRealPath = await realpath(rootPath) async function visit(dirPath: string): Promise { @@ -126,52 +147,52 @@ async function stageDirectoryEntries(rootPath: string): Promise { const statResult = await lstat(filePath) const displayPath = normalizeRelativeUploadPath(relativePath) + // Why: a dropped file's relative path is '', so errors would name nothing. + // The entry keeps '' — only the message falls back to the file's own name. + const displayName = displayPath || basename(filePath) if (statResult.isSymbolicLink()) { - throw new RuntimeUploadSymlinkError(`Symlink not allowed in '${displayPath}'`) + throw new RuntimeUploadSymlinkError(`Symlink not allowed in '${displayName}'`) } if (!statResult.isFile()) { - throw new Error(`Unsupported file type in '${displayPath}'`) + throw new Error(`Unsupported file type in '${displayName}'`) } - if (options?.rootRealPath) { - await assertRealPathInsideRoot(options.rootRealPath, filePath, displayPath) + if (options.rootRealPath) { + await assertRealPathInsideRoot(options.rootRealPath, filePath, displayName) } - const initialTotalBytes = - options?.totalBytesBefore === undefined - ? statResult.size - : options.totalBytesBefore + statResult.size - assertRemoteUploadBudget(relativePath, statResult.size, initialTotalBytes) + assertRemoteUploadBudget(displayName, statResult.size, options.totalBytesBefore + statResult.size) const fileHandle = await open(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) try { const openedStat = await fileHandle.stat() if (!openedStat.isFile()) { - throw new Error(`Unsupported file type in '${displayPath}'`) + throw new Error(`Unsupported file type in '${displayName}'`) } if ( openedStat.size !== statResult.size || (statResult.ino !== 0 && openedStat.ino !== 0 && openedStat.ino !== statResult.ino) || (statResult.dev !== 0 && openedStat.dev !== 0 && openedStat.dev !== statResult.dev) ) { - throw new Error(`File changed during upload staging: '${displayPath}'`) - } - const totalBytes = - options?.totalBytesBefore === undefined - ? openedStat.size - : options.totalBytesBefore + openedStat.size - assertRemoteUploadBudget(relativePath, openedStat.size, totalBytes) - const buffer = await fileHandle.readFile() - const afterReadStat = await fileHandle.stat() - if (afterReadStat.size !== openedStat.size) { - throw new Error(`File changed during upload staging: '${displayPath}'`) + throw new Error(`File changed during upload staging: '${displayName}'`) } + assertRemoteUploadBudget( + displayName, + openedStat.size, + options.totalBytesBefore + openedStat.size + ) + // Why: bytes are read slice-by-slice at upload time, so staging records the + // identity the streamer re-checks rather than the body itself. Size alone + // would let a same-size replacement slip through between the two calls. return { entry: { relativePath: displayPath, kind: 'file', - contentBase64: buffer.toString('base64') + byteLength: openedStat.size, + inode: openedStat.ino, + deviceId: openedStat.dev, + modifiedAtMs: openedStat.mtimeMs }, byteLength: openedStat.size } @@ -197,15 +218,21 @@ async function assertRealPathInsideRoot( } function assertRemoteUploadBudget( - relativePath: string, + displayName: string, fileBytes: number, totalBytes: number ): void { if (fileBytes > REMOTE_IMPORT_MAX_FILE_BYTES) { - throw new Error(`'${relativePath}' is too large for remote import`) + throw new Error( + `'${displayName}' is ${formatByteCeiling(fileBytes)}, over the ` + + `${formatByteCeiling(REMOTE_IMPORT_MAX_FILE_BYTES)} per-file remote import limit` + ) } if (totalBytes > REMOTE_IMPORT_MAX_TOTAL_BYTES) { - throw new Error('Remote import is too large') + throw new Error( + `This import is ${formatByteCeiling(totalBytes)}, over the ` + + `${formatByteCeiling(REMOTE_IMPORT_MAX_TOTAL_BYTES)} total remote import limit` + ) } } diff --git a/src/main/ipc/folder-repo-git-upgrade.test.ts b/src/main/ipc/folder-repo-git-upgrade.test.ts index d5de650f7ce..1cd92144660 100644 --- a/src/main/ipc/folder-repo-git-upgrade.test.ts +++ b/src/main/ipc/folder-repo-git-upgrade.test.ts @@ -183,6 +183,7 @@ describe('folder repo git upgrade watch', () => { expect(store.updateRepo).toHaveBeenCalledWith('folder-repo', { kind: 'git', + folderUpgradeGitRootPath: repoPath.replaceAll('\\', '/'), externalWorktreeVisibility: 'hide' }) expect(prepareLocalWorktreeRootForRepo).toHaveBeenCalledTimes(1) @@ -206,7 +207,10 @@ describe('folder repo git upgrade watch', () => { }) await tick() - expect(store.updateRepo).toHaveBeenCalledWith('folder-repo', { kind: 'git' }) + expect(store.updateRepo).toHaveBeenCalledWith('folder-repo', { + kind: 'git', + folderUpgradeGitRootPath: join(root, 'symlinked-project').replaceAll('\\', '/') + }) }) it('refuses a project that has folder workspaces the git listing would drop', async () => { diff --git a/src/main/ipc/folder-repo-git-upgrade.ts b/src/main/ipc/folder-repo-git-upgrade.ts index ede90ca6ab9..e393ae56158 100644 --- a/src/main/ipc/folder-repo-git-upgrade.ts +++ b/src/main/ipc/folder-repo-git-upgrade.ts @@ -101,7 +101,9 @@ function resolveRealPath(pathValue: string): string { * the path the user picked; when a symlinked parent makes those differ, the root reads * as an *external* worktree, and hiding those would hide the project's only workspace. */ -function resolveUpgrade(repoPath: string): { externalWorktreeVisibility?: 'hide' } | null { +function resolveUpgrade( + repoPath: string +): { folderUpgradeGitRootPath: string; externalWorktreeVisibility?: 'hide' } | null { if (!isGitRepo(repoPath)) { return null } @@ -110,8 +112,8 @@ function resolveUpgrade(repoPath: string): { externalWorktreeVisibility?: 'hide' return null } return normalizeRuntimePathForComparison(gitRoot) === normalizeRuntimePathForComparison(repoPath) - ? { externalWorktreeVisibility: 'hide' } - : {} + ? { folderUpgradeGitRootPath: gitRoot, externalWorktreeVisibility: 'hide' } + : { folderUpgradeGitRootPath: gitRoot } } type UpgradeResult = 'upgraded' | 'blocked' | 'rejected' diff --git a/src/main/ipc/register-core-handlers/register-core-handlers.test.ts b/src/main/ipc/register-core-handlers/register-core-handlers.test.ts index ac9e6a9bb61..870eac98d46 100644 --- a/src/main/ipc/register-core-handlers/register-core-handlers.test.ts +++ b/src/main/ipc/register-core-handlers/register-core-handlers.test.ts @@ -138,7 +138,8 @@ const { vi.mock('electron', () => ({ app: { - getPath: getPathMock + getPath: getPathMock, + once: vi.fn() } })) diff --git a/src/main/ipc/renderer-lifetime-abort.test.ts b/src/main/ipc/renderer-lifetime-abort.test.ts new file mode 100644 index 00000000000..9e5b0f9a7d9 --- /dev/null +++ b/src/main/ipc/renderer-lifetime-abort.test.ts @@ -0,0 +1,91 @@ +import { EventEmitter } from 'node:events' +import { describe, expect, it } from 'vitest' +import { + abortWhenRendererGone, + RENDERER_GONE_MESSAGE, + type RendererLifetimeSender +} from './renderer-lifetime-abort' + +function fakeSender(): RendererLifetimeSender & EventEmitter { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: EventEmitter implements the once/on/removeListener surface this helper uses, and those three are all it calls; WebContents' overloaded signatures cannot be satisfied structurally. + return new EventEmitter() as RendererLifetimeSender & EventEmitter +} + +describe('abortWhenRendererGone', () => { + it('aborts when the renderer is destroyed', () => { + const sender = fakeSender() + const { signal } = abortWhenRendererGone(sender) + + expect(signal.aborted).toBe(false) + sender.emit('destroyed') + + expect(signal.aborted).toBe(true) + expect(String(signal.reason)).toContain(RENDERER_GONE_MESSAGE) + }) + + it('aborts when the render process is gone', () => { + const sender = fakeSender() + const { signal } = abortWhenRendererGone(sender) + + sender.emit('render-process-gone') + + expect(signal.aborted).toBe(true) + }) + + it('aborts once a reload has replaced the document, not on in-app route changes', () => { + const sender = fakeSender() + const { signal } = abortWhenRendererGone(sender) + + sender.emit('did-start-navigation', { + isMainFrame: true, + isSameDocument: true, + url: 'file:///app#x' + }) + sender.emit('did-navigate-in-page', 'file:///app#x') + expect(signal.aborted).toBe(false) + + sender.emit('did-start-navigation', { + isMainFrame: true, + isSameDocument: false, + url: 'file:///app' + }) + sender.emit('did-navigate', 'file:///app', 200, 'OK') + expect(signal.aborted).toBe(true) + }) + + it('ignores a main-frame navigation that starts but is blocked before it commits', () => { + // Why: Electron emits did-start-navigation before will-navigate gets to + // preventDefault() an external link or a stray file drop; the renderer + // document survives those, so the upload must too. + const sender = fakeSender() + const { signal } = abortWhenRendererGone(sender) + + sender.emit('did-start-navigation', { + isMainFrame: true, + isSameDocument: false, + url: 'https://example.invalid/' + }) + sender.emit('will-navigate', { defaultPrevented: true }, 'https://example.invalid/') + sender.emit('did-start-navigation', { + isMainFrame: true, + isSameDocument: false, + url: 'file:///Users/me/dropped.png' + }) + sender.emit('will-navigate', { defaultPrevented: true }, 'file:///Users/me/dropped.png') + + expect(signal.aborted).toBe(false) + }) + + it('leaves no listeners on a long-lived renderer once disposed', () => { + const sender = fakeSender() + const { dispose } = abortWhenRendererGone(sender) + + expect(sender.listenerCount('destroyed')).toBe(1) + dispose() + dispose() + + expect(sender.listenerCount('destroyed')).toBe(0) + expect(sender.listenerCount('render-process-gone')).toBe(0) + expect(sender.listenerCount('did-navigate')).toBe(0) + }) +}) diff --git a/src/main/ipc/renderer-lifetime-abort.ts b/src/main/ipc/renderer-lifetime-abort.ts new file mode 100644 index 00000000000..213abab8f47 --- /dev/null +++ b/src/main/ipc/renderer-lifetime-abort.ts @@ -0,0 +1,45 @@ +import type { WebContents } from 'electron' + +export type RendererLifetimeSender = Pick + +export const RENDERER_GONE_MESSAGE = 'The window that started this upload went away' + +/** + * Abort signal that fires when the calling renderer goes away. + * + * Work the renderer used to do itself died with it. Once it moves into main, + * nothing stops a long transfer from outliving the window that asked for it, + * so the caller's lifetime has to be wired up explicitly. + * + * Always `dispose()` in a finally — otherwise every call leaks a listener on a + * long-lived WebContents. + */ +export function abortWhenRendererGone(sender: RendererLifetimeSender): { + signal: AbortSignal + dispose: () => void +} { + const controller = new AbortController() + const abort = (): void => controller.abort(new Error(RENDERER_GONE_MESSAGE)) + let disposed = false + + sender.once('destroyed', abort) + sender.once('render-process-gone', abort) + // Why: did-start-navigation also fires for navigations that will-navigate then + // blocks — an external link, a stray file drop — and the renderer survives + // those. did-navigate fires only once a new document has replaced the caller, + // and never for same-document route changes inside the live app. + sender.once('did-navigate', abort) + + return { + signal: controller.signal, + dispose: () => { + if (disposed) { + return + } + disposed = true + sender.removeListener('destroyed', abort) + sender.removeListener('render-process-gone', abort) + sender.removeListener('did-navigate', abort) + } + } +} diff --git a/src/main/ipc/runtime-environment-connectivity-handlers.ts b/src/main/ipc/runtime-environment-connectivity-handlers.ts index d461b1d080a..84d2754d556 100644 --- a/src/main/ipc/runtime-environment-connectivity-handlers.ts +++ b/src/main/ipc/runtime-environment-connectivity-handlers.ts @@ -27,7 +27,8 @@ import { import { clearRuntimeEnvironmentManualDisconnect, isRuntimeEnvironmentManuallyDisconnected, - markRuntimeEnvironmentManuallyDisconnected + markRuntimeEnvironmentManuallyDisconnected, + RUNTIME_MANUALLY_DISCONNECTED_MESSAGE } from './runtime-environment-manual-disconnect' import { callRuntimeEnvironment, @@ -42,7 +43,7 @@ function manuallyDisconnectedResponse( ok: false, error: { code: 'runtime_manually_disconnected', - message: 'Runtime environment is manually disconnected.' + message: RUNTIME_MANUALLY_DISCONNECTED_MESSAGE }, _meta: { runtimeId: environment.runtimeId } } diff --git a/src/main/ipc/runtime-environment-manual-disconnect.ts b/src/main/ipc/runtime-environment-manual-disconnect.ts index f9f94e7f438..31c300895df 100644 --- a/src/main/ipc/runtime-environment-manual-disconnect.ts +++ b/src/main/ipc/runtime-environment-manual-disconnect.ts @@ -1,5 +1,7 @@ const manuallyDisconnectedEnvironmentIds = new Set() +export const RUNTIME_MANUALLY_DISCONNECTED_MESSAGE = 'Runtime environment is manually disconnected.' + export function markRuntimeEnvironmentManuallyDisconnected(environmentId: string): void { manuallyDisconnectedEnvironmentIds.add(environmentId) } diff --git a/src/main/ipc/runtime-import-limits.test.ts b/src/main/ipc/runtime-import-limits.test.ts new file mode 100644 index 00000000000..2eaeae603b0 --- /dev/null +++ b/src/main/ipc/runtime-import-limits.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { + formatByteCeiling, + REMOTE_IMPORT_MAX_FILE_BYTES, + REMOTE_IMPORT_MAX_TOTAL_BYTES +} from './runtime-import-limits' + +describe('formatByteCeiling', () => { + it('renders a size one byte over a ceiling as larger than the ceiling', () => { + // "is 2 GB, over the 2 GB limit" reads like a broken check, not a big file. + expect(formatByteCeiling(REMOTE_IMPORT_MAX_FILE_BYTES)).toBe('2 GB') + expect(formatByteCeiling(REMOTE_IMPORT_MAX_FILE_BYTES + 1)).toBe('2.1 GB') + }) + + it('leaves an exact ceiling as a whole number', () => { + expect(formatByteCeiling(REMOTE_IMPORT_MAX_TOTAL_BYTES)).toBe('8 GB') + expect(formatByteCeiling(1024)).toBe('1 KB') + }) + + it('scales through the units', () => { + expect(formatByteCeiling(512)).toBe('512 B') + expect(formatByteCeiling(1024 * 1024)).toBe('1 MB') + expect(formatByteCeiling(1024 ** 4)).toBe('1 TB') + }) + + it('rounds up rather than to nearest', () => { + expect(formatByteCeiling(1024 * 1024 + 1)).toBe('1.1 MB') + }) + + it('does not crash on zero', () => { + expect(formatByteCeiling(0)).toBe('0 B') + }) +}) diff --git a/src/main/ipc/runtime-import-limits.ts b/src/main/ipc/runtime-import-limits.ts new file mode 100644 index 00000000000..80fd680a24e --- /dev/null +++ b/src/main/ipc/runtime-import-limits.ts @@ -0,0 +1,18 @@ +// Why: staging streams slices at upload time and never holds a whole file, so +// these are user-safety ceilings on an unattended transfer, not memory guards. +// They stay until the drop UI can show progress and cancel a running upload. +export const REMOTE_IMPORT_MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024 +export const REMOTE_IMPORT_MAX_TOTAL_BYTES = 8 * 1024 * 1024 * 1024 + +/** Rounds up, so a size over a ceiling never renders as the ceiling itself. */ +export function formatByteCeiling(bytes: number): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let value = bytes + let unit = 0 + while (value >= 1024 && unit < units.length - 1) { + value /= 1024 + unit += 1 + } + const rounded = Math.ceil(value * 10) / 10 + return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)} ${units[unit]}` +} diff --git a/src/main/ipc/runtime-upload-file-stream.test.ts b/src/main/ipc/runtime-upload-file-stream.test.ts new file mode 100644 index 00000000000..4d11bc18652 --- /dev/null +++ b/src/main/ipc/runtime-upload-file-stream.test.ts @@ -0,0 +1,438 @@ +import { lstat, mkdtemp, mkdir, rename, rm, symlink, utimes, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { StagedRuntimeUploadFileIdentity } from '../../shared/runtime-upload-staging-contract' +import type * as RuntimeImportLimits from './runtime-import-limits' + +type RuntimeImportLimitsModule = typeof RuntimeImportLimits + +type ChunkCall = { + relativePath: string + contentBase64: string + append: boolean + expectedSshTargetId?: string + expectedSshConnectionGeneration?: number + expectedExecutionHostId?: string +} +type RuntimeCallOptions = { expectedEnvironmentRuntimeId?: string; signal?: AbortSignal } + +const callRuntimeEnvironment = + vi.fn< + ( + userDataPath: string, + environmentId: string, + method: string, + params: ChunkCall, + timeoutMs?: number, + expectedEnvironmentPairingRevision?: number, + envelope?: unknown, + options?: RuntimeCallOptions + ) => unknown + >() + +vi.mock('./runtime-environment-transport-routing', () => ({ + callRuntimeEnvironment: (...args: Parameters) => + callRuntimeEnvironment(...args) +})) +vi.mock('./filesystem-auth', () => ({ authorizeExternalPath: () => {} })) +// Why: see filesystem-runtime-upload-staging.test.ts — a real over-limit fixture +// would allocate gigabytes on Windows. +vi.mock('./runtime-import-limits', async (importOriginal) => ({ + ...(await importOriginal()), + REMOTE_IMPORT_MAX_FILE_BYTES: 2 * 1024 * 1024 +})) + +const { RUNTIME_UPLOAD_SLICE_BYTES, streamExternalFileToRuntime } = + await import('./runtime-upload-file-stream') +const { + clearRuntimeEnvironmentManualDisconnect, + markRuntimeEnvironmentManuallyDisconnected, + RUNTIME_MANUALLY_DISCONNECTED_MESSAGE +} = await import('./runtime-environment-manual-disconnect') + +let workDir: string + +function chunkCalls(): ChunkCall[] { + return callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.writeBase64Chunk') + .map(([, , , params]) => params) +} + +function uploadedBytes(): Buffer { + return Buffer.concat(chunkCalls().map((call) => Buffer.from(call.contentBase64, 'base64'))) +} + +/** Mirrors what staging records, so tests exercise the real identity contract. */ +async function stagedIdentity(filePath: string): Promise { + const stat = await lstat(filePath) + return { + byteLength: stat.size, + inode: stat.ino, + deviceId: stat.dev, + modifiedAtMs: stat.mtimeMs + } +} + +async function baseArgs(sourceRootPath: string, entryPath?: string) { + return { + userDataPath: '/user-data', + environmentId: 'env-1', + sourceRootPath, + entryRelativePath: entryPath ?? '', + expected: await stagedIdentity(entryPath ? join(sourceRootPath, entryPath) : sourceRootPath), + worktree: 'wt-1', + relativePath: '.upload.tmp' + } +} + +/** A path whose identity was never measured; every field is deliberately absent. */ +function unstagedArgs(sourceRootPath: string, entryPath?: string) { + return { + userDataPath: '/user-data', + environmentId: 'env-1', + sourceRootPath, + entryRelativePath: entryPath ?? '', + expected: { byteLength: 0, inode: 0, deviceId: 0, modifiedAtMs: 0 }, + worktree: 'wt-1', + relativePath: '.upload.tmp' + } +} + +beforeEach(async () => { + workDir = await mkdtemp(join(tmpdir(), 'orca-upload-stream-')) + callRuntimeEnvironment.mockReset() + callRuntimeEnvironment.mockResolvedValue({ id: 'x', ok: true, result: {}, _meta: {} }) +}) + +afterEach(async () => { + await rm(workDir, { force: true, recursive: true }) +}) + +describe('streamExternalFileToRuntime', () => { + it('sends a file larger than the old 25 MB cap as ordered append-only slices', async () => { + const size = RUNTIME_UPLOAD_SLICE_BYTES * 2 + 1234 + const contents = Buffer.alloc(size) + for (let index = 0; index < size; index += 1) { + contents[index] = index % 251 + } + const filePath = join(workDir, 'big.bin') + await writeFile(filePath, contents) + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).resolves.toEqual({ + byteLength: size + }) + + const calls = chunkCalls() + expect(calls).toHaveLength(3) + expect(calls.map((call) => call.append)).toEqual([false, true, true]) + expect(uploadedBytes().equals(contents)).toBe(true) + }) + + it('refuses a source whose size no longer matches what staging measured', async () => { + const filePath = join(workDir, 'grown.bin') + await writeFile(filePath, Buffer.alloc(1024)) + const staged = await stagedIdentity(filePath) + await writeFile(filePath, Buffer.alloc(2048)) + + await expect( + streamExternalFileToRuntime({ ...(await baseArgs(filePath)), expected: staged }) + ).rejects.toThrow("File changed since it was staged: 'grown.bin'") + expect(chunkCalls()).toHaveLength(0) + }) + + it('refuses a source swapped for a different file of the same size', async () => { + const filePath = join(workDir, 'swapped.bin') + await writeFile(filePath, Buffer.alloc(2048, 0x41)) + const staged = await stagedIdentity(filePath) + + // A rename-into-place keeps the size and changes the inode. + const decoyPath = join(workDir, 'decoy.bin') + await writeFile(decoyPath, Buffer.alloc(2048, 0x42)) + await rename(decoyPath, filePath) + + await expect( + streamExternalFileToRuntime({ ...(await baseArgs(filePath)), expected: staged }) + ).rejects.toThrow('File changed since it was staged') + expect(chunkCalls()).toHaveLength(0) + }) + + it('refuses a source rewritten in place at the same size after staging', async () => { + const filePath = join(workDir, 'rewritten.bin') + await writeFile(filePath, Buffer.alloc(2048, 0x41)) + const staged = await stagedIdentity(filePath) + + // Same inode and size; only the modification time moves. + await writeFile(filePath, Buffer.alloc(2048, 0x42)) + const bumped = new Date(staged.modifiedAtMs + 5_000) + await utimes(filePath, bumped, bumped) + + await expect( + streamExternalFileToRuntime({ ...(await baseArgs(filePath)), expected: staged }) + ).rejects.toThrow('File changed since it was staged') + expect(chunkCalls()).toHaveLength(0) + }) + + it('aborts when the source is rewritten at the same size mid-transfer', async () => { + const filePath = join(workDir, 'racing.bin') + const size = RUNTIME_UPLOAD_SLICE_BYTES * 2 + await writeFile(filePath, Buffer.alloc(size, 0x41)) + const args = await baseArgs(filePath) + + let rewritten = false + callRuntimeEnvironment.mockImplementation(async () => { + if (!rewritten) { + rewritten = true + await writeFile(filePath, Buffer.alloc(size, 0x42)) + const bumped = new Date(args.expected.modifiedAtMs + 5_000) + await utimes(filePath, bumped, bumped) + } + return { id: 'x', ok: true, result: {}, _meta: {} } + }) + + await expect(streamExternalFileToRuntime(args)).rejects.toThrow('File changed during upload') + }) + + it('accepts a source that still matches its staged identity', async () => { + const filePath = join(workDir, 'same.bin') + await writeFile(filePath, Buffer.alloc(2048)) + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).resolves.toEqual({ + byteLength: 2048 + }) + }) + + it('refuses a file over the ceiling and names the source, not the temp path', async () => { + const filePath = join(workDir, 'clip.mp4') + await writeFile(filePath, Buffer.alloc(3 * 1024 * 1024)) + + // Why: relativePath here is '.upload.tmp', a path the user never chose. + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).rejects.toThrow( + "'clip.mp4' is 3 MB, over the 2 MB per-file remote import limit" + ) + expect(chunkCalls()).toHaveLength(0) + }) + + it('never buffers more than one slice per chunk', async () => { + const filePath = join(workDir, 'sliced.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES * 2)) + + await streamExternalFileToRuntime(await baseArgs(filePath)) + + for (const call of chunkCalls()) { + expect(Buffer.from(call.contentBase64, 'base64').byteLength).toBeLessThanOrEqual( + RUNTIME_UPLOAD_SLICE_BYTES + ) + } + }) + + it('creates an empty destination for a zero-byte source', async () => { + const filePath = join(workDir, 'empty.txt') + await writeFile(filePath, '') + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).resolves.toEqual({ + byteLength: 0 + }) + + expect(chunkCalls()).toEqual([expect.objectContaining({ append: false, contentBase64: '' })]) + }) + + it('refuses to finish a zero-byte upload whose source gained content mid-write', async () => { + const filePath = join(workDir, 'grows.txt') + await writeFile(filePath, '') + const args = await baseArgs(filePath) + + callRuntimeEnvironment.mockImplementation(async () => { + await writeFile(filePath, 'content arrived during the empty write') + return { id: 'x', ok: true, result: {}, _meta: {} } + }) + + await expect(streamExternalFileToRuntime(args)).rejects.toThrow('File changed during upload') + }) + + it('carries the pairing revision and runtime id on every chunk', async () => { + const filePath = join(workDir, 'guarded.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES + 10)) + + await streamExternalFileToRuntime({ + ...(await baseArgs(filePath)), + expectedEnvironmentPairingRevision: 41, + expectedEnvironmentRuntimeId: 'runtime-7' + }) + + const guards = callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.writeBase64Chunk') + .map(([, , , , , revision, , options]) => ({ + revision, + runtimeId: options?.expectedEnvironmentRuntimeId + })) + expect(guards).toEqual([ + { revision: 41, runtimeId: 'runtime-7' }, + { revision: 41, runtimeId: 'runtime-7' } + ]) + }) + + it('stops mid-transfer when the caller aborts instead of streaming the rest', async () => { + const filePath = join(workDir, 'abandoned.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES * 4)) + const controller = new AbortController() + + callRuntimeEnvironment.mockImplementation(async () => { + controller.abort(new Error('window closed')) + return { id: 'x', ok: true, result: {}, _meta: {} } + }) + + await expect( + streamExternalFileToRuntime({ ...(await baseArgs(filePath)), signal: controller.signal }) + ).rejects.toThrow('window closed') + // One slice went out before the abort; the other three never do. + expect(chunkCalls()).toHaveLength(1) + }) + + it('refuses to start once the caller has already aborted', async () => { + const filePath = join(workDir, 'never.bin') + await writeFile(filePath, Buffer.alloc(1024)) + const controller = new AbortController() + controller.abort(new Error('window closed')) + + await expect( + streamExternalFileToRuntime({ ...(await baseArgs(filePath)), signal: controller.signal }) + ).rejects.toThrow('window closed') + expect(chunkCalls()).toHaveLength(0) + }) + + it('passes the abort signal to every chunk so an in-flight request is cancelled', async () => { + const filePath = join(workDir, 'signalled.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES + 10)) + const controller = new AbortController() + + await streamExternalFileToRuntime({ + ...(await baseArgs(filePath)), + signal: controller.signal + }) + + const signals = callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.writeBase64Chunk') + .map(([, , , , , , , options]) => options?.signal) + expect(signals).toEqual([controller.signal, controller.signal]) + }) + + it('stops at the failing chunk instead of sending the rest of the file', async () => { + const filePath = join(workDir, 'fails.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES * 3)) + callRuntimeEnvironment.mockResolvedValueOnce({ id: 'x', ok: true, result: {}, _meta: {} }) + callRuntimeEnvironment.mockResolvedValueOnce({ + id: 'x', + ok: false, + error: { code: 'write_failed', message: 'disk full' } + }) + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).rejects.toThrow('disk full') + expect(chunkCalls()).toHaveLength(2) + }) + + // symlink() needs privileges or Developer Mode on Windows. + it.skipIf(process.platform === 'win32')('refuses a symlinked source', async () => { + const targetPath = join(workDir, 'secret.txt') + await writeFile(targetPath, 'secret') + const linkPath = join(workDir, 'link.txt') + await symlink(targetPath, linkPath) + + await expect(streamExternalFileToRuntime(unstagedArgs(linkPath))).rejects.toThrow( + 'Symlink not allowed' + ) + expect(chunkCalls()).toHaveLength(0) + }) + + it.skipIf(process.platform === 'win32')( + 'refuses a regular file reached through a symlinked directory inside the root', + async () => { + // Why: the symlink guard only lstats the entry itself, which sees a plain + // file here — realpath containment is the only thing that catches this. + const outsideDir = join(workDir, 'outside') + await mkdir(outsideDir) + await writeFile(join(outsideDir, 'secret.txt'), 'secret') + const rootPath = join(workDir, 'root') + await mkdir(rootPath) + await symlink(outsideDir, join(rootPath, 'sub')) + + await expect( + streamExternalFileToRuntime(unstagedArgs(rootPath, 'sub/secret.txt')) + ).rejects.toThrow('Path escaped upload root during upload') + expect(chunkCalls()).toHaveLength(0) + } + ) + + it('forwards the host ownership expectations into every chunk', async () => { + const filePath = join(workDir, 'owned.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES + 10)) + + await streamExternalFileToRuntime({ + ...(await baseArgs(filePath)), + expectedSshTargetId: 'ssh-1', + expectedSshConnectionGeneration: 5, + expectedExecutionHostId: 'ssh:ssh-1' + }) + + const calls = callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.writeBase64Chunk') + .map(([, , , params]) => params) + expect(calls).toHaveLength(2) + for (const params of calls) { + expect(params).toMatchObject({ + expectedSshTargetId: 'ssh-1', + expectedSshConnectionGeneration: 5, + expectedExecutionHostId: 'ssh:ssh-1' + }) + } + }) + + it.skipIf(process.platform === 'win32')( + 'refuses a symlinked directory entry before it reaches the containment check', + async () => { + const outsidePath = join(workDir, 'outside.txt') + await writeFile(outsidePath, 'outside') + const rootPath = join(workDir, 'root') + await mkdir(rootPath) + await symlink(outsidePath, join(rootPath, 'escape.txt')) + + await expect( + streamExternalFileToRuntime(unstagedArgs(rootPath, 'escape.txt')) + ).rejects.toThrow('Symlink not allowed') + expect(chunkCalls()).toHaveLength(0) + } + ) +}) + +describe('manual disconnect during a transfer', () => { + afterEach(() => { + clearRuntimeEnvironmentManualDisconnect('env-1') + }) + + it('stops at the next slice once the environment is manually disconnected', async () => { + const filePath = join(workDir, 'disconnect.bin') + await writeFile(filePath, Buffer.alloc(RUNTIME_UPLOAD_SLICE_BYTES * 3, 7)) + callRuntimeEnvironment.mockImplementation(async (_u, _e, method) => { + if (method === 'files.writeBase64Chunk' && chunkCalls().length === 1) { + markRuntimeEnvironmentManuallyDisconnected('env-1') + } + return { id: 'x', ok: true, result: {}, _meta: {} } + }) + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).rejects.toThrow( + RUNTIME_MANUALLY_DISCONNECTED_MESSAGE + ) + expect(chunkCalls()).toHaveLength(1) + }) + + it('refuses the first slice when the environment is already disconnected', async () => { + const filePath = join(workDir, 'disconnected.bin') + await writeFile(filePath, Buffer.alloc(16, 1)) + markRuntimeEnvironmentManuallyDisconnected('env-1') + + await expect(streamExternalFileToRuntime(await baseArgs(filePath))).rejects.toThrow( + RUNTIME_MANUALLY_DISCONNECTED_MESSAGE + ) + expect(chunkCalls()).toHaveLength(0) + }) +}) diff --git a/src/main/ipc/runtime-upload-file-stream.ts b/src/main/ipc/runtime-upload-file-stream.ts new file mode 100644 index 00000000000..28271774ce0 --- /dev/null +++ b/src/main/ipc/runtime-upload-file-stream.ts @@ -0,0 +1,213 @@ +import { constants, type Stats } from 'node:fs' +import { lstat, open, realpath } from 'node:fs/promises' +import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path' +import type { + RuntimeUploadFileStreamRequest, + StagedRuntimeUploadFileIdentity +} from '../../shared/runtime-upload-staging-contract' +import { authorizeExternalPath } from './filesystem-auth' +import { formatByteCeiling, REMOTE_IMPORT_MAX_FILE_BYTES } from './runtime-import-limits' +import { + isRuntimeEnvironmentManuallyDisconnected, + RUNTIME_MANUALLY_DISCONNECTED_MESSAGE +} from './runtime-environment-manual-disconnect' +import { callRuntimeEnvironment } from './runtime-environment-transport-routing' + +// Why: base64 turns 3 bytes into 4 chars, so a 384 KiB slice lands on the wire +// as exactly 512 KiB — the chunk size the renderer used before streaming. +export const RUNTIME_UPLOAD_SLICE_BYTES = 384 * 1024 + +const RUNTIME_UPLOAD_CHUNK_TIMEOUT_MS = 30_000 + +export type RuntimeUploadFileStreamArgs = RuntimeUploadFileStreamRequest & { + /** Resolved environment id, not a selector: the manual-disconnect check keys on it. */ + environmentId: string + userDataPath: string + /** Aborts the transfer; the caller's lifetime is what raises it today. */ + signal?: AbortSignal +} + +/** + * Stream one client-local file to a runtime environment in slices. + * + * Replaces reading the whole file into memory and base64-encoding it before the + * first byte moves. Peak memory is one slice, so imports are no longer bounded + * by main-process heap. + */ +export async function streamExternalFileToRuntime( + args: RuntimeUploadFileStreamArgs +): Promise<{ byteLength: number }> { + const sourcePath = resolveEntrySourcePath(args.sourceRootPath, args.entryRelativePath) + + // Why: parity with staging — an OS drop authorizes the paths it hands over. + authorizeExternalPath(sourcePath) + + // Why: relativePath is the hidden .orca-upload- temp destination, so a + // dropped file names its source instead of a path the user never chose. + const displayPath = args.entryRelativePath || basename(args.sourceRootPath) + const lstatResult = await lstat(sourcePath) + if (lstatResult.isSymbolicLink()) { + throw new Error(`Symlink not allowed in '${displayPath}'`) + } + if (!lstatResult.isFile()) { + throw new Error(`Unsupported file type in '${displayPath}'`) + } + if (args.entryRelativePath) { + await assertEntryInsideRoot(args.sourceRootPath, sourcePath, displayPath) + } + assertMatchesStagedIdentity(lstatResult, args.expected, displayPath) + + args.signal?.throwIfAborted() + + const handle = await open(sourcePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + try { + const openedStat = await handle.stat() + if (!openedStat.isFile()) { + throw new Error(`Unsupported file type in '${displayPath}'`) + } + if (!isSameFile(openedStat, lstatResult)) { + throw new Error(`File changed during upload: '${displayPath}'`) + } + // Why: the handle is what the slices are read from, so the staged identity + // has to hold here too — checking only the pre-open lstat leaves a window + // where the path is swapped between lstat and open. + assertMatchesStagedIdentity(openedStat, args.expected, displayPath) + + const totalBytes = openedStat.size + // Why: enforced again where the bytes actually move. Staging is a separate + // call, so the ceiling only holds here if this boundary checks it too. + if (totalBytes > REMOTE_IMPORT_MAX_FILE_BYTES) { + throw new Error( + `'${displayPath}' is ${formatByteCeiling(totalBytes)}, over the ` + + `${formatByteCeiling(REMOTE_IMPORT_MAX_FILE_BYTES)} per-file remote import limit` + ) + } + if (totalBytes === 0) { + // Why: a zero-byte source produces no slices, but the destination still + // has to exist before commitUpload renames it into place. + await sendChunk(args, '', false) + } else { + const buffer = Buffer.allocUnsafe(Math.min(RUNTIME_UPLOAD_SLICE_BYTES, totalBytes)) + let offset = 0 + while (offset < totalBytes) { + // Why: checked per slice, so an abort stops the transfer at the next + // boundary instead of after the whole file has moved. + args.signal?.throwIfAborted() + const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, offset) + if (bytesRead === 0) { + throw new Error(`File truncated during upload: '${displayPath}'`) + } + await sendChunk(args, buffer.subarray(0, bytesRead).toString('base64'), offset > 0) + offset += bytesRead + } + } + + // Why: the destination is a temp path the caller commits, so a source + // rewritten mid-transfer is caught before anything lands at the final path. + // mtime catches an in-place edit that kept the size. An empty source runs + // this too: its chunk is still a round trip the source can change during. + const afterReadStat = await handle.stat() + if (afterReadStat.mtimeMs !== openedStat.mtimeMs || !isSameFile(afterReadStat, openedStat)) { + throw new Error(`File changed during upload: '${displayPath}'`) + } + return { byteLength: totalBytes } + } finally { + await handle.close() + } +} + +/** + * Refuse a source that no longer matches what staging measured. + * + * Inode and device are compared only when both sides report one, because some + * filesystems leave them at 0; size and mtime then carry the check alone. + */ +function assertMatchesStagedIdentity( + observed: Stats, + expected: StagedRuntimeUploadFileIdentity, + displayPath: string +): void { + const changed = + observed.size !== expected.byteLength || + observed.mtimeMs !== expected.modifiedAtMs || + (expected.inode !== 0 && observed.ino !== 0 && observed.ino !== expected.inode) || + (expected.deviceId !== 0 && observed.dev !== 0 && observed.dev !== expected.deviceId) + if (changed) { + throw new Error(`File changed since it was staged: '${displayPath}'`) + } +} + +/** Same inode on the same device, where the filesystem reports them. */ +function isSameFile(a: Stats, b: Stats): boolean { + return ( + a.size === b.size && + (a.ino === 0 || b.ino === 0 || a.ino === b.ino) && + (a.dev === 0 || b.dev === 0 || a.dev === b.dev) + ) +} + +/** Append one base64 slice, carrying the host guards that must hold per chunk. */ +async function sendChunk( + args: RuntimeUploadFileStreamArgs, + contentBase64: string, + append: boolean +): Promise { + // Why: the renderer's per-chunk calls went through an IPC handler that refuses + // a manually disconnected environment. The loop lives in main now, so it makes + // the same check, or a disconnect mid-upload keeps pushing bytes to that host. + if (isRuntimeEnvironmentManuallyDisconnected(args.environmentId)) { + throw new Error(RUNTIME_MANUALLY_DISCONNECTED_MESSAGE) + } + const response = await callRuntimeEnvironment( + args.userDataPath, + args.environmentId, + 'files.writeBase64Chunk', + { + worktree: args.worktree, + relativePath: args.relativePath, + contentBase64, + append, + expectedSshTargetId: args.expectedSshTargetId, + expectedSshConnectionGeneration: args.expectedSshConnectionGeneration, + expectedExecutionHostId: args.expectedExecutionHostId + }, + RUNTIME_UPLOAD_CHUNK_TIMEOUT_MS, + // Why: re-checked per chunk, so a re-pair mid-upload aborts instead of + // appending the rest of the file on a different host. + args.expectedEnvironmentPairingRevision, + undefined, + { + // Why: a replacement runtime keeps the pairing but invalidates its + // predecessor's capability proof, so the identity rides every chunk too. + expectedEnvironmentRuntimeId: args.expectedEnvironmentRuntimeId, + signal: args.signal + } + ) + if (response.ok !== true) { + throw new Error(response.error.message || response.error.code) + } +} + +function resolveEntrySourcePath(sourceRootPath: string, entryRelativePath: string): string { + // Why: staging resolves before authorizing, so the streamer has to agree on + // the same absolute path or the two checks can disagree. + const root = resolve(sourceRootPath) + return entryRelativePath ? join(root, entryRelativePath) : root +} + +async function assertEntryInsideRoot( + sourceRootPath: string, + candidatePath: string, + displayPath: string +): Promise { + const rootRealPath = await realpath(sourceRootPath) + const candidateRealPath = await realpath(candidatePath) + const relativeToRoot = relative(rootRealPath, candidateRealPath) + // Why: `..name` is a valid child path; only `..` and `../...` escape. + if ( + relativeToRoot !== '' && + (relativeToRoot === '..' || relativeToRoot.startsWith(`..${sep}`) || isAbsolute(relativeToRoot)) + ) { + throw new Error(`Path escaped upload root during upload: '${displayPath}'`) + } +} diff --git a/src/main/ipc/runtime-upload-slice-boundaries.test.ts b/src/main/ipc/runtime-upload-slice-boundaries.test.ts new file mode 100644 index 00000000000..0c2bd457b1f --- /dev/null +++ b/src/main/ipc/runtime-upload-slice-boundaries.test.ts @@ -0,0 +1,383 @@ +import { + appendFile, + mkdir, + mkdtemp, + readFile, + rm, + stat, + truncate, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { FileWriteBase64Chunk } from '../../shared/rpc-contract/files-mutation-params' +import type { StagedRuntimeUploadFileIdentity } from '../../shared/runtime-upload-staging-contract' + +// Why: real limits, real host write flags ('wx' then 'a') and the real chunk +// schema — the slice loop is exercised exactly at the boundaries it must respect. +vi.mock('./filesystem-auth', () => ({ authorizeExternalPath: () => {} })) + +type ChunkParams = { relativePath: string; contentBase64: string; append: boolean } +type CallOptions = { expectedEnvironmentRuntimeId?: string; signal?: AbortSignal } +type CallArgs = [ + userDataPath: string, + environmentId: string, + method: string, + params: ChunkParams, + timeoutMs?: number, + expectedEnvironmentPairingRevision?: number, + envelope?: unknown, + options?: CallOptions +] + +const callRuntimeEnvironment = vi.fn<(...args: CallArgs) => Promise>() +// Why: vi.fn retains every call's params; a 2 GiB stream would pin ~2.8 GB of +// base64 in mock.calls and masquerade as a leak. Big tests swap in a plain fn. +let transportImpl: (...args: CallArgs) => Promise = (...args) => + callRuntimeEnvironment(...args) +vi.mock('./runtime-environment-transport-routing', () => ({ + callRuntimeEnvironment: (...args: CallArgs) => transportImpl(...args) +})) + +const { RUNTIME_UPLOAD_SLICE_BYTES, streamExternalFileToRuntime } = + await import('./runtime-upload-file-stream') +const { stageOneSourceForRuntimeUpload } = await import('./filesystem-runtime-upload-staging') +const { REMOTE_IMPORT_MAX_FILE_BYTES, REMOTE_IMPORT_MAX_TOTAL_BYTES, formatByteCeiling } = + await import('./runtime-import-limits') + +const SLICE = RUNTIME_UPLOAD_SLICE_BYTES +const WIRE_CHUNK_CHARS = 512 * 1024 +const OK = { id: 'x', ok: true, result: {}, _meta: {} } + +let workDir: string +let remoteDir: string + +beforeEach(async () => { + workDir = await mkdtemp(join(tmpdir(), 'orca-upload-bounds-')) + remoteDir = join(workDir, 'remote') + await mkdir(remoteDir) + callRuntimeEnvironment.mockReset() + callRuntimeEnvironment.mockResolvedValue(OK) + transportImpl = (...args) => callRuntimeEnvironment(...args) +}) + +afterEach(async () => { + await rm(workDir, { force: true, recursive: true }) +}) + +function chunkCalls(): ChunkParams[] { + return callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.writeBase64Chunk') + .map(([, , , params]) => params) +} + +/** Mirrors the host: first chunk is an exclusive create, appends open with 'a'. */ +function installRealHostWrites(): void { + callRuntimeEnvironment.mockImplementation(async (_u, _e, method, params) => { + if (method === 'files.writeBase64Chunk') { + const parsed = FileWriteBase64Chunk.parse({ worktree: 'wt-1', ...params }) + await writeFile( + join(remoteDir, parsed.relativePath), + Buffer.from(parsed.contentBase64, 'base64'), + { + flag: parsed.append ? 'a' : 'wx' + } + ) + } + return OK + }) +} + +async function identityOf(path: string): Promise { + const s = await stat(path) + return { byteLength: s.size, inode: s.ino, deviceId: s.dev, modifiedAtMs: s.mtimeMs } +} + +async function argsFor(sourceRootPath: string, entryRelativePath = '', relativePath = 'dest.tmp') { + const target = entryRelativePath ? join(sourceRootPath, entryRelativePath) : sourceRootPath + return { + userDataPath: '/user-data', + environmentId: 'env-1', + sourceRootPath, + entryRelativePath, + expected: await identityOf(target), + worktree: 'wt-1', + relativePath, + expectedEnvironmentPairingRevision: 7, + expectedEnvironmentRuntimeId: 'rt-1' + } +} + +function patterned(size: number, seed: number): Buffer { + const buffer = Buffer.allocUnsafe(size) + for (let i = 0; i < size; i += 1) { + buffer[i] = (i * 31 + seed) & 0xff + } + return buffer +} + +describe('slice boundaries', () => { + const sizes = [ + 1, + 2, + 3, + 4, + SLICE - 1, + SLICE, + SLICE + 1, + 2 * SLICE - 1, + 2 * SLICE, + 2 * SLICE + 1, + 3 * SLICE + 7 + ] + + for (const size of sizes) { + it(`streams ${size} bytes as ceil(size/slice) schema-valid chunks that the host reassembles exactly`, async () => { + installRealHostWrites() + const contents = patterned(size, size) + const source = join(workDir, `s-${size}.bin`) + await writeFile(source, contents) + const dest = `dest-${size}.tmp` + + await expect(streamExternalFileToRuntime(await argsFor(source, '', dest))).resolves.toEqual({ + byteLength: size + }) + + const calls = chunkCalls() + const expectedChunks = Math.ceil(size / SLICE) + expect(calls).toHaveLength(expectedChunks) + expect(calls.map((c) => c.append)).toEqual(calls.map((_, i) => i > 0)) + for (const [index, call] of calls.entries()) { + const isLast = index === calls.length - 1 + expect(call.contentBase64.length).toBeLessThanOrEqual(WIRE_CHUNK_CHARS) + if (!isLast) { + expect(call.contentBase64.length).toBe(WIRE_CHUNK_CHARS) + } + expect(call.relativePath).toBe(dest) + } + const remote = await readFile(join(remoteDir, dest)) + expect(remote.equals(contents)).toBe(true) + }) + } + + it('sends a zero-byte file as one empty exclusive create the host schema accepts', async () => { + installRealHostWrites() + const source = join(workDir, 'empty.bin') + await writeFile(source, '') + + await expect( + streamExternalFileToRuntime(await argsFor(source, '', 'empty.tmp')) + ).resolves.toEqual({ + byteLength: 0 + }) + expect(chunkCalls()).toHaveLength(1) + expect(chunkCalls()[0]).toMatchObject({ + relativePath: 'empty.tmp', + contentBase64: '', + append: false + }) + expect((await stat(join(remoteDir, 'empty.tmp'))).size).toBe(0) + }) + + it('carries the pairing revision, runtime id and signal on every chunk', async () => { + const source = join(workDir, 'guards.bin') + await writeFile(source, patterned(2 * SLICE + 1, 3)) + + await streamExternalFileToRuntime(await argsFor(source)) + + const chunkInvocations = callRuntimeEnvironment.mock.calls.filter( + ([, , method]) => method === 'files.writeBase64Chunk' + ) + expect(chunkInvocations).toHaveLength(3) + for (const [, environmentId, , , timeoutMs, revision, envelope, options] of chunkInvocations) { + expect(environmentId).toBe('env-1') + expect(timeoutMs).toBe(30_000) + expect(revision).toBe(7) + expect(envelope).toBeUndefined() + expect(options?.expectedEnvironmentRuntimeId).toBe('rt-1') + } + }) +}) + +describe('staging → streaming end to end on a real filesystem', () => { + it('streams every staged entry of a dropped directory using the identity staging recorded', async () => { + installRealHostWrites() + const root = join(workDir, 'drop me') + await mkdir(join(root, 'sub', 'deeper'), { recursive: true }) + const files: Record = { + 'a.txt': Buffer.from('alpha'), + '..keep': Buffer.from('dot-dot-prefixed name is a valid child'), + 'héllo wörld.bin': patterned(SLICE, 9), + 'sub/empty': Buffer.alloc(0), + 'sub/deeper/big.bin': patterned(2 * SLICE + 5, 11) + } + for (const [rel, body] of Object.entries(files)) { + await writeFile(join(root, rel), body) + } + + const staged = await stageOneSourceForRuntimeUpload(root) + expect(staged.status).toBe('staged') + if (staged.status !== 'staged') { + return + } + const fileEntries = staged.entries.filter((e) => e.kind === 'file') + expect(fileEntries.map((e) => e.relativePath).sort()).toEqual(Object.keys(files).sort()) + + for (const entry of fileEntries) { + if (entry.kind !== 'file') { + continue + } + const dest = `up-${entry.relativePath.replace(/[^a-z0-9]/gi, '_')}.tmp` + await expect( + streamExternalFileToRuntime({ + userDataPath: '/u', + environmentId: 'env-1', + sourceRootPath: staged.sourcePath, + entryRelativePath: entry.relativePath, + expected: { + byteLength: entry.byteLength, + inode: entry.inode, + deviceId: entry.deviceId, + modifiedAtMs: entry.modifiedAtMs + }, + worktree: 'wt-1', + relativePath: dest + }) + ).resolves.toEqual({ byteLength: files[entry.relativePath]!.length }) + const remote = await readFile(join(remoteDir, dest)) + expect(remote.equals(files[entry.relativePath]!)).toBe(true) + } + }) + + it('streams a dropped single file using the identity staging recorded', async () => { + installRealHostWrites() + const source = join(workDir, 'single.bin') + const body = patterned(SLICE + 1, 5) + await writeFile(source, body) + + const staged = await stageOneSourceForRuntimeUpload(source) + expect(staged.status).toBe('staged') + if (staged.status !== 'staged') { + return + } + const entry = staged.entries[0]! + expect(entry.kind).toBe('file') + if (entry.kind !== 'file') { + return + } + + await expect( + streamExternalFileToRuntime({ + userDataPath: '/u', + environmentId: 'env-1', + sourceRootPath: staged.sourcePath, + entryRelativePath: entry.relativePath, + expected: entry, + worktree: 'wt-1', + relativePath: 'single.tmp' + }) + ).resolves.toEqual({ byteLength: body.length }) + expect((await readFile(join(remoteDir, 'single.tmp'))).equals(body)).toBe(true) + }) +}) + +describe('source mutation during transfer', () => { + it('rejects a source that grows during the transfer and never claims success', async () => { + const source = join(workDir, 'growing.bin') + await writeFile(source, patterned(2 * SLICE, 1)) + const args = await argsFor(source) + callRuntimeEnvironment.mockImplementation(async (_u, _e, method) => { + if (method === 'files.writeBase64Chunk' && chunkCalls().length === 1) { + await appendFile(source, 'extra') + } + return OK + }) + + await expect(streamExternalFileToRuntime(args)).rejects.toThrow( + "File changed during upload: 'growing.bin'" + ) + }) + + it('rejects a source truncated during the transfer instead of sending a short file', async () => { + const source = join(workDir, 'shrinking.bin') + await writeFile(source, patterned(3 * SLICE, 2)) + const args = await argsFor(source) + callRuntimeEnvironment.mockImplementation(async (_u, _e, method) => { + if (method === 'files.writeBase64Chunk' && chunkCalls().length === 1) { + await truncate(source, SLICE) + } + return OK + }) + + await expect(streamExternalFileToRuntime(args)).rejects.toThrow( + "File truncated during upload: 'shrinking.bin'" + ) + expect(chunkCalls().length).toBeLessThan(3) + }) + + it('accepts a staged identity whose inode and device are unreported (0) when size and mtime match', async () => { + const source = join(workDir, 'no-ino.bin') + await writeFile(source, patterned(10, 4)) + const args = await argsFor(source) + args.expected = { ...args.expected, inode: 0, deviceId: 0 } + + await expect(streamExternalFileToRuntime(args)).resolves.toEqual({ byteLength: 10 }) + }) + + it('still refuses a wrong inode when only the device is unreported', async () => { + const source = join(workDir, 'wrong-ino.bin') + await writeFile(source, patterned(10, 4)) + const args = await argsFor(source) + args.expected = { ...args.expected, inode: args.expected.inode + 1, deviceId: 0 } + + await expect(streamExternalFileToRuntime(args)).rejects.toThrow( + "File changed since it was staged: 'wrong-ino.bin'" + ) + expect(chunkCalls()).toHaveLength(0) + }) + + it('stops before the next slice when the signal aborts while a chunk is in flight', async () => { + const source = join(workDir, 'abort.bin') + await writeFile(source, patterned(3 * SLICE, 6)) + const controller = new AbortController() + callRuntimeEnvironment.mockImplementation(async (_u, _e, method, _p, _t, _r, _env, options) => { + if (method !== 'files.writeBase64Chunk') { + return OK + } + if (chunkCalls().length === 2) { + return new Promise((_resolve, reject) => { + options?.signal?.addEventListener('abort', () => reject(options.signal?.reason), { + once: true + }) + controller.abort(new Error('window gone')) + }) + } + return OK + }) + + await expect( + streamExternalFileToRuntime({ ...(await argsFor(source)), signal: controller.signal }) + ).rejects.toThrow('window gone') + expect(chunkCalls()).toHaveLength(2) + }) +}) + +describe('formatByteCeiling bounds', () => { + it.each([ + [0, '0 B'], + [1, '1 B'], + [1023, '1023 B'], + [1024, '1 KB'], + [1025, '1.1 KB'], + [25 * 1024 * 1024, '25 MB'], + [25 * 1024 * 1024 + 1, '25.1 MB'], + [REMOTE_IMPORT_MAX_FILE_BYTES, '2 GB'], + [REMOTE_IMPORT_MAX_FILE_BYTES + 1, '2.1 GB'], + [REMOTE_IMPORT_MAX_TOTAL_BYTES, '8 GB'], + [REMOTE_IMPORT_MAX_TOTAL_BYTES + 1, '8.1 GB'], + [1024 ** 5, '1024 TB'] + ])('%i → %s', (bytes, text) => { + expect(formatByteCeiling(bytes)).toBe(text) + }) +}) diff --git a/src/main/ipc/runtime-upload-temp-sweep.test.ts b/src/main/ipc/runtime-upload-temp-sweep.test.ts new file mode 100644 index 00000000000..06e7fe9282c --- /dev/null +++ b/src/main/ipc/runtime-upload-temp-sweep.test.ts @@ -0,0 +1,94 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { RuntimeUploadFileStreamRequest } from '../../shared/runtime-upload-staging-contract' + +const callRuntimeEnvironment = + vi.fn< + ( + userDataPath: string, + environmentId: string, + method: string, + params: { relativePath: string; recursive: boolean }, + timeoutMs?: number, + expectedEnvironmentPairingRevision?: number, + envelope?: unknown, + options?: { expectedEnvironmentRuntimeId?: string } + ) => unknown + >() + +vi.mock('./runtime-environment-transport-routing', () => ({ + callRuntimeEnvironment: (...args: Parameters) => + callRuntimeEnvironment(...args) +})) + +const { sweepAbandonedRuntimeUploadTempPath } = await import('./runtime-upload-temp-sweep') + +const request: RuntimeUploadFileStreamRequest = { + environmentId: 'env-1', + sourceRootPath: '/Users/me/clip.mp4', + entryRelativePath: '', + expected: { byteLength: 4, inode: 1, deviceId: 2, modifiedAtMs: 3 }, + worktree: 'id:wt-1', + relativePath: 'uploads/.clip.mp4.orca-upload-abc', + expectedEnvironmentPairingRevision: 17, + expectedEnvironmentRuntimeId: 'runtime-7', + expectedExecutionHostId: 'local' +} + +function deleteCalls(): { relativePath: string; recursive: boolean }[] { + return callRuntimeEnvironment.mock.calls + .filter(([, , method]) => method === 'files.delete') + .map(([, , , params]) => params) +} + +beforeEach(() => { + vi.useFakeTimers() + callRuntimeEnvironment.mockReset() + callRuntimeEnvironment.mockResolvedValue({ id: 'x', ok: true, result: {}, _meta: {} }) +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('sweepAbandonedRuntimeUploadTempPath', () => { + it('deletes twice, because a straggling append recreates the file with flag a', async () => { + const swept = sweepAbandonedRuntimeUploadTempPath('/user-data', request) + await vi.runAllTimersAsync() + await swept + + expect(deleteCalls()).toEqual([ + expect.objectContaining({ relativePath: request.relativePath, recursive: false }), + expect.objectContaining({ relativePath: request.relativePath, recursive: false }) + ]) + }) + + it('still makes the second pass when the first one fails', async () => { + callRuntimeEnvironment.mockRejectedValueOnce(new Error('connection lost')) + + const swept = sweepAbandonedRuntimeUploadTempPath('/user-data', request) + await vi.runAllTimersAsync() + await expect(swept).resolves.toBeUndefined() + + expect(deleteCalls()).toHaveLength(2) + }) + + it('carries the host ownership guards so it cannot delete on a re-paired host', async () => { + const swept = sweepAbandonedRuntimeUploadTempPath('/user-data', request) + await vi.runAllTimersAsync() + await swept + + for (const call of callRuntimeEnvironment.mock.calls) { + expect(call[5]).toBe(17) + expect(call[7]?.expectedEnvironmentRuntimeId).toBe('runtime-7') + } + }) + + it('never rejects, so cleanup cannot mask the upload failure', async () => { + callRuntimeEnvironment.mockRejectedValue(new Error('runtime gone')) + + const swept = sweepAbandonedRuntimeUploadTempPath('/user-data', request) + await vi.runAllTimersAsync() + + await expect(swept).resolves.toBeUndefined() + }) +}) diff --git a/src/main/ipc/runtime-upload-temp-sweep.ts b/src/main/ipc/runtime-upload-temp-sweep.ts new file mode 100644 index 00000000000..46150fd96e5 --- /dev/null +++ b/src/main/ipc/runtime-upload-temp-sweep.ts @@ -0,0 +1,49 @@ +import { setTimeout } from 'node:timers/promises' +import type { RuntimeUploadFileStreamRequest } from '../../shared/runtime-upload-staging-contract' +import { callRuntimeEnvironment } from './runtime-environment-transport-routing' + +const RUNTIME_UPLOAD_SWEEP_ATTEMPTS = 2 +const RUNTIME_UPLOAD_SWEEP_SETTLE_MS = 250 + +/** + * Sweep an abandoned upload temp path after an abort. + * + * Aborting rejects the in-flight chunk locally, but the host may still apply + * that append — and appends open with `flag: 'a'`, which recreates the file a + * delete just removed. Slices are strictly sequential, so at most one append + * can be outstanding: a second pass after it has had time to land is enough. + * + * Best-effort throughout. The runtime may be why the upload failed, and a + * failed cleanup of a hidden temp file is not actionable. + */ +export async function sweepAbandonedRuntimeUploadTempPath( + userDataPath: string, + args: RuntimeUploadFileStreamRequest +): Promise { + for (let attempt = 0; attempt < RUNTIME_UPLOAD_SWEEP_ATTEMPTS; attempt += 1) { + if (attempt > 0) { + await setTimeout(RUNTIME_UPLOAD_SWEEP_SETTLE_MS) + } + try { + await callRuntimeEnvironment( + userDataPath, + args.environmentId, + 'files.delete', + { + worktree: args.worktree, + relativePath: args.relativePath, + recursive: false, + expectedSshTargetId: args.expectedSshTargetId, + expectedSshConnectionGeneration: args.expectedSshConnectionGeneration, + expectedExecutionHostId: args.expectedExecutionHostId + }, + 15_000, + args.expectedEnvironmentPairingRevision, + undefined, + { expectedEnvironmentRuntimeId: args.expectedEnvironmentRuntimeId } + ) + } catch { + // Nothing to escalate; the next pass (if any) still runs. + } + } +} diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index a6a90f2b9bf..31d587bd056 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -13,6 +13,7 @@ const { resolveEnvironmentMock, rebuildAppMenuMock, applyBrowserSessionProxiesMock, + applySessionSearchSettingsChangeMock, listProfilesMock } = vi.hoisted(() => ({ applyAppIconMock: vi.fn(), @@ -27,6 +28,7 @@ const { resolveEnvironmentMock: vi.fn(), rebuildAppMenuMock: vi.fn(), applyBrowserSessionProxiesMock: vi.fn(), + applySessionSearchSettingsChangeMock: vi.fn(), listProfilesMock: vi.fn(() => []) })) @@ -61,6 +63,10 @@ vi.mock('../app-icon', () => ({ applyAppIcon: applyAppIconMock })) +vi.mock('../ai-vault-search/session-search-enablement', () => ({ + applySessionSearchSettingsChange: applySessionSearchSettingsChangeMock +})) + vi.mock('../agent-hooks/managed-agent-hook-controls', () => ({ applyAgentStatusHooksEnabled: applyAgentStatusHooksEnabledMock })) @@ -113,6 +119,7 @@ describe('registerSettingsHandlers', () => { }) rebuildAppMenuMock.mockClear() applyBrowserSessionProxiesMock.mockReset().mockResolvedValue(undefined) + applySessionSearchSettingsChangeMock.mockClear() listProfilesMock.mockReset().mockReturnValue([]) browserWindowGetAllWindowsMock.mockReset() store.getSettings.mockReset() @@ -827,4 +834,44 @@ describe('registerSettingsHandlers', () => { expect(rebuildAppMenuMock).toHaveBeenCalledTimes(1) }) + + // 3b stores the two booleans and nothing else; the consent copy and the + // history picker are PR 8's. A profile that has never opted in has no key. + it('normalizes an agent-session-search write and hands the change to the index', async () => { + const before = { aiVaultSearch: { enabled: false, historyDays: null } } + store.getSettings.mockReturnValue(before) + store.updateSettings.mockImplementation((args: object) => ({ ...before, ...args })) + registerSettingsHandlers(store as never) + const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( + event: typeof settingsInvokeEvent, + args: unknown + ) => Promise + + await handler(settingsInvokeEvent, { + aiVaultSearch: { enabled: true, historyDays: 30.7, paused: true } + }) + + expect(store.updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ aiVaultSearch: { enabled: true, historyDays: 30 } }), + expect.anything() + ) + expect(applySessionSearchSettingsChangeMock).toHaveBeenCalledWith( + before, + expect.objectContaining({ aiVaultSearch: { enabled: true, historyDays: 30 } }) + ) + }) + + it('leaves the index alone for a settings write that does not mention it', async () => { + store.getSettings.mockReturnValue({ appIcon: 'default' }) + store.updateSettings.mockReturnValue({ appIcon: 'default' }) + registerSettingsHandlers(store as never) + const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( + event: typeof settingsInvokeEvent, + args: unknown + ) => Promise + + await handler(settingsInvokeEvent, { appIcon: 'default' }) + + expect(applySessionSearchSettingsChangeMock).not.toHaveBeenCalled() + }) }) diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index 1d4194825d9..f3c1b8aeaf8 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -36,6 +36,8 @@ import { computerAwakeSettingsForMode, normalizeComputerAwakeMode } from '../../shared/computer-awake-mode' +import { resolveAiVaultSearchSettings } from '../../shared/ai-vault-search-settings' +import { applySessionSearchSettingsChange } from '../ai-vault-search/session-search-enablement' // Why: the whitelist is the source-of-truth for which keys we emit on. Casting // to a Set once at module load lets the IPC handler's per-key membership @@ -160,6 +162,9 @@ export function registerSettingsHandlers( if ('appIcon' in args) { sanitizedArgs.appIcon = normalizeAppIconId(args.appIcon) } + if ('aiVaultSearch' in args) { + sanitizedArgs.aiVaultSearch = resolveAiVaultSearchSettings(args) + } if ('terminalCustomThemes' in args) { sanitizedArgs.terminalCustomThemes = normalizeTerminalCustomThemes(args.terminalCustomThemes) } @@ -266,6 +271,9 @@ export function registerSettingsHandlers( if ('appIcon' in sanitizedArgs && before.appIcon !== result.appIcon) { applyAppIcon(result.appIcon) } + if ('aiVaultSearch' in sanitizedArgs) { + applySessionSearchSettingsChange(before, result) + } // Why: telemetry-plan.md§Settings — fire `settings_changed` only for // whitelisted keys, with `value_kind` distinguishing booleans from diff --git a/src/main/ipc/worktrees-removal-recovery.test.ts b/src/main/ipc/worktrees-removal-recovery.test.ts index e8571449321..55d47bc680a 100644 --- a/src/main/ipc/worktrees-removal-recovery.test.ts +++ b/src/main/ipc/worktrees-removal-recovery.test.ts @@ -402,29 +402,63 @@ describe('registerWorktreeHandlers', () => { } }) - it('retries stale Git registration cleanup after prior local filesystem recovery', async () => { - setPlatform('win32') - const missingWorktreePath = 'C:\\workspace\\already-removed' - const worktreeId = `repo-1::${missingWorktreePath}` - const registeredWorktrees = mockKnownFeatureWorktree(missingWorktreePath) - listWorktreesMock.mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([]) - store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) + it.each([false, true])( + 'retries missing registration cleanup (prunable marker: %s)', + async (prunableMarker) => { + setPlatform('win32') + const missingWorktreePath = prunableMarker + ? 'C:\\workspace\\already-removed\\.git' + : 'C:\\workspace\\already-removed' + const worktreeId = `repo-1::${missingWorktreePath}` + const registeredWorktrees = mockKnownFeatureWorktree(missingWorktreePath).map((row) => + prunableMarker && row.path === missingWorktreePath + ? { ...row, branch: 'refs/heads/feature', prunable: true } + : row + ) + listWorktreesMock.mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([]) + store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) - const result = await handlers['worktrees:remove'](null, { - worktreeId, - force: true - }) + const result = await handlers['worktrees:remove'](null, { + worktreeId, + force: true + }) - expect(result).toEqual({ - preservedBranch: { branchName: 'feature', head: 'feature' } - }) - expect(runHookMock).not.toHaveBeenCalled() - expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() - expect(removeWorktreeMock).not.toHaveBeenCalled() - expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { - cwd: '/workspace/repo' - }) - expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') + expect(result).toEqual({ + preservedBranch: { branchName: 'feature', head: 'feature' } + }) + expect(runHookMock).not.toHaveBeenCalled() + expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { + cwd: '/workspace/repo' + }) + expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') + } + ) + + it('cleans a prunable Git-file row before archive or checkout teardown', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-prunable-ipc-')) + const markerPath = join(root, '.git') + await writeFile(markerPath, 'gitdir: /preserved/admin\n') + const worktreeId = `repo-1::${markerPath}` + const rows = mockKnownFeatureWorktree(markerPath).map((row) => + row.path === markerPath ? { ...row, branch: 'refs/heads/feature', prunable: true } : row + ) + listWorktreesMock.mockResolvedValueOnce(rows).mockResolvedValue([]) + try { + const result = await handlers['worktrees:remove'](null, { worktreeId }) + expect(result).toEqual({ preservedBranch: { branchName: 'feature', head: 'feature' } }) + expect(runHookMock).not.toHaveBeenCalled() + expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { + cwd: '/workspace/repo' + }) + expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') + expect((await lstat(markerPath)).isFile()).toBe(true) + } finally { + await rm(root, { recursive: true, force: true }) + } }) it('preserves a locked missing registration even with force', async () => { diff --git a/src/main/ipc/worktrees/listing/ssh-worktree-fallback.ts b/src/main/ipc/worktrees/listing/ssh-worktree-fallback.ts index ecf8ab3abc1..594b75cc5da 100644 --- a/src/main/ipc/worktrees/listing/ssh-worktree-fallback.ts +++ b/src/main/ipc/worktrees/listing/ssh-worktree-fallback.ts @@ -1,3 +1,4 @@ +import { preserveFolderUpgradeWorktreePath } from '../../../folder-upgrade-worktree-path' import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import { parseWorktreeId, areWorktreePathsEqual, mergeWorktree } from '../../worktree-logic' import { @@ -144,7 +145,9 @@ export function buildDetectedGitWorktrees( const isLegacyRepoForVisibility = isLegacyRepoForExternalWorktreeVisibility(repo) // Why: a prunable registration has no working directory (issue #8389); only this listing omits it — cleanup flows list separately. const liveWorktrees = dedupeWorktreesByPath( - gitWorktrees.filter((gitWorktree) => !gitWorktree.prunable) + preserveFolderUpgradeWorktreePath(repo, gitWorktrees).filter( + (gitWorktree) => !gitWorktree.prunable + ) ) const worktreeVisibilitySourceMatcher = createWorktreeVisibilitySourceMatcher( [repo.path, ...liveWorktrees.map((worktree) => worktree.path)], diff --git a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts index 6f0d91ffef5..443e05627ca 100644 --- a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts +++ b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts @@ -8,8 +8,9 @@ import { getLocalProjectWorktreeGitOptions } from '../../../project-runtime-git- import { listWorktreesStrict as listGitWorktreesStrict } from '../../../git/worktree' import { requireSshGitProvider } from '../../../providers/ssh-git-dispatch' import { resolveWorktreeRemovalMetadata } from '../../../worktree-removal-repo-owner' +import { isPrunableGitFileWorktree } from '../../../worktree-prunable-git-file' import { findRegisteredDeletableWorktree } from '../../../worktree-removal-safety' -import { removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval } from '../../../local-worktree-removal-recovery' +import { removeStaleLocalWorktreeRegistration } from '../../../local-worktree-removal-recovery' import { runHook } from '../../../hooks' import { withWorktreeRemoveStageSpan } from '../../../observability/instrumentation' import { @@ -85,13 +86,14 @@ export async function executeWorktreeRemoval( if ( !repo.connectionId && - args.force === true && - process.platform === 'win32' && - (isWindowsAbsolutePathLike(canonicalWorktreePath) || !!localWorktreeGitOptions.wslDistro) && - removedMeta && - (await isAlreadyRemovedWorktreePath(repo, canonicalWorktreePath, localWorktreeGitOptions)) + ((await isPrunableGitFileWorktree(registeredWorktree, localWorktreeGitOptions)) || + (args.force === true && + process.platform === 'win32' && + (isWindowsAbsolutePathLike(canonicalWorktreePath) || !!localWorktreeGitOptions.wslDistro) && + removedMeta && + (await isAlreadyRemovedWorktreePath(repo, canonicalWorktreePath, localWorktreeGitOptions)))) ) { - const removalResult = await removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval({ + const removalResult = await removeStaleLocalWorktreeRegistration({ canonicalWorktreePath, repoPath: repo.path, localWorktreeGitOptions, diff --git a/src/main/local-worktree-removal-recovery.test.ts b/src/main/local-worktree-removal-recovery.test.ts index 1be0fd02cce..7d5aa442a1e 100644 --- a/src/main/local-worktree-removal-recovery.test.ts +++ b/src/main/local-worktree-removal-recovery.test.ts @@ -22,7 +22,7 @@ vi.mock('./git/worktree', () => ({ import { recoverLocalWindowsWorktreeRemoval, - removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval + removeStaleLocalWorktreeRegistration } from './local-worktree-removal-recovery' async function withPlatform(platform: NodeJS.Platform, fn: () => Promise): Promise { @@ -306,7 +306,7 @@ describe('recoverLocalWindowsWorktreeRemoval', () => { }) }) -describe('removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval', () => { +describe('removeStaleLocalWorktreeRegistration', () => { beforeEach(() => { gitExecFileAsyncMock.mockReset() listWorktreesStrictMock.mockReset() @@ -314,9 +314,27 @@ describe('removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval', () => { listWorktreesStrictMock.mockResolvedValue([]) }) + it('prunes and strictly verifies on the selected WSL host without deleting files or branches', async () => { + const options = { wslDistro: 'Ubuntu' } + const result = await removeStaleLocalWorktreeRegistration({ + canonicalWorktreePath: '/home/dev/feature/.git', + repoPath: '/home/dev/repo', + localWorktreeGitOptions: options, + registeredWorktree: { branch: 'refs/heads/feature', head: 'abc123' }, + deleteBranch: true + }) + expect(result).toEqual({ preservedBranch: { branchName: 'feature', head: 'abc123' } }) + expect(gitExecFileAsyncMock).toHaveBeenCalledExactlyOnceWith(['worktree', 'prune'], { + cwd: '/home/dev/repo', + wslDistro: 'Ubuntu' + }) + expect(listWorktreesStrictMock).toHaveBeenCalledExactlyOnceWith('/home/dev/repo', options) + expect(removeLocalWorktreePathMock).not.toHaveBeenCalled() + }) + it('does not override a locked missing registration', async () => { await expect( - removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval({ + removeStaleLocalWorktreeRegistration({ canonicalWorktreePath: 'C:/workspaces/feature', repoPath: 'C:/repo', localWorktreeGitOptions: {}, @@ -345,7 +363,7 @@ describe('removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval', () => { ]) await expect( - removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval({ + removeStaleLocalWorktreeRegistration({ canonicalWorktreePath: 'C:/workspaces/feature', repoPath: 'C:/repo', localWorktreeGitOptions: {}, diff --git a/src/main/local-worktree-removal-recovery.ts b/src/main/local-worktree-removal-recovery.ts index 630dd338c20..46f1bb15c87 100644 --- a/src/main/local-worktree-removal-recovery.ts +++ b/src/main/local-worktree-removal-recovery.ts @@ -47,7 +47,7 @@ function staleRegistrationRecoveryError( error, canonicalWorktreePath, force - )} The worktree directory was removed, but Git still has stale worktree registration. Retry deletion after resolving the Git registration error.` + )} Git still has stale worktree registration. Retry deletion after resolving the Git registration error.` ) } @@ -151,7 +151,7 @@ async function isRecoverableWindowsFilesystemRemovalFailure( } } -export async function removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval( +export async function removeStaleLocalWorktreeRegistration( args: StaleLocalWorktreeRegistrationArgs ): Promise { return removeRequiredGitWorktreeRegistration(args) diff --git a/src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts b/src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts index c8d48ef336b..d142672f8e5 100644 --- a/src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts +++ b/src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts @@ -16,7 +16,6 @@ import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types' import { hasUnansweredStructuredAgentSessionDispatch } from '../../../shared/structured-agent-session-projection' -import { DISPATCH_DOUBT_CODEX_TURN_UNNAMED } from './journal-dispatch-doubt-reasons' import { dispatchWriteFailureReason } from '../../../shared/structured-agent-session-dispatch-rejection' import { digestPayload } from './journal-payload-bounds' import { @@ -55,6 +54,8 @@ function userMessage(text: string): AgentJournalMessageItem { return { kind: 'message', role: 'user', blocks: [{ type: 'text', text }] } } +const LEGACY_CODEX_TURN_UNNAMED = 'codex app-server started a turn it did not name in time' + const journals = createTrackedJournalOpener() async function open() { @@ -196,6 +197,8 @@ describe('crash between provider accept and journal commit', () => { expect(hasUnansweredStructuredAgentSessionDispatch(restarted.submissions())).toBe(false) }) + // Only an older Orca minted this reason -- Codex now settles a send on the + // provider echo -- but rows written under it still come back from disk. it('keeps a codex turn it could not name in doubt, never rejected', async () => { const journal = await open() await journal.appendSubmission({ @@ -207,7 +210,7 @@ describe('crash between provider accept and journal commit', () => { await journal.resolveDispatch({ clientMessageId: 'cm_codex_unnamed', state: 'unknown', - reason: DISPATCH_DOUBT_CODEX_TURN_UNNAMED, + reason: LEGACY_CODEX_TURN_UNNAMED, fence: 1 }) @@ -218,7 +221,7 @@ describe('crash between provider accept and journal commit', () => { // and it may never become a rejection, which would license a re-delivery. expect(restarted.submissions()[0]).toMatchObject({ dispatchState: 'unknown', - reason: DISPATCH_DOUBT_CODEX_TURN_UNNAMED, + reason: LEGACY_CODEX_TURN_UNNAMED, recovered: true }) }) diff --git a/src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts b/src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts index dbd56132100..206bd19e3ef 100644 --- a/src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts +++ b/src/main/native-chat/agent-session-journal/journal-dispatch-doubt-reasons.ts @@ -23,13 +23,6 @@ export const DISPATCH_DOUBT_PERSISTENCE_FAILED = 'dispatch_result_persistence_fa /** The operation tombstone survived recovery but its journal submission did not. */ export const DISPATCH_DOUBT_SUBMISSION_MISSING = 'durable_send_submission_missing' -/** Codex owns a turn it started but did not name, because its turn-start still - * settles on a deadline. The turn IS running, so this must never be treated as - * proof of non-delivery. Delete it once Codex settles on the app-server's - * turn-start response instead. */ -export const DISPATCH_DOUBT_CODEX_TURN_UNNAMED = - 'codex app-server started a turn it did not name in time' - /** The SDK took the frame, but its input pump did not prove whether the write completed. */ export const DISPATCH_DOUBT_WRITE_OUTCOME_UNKNOWN = 'provider_write_outcome_unknown' diff --git a/src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts b/src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts new file mode 100644 index 00000000000..70052865242 --- /dev/null +++ b/src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts @@ -0,0 +1,87 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types' +import { + DISPATCH_REJECTED_WRITE_FAILED, + dispatchRejectionReasonIsInternal, + dispatchRejectionWasTransportWriteFailure +} from '../../../shared/structured-agent-session-dispatch-rejection' +import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from './journal-payload-bounds' +import type { openAgentSessionJournal } from './journal-store-factory' +import { createTrackedJournalOpener } from './journal-store-test-open' + +const IDENTITY: AgentSessionJournalIdentity = { + sessionId: 'session-1', + workspaceId: 'ws-1', + hostId: 'host-1', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: 'thread-1' } +} + +const HUGE = 'x'.repeat(4 * DEFAULT_JOURNAL_PAYLOAD_LIMITS.inlineHeadBytes) + +let root: string +let clock = 1_000 + +const journals = createTrackedJournalOpener() + +async function open(overrides: Partial[0]> = {}) { + return journals.open({ + identity: IDENTITY, + journalDir: root, + now: () => (clock += 1), + mintEpoch: () => `epoch-${clock}`, + ...overrides + }) +} + +async function settle(reason: string): Promise { + const journal = await open() + await journal.appendSubmission({ + clientMessageId: 'msg-1', + payloadFingerprint: 'e'.repeat(64), + body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hi' }] }, + fence: 1 + }) + await journal.resolveDispatch({ clientMessageId: 'msg-1', state: 'rejected', reason, fence: 1 }) + return journal.snapshot().submissions[0]?.reason ?? null +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-dispatch-reason-')) + clock = 1_000 +}) + +afterEach(async () => { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) +}) + +describe('dispatch reason bounding', () => { + it('bounds an oversized provider error before it reaches the row', async () => { + const stored = await settle(HUGE) + expect(stored).not.toBeNull() + expect(stored?.length).toBeLessThan(HUGE.length) + }) + + it('marks the clipped reason rather than truncating it silently', async () => { + const stored = await settle(HUGE) + expect(stored).toContain('[Orca: output truncated') + }) + + it('leaves a reason that already fits exactly as written', async () => { + const stored = await settle(`${DISPATCH_REJECTED_WRITE_FAILED}: broken pipe`) + expect(stored).toBe(`${DISPATCH_REJECTED_WRITE_FAILED}: broken pipe`) + }) + + // Head-first, not hash-replacing: the classifier prefix-matches, so a bound that kept + // the tail would render raw provider text to the user as an ordinary rejection notice. + it('keeps a clipped transport failure classifiable', async () => { + const stored = await settle(`${DISPATCH_REJECTED_WRITE_FAILED}: ${HUGE}`) + expect(stored).not.toBe(`${DISPATCH_REJECTED_WRITE_FAILED}: ${HUGE}`) + expect(dispatchRejectionWasTransportWriteFailure(stored)).toBe(true) + expect(dispatchRejectionReasonIsInternal(stored)).toBe(true) + }) +}) diff --git a/src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts b/src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts index 58d12dfcf89..2ecd4e22cb6 100644 --- a/src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts +++ b/src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts @@ -2,6 +2,7 @@ import type { AgentJournalApprovalItem, AgentJournalItemBody, AgentJournalPromptOption, + AgentJournalQuestion, AgentJournalQuestionItem } from '../../../shared/agent-session-journal-types' import { @@ -11,6 +12,7 @@ import { } from './journal-payload-bounds' export const MAX_JOURNAL_PROMPT_OPTIONS = 64 +export const MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS = 4 const JOURNAL_PROMPT_OPTION_LIMITS = { inlineHeadBytes: 1024 } const JOURNAL_PROMPT_ID_MAX_BYTES = 1024 @@ -37,7 +39,12 @@ export function boundJournalStatusText(text: string): string { return boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text } -function boundJournalPromptBody( +export function boundJournalPromptBody(body: AgentJournalApprovalItem): AgentJournalApprovalItem +export function boundJournalPromptBody(body: AgentJournalQuestionItem): AgentJournalQuestionItem +export function boundJournalPromptBody( + body: AgentJournalApprovalItem | AgentJournalQuestionItem +): AgentJournalApprovalItem | AgentJournalQuestionItem +export function boundJournalPromptBody( body: AgentJournalApprovalItem | AgentJournalQuestionItem ): AgentJournalApprovalItem | AgentJournalQuestionItem { if (body.kind === 'approval') { @@ -52,18 +59,41 @@ function boundJournalPromptBody( ...body, question: boundPromptText(body.question), options: boundPromptOptions(body.options), + ...(body.questions + ? { + questions: body.questions + .slice(0, MAX_JOURNAL_GROUPED_PROMPT_QUESTIONS) + .map(boundPromptQuestion) + } + : {}), ...(body.freeTextQuestionId ? { freeTextQuestionId: boundPromptIdentifier(body.freeTextQuestionId) } : {}) } } +function boundPromptQuestion(question: AgentJournalQuestion): AgentJournalQuestion { + return { + id: boundPromptIdentifier(question.id), + question: boundPromptText(question.question), + ...(question.header === undefined ? {} : { header: boundPromptText(question.header) }), + multiSelect: question.multiSelect, + options: boundPromptOptions(question.options), + ...(question.freeTextQuestionId + ? { freeTextQuestionId: boundPromptIdentifier(question.freeTextQuestionId) } + : {}) + } +} + function boundPromptOptions( options: readonly AgentJournalPromptOption[] ): AgentJournalPromptOption[] { return options.slice(0, MAX_JOURNAL_PROMPT_OPTIONS).map((option) => ({ id: boundPromptIdentifier(option.id), - label: boundInlineText(option.label, JOURNAL_PROMPT_OPTION_LIMITS).text + label: boundInlineText(option.label, JOURNAL_PROMPT_OPTION_LIMITS).text, + ...(option.description === undefined + ? {} + : { description: boundInlineText(option.description, JOURNAL_PROMPT_OPTION_LIMITS).text }) })) } diff --git a/src/main/native-chat/agent-session-journal/journal-row-builders.ts b/src/main/native-chat/agent-session-journal/journal-row-builders.ts index be2c2552775..e5e376940fe 100644 --- a/src/main/native-chat/agent-session-journal/journal-row-builders.ts +++ b/src/main/native-chat/agent-session-journal/journal-row-builders.ts @@ -20,6 +20,7 @@ import { MAX_JOURNAL_LIFECYCLE_BATCH_BYTES, MAX_JOURNAL_LIFECYCLE_BATCH_MUTATIONS } from './journal-row-schema' +import { boundInlineText, DEFAULT_JOURNAL_PAYLOAD_LIMITS } from './journal-payload-bounds' import type { ResolveDispatchInput } from './journal-store-contracts' type RowBuilder = (seq: number, ts: number) => T @@ -76,8 +77,7 @@ export function journalDispatchRowBuilder( clientMessageId: input.clientMessageId, dispatchState: input.state, providerItemId, - reason: - input.state === 'accepted' || input.state === 'pending' ? null : (input.reason ?? null), + reason: boundedDispatchReason(input), seq, fence: input.fence, ts, @@ -85,6 +85,17 @@ export function journalDispatchRowBuilder( }) } +/** `reason` is the only unbounded field written by Orca's own code: a provider error is + * arbitrary text, and a multi-megabyte one reached the row verbatim. Bounded head-first, + * because `dispatchRejectionWasTransportWriteFailure` prefix-matches the value. Rows + * written before this keep their full text, so readers still meet unbounded ones. */ +function boundedDispatchReason(input: ResolveDispatchInput): string | null { + if (input.state === 'accepted' || input.state === 'pending' || !input.reason) { + return null + } + return boundInlineText(input.reason, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text +} + export type JournalLifecycleMutationInput = | { kind: 'item'; identity: AgentJournalItemIdentity; body: AgentJournalItemBody } | { kind: 'tombstone'; identity: AgentJournalItemIdentity } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index 75e539b5641..e5daa9991d9 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -48,6 +48,13 @@ export class AgentSessionRewindRefusal extends AgentSessionAcquisitionRefusal { } } +export class AgentSessionPromptUnavailableError extends Error { + constructor(itemId: string) { + super(`The provider is no longer waiting on ${itemId}.`) + this.name = 'AgentSessionPromptUnavailableError' + } +} + /** * The provider's own root process was observed to exit, but its descendant tree * could not be verified. The lease keys on the root's pid and start time, so its @@ -196,6 +203,7 @@ export type StructuredAgentSessionAdapter = { sessionId: string turnId: string fence: number + prompt?: { itemId: string } }): Promise<{ cancelled: boolean }> stopBackgroundTasks?(input: { sessionId: string @@ -206,14 +214,15 @@ export type StructuredAgentSessionAdapter = { /** The `/` surface the running provider reports for itself. Undefined when the * provider never reports one, which is what keeps the client on its catalog. */ readCommands?(sessionId: string): AgentSessionSlashCommand[] | undefined - /** Fires the provider callback for an approval or a question. The wire calls - * this only after the durable compare-and-set won, so it runs exactly once. */ + /** Claims the live callback, commits the journal CAS while that claim is held, then answers it. + * A prompt cancel claims the same callback, so only one operation can commit. */ answerPrompt(input: { sessionId: string itemId: string kind: 'approval' | 'question' optionId: string fence: number + commit: () => Promise }): Promise setOption( input: StructuredAgentSessionSetOptionInput diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts index f3744a015b9..c85536ff679 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-attach-orchestration.ts @@ -22,7 +22,7 @@ import { } from './structured-agent-session-launch-env' import { refuseAgentSessionMutation } from './structured-agent-session-mutation-admission' import { retryPendingStructuredAgentSessionSettlement } from './structured-agent-session-settlement-retry' -import { settleStaleRunningTurnsOnAcquire } from './structured-agent-session-stale-turn-verdict' +import { settleStaleSessionStateOnAcquire } from './structured-agent-session-stale-turn-verdict' import type { StructuredAgentSessionAttachContext } from './structured-agent-session-attach-context' import { forgetStructuredAgentSession } from './structured-agent-session-host-lifetime' import type { DeferredStructuredAgentSessionEventSink } from './structured-agent-session-event-sink' @@ -122,7 +122,7 @@ export function attachStructuredAgentSession( try { if (acquiredOwner) { // Before the drain: the buffered events are the new child's, never a stale row's. - await settleStaleRunningTurnsOnAcquire({ + await settleStaleSessionStateOnAcquire({ journal: attached.journal, sessionId, fence, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts index 6082ab074f5..bbc1ec49d4c 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts @@ -2,12 +2,11 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' +import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key' import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope' import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire' import { encodeAgentSessionQuestionAnswers } from '../../../shared/agent-session-question-answer' import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' -import { journalDirectoryFor } from '../agent-session-journal/journal-paths' -import { openAgentSessionJournal } from '../agent-session-journal/journal-store-factory' import type { AgentSessionDispatchOutcome, StructuredAgentSessionAdapter @@ -66,45 +65,43 @@ function adapter(): StructuredAgentSessionAdapter { } async function seedGroupedQuestion(): Promise<{ itemId: string; revision: number }> { - const journal = await openAgentSessionJournal({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: { kind: 'codex', threadId: THREAD } - }, - journalDir: journalDirectoryFor(root, { workspaceId: 'workspace-1', sessionId: SESSION }) + const identity = { provider: 'codex' as const, threadId: THREAD, turnId: 'turn-1', ordinal: 100 } + const events = acquire.mock.calls.at(-1)?.[0].events + if (!events) { + throw new Error('seedGroupedQuestion requires an acquired session') + } + events.appendItem(identity, { + kind: 'question', + question: '2 grouped questions from Claude', + options: [], + questions: [ + { + id: 'q1', + question: 'Targets', + multiSelect: true, + options: [ + { id: 'target-web', label: 'Web' }, + { id: 'target-mobile', label: 'Mobile' } + ] + }, + { + id: 'q2', + question: 'Host', + multiSelect: false, + options: [], + freeTextQuestionId: 'q2' + } + ], + resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } }) - const appended = await journal.appendItem( - { provider: 'codex', threadId: THREAD, turnId: 'turn-1', ordinal: 100 }, - { - kind: 'question', - question: '2 grouped questions from Claude', - options: [], - questions: [ - { - id: 'q1', - question: 'Targets', - multiSelect: true, - options: [ - { id: 'target-web', label: 'Web' }, - { id: 'target-mobile', label: 'Mobile' } - ] - }, - { - id: 'q2', - question: 'Host', - multiSelect: false, - options: [], - freeTextQuestionId: 'q2' - } - ], - resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } - }, - { fence: 1 } - ) - return { itemId: appended.itemId, revision: appended.revision } + await host.flushStreamedEvents(SESSION) + const itemId = agentJournalItemKey(identity) + const page = host.history({ sessionId: SESSION, direction: 'tail' }) + const appended = page.ok ? page.page.items.find((item) => item.itemId === itemId) : null + if (!appended) { + throw new Error('provider question was not written to the journal') + } + return { itemId, revision: appended.revision } } beforeEach(async () => { @@ -126,7 +123,7 @@ beforeEach(async () => { observedAt: NOW } })) - answerPrompt = vi.fn(async () => undefined) + answerPrompt = vi.fn(async ({ commit }) => commit()) store = await AgentSessionRecordStore.open({ directory: join(root, 'store'), hostId: 'local' }) host = new StructuredAgentSessionHost({ store, @@ -145,9 +142,9 @@ afterEach(async () => { describe('grouped question admission', () => { it('admits renderer question-group payloads with child ids and multi-select answers', async () => { - const prompt = await seedGroupedQuestion() const attached = await host.attach(CALLER, attachParams()) expect(attached.ok).toBe(true) + const prompt = await seedGroupedQuestion() const optionId = encodeAgentSessionQuestionAnswers([ { questionId: 'q1', optionIds: ['target-web', 'target-mobile'] }, { questionId: 'q2', optionIds: [], other: 'SSH host' } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts index 9d50adaaa1a..abc468c9ea7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts @@ -37,6 +37,7 @@ export type StructuredAgentSessionMutationContext = { deps: StructuredAgentSessionHostDeps sessions: Map publish: (sessionId: string, journal: StructuredAgentSessionHostSession['journal']) => void + flushStreamedEvents: (sessionId: string) => Promise requireSession: (sessionId: string) => StructuredAgentSessionHostSession serialize: (sessionId: string, task: () => Promise) => Promise now: () => number @@ -57,6 +58,7 @@ function mutate( plan, journal: context.sessions.get(envelope.sessionId)?.journal, publish: (journal) => context.publish(envelope.sessionId, journal), + flushStreamedEvents: context.flushStreamedEvents, now: () => context.now() }) ) @@ -109,6 +111,7 @@ export function cancelStructuredAgentSessionTurn( turnId: string scope?: 'background-tasks' taskId?: string + prompt?: { itemId: string; expectedRevision: number } } ): Promise> { const command = context.deps.store.getRecord(params.envelope.sessionId)?.conversationCommand @@ -177,20 +180,28 @@ export async function settleStructuredAgentSessionLateDispatch( input: { sessionId: string clientMessageId: string - providerIdentity: AgentJournalItemIdentity - } + } & ({ providerIdentity: AgentJournalItemIdentity } | { state: 'rejected'; reason: string }) ): Promise { const session = context.sessions.get(input.sessionId) if (!session) { return } // The journal queue drains before close; the host queue would defer this past teardown. - await session.journal.resolveDispatch({ - clientMessageId: input.clientMessageId, - state: 'accepted', - providerIdentity: input.providerIdentity, - fence: session.fence - }) + await session.journal.resolveDispatch( + 'providerIdentity' in input + ? { + clientMessageId: input.clientMessageId, + state: 'accepted', + providerIdentity: input.providerIdentity, + fence: session.fence + } + : { + clientMessageId: input.clientMessageId, + state: 'rejected', + reason: input.reason, + fence: session.fence + } + ) context.publish(input.sessionId, session.journal) } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts index c67a8fabf58..f14b1308810 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-test-harness.ts @@ -3,10 +3,10 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, expect, vi, type Mock } from 'vitest' import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope' +import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key' import type { AgentSessionRecord } from '../../../shared/agent-session-record' import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire' import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' -import { journalDirectoryFor } from '../agent-session-journal/journal-paths' import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open' import type { AgentSessionDispatchOutcome, @@ -87,33 +87,28 @@ async function attach(): Promise { return store.getRecord(SESSION) } -/** Puts a pending approval in the journal BEFORE attach, which is the only way - * 1d can stage one: the adapter that would emit it is phase 2's. */ +/** Emits a pending approval through the acquired provider sink. */ async function seedApproval(optionId = 'allow'): Promise<{ itemId: string; revision: number }> { const identity = { provider: 'codex' as const, threadId: THREAD, turnId: 'turn-1', ordinal: 99 } - const journalDir = journalDirectoryFor(root, { workspaceId: 'workspace-1', sessionId: SESSION }) - const journal = await journals.open({ - identity: { - sessionId: SESSION, - workspaceId: 'workspace-1', - hostId: 'local', - agent: 'codex', - providerHandle: { kind: 'codex', threadId: THREAD } - }, - journalDir + const events = acquire.mock.calls.at(-1)?.[0].events + if (!events) { + throw new Error('seedApproval requires an acquired session') + } + events.appendItem(identity, { + kind: 'approval', + title: 'Run the command?', + detail: null, + options: [{ id: optionId, label: 'Allow' }], + resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } }) - const appended = await journal.appendItem( - identity, - { - kind: 'approval', - title: 'Run the command?', - detail: null, - options: [{ id: optionId, label: 'Allow' }], - resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null } - }, - { fence: 1 } - ) - return { itemId: appended.itemId, revision: appended.revision } + await host.flushStreamedEvents(SESSION) + const itemId = agentJournalItemKey(identity) + const page = host.history({ sessionId: SESSION, direction: 'tail' }) + const appended = page.ok ? page.page.items.find((item) => item.itemId === itemId) : null + if (!appended) { + throw new Error('provider approval was not written to the journal') + } + return { itemId, revision: appended.revision } } beforeEach(async () => { @@ -138,7 +133,7 @@ beforeEach(async () => { releaseAcquisition = vi.fn(async () => true) dispatch = vi.fn(async () => accepted()) cancelTurn = vi.fn(async () => ({ cancelled: true })) - answerPrompt = vi.fn(async () => undefined) + answerPrompt = vi.fn(async ({ commit }) => commit()) setOption = vi.fn(async () => undefined) store = await AgentSessionRecordStore.open({ directory: join(root, 'store'), hostId: 'local' }) host = new StructuredAgentSessionHost({ diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts index d53c3c30e50..a19735657d6 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts @@ -234,12 +234,117 @@ describe('cancel', () => { }) expect(cancelTurn).toHaveBeenCalledTimes(1) }) + + it.each([ + ['a missing prompt item', { itemId: 'missing-item', expectedRevision: 1 }], + ['a stale prompt revision', { itemId: 'seeded', expectedRevision: 2 }] + ])('refuses %s before interrupting the provider', async (_case, requestedPrompt) => { + await attach() + const prompt = await seedApproval() + const strictPrompt = { + ...requestedPrompt, + ...(requestedPrompt.itemId === 'seeded' ? { itemId: prompt.itemId } : {}) + } + const fields = { turnId: 'turn-1', prompt: strictPrompt } + + expect( + await host.cancel(CALLER, { + envelope: envelope('agentSession.cancel', fields), + ...fields + }) + ).toMatchObject({ ok: false }) + expect(cancelTurn).not.toHaveBeenCalled() + }) + + it('refuses cancellation after an answer has already resolved the prompt', async () => { + await attach() + const prompt = await seedApproval() + const answer = { + itemId: prompt.itemId, + expectedRevision: prompt.revision, + optionId: 'allow' + } + await host.respondToPrompt(CALLER, { + envelope: envelope('agentSession.respondTo:approval', answer), + kind: 'approval', + ...answer + }) + const fields = { + turnId: 'turn-1', + prompt: { itemId: prompt.itemId, expectedRevision: prompt.revision } + } + + expect( + await host.cancel(CALLER, { + envelope: envelope('agentSession.cancel', fields), + ...fields + }) + ).toMatchObject({ + ok: false, + refusal: { code: 'agent_session_item_revision_stale' } + }) + expect(cancelTurn).not.toHaveBeenCalled() + }) + + it('records an unknown outcome when lifecycle draining fails and never interrupts on replay', async () => { + await attach() + const prompt = await seedApproval() + vi.spyOn(host, 'flushStreamedEvents').mockRejectedValueOnce(new Error('journal drain failed')) + const fields = { + turnId: 'turn-1', + prompt: { itemId: prompt.itemId, expectedRevision: prompt.revision } + } + const params = { + envelope: envelope('agentSession.cancel', fields), + ...fields + } + + await expect(host.cancel(CALLER, params)).rejects.toThrow('journal drain failed') + expect(await host.cancel(CALLER, params)).toMatchObject({ + ok: false, + refusal: { code: 'agent_session_operation_unknown' } + }) + expect(cancelTurn).toHaveBeenCalledTimes(1) + }) + + it('records an unknown outcome when strict prompt interruption throws and never retries it', async () => { + await attach() + const prompt = await seedApproval() + cancelTurn.mockRejectedValueOnce(new Error('interrupt receipt lost')) + const fields = { + turnId: 'turn-1', + prompt: { itemId: prompt.itemId, expectedRevision: prompt.revision } + } + const params = { + envelope: envelope('agentSession.cancel', fields), + ...fields + } + + await expect(host.cancel(CALLER, params)).rejects.toThrow('interrupt receipt lost') + expect(await host.cancel(CALLER, params)).toMatchObject({ + ok: false, + refusal: { code: 'agent_session_operation_unknown' } + }) + expect(cancelTurn).toHaveBeenCalledTimes(1) + expect(host.history({ sessionId: SESSION, direction: 'tail' })).toMatchObject({ + ok: true, + page: { + items: [ + expect.objectContaining({ + body: expect.objectContaining({ + resolution: expect.objectContaining({ state: 'pending' }) + }) + }) + ] + } + }) + }) }) describe('respondToPrompt', () => { it('commits the answer before the provider callback', async () => { - const prompt = await seedApproval() await attach() + const prompt = await seedApproval() const fields = { itemId: prompt.itemId, expectedRevision: prompt.revision, optionId: 'allow' } const result = await host.respondToPrompt(CALLER, { envelope: envelope('agentSession.respondTo:approval', fields), @@ -254,8 +359,8 @@ describe('respondToPrompt', () => { }) it('refuses a second answer to one prompt and says which answer won', async () => { - const prompt = await seedApproval() await attach() + const prompt = await seedApproval() const fields = { itemId: prompt.itemId, expectedRevision: prompt.revision, optionId: 'allow' } await host.respondToPrompt(CALLER, { envelope: envelope('agentSession.respondTo:approval', fields), @@ -281,8 +386,8 @@ describe('respondToPrompt', () => { }) it('refuses an option the prompt does not offer', async () => { - const prompt = await seedApproval() await attach() + const prompt = await seedApproval() const fields = { itemId: prompt.itemId, expectedRevision: prompt.revision, optionId: 'deny' } expect( await host.respondToPrompt(CALLER, { @@ -295,8 +400,8 @@ describe('respondToPrompt', () => { }) it("does not turn a recorded refusal into another client's successful answer", async () => { - const prompt = await seedApproval() await attach() + const prompt = await seedApproval() const rejectedFields = { itemId: prompt.itemId, expectedRevision: prompt.revision, @@ -326,9 +431,12 @@ describe('respondToPrompt', () => { }) it('keeps the answer and reports it undelivered when the provider callback throws', async () => { - const prompt = await seedApproval() await attach() - answerPrompt.mockRejectedValueOnce(new Error('pipe closed')) + const prompt = await seedApproval() + answerPrompt.mockImplementationOnce(async ({ commit }) => { + await commit() + throw new Error('pipe closed') + }) const fields = { itemId: prompt.itemId, expectedRevision: prompt.revision, optionId: 'allow' } const result = await host.respondToPrompt(CALLER, { envelope: envelope('agentSession.respondTo:approval', fields), diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index c60d9e5db26..8fcdef95480 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -267,6 +267,7 @@ export class StructuredAgentSessionHost { deps: this.deps, sessions: this.sessions, publish: (sessionId, journal) => this.subscribers.publish(sessionId, journal), + flushStreamedEvents: this.flushStreamedEvents, requireSession: (sessionId) => this.requireSession(sessionId), serialize: (sessionId, task) => this.serialize(sessionId, task), now: () => this.now() diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts index 293f6ab2d8c..1874f7ee1c6 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope' +import { DISPATCH_REJECTED_CANCELLED } from '../../../shared/structured-agent-session-dispatch-rejection' import type { AgentSessionMutationEnvelope, AgentSessionSubscribeEvent @@ -204,6 +205,27 @@ describe('settling a send the provider proves it received after the ack window', expect(dispatch).toHaveBeenCalledTimes(1) }) + it('settles a provider-cancelled queued send as rejected', async () => { + dispatch.mockResolvedValueOnce({ state: 'admitted' }) + const params = sendParams('queued behind the active turn') + await host.send(CALLER, params) + + await host.settleLateDispatch({ + sessionId: SESSION, + clientMessageId: params.envelope.clientOperationId, + state: 'rejected', + reason: DISPATCH_REJECTED_CANCELLED + }) + + expect(submissions()).toMatchObject([ + { + clientMessageId: params.envelope.clientOperationId, + dispatchState: 'rejected', + reason: DISPATCH_REJECTED_CANCELLED + } + ]) + }) + it('accepts from the durable echo row when the direct settlement write fails', async () => { dispatch.mockResolvedValueOnce({ state: 'admitted' }) const params = sendParams('settle from provider echo') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts index c59a538ead7..34e8004cc81 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts @@ -45,6 +45,7 @@ export type AgentSessionMutationRequest = { /** Journal of the attached session; absent when this host holds none. */ journal: AgentSessionJournal | undefined publish: (journal: AgentSessionJournal) => void + flushStreamedEvents: (sessionId: string) => Promise now: () => number } @@ -147,6 +148,7 @@ function turnContext( .then(() => undefined), resolvedBy: request.callerKey, publish: () => request.publish(journal), + flushStreamedEvents: () => request.flushStreamedEvents(request.envelope.sessionId), now: () => request.now() } } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts index 96c50036701..d8d2876d272 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts @@ -96,20 +96,23 @@ export function cancelPlan(params: { turnId: string scope?: 'background-tasks' taskId?: string + prompt?: { itemId: string; expectedRevision: number } }): MutationPlan { return { method: 'agentSession.cancel', fields: { turnId: params.turnId, ...(params.scope ? { scope: params.scope } : {}), - ...(params.taskId ? { taskId: params.taskId } : {}) + ...(params.taskId ? { taskId: params.taskId } : {}), + ...(params.prompt ? { prompt: params.prompt } : {}) }, run: (ctx) => performCancel(ctx, { clientOperationId: params.envelope.clientOperationId, turnId: params.turnId, ...(params.scope ? { scope: params.scope } : {}), - ...(params.taskId ? { taskId: params.taskId } : {}) + ...(params.taskId ? { taskId: params.taskId } : {}), + ...(params.prompt ? { prompt: params.prompt } : {}) }), // Interrupting twice would kill a turn the client never asked to stop, so a // replay reports the turn as already handled instead. diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts new file mode 100644 index 00000000000..aeb2c41095f --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts @@ -0,0 +1,212 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types' +import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open' +import type { AgentSessionJournal } from '../agent-session-journal/journal-store' +import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter' +import { performCancel, type AgentSessionTurnContext } from './structured-agent-session-turns' + +const IDENTITY: AgentSessionJournalIdentity = { + sessionId: 'session-1', + workspaceId: 'workspace-1', + hostId: 'host-1', + agent: 'codex', + providerHandle: { kind: 'codex', threadId: 'thread-1' } +} +const PROMPT_IDENTITY = { + provider: 'codex' as const, + threadId: 'thread-1', + turnId: 'turn-1', + ordinal: 1 +} + +const journals = createTrackedJournalOpener() +let root: string | null = null + +afterEach(async () => { + await journals.closeAll() + if (root) { + await rm(root, { recursive: true, force: true }) + root = null + } +}) + +async function pendingPrompt(): Promise<{ journal: AgentSessionJournal; itemId: string }> { + root = await mkdtemp(join(tmpdir(), 'orca-prompt-cancel-')) + const journal = await journals.open({ identity: IDENTITY, journalDir: root }) + const item = await journal.appendItem( + PROMPT_IDENTITY, + { + kind: 'approval', + title: 'Approve?', + detail: null, + options: [{ id: 'allow', label: 'Allow' }], + resolution: { + state: 'pending', + selectedOptionId: null, + resolvedBy: null, + resolvedAt: null + } + }, + { fence: 1 } + ) + return { journal, itemId: item.itemId } +} + +function context( + journal: AgentSessionJournal, + cancelTurn: StructuredAgentSessionAdapter['cancelTurn'], + flushStreamedEvents: () => Promise +): AgentSessionTurnContext { + return { + sessionId: 'session-1', + journal, + fence: 1, + adapter: { cancelTurn } as unknown as StructuredAgentSessionAdapter, + persistOptions: async () => undefined, + resolvedBy: 'client-1', + publish: vi.fn(), + flushStreamedEvents, + now: () => 1 + } +} + +describe('performCancel for a pending prompt', () => { + it('refuses a stale prompt revision before reaching the provider', async () => { + const { journal, itemId } = await pendingPrompt() + const cancelTurn = vi.fn(async () => ({ cancelled: true })) + const flush = vi.fn(async () => undefined) + + const result = await performCancel(context(journal, cancelTurn, flush), { + clientOperationId: 'cancel-1', + turnId: 'turn-1', + prompt: { itemId, expectedRevision: 2 } + }) + + expect(result).toMatchObject({ + ok: false, + refusal: { code: 'agent_session_item_revision_stale', currentRevision: 1 } + }) + expect(cancelTurn).not.toHaveBeenCalled() + expect(flush).not.toHaveBeenCalled() + }) + + it('drains terminal lifecycle before recording a confirmed cancellation', async () => { + const { journal, itemId } = await pendingPrompt() + const order: string[] = [] + const cancelTurn = vi.fn(async () => { + order.push('interrupt') + return { cancelled: true } + }) + const flush = vi.fn(async () => { + order.push('lifecycle') + const current = journal.snapshot().items.find((item) => item.itemId === itemId)! + if (current.body.kind !== 'approval') { + throw new Error('expected approval prompt') + } + await journal.appendItem( + PROMPT_IDENTITY, + { + ...current.body, + resolution: { + state: 'cancelled', + selectedOptionId: null, + resolvedBy: null, + resolvedAt: null + } + }, + { fence: 1 } + ) + }) + + await expect( + performCancel(context(journal, cancelTurn, flush), { + clientOperationId: 'cancel-1', + turnId: 'turn-1', + prompt: { itemId, expectedRevision: 1 } + }) + ).resolves.toEqual({ ok: true, value: { turnId: 'turn-1', cancelled: true } }) + + expect(order).toEqual(['interrupt', 'lifecycle']) + expect(cancelTurn).toHaveBeenCalledWith({ + sessionId: 'session-1', + turnId: 'turn-1', + fence: 1, + prompt: { itemId } + }) + expect(journal.snapshot().items.map((item) => item.body)).toEqual([ + expect.objectContaining({ resolution: expect.objectContaining({ state: 'cancelled' }) }), + { kind: 'status', text: 'Cancellation requested.' } + ]) + }) + + it('keeps the callback answerable when interruption is declined', async () => { + const { journal, itemId } = await pendingPrompt() + const flush = vi.fn(async () => undefined) + + await expect( + performCancel( + context(journal, async () => ({ cancelled: false }), flush), + { + clientOperationId: 'cancel-1', + turnId: 'turn-1', + prompt: { itemId, expectedRevision: 1 } + } + ) + ).resolves.toEqual({ ok: true, value: { turnId: 'turn-1', cancelled: false } }) + + expect(flush).not.toHaveBeenCalled() + expect(journal.snapshot().items.map((item) => item.body)).toEqual([ + expect.objectContaining({ resolution: expect.objectContaining({ state: 'pending' }) }), + { kind: 'status', text: 'The provider had already finished this turn.' } + ]) + }) + + it('propagates an unconfirmed adapter failure and leaves the prompt pending', async () => { + const { journal, itemId } = await pendingPrompt() + const flush = vi.fn(async () => undefined) + + await expect( + performCancel( + context( + journal, + async () => { + throw new Error('interrupt receipt lost') + }, + flush + ), + { + clientOperationId: 'cancel-1', + turnId: 'turn-1', + prompt: { itemId, expectedRevision: 1 } + } + ) + ).rejects.toThrow('interrupt receipt lost') + + expect(flush).not.toHaveBeenCalled() + expect(journal.snapshot().items.map((item) => item.body)).toEqual([ + expect.objectContaining({ resolution: expect.objectContaining({ state: 'pending' }) }) + ]) + }) + + it('surfaces a lifecycle drain failure after the provider confirms interruption', async () => { + const { journal, itemId } = await pendingPrompt() + const flush = vi.fn(async () => { + throw new Error('journal drain failed') + }) + + await expect( + performCancel( + context(journal, async () => ({ cancelled: true }), flush), + { + clientOperationId: 'cancel-1', + turnId: 'turn-1', + prompt: { itemId, expectedRevision: 1 } + } + ) + ).rejects.toThrow('journal drain failed') + expect(journal.snapshot().items).toHaveLength(1) + }) +}) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-state.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-state.ts new file mode 100644 index 00000000000..7f71a9c28ce --- /dev/null +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-prompt-state.ts @@ -0,0 +1,59 @@ +import type { + AgentJournalItemBody, + AgentJournalRenderItem +} from '../../../shared/agent-session-journal-types' +import type { AgentSessionWireRefusal } from '../../../shared/agent-session-wire' +import type { AgentSessionTurnContext } from './structured-agent-session-turns' + +type PendingPromptBody = Extract + +export type PendingPromptValidation = + | { ok: true; item: AgentJournalRenderItem; prompt: PendingPromptBody } + | { ok: false; refusal: AgentSessionWireRefusal } + +function invalid(message: string): PendingPromptValidation { + return { ok: false, refusal: { code: 'agent_session_operation_invalid', message } } +} + +export function validatePendingPrompt( + ctx: Pick, + input: { + itemId: string + expectedRevision: number + kind?: 'approval' | 'question' + } +): PendingPromptValidation { + const item = ctx.journal.snapshot().items.find((entry) => entry.itemId === input.itemId) + if (!item) { + return invalid(`No item ${input.itemId} in session ${ctx.sessionId}.`) + } + const prompt = item.body.kind === 'approval' || item.body.kind === 'question' ? item.body : null + if (!prompt || (input.kind !== undefined && prompt.kind !== input.kind)) { + return invalid( + `Item ${input.itemId} is not a pending${input.kind ? ` ${input.kind}` : ' prompt'}.` + ) + } + if (item.revision !== input.expectedRevision) { + return { + ok: false, + refusal: { + code: 'agent_session_item_revision_stale', + message: `Item ${input.itemId} has moved on.`, + currentRevision: item.revision, + resolution: prompt.resolution + } + } + } + if (prompt.resolution.state !== 'pending') { + return { + ok: false, + refusal: { + code: 'agent_session_already_resolved', + message: `Item ${input.itemId} was already ${prompt.resolution.state}.`, + currentRevision: item.revision, + resolution: prompt.resolution + } + } + } + return { ok: true, item, prompt } +} diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts index cb7cbb1f20b..c301845c7c1 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts @@ -38,6 +38,7 @@ export async function rewindStructuredAgentSession( envelope: params.envelope, journal: context.sessions.get(sessionId)?.journal, publish: (journal) => context.publish(sessionId, journal), + flushStreamedEvents: context.flushStreamedEvents, now: context.now, plan: { method: 'agentSession.rewind', diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts index c0b56aa7d14..0f75c9a3322 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts @@ -8,7 +8,6 @@ import { structuredAgentSessionPayloadFingerprint } from '../../../shared/struct import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open' import type { AgentSessionJournal } from '../agent-session-journal/journal-store' import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter' -import { DISPATCH_DOUBT_CODEX_TURN_UNNAMED } from '../agent-session-journal/journal-dispatch-doubt-reasons' import { performSend, type AgentSessionTurnContext } from './structured-agent-session-turns' const journals = createTrackedJournalOpener() @@ -39,7 +38,10 @@ describe('structured send idempotency', () => { it.each([ ['a refused write', 'provider_write_failed: broken pipe'], ['a dead host', 'host_restarted_before_acknowledgement'], - ['a codex turn it could not name', DISPATCH_DOUBT_CODEX_TURN_UNNAMED] + [ + 'a codex turn an older Orca could not name', + 'codex app-server started a turn it did not name in time' + ] ])('never puts an unknown back on the wire after %s', async (_case, reason) => { const body: AgentJournalMessageItem = { kind: 'message', @@ -61,6 +63,7 @@ describe('structured send idempotency', () => { persistOptions: async () => undefined, resolvedBy: 'caller', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 }, input @@ -101,6 +104,7 @@ describe('structured send idempotency', () => { persistOptions: async () => undefined, resolvedBy: 'caller', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 } const input = { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts index e2f294f40f7..f2d1ab12230 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts @@ -6,7 +6,10 @@ import type { AgentSessionRecordStore } from '../../runtime/agent-session-record import type { StructuredAgentSessionHost } from './structured-agent-session-host' import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter' import type { AgentSessionJournal } from '../agent-session-journal/journal-store' -import { DISPATCH_DOUBT_SUBMISSION_MISSING } from '../agent-session-journal/journal-dispatch-doubt-reasons' +import { + DISPATCH_DOUBT_PROVIDER_EXITED, + DISPATCH_DOUBT_SUBMISSION_MISSING +} from '../agent-session-journal/journal-dispatch-doubt-reasons' import { accepted, attach, @@ -169,13 +172,15 @@ describe('send', () => { expect(state.ok && state.page.submissions).toHaveLength(2) }) - it('refuses to redeliver a retry for a turn the provider already owns', async () => { + it('refuses to redeliver a retry for a message the provider may already hold', async () => { await attach() + // A dead child ends the wait without proving non-delivery: the message was + // already written to that child's stdin. dispatch.mockImplementationOnce(async () => ({ state: 'unknown' as const, - reason: 'codex app-server started a turn it did not name in time' + reason: DISPATCH_DOUBT_PROVIDER_EXITED })) - const body = hostTestMessage('a turn codex owns but did not name') + const body = hostTestMessage('a message the provider may already hold') const params = { envelope: envelope('agentSession.send', { body }), body } const first = await host.send(CALLER, params) @@ -183,8 +188,8 @@ describe('send', () => { ok: true, value: { submission: { dispatchState: 'unknown' } } }) - // The turn is running; a second delivery would be a duplicate, so Retry - // replays the recorded outcome instead of re-sending. + // No `unknown` is re-delivered under its own id, whatever its reason says, + // so Retry replays the recorded outcome instead of writing again. await expect(host.send(CALLER, { ...params, retryUnknown: true })).resolves.toMatchObject({ ok: true, value: { submission: { dispatchState: 'unknown' } } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts index 8ffb7acf6ce..d0c9f04c27f 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts @@ -4,7 +4,7 @@ import type { AgentJournalRenderItem } from '../../../shared/agent-session-journ import type { AgentSessionJournal } from '../agent-session-journal/journal-store' import { runningTurnLifecycleRevisions, - settleStaleRunningTurnsOnAcquire, + settleStaleSessionStateOnAcquire, turnVerdictFromDeathEvidence } from './structured-agent-session-stale-turn-verdict' @@ -43,6 +43,32 @@ function legacyLifecycleItem(turnId: string, startedAt: number): AgentJournalRen } } +function promptItem(state: 'pending' | 'resolved', sequence: number): AgentJournalRenderItem { + return { + itemId: agentJournalItemKey({ + provider: 'legacy', + agent: 'codex', + sessionId: 'session-1', + recordId: `approval-${state}` + }), + revision: 1, + sequence, + observedAt: sequence, + body: { + kind: 'approval', + title: 'Approve?', + detail: null, + options: [], + resolution: { + state, + selectedOptionId: state === 'resolved' ? 'allow' : null, + resolvedBy: state === 'resolved' ? 'client-1' : null, + resolvedAt: state === 'resolved' ? 10 : null + } + } + } +} + describe('turn verdict from death evidence', () => { it('earns an end time only from an observed exit', () => { expect( @@ -105,7 +131,7 @@ describe('running turn lifecycle revisions', () => { }) }) -describe('stale running turns on a cold acquire', () => { +describe('stale session state on a cold acquire', () => { function journalWith(items: AgentJournalRenderItem[]) { const appendLifecycleBatch = vi.fn(async () => ({ epoch: 'epoch-1', sequence: 9 })) const journal = { @@ -123,7 +149,7 @@ describe('stale running turns on a cold acquire', () => { ]) await expect( - settleStaleRunningTurnsOnAcquire({ + settleStaleSessionStateOnAcquire({ journal, sessionId: 'session-1', fence: 14, @@ -132,7 +158,7 @@ describe('stale running turns on a cold acquire', () => { ).resolves.toBe(1) expect(appendLifecycleBatch).toHaveBeenCalledExactlyOnceWith({ - settlementId: 'stale-turn:session-1:14:generation-2', + settlementId: 'stale-session:session-1:14:generation-2', fence: 14, recovered: true, mutations: [ @@ -145,12 +171,53 @@ describe('stale running turns on a cold acquire', () => { }) }) + it('cancels only prompts whose callbacks were lost with the prior owner', async () => { + const pending = promptItem('pending', 1) + const resolved = promptItem('resolved', 2) + const { journal, appendLifecycleBatch } = journalWith([pending, resolved]) + + await expect( + settleStaleSessionStateOnAcquire({ + journal, + sessionId: 'session-1', + fence: 14, + acquisitionGeneration: 'generation-2' + }) + ).resolves.toBe(1) + + expect(appendLifecycleBatch).toHaveBeenCalledExactlyOnceWith({ + settlementId: 'stale-session:session-1:14:generation-2', + fence: 14, + recovered: true, + mutations: [ + { + kind: 'item', + identity: { + provider: 'legacy', + agent: 'codex', + sessionId: 'session-1', + recordId: 'approval-pending' + }, + body: { + ...pending.body, + resolution: { + state: 'cancelled', + selectedOptionId: null, + resolvedBy: null, + resolvedAt: null + } + } + } + ] + }) + }) + it('writes nothing when no turn is running and keys on the journal position without a generation', async () => { const idle = journalWith([ lifecycleItem('turn-1', 'completed', 1, { startedAt: 10, completedAt: 20 }) ]) await expect( - settleStaleRunningTurnsOnAcquire({ + settleStaleSessionStateOnAcquire({ journal: idle.journal, sessionId: 'session-1', fence: 14, @@ -160,14 +227,14 @@ describe('stale running turns on a cold acquire', () => { expect(idle.appendLifecycleBatch).not.toHaveBeenCalled() const running = journalWith([lifecycleItem('turn-2', 'running', 2)]) - await settleStaleRunningTurnsOnAcquire({ + await settleStaleSessionStateOnAcquire({ journal: running.journal, sessionId: 'session-1', fence: 14, acquisitionGeneration: null }) expect(running.appendLifecycleBatch).toHaveBeenCalledWith( - expect.objectContaining({ settlementId: 'stale-turn:session-1:14:seq-8' }) + expect.objectContaining({ settlementId: 'stale-session:session-1:14:seq-8' }) ) }) }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts index 940b0c8be17..78609bd199b 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.ts @@ -17,6 +17,7 @@ import type { AgentSessionDeathEvidence } from '../../../shared/agent-session-re import { partitionJournalLifecycleMutations } from '../agent-session-journal/journal-lifecycle-batch-partition' import type { JournalLifecycleMutationInput } from '../agent-session-journal/journal-row-builders' import type { AgentSessionJournal } from '../agent-session-journal/journal-store' +import { cancelledJournalPromptBody } from '../agent-session-journal/journal-prompt-body-bounds' export type StructuredAgentSessionTurnVerdict = | { state: 'interrupted'; completedAt: number } @@ -58,6 +59,28 @@ export function runningTurnLifecycleRevisions( return revisions } +function staleSessionLifecycleRevisions( + items: readonly AgentJournalRenderItem[] +): JournalLifecycleMutationInput[] { + const revisions: JournalLifecycleMutationInput[] = [] + for (const item of items) { + const identity = parseAgentJournalItemKey(item.itemId) + if (!identity) { + continue + } + const cancelled = + (item.body.kind === 'approval' || item.body.kind === 'question') && + item.body.resolution.state === 'pending' + ? cancelledJournalPromptBody(item.body) + : null + if (cancelled) { + revisions.push({ kind: 'item', identity, body: cancelled }) + } + } + revisions.push(...runningTurnLifecycleRevisions(items, UNVERIFIABLE_TURN_VERDICT)) + return revisions +} + function settledLifecycle( lifecycle: AgentJournalTurnLifecycle, verdict: StructuredAgentSessionTurnVerdict @@ -77,19 +100,16 @@ function settledLifecycle( /** A running row found when a NEW child is acquired belongs to a generation whose exit nobody * observed. Must run before that child's buffered events land, or a live turn would be judged. */ -export async function settleStaleRunningTurnsOnAcquire(input: { +export async function settleStaleSessionStateOnAcquire(input: { journal: AgentSessionJournal sessionId: string fence: number acquisitionGeneration: string | null }): Promise { const { journal } = input - const revisions = runningTurnLifecycleRevisions( - journal.snapshot().items, - UNVERIFIABLE_TURN_VERDICT - ) + const revisions = staleSessionLifecycleRevisions(journal.snapshot().items) const generation = input.acquisitionGeneration ?? `seq-${journal.cursor().sequence}` - const settlementId = `stale-turn:${input.sessionId}:${input.fence}:${generation}` + const settlementId = `stale-session:${input.sessionId}:${input.fence}:${generation}` for (const chunk of partitionJournalLifecycleMutations(settlementId, revisions)) { await journal.appendLifecycleBatch({ settlementId: chunk.settlementId, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.ts index 26ad85b5cfb..6ac29cab0ab 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.ts @@ -3,28 +3,17 @@ import { decodeAgentSessionQuestionAnswers, isValidAgentSessionQuestionAnswers } from '../../../shared/agent-session-question-answer' -import type { - AgentJournalItemBody, - AgentJournalQuestion, - AgentJournalResolution -} from '../../../shared/agent-session-journal-types' +import type { AgentJournalResolution } from '../../../shared/agent-session-journal-types' import type { AgentSessionPromptResult } from '../../../shared/agent-session-wire' import { decodeCodexQuestionOptionId } from '../../codex/codex-structured-prompt-replies' +import { AgentSessionPromptUnavailableError } from './structured-agent-session-adapter' +import { validatePendingPrompt } from './structured-agent-session-prompt-state' import type { AgentSessionTurnContext, TurnOutcome } from './structured-agent-session-turns' function invalid(message: string): TurnOutcome { return { ok: false, refusal: { code: 'agent_session_operation_invalid', message } } } -function promptBodyOf(body: AgentJournalItemBody): { - options: readonly { id: string }[] - freeTextQuestionId?: string - questions?: AgentJournalQuestion[] - resolution: AgentJournalResolution -} | null { - return body.kind === 'approval' || body.kind === 'question' ? body : null -} - export async function performPrompt( ctx: AgentSessionTurnContext, input: { @@ -34,50 +23,22 @@ export async function performPrompt( kind: 'approval' | 'question' } ): Promise> { - const item = ctx.journal.snapshot().items.find((entry) => entry.itemId === input.itemId) - if (!item) { - return invalid(`No item ${input.itemId} in session ${ctx.sessionId}.`) - } - const prompt = promptBodyOf(item.body) - if (!prompt || item.body.kind !== input.kind) { - return invalid(`Item ${input.itemId} is not a pending ${input.kind}.`) - } - if (item.revision !== input.expectedRevision) { - return { - ok: false, - refusal: { - code: 'agent_session_item_revision_stale', - message: `Item ${input.itemId} has moved on.`, - currentRevision: item.revision, - resolution: prompt.resolution - } - } - } - if (prompt.resolution.state !== 'pending') { - return { - ok: false, - refusal: { - code: 'agent_session_already_resolved', - message: `Item ${input.itemId} was already ${prompt.resolution.state}.`, - currentRevision: item.revision, - resolution: prompt.resolution - } - } + const validated = validatePendingPrompt(ctx, input) + if (!validated.ok) { + return validated } + const { prompt } = validated + const question = prompt.kind === 'question' ? prompt : null const freeText = decodeCodexQuestionOptionId(input.optionId) const acceptsFreeText = - item.body.kind === 'question' && - prompt.freeTextQuestionId !== undefined && - freeText?.questionId === prompt.freeTextQuestionId && + question?.freeTextQuestionId !== undefined && + freeText?.questionId === question.freeTextQuestionId && freeText.answer.trim().length > 0 - const grouped = - item.body.kind === 'question' && prompt.questions - ? decodeAgentSessionQuestionAnswers(input.optionId) - : null + const grouped = question?.questions ? decodeAgentSessionQuestionAnswers(input.optionId) : null const acceptsGrouped = grouped !== null && - prompt.questions !== undefined && - isValidAgentSessionQuestionAnswers(prompt.questions, grouped) + question?.questions !== undefined && + isValidAgentSessionQuestionAnswers(question.questions, grouped) if ( !acceptsFreeText && !acceptsGrouped && @@ -96,24 +57,32 @@ export async function performPrompt( resolvedBy: ctx.resolvedBy, resolvedAt: ctx.now() } - const appended = await ctx.journal.appendItem( - identity, - { ...item.body, resolution }, - { - fence: ctx.fence - } - ) - ctx.publish() - + const committed: { item?: Awaited> } = {} try { await ctx.adapter.answerPrompt({ sessionId: ctx.sessionId, itemId: input.itemId, kind: input.kind, optionId: input.optionId, - fence: ctx.fence + fence: ctx.fence, + commit: async () => { + committed.item = await ctx.journal.appendItem( + identity, + { ...prompt, resolution }, + { + fence: ctx.fence + } + ) + ctx.publish() + } }) } catch (error) { + if (!committed.item && error instanceof AgentSessionPromptUnavailableError) { + return invalid(error.message) + } + if (!committed.item) { + throw error + } await ctx.journal.appendItem( { provider: 'orca', clientMessageId: `${input.itemId}#delivery` }, { @@ -126,6 +95,10 @@ export async function performPrompt( ) ctx.publish() } + const appended = committed.item + if (!appended) { + throw new Error(`Provider adapter did not commit prompt ${input.itemId}.`) + } return { ok: true, value: { itemId: appended.itemId, revision: appended.revision, resolution } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts index aa0785da31a..59b1e74898c 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts @@ -54,6 +54,7 @@ describe('performCancel', () => { persistOptions: async () => undefined, resolvedBy: 'client-1', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 } @@ -101,6 +102,7 @@ describe('performCancel', () => { persistOptions: async () => undefined, resolvedBy: 'client-1', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 } @@ -133,6 +135,7 @@ describe('performCancel', () => { persistOptions: async () => undefined, resolvedBy: 'client-1', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 } @@ -164,6 +167,7 @@ describe('performCancel', () => { persistOptions: async () => undefined, resolvedBy: 'client-1', publish: vi.fn(), + flushStreamedEvents: async () => undefined, now: () => 1 } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts index 4c275ecd738..c666047e1e7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts @@ -21,6 +21,7 @@ import type { AgentSessionDispatchOutcome, StructuredAgentSessionAdapter } from './structured-agent-session-adapter' +import { validatePendingPrompt } from './structured-agent-session-prompt-state' export { performSetOption } from './structured-agent-session-turns-options' export { performPrompt } from './structured-agent-session-turns-prompt' @@ -34,6 +35,8 @@ export type AgentSessionTurnContext = { /** Opaque client identity recorded as the resolver of a prompt. */ resolvedBy: string publish: () => void + /** Drains provider lifecycle already accepted by the execution host. */ + flushStreamedEvents: () => Promise now: () => number } @@ -186,8 +189,15 @@ export async function performCancel( turnId: string scope?: 'background-tasks' taskId?: string + prompt?: { itemId: string; expectedRevision: number } } ): Promise> { + if (input.prompt) { + const validated = validatePendingPrompt(ctx, input.prompt) + if (!validated.ok) { + return validated + } + } let cancelled = false let note = 'Cancellation requested.' try { @@ -203,17 +213,24 @@ export async function performCancel( await ctx.adapter.cancelTurn({ sessionId: ctx.sessionId, turnId: input.turnId, - fence: ctx.fence + fence: ctx.fence, + ...(input.prompt ? { prompt: { itemId: input.prompt.itemId } } : {}) }) ).cancelled if (!cancelled) { note = 'The provider had already finished this turn.' } } catch (error) { + if (input.prompt) { + throw error + } note = `Cancellation was not confirmed: ${ error instanceof Error ? error.message : String(error) }` } + if (cancelled && input.prompt) { + await ctx.flushStreamedEvents() + } if (input.scope) { return { ok: true, value: { turnId: input.turnId, cancelled } } } diff --git a/src/main/native-chat/agent-session-wire/structured-conversation-command.ts b/src/main/native-chat/agent-session-wire/structured-conversation-command.ts index 9a2bff7c9fc..44131886f5b 100644 --- a/src/main/native-chat/agent-session-wire/structured-conversation-command.ts +++ b/src/main/native-chat/agent-session-wire/structured-conversation-command.ts @@ -54,6 +54,7 @@ export function runStructuredConversationCommand( envelope, journal: context.sessions.get(sessionId)?.journal, publish: (journal) => context.publish(sessionId, journal), + flushStreamedEvents: context.flushStreamedEvents, now: context.now, plan: { method: 'agentSession.conversationCommand', diff --git a/src/main/native-chat/transcript-stream-lines.test.ts b/src/main/native-chat/transcript-stream-lines.test.ts index 48feedfbd98..25ac54d904a 100644 --- a/src/main/native-chat/transcript-stream-lines.test.ts +++ b/src/main/native-chat/transcript-stream-lines.test.ts @@ -1,6 +1,6 @@ import { Readable } from 'node:stream' import { describe, expect, it, vi } from 'vitest' -import { decodeTranscriptStream } from './transcript-stream-lines' +import { decodeTranscriptStream, splitTranscriptStreamLines } from './transcript-stream-lines' const decode = (line: string, id: string) => ({ id, @@ -171,3 +171,36 @@ describe('decodeTranscriptStream', () => { expect(result.consumedBytes).toBe(Buffer.byteLength(complete, 'utf8')) }) }) + +describe('bounded transcript records', () => { + async function collect(chunks: (Buffer | string)[], limit: number) { + const records: string[] = [] + for await (const record of splitTranscriptStreamLines(Readable.from(chunks), limit)) { + records.push(record.line) + } + return records + } + + it.each(['', '\n', '\nnext\n'])('rejects an oversized record ending in %j', async (ending) => { + await expect(collect(['1234', `5${ending}`], 4)).rejects.toThrow('record exceeds 4 byte limit') + await expect(collect([`12345${ending}`], 4)).rejects.toThrow('record exceeds 4 byte limit') + }) + + it('resets the byte budget per record and accepts the exact limit', async () => { + expect(await collect(['1234\n123', '4\n1234'], 4)).toEqual(['1234', '1234', '1234']) + }) + + it('counts UTF-8 bytes across split codepoints', async () => { + const bytes = Buffer.from('😀é') + const chunks = [bytes.subarray(0, 2), bytes.subarray(2, 5), bytes.subarray(5)] + expect((await collect(chunks, 6))[0]).toBe('😀é') + await expect(collect(chunks, 5)).rejects.toThrow('record exceeds 5 byte limit') + expect((await collect(['\ud83d', '\ude00'], 4))[0]).toBe('😀') + }) + + it('checks the decoder tail before emitting it', async () => { + await expect(collect([Buffer.from([0x61, 0xf0, 0x9f])], 3)).rejects.toThrow( + 'record exceeds 3 byte limit' + ) + }) +}) diff --git a/src/main/native-chat/transcript-stream-lines.ts b/src/main/native-chat/transcript-stream-lines.ts index 4a58b4e180f..ce22822b76c 100644 --- a/src/main/native-chat/transcript-stream-lines.ts +++ b/src/main/native-chat/transcript-stream-lines.ts @@ -42,12 +42,13 @@ export async function decodeTranscriptStream( type TranscriptLine = { line: string; byteLength: number; terminated: boolean } export async function* splitTranscriptStreamLines( - stream: AsyncIterable + stream: AsyncIterable, + maxRecordBytes = Infinity ): AsyncGenerator { let records: TranscriptLine[] = [] const framer = createTranscriptLineFramer((line, byteLength, terminated) => { records.push({ line, byteLength, terminated }) - }) + }, maxRecordBytes) for await (const chunk of stream) { framer.write(chunk) for (const record of records) { @@ -63,10 +64,12 @@ export async function* splitTranscriptStreamLines( /** Frame chunks synchronously so native decoding avoids a promise per record. */ function createTranscriptLineFramer( - emit: (line: string, byteLength: number, terminated: boolean) => void + emit: (line: string, byteLength: number, terminated: boolean) => void, + maxRecordBytes = Infinity ): { write(chunk: Buffer | string): void; end(): void } { const decoder = new StringDecoder('utf8') let pending: string[] = [] + let pendingBytes = 0 return { write, end } function write(chunk: Buffer | string): void { @@ -75,23 +78,44 @@ function createTranscriptLineFramer( let newlineIndex = text.indexOf('\n') while (newlineIndex !== -1) { let segment = text.slice(lineStart, newlineIndex + 1) + checkRecordBytes(segment.slice(0, -1)) if (pending.length > 0) { pending.push(segment) segment = pending.join('') pending = [] } + pendingBytes = 0 emit(segment.slice(0, -1), Buffer.byteLength(segment, 'utf8'), true) lineStart = newlineIndex + 1 newlineIndex = text.indexOf('\n', lineStart) } if (lineStart < text.length) { - pending.push(text.slice(lineStart)) + const segment = text.slice(lineStart) + checkRecordBytes(segment) + pending.push(segment) + } + } + + function checkRecordBytes(segment: string): void { + if (maxRecordBytes === Infinity) { + return + } + pendingBytes += Buffer.byteLength(segment, 'utf8') + const previous = pending.at(-1) + // Separately encoded surrogate halves become one four-byte codepoint when joined. + if (previous && /[\uD800-\uDBFF]$/.test(previous) && /^[\uDC00-\uDFFF]/.test(segment)) { + pendingBytes -= 2 + } + if (pendingBytes > maxRecordBytes) { + pending = [] + throw new Error(`Session transcript record exceeds ${maxRecordBytes} byte limit`) } } function end(): void { const tail = decoder.end() if (tail) { + checkRecordBytes(tail) pending.push(tail) } const line = pending.join('') diff --git a/src/main/orcad/orcad-command-arguments.ts b/src/main/orcad/orcad-command-arguments.ts new file mode 100644 index 00000000000..f4fd748de61 --- /dev/null +++ b/src/main/orcad/orcad-command-arguments.ts @@ -0,0 +1,43 @@ +import type { OrcadOptions } from './orcad-entry' + +/** + * orcad's flags. A value-taking flag consumes the next token whatever it looks + * like, so `--bind --json` binds to the literal `--json`; only a missing token + * is an error. Pinned by orcad-launch-contract.test.ts. + */ +export function parseArgs(argv: string[]): OrcadOptions { + const options: OrcadOptions = {} + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + if (arg === '--port') { + const raw = argv[i + 1] + const port = Number(raw) + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new Error(`--port expects an integer 0-65535, got ${raw ?? "''"}`) + } + options.port = port + i += 1 + } else if (arg === '--json') { + options.json = true + } else if (arg === '--no-pairing') { + options.noPairing = true + } else if (arg === '--bind') { + const value = argv[i + 1] + if (value === undefined) { + throw new Error('--bind expects a value') + } + options.bind = value + i += 1 + } else if (arg === '--pairing-address') { + const value = argv[i + 1] + if (!value) { + throw new Error('--pairing-address expects a value') + } + options.pairingAddress = value + i += 1 + } else { + throw new Error(`Unknown argument: ${arg}`) + } + } + return options +} diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index 137894f87b8..3dc84906c99 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -25,6 +25,9 @@ import { } from './orcad-bind-address' import { acquireOrcadInstanceLock, OrcadInstanceLockError } from './orcad-instance-lock' import { startOrcadWithLifecycle } from './orcad-lifecycle' +import { parseArgs } from './orcad-command-arguments' + +export { parseArgs } let runOrcadQuitHandlers = (): void => {} @@ -242,6 +245,13 @@ async function startOrcadRuntime( isAgentStatusHooksEnabled(store.getSettings()) ? agentHookServer.buildPtyEnv() : {} }) + const { installOrcadSessionSearchService } = await import('./orcad-session-search') + const sessionSearch = await installOrcadSessionSearchService({ + userDataPath: runtimeUserDataPath, + getSettings: () => store.getSettings() + }) + getAppEnvironment().onWillQuit(() => sessionSearch?.dispose()) + // Why here too and not only on the desktop: nothing else republishes `session.tabs` when a // pane's status row changes, and orcad's whole job is serving paired clients. uninstallHookStatusRepublish = installHookStatusSessionTabsRepublish( @@ -338,43 +348,6 @@ async function startOrcadRuntime( return { readiness } } -export function parseArgs(argv: string[]): OrcadOptions { - const options: OrcadOptions = {} - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i] - if (arg === '--port') { - const raw = argv[i + 1] - const port = Number(raw) - if (!Number.isInteger(port) || port < 0 || port > 65535) { - throw new Error(`--port expects an integer 0-65535, got ${raw ?? "''"}`) - } - options.port = port - i += 1 - } else if (arg === '--json') { - options.json = true - } else if (arg === '--no-pairing') { - options.noPairing = true - } else if (arg === '--bind') { - const value = argv[i + 1] - if (value === undefined) { - throw new Error('--bind expects a value') - } - options.bind = value - i += 1 - } else if (arg === '--pairing-address') { - const value = argv[i + 1] - if (!value) { - throw new Error('--pairing-address expects a value') - } - options.pairingAddress = value - i += 1 - } else { - throw new Error(`Unknown argument: ${arg}`) - } - } - return options -} - /** * Exit codes a supervisor can act on. Closed set — see docs/reference/orcad-operations.md. * diff --git a/src/main/orcad/orcad-session-search.ts b/src/main/orcad/orcad-session-search.ts new file mode 100644 index 00000000000..7f5d67010a8 --- /dev/null +++ b/src/main/orcad/orcad-session-search.ts @@ -0,0 +1,26 @@ +import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { resolveAiVaultSearchSettings } from '../../shared/ai-vault-search-settings' +import type { GlobalSettings } from '../../shared/global-settings-types' +import { localAiVaultScanRoots } from '../ai-vault/cached-session-list' +import { installInProcessSessionSearchService } from '../ai-vault-search/session-search-in-process-service' + +/** + * orcad's session search registration. + * + * In this process and not a scanner child: orcad ships only the watcher and the + * daemon entries beside `orcad.js`, so there is no scanner-service child here to + * own the index — and this process is the sole writer, so nothing can race it. + * Null on a host whose Node has no `node:sqlite`, which is orcad's stated floor. + */ +export async function installOrcadSessionSearchService(args: { + userDataPath: string + getSettings: () => Pick +}): Promise<{ dispose(): void } | null> { + return installInProcessSessionSearchService({ + dataRoot: args.userDataPath, + roots: { executionHostId: LOCAL_EXECUTION_HOST_ID }, + resolveRoots: localAiVaultScanRoots, + settings: resolveAiVaultSearchSettings(args.getSettings()), + onError: (error) => console.error('[orcad] session search:', error) + }) +} diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index e5c75c1ff01..ec1df02d714 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -172,6 +172,7 @@ export class RepoLifecycleOperations { | 'worktreeBaseRef' | 'worktreeBasePath' | 'kind' + | 'folderUpgradeGitRootPath' | 'executionHostId' | 'symlinkPaths' | 'issueSourcePreference' diff --git a/src/main/persistence/tracking-repos/repo-hydration.ts b/src/main/persistence/tracking-repos/repo-hydration.ts index 10728f55b67..3c5a455419c 100644 --- a/src/main/persistence/tracking-repos/repo-hydration.ts +++ b/src/main/persistence/tracking-repos/repo-hydration.ts @@ -28,6 +28,7 @@ export function repoGitUsernameCacheKey( export function hydrateRepo(repo: Repo, gitUsernameCache: ReadonlyMap): Repo { const { + folderUpgradeGitRootPath, repoIcon: rawRepoIcon, upstream: rawUpstream, gitRemoteIdentity: rawGitRemoteIdentity, @@ -57,6 +58,9 @@ export function hydrateRepo(repo: Repo, gitUsernameCache: ReadonlyMap ({ resolveForegroundMock: vi.fn() })) + +vi.mock('./agent-foreground-process', () => ({ + resolveAgentForegroundProcessWithAvailability: resolveForegroundMock, + confirmShellForegroundProcess: vi.fn() +})) +import { + hasLocalPtyChildProcesses, + inspectLocalPtyChildProcesses +} from './local-pty-foreground-inspection' +import { LocalPtyProvider } from './local-pty-provider' +import { ptyProcesses, ptyShellName } from './local-pty-provider-state' +import { inspectPtyProviderProcess } from './pty-process-inspection' + +function registerPane(id: string, foreground: string | (() => string), shell?: string): void { + const pane: pty.IPty = { + pid: 4242, + cols: 80, + rows: 24, + get process(): string { + return typeof foreground === 'function' ? foreground() : foreground + }, + handleFlowControl: false, + onData: () => ({ dispose() {} }), + onExit: () => ({ dispose() {} }), + resize() {}, + clear() {}, + write() {}, + kill() {}, + pause() {}, + resume() {} + } + ptyProcesses.set(id, pane) + if (shell) { + ptyShellName.set(id, shell) + } +} + +beforeEach(() => { + resolveForegroundMock.mockReset() + resolveForegroundMock.mockResolvedValue({ available: true, processName: '/bin/zsh' }) +}) + +afterEach(() => { + ptyProcesses.clear() + ptyShellName.clear() +}) + +describe('inspectLocalPtyChildProcesses', () => { + it('reports unverifiable when the pty fd cannot be read', () => { + registerPane( + 'pty-closed', + () => { + throw new Error('EBADF: bad file descriptor') + }, + '/bin/zsh' + ) + + // Not `no-children`: the close guard reads that as "nothing is running here" and kills the pane. + expect(inspectLocalPtyChildProcesses('pty-closed')).toBe('unverifiable') + }) + + it('still answers no-children when the shell itself is in the foreground', () => { + registerPane('pty-idle', '/bin/zsh', '/bin/zsh') + expect(inspectLocalPtyChildProcesses('pty-idle')).toBe('no-children') + }) + + it('answers children when something else is in the foreground', () => { + registerPane('pty-busy', 'vim', '/bin/zsh') + expect(inspectLocalPtyChildProcesses('pty-busy')).toBe('children') + }) + + it('treats a pane this provider does not hold as a real negative', () => { + expect(inspectLocalPtyChildProcesses('pty-absent')).toBe('no-children') + }) + + it('collapses uncertainty to false only in the boolean adapter', async () => { + registerPane( + 'pty-closed', + () => { + throw new Error('EBADF: bad file descriptor') + }, + '/bin/zsh' + ) + + // The adapter exists for `IPtyProvider.hasChildProcesses`, which has no third slot. + await expect(hasLocalPtyChildProcesses('pty-closed')).resolves.toBe(false) + }) +}) + +describe('inspectPtyProviderProcess child-process evidence', () => { + const provider = new LocalPtyProvider() + + it('carries unverifiable evidence when the child read fails after foreground inspection', async () => { + let reads = 0 + registerPane( + 'pty-closing', + () => { + reads += 1 + if (reads > 1) { + throw new Error('EBADF: bad file descriptor') + } + return '/bin/zsh' + }, + '/bin/zsh' + ) + + await expect(inspectPtyProviderProcess(provider, 'pty-closing')).resolves.toEqual({ + foregroundProcess: '/bin/zsh', + hasChildProcesses: false, + childProcessEvidence: 'unverifiable' + }) + }) + + it('samples child evidence after foreground inspection', async () => { + let reads = 0 + registerPane('pty-became-busy', () => (reads++ === 0 ? '/bin/zsh' : 'vim'), '/bin/zsh') + + const inspection = await inspectPtyProviderProcess(provider, 'pty-became-busy') + expect(inspection.hasChildProcesses).toBe(true) + expect(inspection.childProcessEvidence).toBe('children') + }) + + it('carries no-children evidence from the local inspectProcess operation', async () => { + registerPane('pty-idle', '/bin/zsh', '/bin/zsh') + + const inspection = await inspectPtyProviderProcess(provider, 'pty-idle') + expect(inspection.hasChildProcesses).toBe(false) + expect(inspection.childProcessEvidence).toBe('no-children') + }) + + it('carries children evidence from the local inspectProcess operation', async () => { + registerPane('pty-busy', 'vim', '/bin/zsh') + + const inspection = await inspectPtyProviderProcess(provider, 'pty-busy') + expect(inspection.hasChildProcesses).toBe(true) + expect(inspection.childProcessEvidence).toBe('children') + }) +}) diff --git a/src/main/providers/local-pty-foreground-inspection.ts b/src/main/providers/local-pty-foreground-inspection.ts index d4a717a9de2..eec6a19a621 100644 --- a/src/main/providers/local-pty-foreground-inspection.ts +++ b/src/main/providers/local-pty-foreground-inspection.ts @@ -1,3 +1,4 @@ +import type { PtyChildProcessVerdict } from '../../shared/terminal-process-inspection' import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' import { getCheapProcessTableSnapshot } from '../../shared/cheap-process-table-snapshot-reader' import { getProcessTableSnapshot } from '../../shared/process-table-snapshot-reader' @@ -21,23 +22,28 @@ import { import { readWindowsConsoleAttachedProcessIds } from './windows-console-attached-processes' import { isWindowsPtyJobReadable, readWindowsPtyJobProcessIds } from './windows-pty-job-membership' -export async function hasLocalPtyChildProcesses(id: string): Promise { +export function inspectLocalPtyChildProcesses(id: string): PtyChildProcessVerdict { const proc = ptyProcesses.get(id) if (!proc) { - return false + return 'no-children' } try { const foreground = proc.process const shell = ptyShellName.get(id) if (!shell) { - return true + return 'children' } - return foreground !== shell + return foreground === shell ? 'no-children' : 'children' } catch { - return false + // An unreadable PTY is not evidence that its children exited. + return 'unverifiable' } } +export async function hasLocalPtyChildProcesses(id: string): Promise { + return inspectLocalPtyChildProcesses(id) === 'children' +} + /** * POSIX twin of the Windows job-membership short-circuit below: a pane that already holds a * recognized agent re-proves it from the cheap `ps` tier when the subtree fingerprint is diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index f50ad36d34c..056a829d1dd 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -9,9 +9,11 @@ import { confirmLocalPtyForegroundProcess, confirmLocalPtyShellForeground, getLocalPtyForegroundProcess, - hasLocalPtyChildProcesses + hasLocalPtyChildProcesses, + inspectLocalPtyChildProcesses } from './local-pty-foreground-inspection' import type { LocalPtyProviderOptions } from './local-pty-provider-types' +import type { PtyProcessInspection } from './pty-process-inspection' import { advanceLoadGeneration, clearPtyState, @@ -127,6 +129,16 @@ export class LocalPtyProvider implements IPtyProvider { return hasLocalPtyChildProcesses(id) } + async inspectProcess(id: string): Promise { + const foregroundProcess = await getLocalPtyForegroundProcess(id) + const childProcessEvidence = inspectLocalPtyChildProcesses(id) + return { + foregroundProcess, + hasChildProcesses: childProcessEvidence === 'children', + childProcessEvidence + } + } + getForegroundProcess(id: string): Promise { return getLocalPtyForegroundProcess(id) } diff --git a/src/main/pty/omp-shell-wrapper-alias-safety.test.ts b/src/main/pty/omp-shell-wrapper-alias-safety.test.ts index 3863b3aacaf..d1d0e0aad83 100644 --- a/src/main/pty/omp-shell-wrapper-alias-safety.test.ts +++ b/src/main/pty/omp-shell-wrapper-alias-safety.test.ts @@ -68,3 +68,28 @@ describe.skipIf(process.platform === 'win32')('omp wrapper under a user alias na expectAliasedOmpNameSurvives('/bin/zsh', 'setopt aliases') }) }) + +describe.skipIf(process.platform === 'win32' || !zshAvailable)('OMP wrapper global aliases', () => { + it.each(['--help', '-v', 'models'])('parses with hostile global alias %s', (token) => { + const root = mkdtempSync(join(tmpdir(), 'orca-omp-global-alias-')) + roots.push(root) + const startup = join(root, 'startup.zsh') + writeFileSync( + startup, + [ + `alias -g -- ${token}='${token} 2>&1 | cat'`, + getPosixOmpShellWrapper(), + `if ! __orca_omp_should_skip_extension '${token}'; then exit 1; fi`, + 'printf "parsed\\n"', + `alias -g -- '${token}'` + ].join('\n') + ) + const result = spawnSync('/bin/zsh', ['-f', startup], { + encoding: 'utf8', + env: { ...process.env, HOME: root, ZDOTDIR: root } + }) + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('parsed') + expect(result.stdout).toContain('2>&1 | cat') + }) +}) diff --git a/src/main/pty/omp-shell-wrapper.ts b/src/main/pty/omp-shell-wrapper.ts index f5bc25421bb..de1d9bed2af 100644 --- a/src/main/pty/omp-shell-wrapper.ts +++ b/src/main/pty/omp-shell-wrapper.ts @@ -40,13 +40,13 @@ const OMP_SUBCOMMANDS = [ ] as const export function getPosixOmpShellWrapper(): string { - const subcommands = OMP_SUBCOMMANDS.join('|') + const subcommands = OMP_SUBCOMMANDS.map((value) => `'${value}'`).join('|') return `# Why: OMP does not auto-load Orca's managed status extension; wrap only # interactive launch invocations so subcommands such as \`omp config\` keep # their normal argv shape. __orca_omp_should_skip_extension() { case "\${1:-}" in - help|--help|-h|--version|-v) return 0 ;; + 'help'|'--help'|'-h'|'--version'|'-v') return 0 ;; ${subcommands}) return 0 ;; esac return 1 diff --git a/src/main/repo-worktrees.test.ts b/src/main/repo-worktrees.test.ts index a6b20c5445f..f211d604002 100644 --- a/src/main/repo-worktrees.test.ts +++ b/src/main/repo-worktrees.test.ts @@ -9,7 +9,8 @@ const { listWorktreeGraphMock, listWorktreesMock, listWorktreesStrictMock } = vi vi.mock('./git/worktree', () => ({ listWorktreeGraph: listWorktreeGraphMock, listWorktrees: listWorktreesMock, - listWorktreesStrict: listWorktreesStrictMock + listWorktreesStrict: listWorktreesStrictMock, + listWorktreesSharedStrictAllowingTrueEmpty: listWorktreesStrictMock })) import { @@ -17,7 +18,8 @@ import { isRepoRoot, listLocalRepoWorktreesStrict, listRepoWorktreeGraph, - listRepoWorktrees + listRepoWorktrees, + listRepoWorktreesForDetectedScan } from './repo-worktrees' import { registerSshGitProvider, unregisterSshGitProvider } from './providers/ssh-git-dispatch' import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability' @@ -270,3 +272,37 @@ describe('repo-worktrees', () => { expect(isRepoRoot(repos, String.raw`c:\repo`)).toBe(true) }) }) + +it('keeps an upgraded linked folder locator in every local listing, including restart hydration', async () => { + const repo = { + id: 'folder', + path: 'C:\\projects\\draft', + displayName: 'draft', + badgeColor: 'blue', + addedAt: 0, + kind: 'git' as const, + folderUpgradeGitRootPath: 'C:/projects/draft' + } + const raw = [ + { path: 'C:/projects/main', head: 'abc', branch: 'main', isBare: false, isMainWorktree: true }, + { + path: 'C:/projects/draft', + head: 'def', + branch: 'draft', + isBare: false, + isMainWorktree: false + } + ] + listWorktreesMock.mockResolvedValue(raw) + listWorktreeGraphMock.mockResolvedValue(raw) + listWorktreesStrictMock.mockResolvedValue(raw) + for (const list of [ + listRepoWorktrees, + listRepoWorktreesForDetectedScan, + listRepoWorktreeGraph, + listLocalRepoWorktreesStrict + ]) { + expect(await list(repo)).toEqual([raw[0], { ...raw[1], path: repo.path }]) + } + expect(raw[1].path).toBe('C:/projects/draft') +}) diff --git a/src/main/repo-worktrees.ts b/src/main/repo-worktrees.ts index f5d67523286..02495c9ff5c 100644 --- a/src/main/repo-worktrees.ts +++ b/src/main/repo-worktrees.ts @@ -1,3 +1,4 @@ +import { preserveFolderUpgradeWorktreePath } from './folder-upgrade-worktree-path' import type { Repo } from '../shared/repo-types' import type { GitWorktreeInfo } from '../shared/worktree/types' import { @@ -93,9 +94,10 @@ async function listRoutedRepoWorktrees( } return await route.provider.listWorktrees(repo.path) } - return hasLocalRepoWorktreeListOptions(options) + const worktrees = hasLocalRepoWorktreeListOptions(options) ? await listLocal(repo.path, options) : await listLocal(repo.path) + return preserveFolderUpgradeWorktreePath(repo, worktrees) } /** @@ -122,9 +124,10 @@ export async function listRepoWorktreeGraph( if (route.kind === 'ssh') { return route.provider ? await route.provider.listWorktrees(repo.path) : [] } - return hasLocalRepoWorktreeListOptions(options) + const worktrees = hasLocalRepoWorktreeListOptions(options) ? await listWorktreeGraph(repo.path, options) : await listWorktreeGraph(repo.path) + return preserveFolderUpgradeWorktreePath(repo, worktrees) } export async function listLocalRepoWorktreesStrict( @@ -137,7 +140,8 @@ export async function listLocalRepoWorktreesStrict( if (isFolderRepo(repo)) { return [createFolderWorktree(repo)] } - return hasLocalRepoWorktreeListOptions(options) + const worktrees = hasLocalRepoWorktreeListOptions(options) ? await listWorktreesStrict(repo.path, options) : await listWorktreesStrict(repo.path) + return preserveFolderUpgradeWorktreePath(repo, worktrees) } diff --git a/src/main/runtime/__fixtures__/omp-native-title-win32.meta.json b/src/main/runtime/__fixtures__/omp-native-title-win32.meta.json new file mode 100644 index 00000000000..6f64734a888 --- /dev/null +++ b/src/main/runtime/__fixtures__/omp-native-title-win32.meta.json @@ -0,0 +1,9 @@ +{ + "capturedAt": "2026-09-14T11:25:01.730Z", + "platform": "darwin", + "command": ["bun", "tests/tools/omp-native-title-capture.mjs", ""], + "cols": 100, + "rows": 30, + "note": "OMP source ne7546987ca526eac8f605fac19ef9805b8f01898 buildTerminalTitleWithState; explicit win32 argument on macOS PTY, synthetic state transitions, no model/account. Not a Windows runtime capture.", + "exitCode": 0 +} diff --git a/src/main/runtime/__fixtures__/omp-native-title-win32.txt b/src/main/runtime/__fixtures__/omp-native-title-win32.txt new file mode 100644 index 00000000000..ac67e688dce --- /dev/null +++ b/src/main/runtime/__fixtures__/omp-native-title-win32.txt @@ -0,0 +1 @@ +]0;π : Run a long task]0;π : release | π : note | OMP ! action required ✦]0;π > Run a long task]0;π > release | π : note | OMP ! action required ✦]0;π ! Run a long task]0;π ! release | π : note | OMP ! action required ✦ \ No newline at end of file diff --git a/src/main/runtime/agent-session-backup-recovery-fence.ts b/src/main/runtime/agent-session-backup-recovery-fence.ts index 88c4ad8d720..dc0fd835fa8 100644 --- a/src/main/runtime/agent-session-backup-recovery-fence.ts +++ b/src/main/runtime/agent-session-backup-recovery-fence.ts @@ -1,15 +1,14 @@ // Recovering the agent-session store from its backup, without minting a second writer. // // The backup is the previous committed generation. The commit that never landed may have granted a -// fence one higher than anything the backup records show, and `isAgentSessionFenceCurrent` compares -// with STRICT EQUALITY — so a next-fence of `recordFence + 1` would *equal* that lost grant and -// accept a writer holding it. `+2` strictly dominates it. +// fence chosen by `nextAgentSessionFence` from the backup lease, and +// `isAgentSessionFenceCurrent` compares with STRICT EQUALITY. That choice may already be above +// `runtimeFence + 1` after an earlier recovery; the new floor must strictly dominate it. // -// The bound "one lost commit can advance a session's fence by at most 1" is what makes +2 enough. -// It holds because every mint site routes through `nextAgentSessionFence` and each performs one -// transition per transaction, and because the save path aborts rather than letting the primary -// advance past a stale backup. A batching refactor would break it silently, so it is pinned by a -// test. +// The bound is one lost mint per backup generation: each mint site uses +// `nextAgentSessionFence` once per transaction, and the save path aborts rather than advancing the +// primary past a stale backup. A source-level ratchet rejects direct `+ 1` mints; an indirected +// mint is not caught. // // This records a FLOOR for the next grant and leaves the current fence alone. Rewriting the current // fence is what an earlier version did, and it corrupted exactly the records it meant to save: a @@ -24,19 +23,20 @@ // once transactions are admitted. Nulling that evidence is how you get two writers on one provider // session; the fence protects the store, not the provider session. +import { nextAgentSessionFence } from '../../shared/agent-session-next-fence' import type { AgentSessionStoreState } from './agent-session-record-store-file' -/** Strictly above any fence the lost commit could have granted for that session. */ -export const AGENT_SESSION_BACKUP_RECOVERY_FENCE_MARGIN = 2 - export function raiseAgentSessionFencesAfterBackupRecovery(state: AgentSessionStoreState): void { for (const [sessionId, record] of state.records) { - const floor = record.lease.runtimeFence + AGENT_SESSION_BACKUP_RECOVERY_FENCE_MARGIN + const floor = nextAgentSessionFence(record.lease) + 1 + if (!Number.isSafeInteger(floor)) { + throw new Error('agent_session_fence_exhausted') + } state.records.set(sessionId, { ...record, lease: { ...record.lease, - minimumNextFence: Math.max(floor, record.lease.minimumNextFence ?? 0) + minimumNextFence: floor } }) } diff --git a/src/main/runtime/agent-session-backup-recovery.test.ts b/src/main/runtime/agent-session-backup-recovery.test.ts index 18d1ec00bda..0ea430774aa 100644 --- a/src/main/runtime/agent-session-backup-recovery.test.ts +++ b/src/main/runtime/agent-session-backup-recovery.test.ts @@ -187,6 +187,120 @@ describe('recovery from the committed backup', () => { expect(granted.decision === 'granted' && granted.nextFence).toBeGreaterThan(fence + 1) }) + it('does not reissue a grant after two backup fallbacks and a backup rotation', async () => { + await seedSession('session-a') + await seedSession('session-b') + const loaded = await loadAgentSessionStore(storePath, 'local') + const record = loaded.state.records.get('session-a') + if (!record) { + throw new Error('seeded session missing') + } + loaded.state.records.set('session-a', { + ...record, + lease: { + ...record.lease, + runtimeFence: 7, + claimStatus: 'released', + handoffStage: null, + reservedSpawnToken: null + } + }) + // Two commits put the prepared generation in the backup, just as normal rotation would. + await saveAgentSessionStore(storePath, loaded.state, { primaryStatus: 'validated' }) + await saveAgentSessionStore(storePath, loaded.state, { primaryStatus: 'validated' }) + const identity = { + location: record.location, + provider: record.provider, + accountHome: record.accountHome, + runtimeKind: record.lease.runtimeKind, + claimKeyId: record.lease.claimKeyId + } + + await rm(storePath, { force: true }) + const first = await openStore() + await first.retireClaimKey(`retire-${operationId()}`, NOW) + await first.reconcileOnRestart({ + probe: async () => ({ outcome: 'reservation-unused' }), + now: NOW + }) + expect(first.getRecord('session-a')?.lease).toMatchObject({ + runtimeFence: 7, + minimumNextFence: 9, + unreconciled: false + }) + const firstGrant = await first.reserveOwner({ + ...identity, + sessionId: 'session-a', + expectedFence: 7, + spawnToken: 'first-recovery', + handoffOperationId: null, + probe: { outcome: 'reservation-unused' }, + operation: { callerKey: 'test', operationId: operationId(), fingerprint: 'first-recovery' }, + now: NOW + }) + const firstFence = firstGrant.record.lease.runtimeFence + const rotated = (await loadAgentSessionStore(`${storePath}.bak`, 'local')).state.records.get( + 'session-a' + ) + expect(rotated?.lease).toMatchObject({ runtimeFence: 7, minimumNextFence: 9 }) + + // The primary's grant is lost, but its owner may still hold that exact fence. + await rm(storePath, { force: true }) + const second = await openStore() + await second.retireClaimKey(`retire-${operationId()}`, NOW) + await second.reconcileOnRestart({ + probe: async () => ({ outcome: 'reservation-unused' }), + now: NOW + }) + const secondGrant = await second.reserveOwner({ + ...identity, + sessionId: 'session-a', + expectedFence: 7, + spawnToken: 'second-recovery', + handoffOperationId: null, + probe: { outcome: 'reservation-unused' }, + operation: { callerKey: 'test', operationId: operationId(), fingerprint: 'second-recovery' }, + now: NOW + }) + expect(secondGrant.record.lease.runtimeFence).toBeGreaterThan(firstFence) + expect((await openStore()).getRecord('session-a')?.lease.runtimeFence).toBe( + secondGrant.record.lease.runtimeFence + ) + }) + + it.each([ + [Number.MAX_SAFE_INTEGER - 2, Number.MAX_SAFE_INTEGER], + [Number.MAX_SAFE_INTEGER - 1, null] + ])('keeps the recovered floor safe at fence %i', async (runtimeFence, expectedFloor) => { + await seedSession('session-a') + await seedSession('session-b') + const backupPath = `${storePath}.bak` + const backup = JSON.parse(await readFile(backupPath, 'utf-8')) + backup.records['session-a'].lease.runtimeFence = runtimeFence + await writeFile(backupPath, JSON.stringify(backup)) + await rm(storePath, { force: true }) + + const recovered = await openStore() + if (expectedFloor === null) { + await expect(recovered.retireClaimKey(`retire-${operationId()}`, NOW)).rejects.toThrow( + 'agent_session_fence_exhausted' + ) + await expect(stat(storePath)).rejects.toMatchObject({ code: 'ENOENT' }) + const preserved = (await loadAgentSessionStore(backupPath, 'local')).state.records.get( + 'session-a' + )?.lease + expect(preserved?.runtimeFence).toBe(runtimeFence) + expect(preserved?.minimumNextFence).toBeUndefined() + } else { + await recovered.retireClaimKey(`retire-${operationId()}`, NOW) + expect(recovered.getRecord('session-a')?.lease).toMatchObject({ + runtimeFence, + minimumNextFence: expectedFloor + }) + expect((await openStore()).getRecord('session-a')?.lease.minimumNextFence).toBe(expectedFloor) + } + }) + it('leaves recovered records valid, so the next load does not quarantine them', async () => { await seedLiveSession('session-a') await seedSession('session-b') diff --git a/src/main/runtime/agent-session-surface-release-transition.test.ts b/src/main/runtime/agent-session-surface-release-transition.test.ts new file mode 100644 index 00000000000..5e671acc77f --- /dev/null +++ b/src/main/runtime/agent-session-surface-release-transition.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { + agentSessionLeaseFixture, + agentSessionRecordFixture +} from '../../shared/agent-session-record.test-fixture' +import { releaseAgentSessionOwnerAfterSurfaceClose } from './agent-session-surface-release-transition' + +describe('agent session surface release transition', () => { + it('honours a recovery floor when releasing the owner', () => { + const record = agentSessionRecordFixture( + agentSessionLeaseFixture({ runtimeKind: 'native', minimumNextFence: 9 }) + ) + + const released = releaseAgentSessionOwnerAfterSurfaceClose({ + record, + expectedFence: 7, + now: 1_800_000_001_000 + }) + + expect(released.lease.runtimeFence).toBe(9) + }) +}) diff --git a/src/main/runtime/agent-session-surface-release-transition.ts b/src/main/runtime/agent-session-surface-release-transition.ts index ef08a2186f5..3d1f6d33954 100644 --- a/src/main/runtime/agent-session-surface-release-transition.ts +++ b/src/main/runtime/agent-session-surface-release-transition.ts @@ -9,6 +9,7 @@ // against the dead generation land on the next one. import type { AgentSessionRecord } from '../../shared/agent-session-record' +import { nextAgentSessionFence } from '../../shared/agent-session-next-fence' import { assertFence, withLease } from './agent-session-lease-transitions' import type { AgentSessionRecordStore } from './agent-session-record-store' @@ -40,7 +41,7 @@ export function releaseAgentSessionOwnerAfterSurfaceClose(args: { } return withLease(record, { ...record.lease, - runtimeFence: record.lease.runtimeFence + 1, + runtimeFence: nextAgentSessionFence(record.lease), ownerProcess: null, reservedSpawnToken: null, processlessAt: null, diff --git a/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts b/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts index fc86de47451..3459c7c8b6e 100644 --- a/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts +++ b/src/main/runtime/orca-runtime-apply-tracked-pty-title.ts @@ -138,7 +138,16 @@ export class OrcaRuntimeWithApplyTrackedPtyTitle extends OrcaRuntimeWithGetUnper // an agent whose first live title is already idle (claude --resume at its // prompt) then shows no transition — the row would strand, which is // exactly #12536. Waiter semantics stay transition-only above. - if (agentStatus === 'idle' && (prevStatus !== 'idle' || !prevObservedLive)) { + // Why the title change joins the edge: a name-only frame routinely lands before the + // hook's `X ready`, and it consumes the working→idle transition. The later ready title + // is an idle→idle step, so gating delivery on `prevStatus !== 'idle'` meant the + // strongest evidence this pane will ever emit never reached delivery at all. The + // waiter branch above already re-offers on that step; the gate makes a repeat harmless. + if ( + agentStatus === 'idle' && + (prevStatus !== 'idle' || !prevObservedLive || prevLeafTitle !== recordedTitle) && + this.checkDeliverySettledAndArmRecheck(leaf) + ) { this.deliverPendingMessagesForLeaf(leaf) } } diff --git a/src/main/runtime/orca-runtime-deliver-pending-messages.ts b/src/main/runtime/orca-runtime-deliver-pending-messages.ts index 8d55cddd112..55c6a8dad5e 100644 --- a/src/main/runtime/orca-runtime-deliver-pending-messages.ts +++ b/src/main/runtime/orca-runtime-deliver-pending-messages.ts @@ -111,7 +111,8 @@ export class OrcaRuntimeWithDeliverPendingMessages extends OrcaRuntimeWithResolv if ( currentLeaf?.ptyId === probedPtyId && currentLeaf.lastAgentStatus === 'idle' && - currentLeaf.lastAgentStatusObservedLive + currentLeaf.lastAgentStatusObservedLive && + this.checkDeliverySettledAndArmRecheck(currentLeaf) ) { this.deliverPendingMessages(currentLeaf, { mailboxHandle, diff --git a/src/main/runtime/orca-runtime-remove-managed-worktree.ts b/src/main/runtime/orca-runtime-remove-managed-worktree.ts index 25a294c2a90..9cb95e5d354 100644 --- a/src/main/runtime/orca-runtime-remove-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-remove-managed-worktree.ts @@ -13,13 +13,14 @@ import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth' import { resolveWorktreeRemovalRoute } from '../worktree-removal-execution-host-route' import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' import { listWorktreesStrict } from '../git/worktree' +import { isPrunableGitFileWorktree } from '../worktree-prunable-git-file' import { findRegisteredDeletableWorktree } from '../worktree-removal-safety' import { removeRuntimeUnregisteredWorktree } from './runtime-unregistered-worktree-removal' import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { formatWorktreeRemovalError } from '../ipc/worktree-logic' import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' import { isRuntimeWorktreePathMissing } from './runtime-worktree-filesystem' -import { removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval } from '../local-worktree-removal-recovery' +import { removeStaleLocalWorktreeRegistration } from '../local-worktree-removal-recovery' import { cleanupUnusedWorktreePushTargetRemote } from '../ipc/worktree-remote' import { removeRuntimeRegisteredRemoteWorktree } from './runtime-registered-remote-worktree-removal' import { removeRuntimeRegisteredLocalWorktree } from './runtime-registered-local-worktree-removal' @@ -146,18 +147,19 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM } if ( route.kind === 'local' && - force === true && - process.platform === 'win32' && - (isWindowsAbsolutePathLike(canonicalWorktreePath) || - !!localWorktreeGitOptions.wslDistro) && - removedMeta && - (await isRuntimeWorktreePathMissing( - route.hostId, - canonicalWorktreePath, - localWorktreeGitOptions - )) + ((await isPrunableGitFileWorktree(registeredWorktree, localWorktreeGitOptions)) || + (force === true && + process.platform === 'win32' && + (isWindowsAbsolutePathLike(canonicalWorktreePath) || + !!localWorktreeGitOptions.wslDistro) && + removedMeta && + (await isRuntimeWorktreePathMissing( + route.hostId, + canonicalWorktreePath, + localWorktreeGitOptions + )))) ) { - const removalResult = await removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval({ + const removalResult = await removeStaleLocalWorktreeRegistration({ canonicalWorktreePath, repoPath: repo.path, localWorktreeGitOptions, diff --git a/src/main/runtime/orca-runtime-resolve-exit-waiters.ts b/src/main/runtime/orca-runtime-resolve-exit-waiters.ts index 221bb873545..ea6329db3e6 100644 --- a/src/main/runtime/orca-runtime-resolve-exit-waiters.ts +++ b/src/main/runtime/orca-runtime-resolve-exit-waiters.ts @@ -117,6 +117,76 @@ export class OrcaRuntimeWithResolveExitWaiters extends OrcaRuntimeWithBindPtyInc }) } + /** + * Settled-enough-to-type check that also arms a retry when it says no. + * + * Why the retry: the wait path POLLS, so weak evidence that only becomes valid with the + * passage of time (a pane going quiet) eventually satisfies it. Delivery is edge-driven — + * a title transition, a graph sync, a new message — with no poll behind it, so a refusal + * at an edge is final unless another edge happens to arrive. A hookless Codex pane never + * emits an explicit `X ready`, so the refusal below would strand the queued message + * permanently once the pane fell quiet. One-shot timer, armed only for a leaf that + * actually refused, cleared as soon as any path delivers. + */ + protected checkDeliverySettledAndArmRecheck(leaf: { tabId: string; leafId: string }): boolean { + const leafKey = this.getLeafKey(leaf.tabId, leaf.leafId) + if (this.isAgentSettledForDelivery(leaf)) { + this.clearDeliveryRecheck(leafKey) + return true + } + this.armDeliveryRecheck(leafKey) + return false + } + + protected clearDeliveryRecheck(leafKey: string): void { + const timer = this.deliveryRecheckTimersByLeafKey.get(leafKey) + if (timer) { + clearTimeout(timer) + this.deliveryRecheckTimersByLeafKey.delete(leafKey) + } + } + + private armDeliveryRecheck(leafKey: string): void { + if (this.deliveryRecheckTimersByLeafKey.has(leafKey)) { + return + } + const live = this.leaves.get(leafKey) + // Why this delay: the only refusal that time alone can lift is tier 3 waiting on the + // stream to go quiet, so wake just after the window could have elapsed. A pane that is + // still producing output re-arms from its own fresher timestamp rather than spinning. + const elapsed = live?.lastOutputAt ? Date.now() - live.lastOutputAt : 0 + const delay = Math.max(TUI_IDLE_QUIESCENCE_MS - elapsed, 0) + 50 + const timer = setTimeout(() => { + this.deliveryRecheckTimersByLeafKey.delete(leafKey) + const current = this.leaves.get(leafKey) + if (!current) { + return + } + // Why the gate again here: delivery sites gate at the CALL, not inside + // deliverPendingMessagesForLeaf, so firing straight into it would hand the retry the + // very injection the gate exists to prevent. A pane that went busy again re-arms. + if (this.checkDeliverySettledAndArmRecheck(current)) { + this.deliverPendingMessagesForLeaf(current) + } + }, delay) + timer.unref?.() + this.deliveryRecheckTimersByLeafKey.set(leafKey, timer) + } + + /** + * Whether this pane is settled enough to TYPE INTO. + * + * Why the same ranking as the wait path: mailbox delivery writes the pointer plus Enter + * into the pane, so acting on a name-only `Codex` title mid-turn injects keystrokes into + * a running agent's session. That is the #6011 mis-settlement in a path with a worse + * failure mode than a racing script. Liveness stays a separate requirement — callers + * keep their own `lastAgentStatusObservedLive` checks. + */ + protected isAgentSettledForDelivery(leaf: { tabId: string; leafId: string }): boolean { + const live = this.leaves.get(this.getLeafKey(leaf.tabId, leaf.leafId)) + return live ? this.isTuiIdleSatisfiedForLeaf(live) : false + } + protected isTuiIdleSatisfiedForPty(pty: RuntimePtyWorktreeRecord): boolean { return isTuiIdleSatisfied({ record: pty, diff --git a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts index 0d934548332..44f4524c782 100644 --- a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts +++ b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts @@ -134,7 +134,13 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith return { handle, tabId: leaf.tabId, title } } } - return { handle, tabId: pty.pty.tabId ?? pty.record.tabId, title } + const tabId = pty.pty.tabId ?? pty.record.tabId + // A notifier can exist before its pane graph; retain the rename on the known tab. + if (this.notifier?.renameTerminal && tabId) { + this.persistHeadlessTerminalTitle(pty.pty.worktreeId, tabId, title) + this.notifier.renameTerminal(tabId, title) + } + return { handle, tabId, title } } this.assertGraphReady() const { leaf } = this.getLiveLeafForHandle(handle) diff --git a/src/main/runtime/orca-runtime-runtime-id.ts b/src/main/runtime/orca-runtime-runtime-id.ts index 0806b705fdf..00ccdde9da9 100644 --- a/src/main/runtime/orca-runtime-runtime-id.ts +++ b/src/main/runtime/orca-runtime-runtime-id.ts @@ -274,6 +274,9 @@ export class OrcaRuntimeWithRuntimeId { return pty?.launchAgent ?? pty?.foregroundAgent ?? null } + /** One-shot delivery retries, keyed by leaf. See checkDeliverySettledAndArmRecheck. */ + protected deliveryRecheckTimersByLeafKey = new Map>() + protected leaves = new Map() // Why: PTY output is a per-keystroke hot path. Looking up affected leaves by diff --git a/src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts b/src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts index 1e321bc7584..dd3afbfa23e 100644 --- a/src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts +++ b/src/main/runtime/orca-runtime-serialize-agent-prompt-submission.ts @@ -139,7 +139,11 @@ export class OrcaRuntimeWithSerializeAgentPromptSubmission extends OrcaRuntimeWi leaf.lastAgentStatus = restoredStatus if (restoredStatus === 'idle') { this.resolveTuiIdleWaiters(leaf) - this.deliverPendingMessagesForLeaf(leaf) + // Why gated like every other delivery edge: a neutral-title restoration can + // reinstate `idle` from a name-only title, which is not evidence a turn ended. + if (this.checkDeliverySettledAndArmRecheck(leaf)) { + this.deliverPendingMessagesForLeaf(leaf) + } } } } diff --git a/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts b/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts index c7f883fcfcb..4989245fdf6 100644 --- a/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts +++ b/src/main/runtime/orca-runtime-stop-requested-pty-ids.ts @@ -203,6 +203,7 @@ export class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWithRuntimeId getLeaf: (leafKey) => this.leaves.get(leafKey), getLeafKey: (tabId, leafId) => this.getLeafKey(tabId, leafId), getLiveLeafForHandle: (handle) => this.getLiveLeafForHandle(handle).leaf, + isAgentSettledForDelivery: (leaf) => this.checkDeliverySettledAndArmRecheck(leaf), getMessageWaiters: (mailboxHandle) => this.messageWaiters.get(mailboxHandle), getTabTitle: (tabId) => this.tabs.get(tabId)?.title, getCliCommand: (terminalHandle) => this.getTerminalOrchestrationCliCommand(terminalHandle), diff --git a/src/main/runtime/orca-runtime-sync-window-graph.ts b/src/main/runtime/orca-runtime-sync-window-graph.ts index 3a784fcd622..e820d15130b 100644 --- a/src/main/runtime/orca-runtime-sync-window-graph.ts +++ b/src/main/runtime/orca-runtime-sync-window-graph.ts @@ -283,6 +283,7 @@ export class OrcaRuntimeWithSyncWindowGraph extends OrcaRuntimeWithAttachWindow this._orchestrationDb && leaf.lastAgentStatus === 'idle' && leaf.lastAgentStatusObservedLive && + this.checkDeliverySettledAndArmRecheck(leaf) && leaf.writable && (!graphWasReady || previousLeaf?.ptyId !== leaf.ptyId || diff --git a/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts b/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts new file mode 100644 index 00000000000..87aabaf4c54 --- /dev/null +++ b/src/main/runtime/orca-runtime-terminal-rename-retention.test.ts @@ -0,0 +1,92 @@ +import './orca-runtime-test-lifecycle.spec' +import type { RuntimeStore } from './runtime-store-contract' +import { describe, expect, it, vi } from 'vitest' +import { createMobileCreateTestNotifier } from './orca-runtime-test-scenario-builders.spec' +import { OrcaRuntimeService } from './orca-runtime-test-mocks.spec' +import { + HEADLESS_LEAF_ID, + TEST_WORKTREE_ID, + makeRuntimeStoreWithWorkspaceSession, + makeWorkspaceSessionWithHeadlessTerminal +} from './orca-runtime-test-fixtures.spec' + +describe('terminal rename before renderer graph hydration', () => { + it.each(['Media Engine Orch', null])( + 'persists and forwards title %s across PTY replacement', + async (title) => { + const session = makeWorkspaceSessionWithHeadlessTerminal() + session.tabsByWorktree[TEST_WORKTREE_ID][0].customTitle = 'Previous name' + const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession(session) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The shared fixture supplies RuntimeStore methods; its legacy Mock return type loses callable signatures. + const checkedStore = runtimeStore as RuntimeStore + const runtime = new OrcaRuntimeService(checkedStore) + const renameTerminal = vi.fn() + runtime.setPtyController({ + spawn: vi.fn(async () => ({ id: 'omp-initial-pty' })), + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + runtime.setNotifier({ + worktreesChanged: vi.fn(), + reposChanged: vi.fn(), + activateWorktree: vi.fn(), + createTerminal: vi.fn(), + splitTerminal: vi.fn(), + renameTerminal, + focusTerminal: vi.fn(), + closeTerminal: vi.fn(), + sleepWorktree: vi.fn(), + terminalFitOverrideChanged: vi.fn(), + terminalDriverChanged: vi.fn() + }) + const created = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID + }) + + await runtime.renameTerminal(created.handle, title) + + expect(getSession().tabsByWorktree[TEST_WORKTREE_ID][0].customTitle).toBe(title) + expect(renameTerminal).toHaveBeenCalledWith('host-tab', title) + runtime.onPtyExit('omp-initial-pty', 0) + const restored = new OrcaRuntimeService(checkedStore) + restored.setPtyController({ + spawn: vi.fn(async () => ({ id: 'omp-replacement-pty' })), + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + await restored.createTerminal(`id:${TEST_WORKTREE_ID}`, { + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID + }) + expect(getSession().tabsByWorktree[TEST_WORKTREE_ID][0].customTitle).toBe(title) + } + ) + it('does not recreate a closed persisted tab from a surviving PTY record', async () => { + const session = makeWorkspaceSessionWithHeadlessTerminal() + const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(session) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Shared fixture implements RuntimeStore; its legacy Mock typing loses callable signatures. + const runtime = new OrcaRuntimeService(runtimeStore as RuntimeStore) + const notifier = createMobileCreateTestNotifier(vi.fn()) + runtime.setNotifier(notifier) + runtime.setPtyController({ + spawn: vi.fn(async () => ({ id: 'surviving-pty' })), + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + const created = await runtime.createTerminal(`id:${TEST_WORKTREE_ID}`, { + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID + }) + setSession({ ...getSession(), tabsByWorktree: { [TEST_WORKTREE_ID]: [] } }) + runtimeStore.setWorkspaceSession.mockClear() + + await runtime.renameTerminal(created.handle, 'Late rename') + + expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) + expect(runtimeStore.setWorkspaceSession).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts b/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts index a0b19b2d27a..91fcac67bd1 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-delivery-contract.ts @@ -22,6 +22,8 @@ export type PointerDeliveryDependencies OrchestrationMailboxLeaf | undefined getLeafKey: (tabId: string, leafId: string) => string getLiveLeafForHandle: (handle: string) => OrchestrationMailboxLeaf + /** Whether the pane is settled enough to type the pointer plus Enter into it. */ + isAgentSettledForDelivery: (leaf: OrchestrationMailboxLeaf) => boolean getMessageWaiters: (mailboxHandle: string) => ReadonlySet | undefined getTabTitle: (tabId: string) => string | null | undefined getCliCommand: (terminalHandle: string) => OrchestrationCliCommand diff --git a/src/main/runtime/orchestration/mailbox-pointer-delivery.ts b/src/main/runtime/orchestration/mailbox-pointer-delivery.ts index b16e5c474b3..b8c8ad7a0c4 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-delivery.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-delivery.ts @@ -70,6 +70,16 @@ export class OrchestrationMailboxPointerDelivery WriteSettlement) { getLeaf: () => LEAF, getLeafKey: () => 'tab-1:leaf-1', getLiveLeafForHandle: () => LEAF, + // These cases exercise staging and Enter phases, not the idle gate; the pane is settled. + isAgentSettledForDelivery: () => true, getMessageWaiters: () => undefined, getTabTitle: () => null, getCliCommand: () => 'orca' as const, diff --git a/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts b/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts index fe08e5f808d..91bf1158e07 100644 --- a/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts +++ b/src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts @@ -42,8 +42,8 @@ describe('structured mailbox pointer host', () => { // The defect this pins: a running turn is announced by ONE lifecycle item, and settlement // tombstones it rather than rewriting it. A long tool-calling turn pushes that item arbitrarily // far from the tail, so any page-sized read reports a busy worker as idle — and the pointer is - // then delivered mid-turn, which Codex answers with `turn already running` and Claude settles - // `unknown` while the message is really queued. + // then delivered mid-turn, which Codex coalesces into the running turn and Claude queues behind + // it -- either way folded into work already in flight rather than read as a new instruction. const items = [runningTurn(), ...transcript(500)] hostRef.current = { journalSnapshot: () => ({ items }) } expect(createStructuredMailboxPointerHost().readGateFacts('s1')).toEqual({ diff --git a/src/main/runtime/orchestration/structured-session-pointer-delivery.ts b/src/main/runtime/orchestration/structured-session-pointer-delivery.ts index 272d7799947..dd8ccf64f71 100644 --- a/src/main/runtime/orchestration/structured-session-pointer-delivery.ts +++ b/src/main/runtime/orchestration/structured-session-pointer-delivery.ts @@ -81,11 +81,14 @@ export function structuredSessionGateFacts( * Decide whether the nudge may be sent right now. * * Mid-turn delivery is refused for both providers rather than delegated to - * them: Codex answers a mid-turn `turn/start` with `turn already running`, and - * Claude accepts the frame but cannot acknowledge it inside the dispatch ack - * window, settling `unknown` while the message is really queued. Waiting for - * the turn to settle is the one contract that holds for both, and it preserves - * orchestration's existing idle-edge-only delivery policy. + * them. Neither refuses the frame: Codex COALESCES a mid-turn `turn/start` into + * the running turn -- measured on codex-cli 0.147.0, 0.150.1 and 0.153.4, none + * of which refuse it and none of which fire a second `turn/started` -- and + * Claude queues it behind the turn. Both therefore + * fold the nudge into work already in flight, where it reads as part of the + * running turn rather than a new instruction. Waiting for the turn to settle is + * the one contract that holds for both, and it preserves orchestration's + * existing idle-edge-only delivery policy. */ export function decideStructuredPointerDelivery(input: { refusal: AgentSessionPtyWriteRefusal diff --git a/src/main/runtime/orchestration/structured-worker-group-addressing.ts b/src/main/runtime/orchestration/structured-worker-group-addressing.ts index 8abad118de7..168bd870118 100644 --- a/src/main/runtime/orchestration/structured-worker-group-addressing.ts +++ b/src/main/runtime/orchestration/structured-worker-group-addressing.ts @@ -49,8 +49,8 @@ export function listAddressableStructuredWorkers(): OrchestrationAddressableAgen * A structured worker's agent status, in the vocabulary `@idle` already matches on. * * Null when the session cannot be read: unknown must not read as idle, or a broadcast to `@idle` - * would wake a worker mid-turn — which Codex answers with `turn already running` and Claude queues - * behind the running turn. + * would wake a worker mid-turn — which Codex coalesces into the running turn and Claude queues + * behind it. */ export function structuredWorkerAgentStatus(sessionId: string): string | null { const facts = readStructuredSessionGateFacts(sessionId) diff --git a/src/main/runtime/repo-worktree-row-resolution.test.ts b/src/main/runtime/repo-worktree-row-resolution.test.ts index 32111ef2a53..3576b89fe88 100644 --- a/src/main/runtime/repo-worktree-row-resolution.test.ts +++ b/src/main/runtime/repo-worktree-row-resolution.test.ts @@ -4,6 +4,8 @@ import type { Repo } from '../../shared/repo-types' import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import type { Store } from '../persistence' +import { mergeWorktreeMetaForWrite } from '../persistence/loading-store/worktree-meta-write-normalization' +import { buildDetectedGitWorktrees } from '../ipc/worktrees/listing/ssh-worktree-fallback' import { listStoredWorktreeRowsForRepo, resolveRepoWorktreeRows, @@ -350,3 +352,42 @@ describe('scoped worktree id resolution across path spellings (#16243)', () => { expect(deps.scanRepo).not.toHaveBeenCalled() }) }) + +describe('folder-to-Git checkout identity', () => { + it.each([ + ['C:\\projects\\draft', 'C:/projects/draft'], + ['C:\\projects\\draft', 'c:/projects/draft'] + ])( + 'preserves the live folder locator %s in desktop and runtime listings', + async (folderPath, gitPath) => { + const owner = { + ...repo('folder', folderPath), + kind: 'git' as const, + folderUpgradeGitRootPath: gitPath + } + const deps = createDeps([owner]) + const oldId = `folder::${folderPath}` + const metadata = mergeWorktreeMetaForWrite(undefined, { + hostId: 'local', + instanceId: 'existing-omp', + comment: 'keep me' + }) + deps.metaById[oldId] = metadata + Object.assign(deps.store, { getProjectHostSetups: () => [] }) + deps.scanRepo.mockResolvedValue({ ok: true, worktrees: [gitWorktree(gitPath)] }) + + const detected = buildDetectedGitWorktrees(deps.store, owner, [gitWorktree(gitPath)]) + const rows = await resolveRepoWorktreeRows(deps, owner, deps.metaById, new Map()) + for (const result of [detected, rows]) { + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + id: oldId, + path: folderPath, + instanceId: 'existing-omp', + comment: 'keep me' + }) + } + expect(Object.keys(deps.metaById)).toEqual([oldId]) + } + ) +}) diff --git a/src/main/runtime/repo-worktree-row-resolution.ts b/src/main/runtime/repo-worktree-row-resolution.ts index e5e88607629..6fca5a5f3b2 100644 --- a/src/main/runtime/repo-worktree-row-resolution.ts +++ b/src/main/runtime/repo-worktree-row-resolution.ts @@ -1,3 +1,4 @@ +import { preserveFolderUpgradeWorktreePath } from '../folder-upgrade-worktree-path' import { splitWorktreeId, splitWorktreeIdForFilesystem, @@ -132,7 +133,7 @@ export async function resolveRepoWorktreeRows( RESOLVED_WORKTREE_REPO_TIMEOUT_MS, null )) ?? { ok: false, worktrees: listStoredWorktreeRowsForRepo(store, repo, repoOwnerCount) } - const gitWorktrees = scan.worktrees + const gitWorktrees = preserveFolderUpgradeWorktreePath(repo, scan.worktrees) if (scan.ok) { pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees) } diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index c2d46b09818..73e3113aea8 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -606,6 +606,19 @@ describe('method routing', () => { expect(hostCalls.cancel).toHaveBeenCalledWith(expect.anything(), params) }) + it('routes strict prompt identity through cancellation', async () => { + const params = { + envelope: envelope(), + turnId: 'turn-1', + prompt: { itemId: 'prompt-1', expectedRevision: 2 } + } + + const response = await call('agentSession.cancel', params, STRUCTURED_CLIENT) + + expect(response).toMatchObject({ ok: true }) + expect(hostCalls.cancel).toHaveBeenCalledWith(expect.anything(), params) + }) + it('routes the structured handoff mutation through the host', async () => { const response = await call('agentSession.requestHandoff', { envelope: envelope(), @@ -648,6 +661,17 @@ describe('parameter validation', () => { turnId: 'turn-1', taskId: 'task-2' }) + await rejects('agentSession.cancel', { + envelope: envelope(), + turnId: 'background-tasks', + scope: 'background-tasks', + prompt: { itemId: 'prompt-1', expectedRevision: 1 } + }) + await rejects('agentSession.cancel', { + envelope: envelope(), + turnId: 'turn-1', + prompt: { itemId: 'prompt-1', expectedRevision: 0 } + }) expect(hostCalls.cancel).not.toHaveBeenCalled() }) diff --git a/src/main/runtime/runtime-terminal-idle-polls.ts b/src/main/runtime/runtime-terminal-idle-polls.ts index 4fb5c470c2f..e1be9654611 100644 --- a/src/main/runtime/runtime-terminal-idle-polls.ts +++ b/src/main/runtime/runtime-terminal-idle-polls.ts @@ -17,6 +17,19 @@ import { type FirstPartyAgentStatus } from './tui-idle-evidence' import type { TuiAgent } from '../../shared/tui-agent' + +/** + * Why null counts as quiet: a record with no output timestamp has produced nothing the + * RUNTIME OBSERVED since it was created. That is not the same as silence — the reachable + * case is a daemon-hosted pane whose bytes never reach the runtime, which may still be + * streaming. The trade is deliberate: "never settles" becomes "settles uncorroborated", + * the caller keeps its timeout, and delivery cannot reach this lane. Reading it as `0ms since output` + * inverted that — `0 >= quiescenceMs` is false forever, so an adopted pane that never + * emitted could not settle no matter how long the caller waited. + */ +function isQuietForQuiescence(lastOutputAt: number | null, quiescenceMs: number): boolean { + return lastOutputAt === null ? true : Date.now() - lastOutputAt >= quiescenceMs +} import type { TerminalWaiter } from './runtime-terminal-contracts' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' @@ -141,7 +154,7 @@ export class RuntimeTerminalIdlePolls { if ( foreground && !isShellProcess(foreground) && - (live.lastOutputAt ? Date.now() - live.lastOutputAt : 0) >= this.deps.quiescenceMs + isQuietForQuiescence(live.lastOutputAt, this.deps.quiescenceMs) ) { this.stop(entry) this.deps.resolve(waiter, buildTerminalWaitResult(waiter.handle, 'tui-idle', live)) @@ -206,7 +219,7 @@ export class RuntimeTerminalIdlePolls { if ( foreground && !isShellProcess(foreground) && - (pty.lastOutputAt ? Date.now() - pty.lastOutputAt : 0) >= this.deps.quiescenceMs + isQuietForQuiescence(pty.lastOutputAt, this.deps.quiescenceMs) ) { this.stop(entry) this.deps.resolve(waiter, buildPtyTerminalWaitResult(waiter.handle, 'tui-idle', pty)) diff --git a/src/main/runtime/structured-agent-session-integration-replay.test.ts b/src/main/runtime/structured-agent-session-integration-replay.test.ts index 990aa293ee4..4fa390be0fb 100644 --- a/src/main/runtime/structured-agent-session-integration-replay.test.ts +++ b/src/main/runtime/structured-agent-session-integration-replay.test.ts @@ -17,7 +17,10 @@ import type { } from '../codex/codex-app-server-connection' import type { CodexStructuredSessionAdapter } from '../codex/codex-structured-session-adapter' import { computeAgentSessionPayloadFingerprint } from '../../shared/agent-session-mutation-envelope' -import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../shared/protocol-version' +import { + AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY +} from '../../shared/protocol-version' import type { AgentJournalRenderItem } from '../../shared/agent-session-journal-types' import { attachFingerprintFields } from '../native-chat/agent-session-wire/structured-agent-session-attach' import { journalDirectoryFor } from '../native-chat/agent-session-journal/journal-paths' @@ -37,10 +40,16 @@ const SESSION = 'session-integration-1' const THREAD = 'thread-integration' const TURN = 'turn-1' const WORKSPACE = 'workspace-1' +// The capability set the desktop renderer advertises. Without the pending-send +// one the host holds the reply until the send settles, which is a shim for +// clients too old to render a pending bubble — not what this suite models. const CLIENT = { clientId: 'device-a', clientKind: 'runtime' as const, - clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY] + clientCapabilities: [ + AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY + ] } // ─── the fake `codex app-server` ──────────────────────────────────────────── diff --git a/src/main/runtime/structured-agent-session-integration.test.ts b/src/main/runtime/structured-agent-session-integration.test.ts index f5a59033d73..219b132566e 100644 --- a/src/main/runtime/structured-agent-session-integration.test.ts +++ b/src/main/runtime/structured-agent-session-integration.test.ts @@ -16,8 +16,14 @@ import type { openCodexAppServerConnection } from '../codex/codex-app-server-connection' import { computeAgentSessionPayloadFingerprint } from '../../shared/agent-session-mutation-envelope' -import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../shared/protocol-version' -import type { AgentJournalRenderItem } from '../../shared/agent-session-journal-types' +import { + AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY +} from '../../shared/protocol-version' +import type { + AgentJournalRenderItem, + AgentJournalSubmission +} from '../../shared/agent-session-journal-types' import type { AgentSessionHistoryResult, AgentSessionSubscribeEvent @@ -43,10 +49,16 @@ const SESSION = 'session-integration-1' const THREAD = 'thread-integration' const TURN = 'turn-1' const WORKSPACE = 'workspace-1' +// The capability set the desktop renderer advertises. Without the pending-send +// one the host holds the reply until the send settles, which is a shim for +// clients too old to render a pending bubble — not what this suite models. const CLIENT = { clientId: 'device-a', clientKind: 'runtime' as const, - clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY] + clientCapabilities: [ + AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY, + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY + ] } // ─── the fake `codex app-server` ──────────────────────────────────────────── @@ -269,6 +281,13 @@ function textOf(item: AgentJournalRenderItem): string { : '' } +/** The durable submission row, which settlement rewrites after the send returns. */ +function submissionOf(clientMessageId: string): AgentJournalSubmission | undefined { + return getStructuredAgentSessionHost() + ?.journalSnapshot(SESSION) + .submissions.find((entry) => entry.clientMessageId === clientMessageId) +} + async function historyPage( direction: 'tail' | 'before' | 'after', extra: Record = {} @@ -438,18 +457,25 @@ describe('a structured codex session over agentSession.*', () => { envelope: envelope('agentSession.send', { body }, created.fence), body }) - expect(sent.submission).toMatchObject({ - dispatchState: 'accepted', - providerItemId: `codex:${THREAD}:${TURN}:0` - }) + // Admission, not identity. `turn/start` proves Codex owns the message, but a + // send coalesced into a running turn is answered with that turn's id, so + // which message landed where is knowable only from the echo. + expect(sent.submission).toMatchObject({ dispatchState: 'pending', providerItemId: null }) expect(codex.live().calls.at(-1)).toMatchObject({ method: 'turn/start', params: { threadId: THREAD, clientUserMessageId: sent.clientMessageId } }) codex.notify('turn/started', { turn: { id: TURN } }) + // Codex echoes the message back carrying the `clientId` it was sent under, + // which is the only thing that names which submission this row settles. codex.notify('item/completed', { - item: { type: 'userMessage', id: 'item-0', content: [{ type: 'text', text: 'hi' }] } + item: { + type: 'userMessage', + id: 'item-0', + clientId: sent.clientMessageId, + content: [{ type: 'text', text: 'hi' }] + } }) codex.notify('item/started', { item: { type: 'agentMessage', id: 'item-1', text: '' } }) codex.notify('item/agentMessage/delta', { itemId: 'item-1', delta: 'Hello.' }) @@ -459,6 +485,15 @@ describe('a structured codex session over agentSession.*', () => { await drainStreamedEvents() expect(itemsOf(stream).map(textOf).filter(Boolean)).toEqual(['hi', 'Hello.']) + // The echo is the first item of this turn, so the settled key is ordinal 0 — + // minted by the same `identityFor` a history replay computes with, rather + // than guessed from the turn/start response. + await vi.waitFor(() => + expect(submissionOf(sent.clientMessageId)).toMatchObject({ + dispatchState: 'accepted', + providerItemId: `codex:${THREAD}:${TURN}:0` + }) + ) }) it('runs create → send → stream → approval → cancel → reconnect → page history', async () => { @@ -513,12 +548,10 @@ describe('a structured codex session over agentSession.*', () => { envelope: envelope('agentSession.send', { body }, fence), body }) - // Codex named the turn, so the submission is accepted rather than - // "delivery unconfirmed", and adopts the provider's own item identity. - expect(sent.submission).toMatchObject({ - dispatchState: 'accepted', - providerItemId: `codex:${THREAD}:${TURN}:0` - }) + // Codex took the message, so the submission is pending rather than + // "delivery unconfirmed" — it carries no identity yet, because the response + // to a coalesced send names the running turn rather than this message. + expect(sent.submission).toMatchObject({ dispatchState: 'pending', providerItemId: null }) expect(codex.live().calls.at(-1)).toMatchObject({ method: 'turn/start', params: { @@ -531,14 +564,28 @@ describe('a structured codex session over agentSession.*', () => { // ── stream ────────────────────────────────────────────────────────────── codex.notify('turn/started', { turn: { id: TURN } }) - // Codex echoes the user message back as ordinal 0 of the turn. That is the - // key the submission adopted, so the echo has to reconcile into the bubble - // the client already has rather than append a second copy of it. + // Codex echoes the user message back as ordinal 0 of the turn, carrying the + // `clientId` it was sent under. That echo settles the submission's identity, + // and has to reconcile into the bubble the client already has rather than + // append a second copy of it. codex.notify('item/completed', { - item: { type: 'userMessage', id: 'item-0', content: [{ type: 'text', text: 'list files' }] } + item: { + type: 'userMessage', + id: 'item-0', + clientId: sent.clientMessageId, + content: [{ type: 'text', text: 'list files' }] + } }) await drainStreamedEvents() expect(itemsOf(stream).filter((item) => textOf(item) === 'list files')).toHaveLength(1) + // Settled from the echo's own journal identity, so it is by construction the + // key a replay recomputes for this row. + await vi.waitFor(() => + expect(submissionOf(sent.clientMessageId)).toMatchObject({ + dispatchState: 'accepted', + providerItemId: `codex:${THREAD}:${TURN}:0` + }) + ) codex.notify('item/started', { item: { type: 'agentMessage', id: 'item-1', text: '' } }) codex.notify('item/agentMessage/delta', { itemId: 'item-1', delta: 'Two ' }) diff --git a/src/main/runtime/structured-agent-session-runtime-exit.test.ts b/src/main/runtime/structured-agent-session-runtime-exit.test.ts index 506a45ae821..7a3736b9009 100644 --- a/src/main/runtime/structured-agent-session-runtime-exit.test.ts +++ b/src/main/runtime/structured-agent-session-runtime-exit.test.ts @@ -121,9 +121,13 @@ describe('structured session runtime provider-exit wiring', () => { }) } + // `pending` is this send's real answer now, not a weaker one: admission settles + // when the transport takes the frame, and identity arrives later on the + // provider's echo. What proves the message reached the REACQUIRED provider is + // the turn it starts below, which is what this test exists to check. await expect( host.send({ callerKey: 'runtime-test' }, { envelope, body }) - ).resolves.toMatchObject({ ok: true, value: { submission: { dispatchState: 'accepted' } } }) + ).resolves.toMatchObject({ ok: true, value: { submission: { dispatchState: 'pending' } } }) expect(turn).toBe(1) }) diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index fbc13b58fca..92643fcae57 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -246,6 +246,16 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise[0] + ): void => { + void host?.settleLateDispatch(settlement).catch((error) => + deps.onError?.({ + scope: `structured-agent-session-late-settlement:${settlement.sessionId}`, + error + }) + ) + } const codex = new CodexStructuredSessionAdapter({ resolveLaunch: createCodexStructuredLaunchResolver({ store, @@ -257,6 +267,7 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise host?.publishBackgroundTaskState(sessionId, state), + onDispatchSettledLate, onEvent: (event) => { if (event.type !== 'ended' || !('cause' in event) || event.cause !== 'unexpected-exit') { return @@ -298,14 +309,7 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise host?.publishBackgroundTaskState(sessionId, state), - onDispatchSettledLate: (settlement) => { - void host?.settleLateDispatch(settlement).catch((error) => - deps.onError?.({ - scope: `structured-agent-session-late-settlement:${settlement.sessionId}`, - error - }) - ) - }, + onDispatchSettledLate, ...(deps.openClaudeConnection ? { openClaudeConnection: deps.openClaudeConnection } : {}), ...(deps.readProcessStartTime ? { readProcessStartTime: deps.readProcessStartTime } : {}) }) diff --git a/src/main/runtime/tui-idle-delivery-and-quiescence.test.ts b/src/main/runtime/tui-idle-delivery-and-quiescence.test.ts new file mode 100644 index 00000000000..02d3a62dcc9 --- /dev/null +++ b/src/main/runtime/tui-idle-delivery-and-quiescence.test.ts @@ -0,0 +1,190 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { makeTuiIdleRuntime } from './tui-idle-wait-test-harness' +import type { RuntimeSyncWindowGraph } from '../../shared/runtime-types' +import type { OrcaRuntimeService } from './orca-runtime' +import type { TuiAgent } from '../../shared/tui-agent' + +// Follow-ons to #6011. The evidence ranking that fixed the wait path did not reach two +// other consumers of the same signal: mailbox delivery, which TYPES INTO the pane, and +// the idle poll's quiescence gate, which read a missing output clock as "never quiet". + +const WORKTREE_ID = 'repo-1::/tmp/followups' +const TAB_ID = 'c1c1c1c1-c1c1-4c1c-8c1c-c1c1c1c1c1c1' +const LEAF_ID = 'c2c2c2c2-c2c2-4c2c-8c2c-c2c2c2c2c2c2' +const PTY_ID = 'pty-followups' +const ESC = String.fromCharCode(27) +const BEL = String.fromCharCode(7) +const osc = (title: string) => `${ESC}]0;${title}${BEL}` +const agentStatus = (state: string, agentType: string) => + `${ESC}]9999;{"state":"${state}","agentType":"${agentType}"}${BEL}` + +const GRAPH: RuntimeSyncWindowGraph = { + tabs: [ + { tabId: TAB_ID, worktreeId: WORKTREE_ID, title: 'Agent', activeLeafId: LEAF_ID, layout: null } + ], + leaves: [ + { + tabId: TAB_ID, + worktreeId: WORKTREE_ID, + leafId: LEAF_ID, + paneRuntimeId: 1, + ptyId: PTY_ID, + paneTitle: null, + title: '' + } + ] +} + +async function makeRuntime(launchAgent: TuiAgent | null, foreground = 'codex') { + const runtime = makeTuiIdleRuntime({ + repoPath: '/tmp/followups', + getForegroundProcess: async () => foreground + }) + runtime.attachWindow(1) + runtime.syncWindowGraph(1, GRAPH) + runtime.registerPty(PTY_ID, WORKTREE_ID, null, { + tabId: TAB_ID, + leafId: LEAF_ID, + incarnationId: 'followups-inc', + ...(launchAgent ? { agentLaunchAuthority: { launchToken: 'tok', launchAgent } } : {}) + }) + const { terminals } = await runtime.listTerminals(`id:${WORKTREE_ID}`) + return { runtime, handle: terminals[0].handle } +} + +/** Counts real delivery attempts. Spies on the delivery entry point, NOT on the gate + * under test — the gate runs for real and decides whether this is ever reached. */ +function watchDelivery(runtime: OrcaRuntimeService) { + return vi + .spyOn( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the delivery entry point is protected; the spy only needs its name and signature. + runtime as never as { deliverPendingMessagesForLeaf: (leaf: unknown) => void }, + 'deliverPendingMessagesForLeaf' + ) + .mockImplementation(() => {}) +} + +// Why fake timers: the retry fires on a real 3s quiescence window, and asserting around it +// with wall-clock sleeps made the result depend on how promptly a loaded CI runner schedules +// an interval. The clock is the thing under test, so it has to be the deterministic part. +describe('mailbox delivery honours the tui-idle evidence ranking', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.useRealTimers() + }) + + it('does not deliver into a pane that is only showing its agent name mid-turn', async () => { + const { runtime } = await makeRuntime('codex') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('⠋ Codex')}working\n`, Date.now()) + expect(deliver).not.toHaveBeenCalled() + + // The busy agent repaints its title to the bare product name. That reads as `idle` + // for display, but it is emitted just as often mid-turn — typing into the pane here + // injects the pointer plus Enter into a running turn. + runtime.onPtyData(PTY_ID, `${osc('Codex')}still working\n`, Date.now()) + expect(deliver).not.toHaveBeenCalled() + }) + + it('delivers once the agent states it is done', async () => { + const { runtime } = await makeRuntime('codex') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('⠋ Codex')}working\n`, Date.now()) + runtime.onPtyData(PTY_ID, `${osc('Codex ready')}done\n`, Date.now()) + expect(deliver).toHaveBeenCalled() + }) + + // Why this case exists: the wait path POLLS, so weak evidence that only becomes valid + // with time eventually satisfies it. Delivery is edge-driven with no poll behind it, so a + // refusal at an edge is final unless another edge arrives. A hookless Codex never emits an + // explicit `X ready`, so without a retry the queued message strands permanently once the + // pane falls quiet — trading a visible mis-delivery for an invisible lost message. + it('retries a refused delivery once the pane falls quiet', async () => { + const { runtime } = await makeRuntime('codex') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('\u280b Codex')}working\n`, Date.now()) + runtime.onPtyData(PTY_ID, `${osc('Codex')}output\n`, Date.now()) + expect(deliver).not.toHaveBeenCalled() + + // Output stops. No further title frame and no renderer graph sync — a daemon-hosted + // pane has nobody publishing one, so nothing re-fires an edge on its own. + await vi.advanceTimersByTimeAsync(5_000) + expect(deliver).toHaveBeenCalled() + }) + + it('does not retry into a pane that went busy again', async () => { + const { runtime } = await makeRuntime('codex') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('Codex')}output\n`, Date.now()) + // Keep the stream alive across the whole retry window. + // Deterministic streaming: one chunk every 250ms of virtual time, so the gap between + // chunks can never drift past the quiescence window the way a real interval can. + for (let tick = 0; tick < 20; tick += 1) { + runtime.onPtyData(PTY_ID, 'more output\n', Date.now()) + await vi.advanceTimersByTimeAsync(250) + } + expect(deliver).not.toHaveBeenCalled() + }) + + // Case B, the mainline path: a hooked Codex emits a name-only frame BEFORE the hook's + // `Codex ready`. The name-only frame consumes the working->idle transition, leaving the + // ready title as an idle->idle step that delivery was never offered — so the strongest + // evidence the agent ever emits could not reach it. + it('delivers when the ready title arrives after a name-only frame', async () => { + const { runtime } = await makeRuntime('codex') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('\u280b Codex')}working\n`, Date.now()) + runtime.onPtyData(PTY_ID, `${osc('Codex')}out\n`, Date.now()) + expect(deliver).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(100) + runtime.onPtyData(PTY_ID, osc('Codex ready'), Date.now()) + // Promptly, on the ready title itself — not after waiting out a quiescence window. + expect(deliver).toHaveBeenCalled() + }) + + // Case C: the agent's own status stream vetoes the idle title, then reports done with no + // edge behind it. `working` stays fresh for 30 minutes, so without a re-offer the veto + // outlives the turn it described. + it('delivers when a done status lands after the idle title was vetoed', async () => { + const { runtime } = await makeRuntime('claude') + const deliver = watchDelivery(runtime) + runtime.onPtyData( + PTY_ID, + `${agentStatus('working', 'claude')}${osc('\u280b Claude')}w\n`, + Date.now() + ) + runtime.onPtyData(PTY_ID, `${osc('claude')}out\n`, Date.now()) + expect(deliver).not.toHaveBeenCalled() + + runtime.onPtyData(PTY_ID, agentStatus('done', 'claude'), Date.now()) + await vi.advanceTimersByTimeAsync(4_500) + expect(deliver).toHaveBeenCalled() + }) + + it('still delivers for an agent whose name is its only rest signal', async () => { + const { runtime } = await makeRuntime('grok', 'grok') + const deliver = watchDelivery(runtime) + runtime.onPtyData(PTY_ID, `${osc('⠋ Grok')}working\n`, Date.now()) + runtime.onPtyData(PTY_ID, `${osc('grok')}banner\n`, Date.now()) + expect(deliver).toHaveBeenCalled() + }) +}) + +describe('quiescence treats a missing output clock as quiet', () => { + it('settles a pane that has never produced output but holds a live agent process', async () => { + // No launch metadata: Orca did not start this agent, so the quiet-foreground lane is + // the only evidence available, and `lastOutputAt` is null because nothing ever arrived. + const { runtime, handle } = await makeRuntime(null, 'codex') + const leaves = + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: reading the runtime's own leaf map to assert the precondition this test depends on. + (runtime as never as { leaves: Map }).leaves + expect([...leaves.values()][0].lastOutputAt).toBeNull() + + await expect( + runtime.waitForTerminal(handle, { condition: 'tui-idle', timeoutMs: 8_000 }) + ).resolves.toMatchObject({ condition: 'tui-idle', satisfied: true }) + }, 20_000) +}) diff --git a/src/main/startup/main-process-runtime-service.ts b/src/main/startup/main-process-runtime-service.ts index 3aac4a03b3c..a0a65d54e7a 100644 --- a/src/main/startup/main-process-runtime-service.ts +++ b/src/main/startup/main-process-runtime-service.ts @@ -1,3 +1,5 @@ +import { installChildSessionSearchService } from '../ai-vault-search/session-search-enablement' +import { getCanonicalUserDataPath } from '../persistence/loading-store/user-data-path' import { app } from 'electron' import { OrcaRuntimeService } from '../runtime/orca-runtime' import { getLocalPtyProvider, getSshPtyProvider, clearProviderPtyState } from '../ipc/pty' @@ -131,6 +133,12 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService { orchestrationEnvironmentTransport, skillTransactionRecovery: state.skillTransactionRecovery }) + // Both desktop and headless serve own a host-local search service. + const sessionSearch = installChildSessionSearchService({ + dataRoot: getCanonicalUserDataPath(), + getSettings: () => store.getSettings() + }) + app.once('will-quit', () => sessionSearch?.dispose()) state.runtime = runtime agentHookServer.subscribeEnrichedStatus((enriched) => recordObservedAgentStatusPaneIdentity(observedPaneIdentities, enriched.paneKey, runtime) diff --git a/src/main/worktree-prunable-git-file.test.ts b/src/main/worktree-prunable-git-file.test.ts new file mode 100644 index 00000000000..f05e5e5cb4c --- /dev/null +++ b/src/main/worktree-prunable-git-file.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import { isPrunableGitFileWorktree } from './worktree-prunable-git-file' + +const { statPath, pathAccess, runtimePath } = vi.hoisted(() => ({ + statPath: vi.fn(), + pathAccess: vi.fn(), + runtimePath: vi.fn() +})) +vi.mock('./local-worktree-filesystem', () => ({ + getLocalWorktreePathAccess: pathAccess, + toLocalWorktreeRuntimePath: runtimePath +})) +const worktree: GitWorktreeInfo = { + path: '/workspaces/feature/.git', + branch: 'refs/heads/feature', + head: 'a'.repeat(40), + isMainWorktree: false, + isBare: false, + prunable: true +} +beforeEach(() => { + vi.resetAllMocks() + statPath.mockResolvedValue({ isFile: () => true }) + pathAccess.mockReturnValue({ statPath }) + runtimePath.mockImplementation((path) => path) +}) +describe('prunable Git-file registration proof', () => { + it('accepts an attested named-branch file without reading or changing its parent', async () => { + await expect(isPrunableGitFileWorktree(worktree)).resolves.toBe(true) + expect(statPath).toHaveBeenCalledExactlyOnceWith(worktree.path) + }) + it.each([ + { prunable: false }, + { prunable: undefined }, + { isMainWorktree: true }, + { isBare: true }, + { locked: true }, + { branch: '' }, + { branch: 'refs/tags/feature' }, + { branch: 'refs/heads/' }, + { head: '' }, + { path: '/workspaces/feature' } + ])('refuses insufficient registration evidence %j', async (override) => { + await expect(isPrunableGitFileWorktree({ ...worktree, ...override })).resolves.toBe(false) + expect(statPath).not.toHaveBeenCalled() + }) + it.each([{ isFile: () => false }, { type: 'directory' }, { type: 'symlink' }, {}, null])( + 'refuses non-file or unknown filesystem evidence %j', + async (entry) => { + statPath.mockResolvedValue(entry) + await expect(isPrunableGitFileWorktree(worktree)).resolves.toBe(false) + } + ) + it('leaves a vanished marker to existing missing-path recovery', async () => { + statPath.mockRejectedValue(Object.assign(new Error('marker vanished'), { code: 'ENOENT' })) + await expect(isPrunableGitFileWorktree(worktree)).resolves.toBe(false) + }) + it('does not turn host failure into cleanup permission', async () => { + statPath.mockRejectedValue(new Error('host unavailable')) + await expect(isPrunableGitFileWorktree(worktree)).rejects.toThrow('host unavailable') + }) + it('uses the selected WSL distro and translated execution path', async () => { + const options = { wslDistro: 'Ubuntu' } + runtimePath.mockReturnValue('/home/dev/feature/.git') + statPath.mockResolvedValue({ type: 'file' }) + await expect( + isPrunableGitFileWorktree({ ...worktree, path: 'C:\\workspaces\\feature\\.git' }, options) + ).resolves.toBe(true) + expect(pathAccess).toHaveBeenCalledExactlyOnceWith(options) + expect(runtimePath).toHaveBeenCalledWith('C:\\workspaces\\feature\\.git', options) + expect(statPath).toHaveBeenCalledExactlyOnceWith('/home/dev/feature/.git') + }) +}) diff --git a/src/main/worktree-prunable-git-file.ts b/src/main/worktree-prunable-git-file.ts new file mode 100644 index 00000000000..a89f3408eb3 --- /dev/null +++ b/src/main/worktree-prunable-git-file.ts @@ -0,0 +1,41 @@ +import { isENOENT } from './ipc/filesystem-path-containment' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import type { LocalWorktreeFilesystemOptions } from './local-worktree-filesystem' +import { getLocalWorktreePathAccess, toLocalWorktreeRuntimePath } from './local-worktree-filesystem' + +/** Registration cleanup must never reinterpret a malformed .git row as its parent checkout. */ +export async function isPrunableGitFileWorktree( + worktree: GitWorktreeInfo, + options: LocalWorktreeFilesystemOptions = {} +): Promise { + if ( + worktree.prunable !== true || + worktree.isMainWorktree || + worktree.isBare || + worktree.locked || + !worktree.branch.startsWith('refs/heads/') || + worktree.branch === 'refs/heads/' || + !worktree.head || + worktree.path.split(/[\\/]/).at(-1) !== '.git' + ) { + return false + } + const access = getLocalWorktreePathAccess(options) + const entry = await access + .statPath(toLocalWorktreeRuntimePath(worktree.path, options)) + .catch((error: unknown) => { + // A vanished marker leaves missing-path recovery to its existing stricter gate. + if (isENOENT(error)) { + return null + } + throw error + }) + if (!entry || typeof entry !== 'object') { + return false + } + // WSL returns the owning guest's lstat-equivalent type; native lstat rejects symlinks too. + return ( + ('type' in entry && entry.type === 'file') || + ('isFile' in entry && typeof entry.isFile === 'function' && entry.isFile() === true) + ) +} diff --git a/src/main/worktree-trash.test.ts b/src/main/worktree-trash.test.ts index 5bec9f53475..2819c789bf5 100644 --- a/src/main/worktree-trash.test.ts +++ b/src/main/worktree-trash.test.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs' -import { mkdir, mkdtemp, readdir, rm, symlink, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -45,6 +45,26 @@ describe('moveWorktreeDirectoryToTrash', () => { expect(existsSync(join(trashPath!, 'node_modules', 'pkg', 'index.js'))).toBe(true) }) + it('leaves a file target untouched without creating a trash root', async () => { + const worktreePath = join(scratchDir, '.git') + await writeFile(worktreePath, 'gitdir: /preserved/admin\n') + + expect(await moveWorktreeDirectoryToTrash(worktreePath)).toBeUndefined() + expect(await readFile(worktreePath, 'utf8')).toBe('gitdir: /preserved/admin\n') + expect(existsSync(getWorktreeTrashRoot(worktreePath))).toBe(false) + }) + + it('leaves a directory symlink and its target untouched', async () => { + const target = join(scratchDir, 'target') + const worktreePath = join(scratchDir, 'link') + await createWorktreeDirectory(target) + await symlink(target, worktreePath, process.platform === 'win32' ? 'junction' : 'dir') + + expect(await moveWorktreeDirectoryToTrash(worktreePath)).toBeUndefined() + expect(existsSync(join(worktreePath, 'node_modules', 'pkg', 'index.js'))).toBe(true) + expect(existsSync(getWorktreeTrashRoot(worktreePath))).toBe(false) + }) + it('generates sweepable, collision-free entry names', async () => { const first = await moveWorktreeDirectoryToTrash(await seededWorktree('one')) const second = await moveWorktreeDirectoryToTrash(await seededWorktree('two')) diff --git a/src/main/worktree-trash.ts b/src/main/worktree-trash.ts index cec17bf87cd..cb4d3d52e03 100644 --- a/src/main/worktree-trash.ts +++ b/src/main/worktree-trash.ts @@ -40,6 +40,11 @@ export async function moveWorktreeDirectoryToTrash( const trashRoot = getWorktreeTrashRoot(worktreePath) const trashPath = join(trashRoot, `wt-${Date.now()}-${randomBytes(4).toString('hex')}`) try { + // A malformed Git registration can name the checkout's .git file. + const worktreeStat = await lstat(worktreePath) + if (!worktreeStat.isDirectory() || worktreeStat.isSymbolicLink()) { + return undefined + } await mkdir(trashRoot, { recursive: true }) const trashRootStat = await lstat(trashRoot) if (!trashRootStat.isDirectory() || trashRootStat.isSymbolicLink()) { diff --git a/src/preload/api/filesystem-api.ts b/src/preload/api/filesystem-api.ts index 478f3040f72..acb49f500d3 100644 --- a/src/preload/api/filesystem-api.ts +++ b/src/preload/api/filesystem-api.ts @@ -5,6 +5,11 @@ import type { FsChangedPayload, MarkdownDocument } from '../../shared/filesystem-entry-types' +import type { + ImportItemResult, + ResolveDroppedPathsResult, + StagedExternalImportSource +} from '../../shared/filesystem-import-result-types' import type { LocalLogTailChangedPayload, LocalLogTailReadArgs, @@ -12,6 +17,7 @@ import type { LocalLogTailWatchArgs } from '../../shared/local-log-tail-types' import type { SshMutationExpectation } from '../../shared/ssh-types' +import type { RuntimeUploadFileStreamRequest } from '../../shared/runtime-upload-staging-contract' export type ExportApi = { htmlToPdf: (args: { @@ -134,65 +140,20 @@ export type FilesystemApi = { connectionId?: string ensureDir?: boolean } & SshMutationExpectation - ) => Promise<{ - results: ( - | { - sourcePath: string - status: 'imported' - destPath: string - kind: 'file' | 'directory' - renamed: boolean - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> - stageExternalPathsForRuntimeUpload: (args: { sourcePaths: string[] }) => Promise<{ - sources: ( - | { - sourcePath: string - status: 'staged' - name: string - kind: 'file' | 'directory' - entries: ( - | { relativePath: string; kind: 'directory' } - | { relativePath: string; kind: 'file'; contentBase64: string } - )[] - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> + ) => Promise<{ results: ImportItemResult[] }> + stageExternalPathsForRuntimeUpload: (args: { + sourcePaths: string[] + }) => Promise<{ sources: StagedExternalImportSource[] }> + uploadExternalFileToRuntime: ( + args: RuntimeUploadFileStreamRequest + ) => Promise<{ byteLength: number }> resolveDroppedPathsForAgent: ( args: { paths: string[] worktreePath: string connectionId?: string } & SshMutationExpectation - ) => Promise<{ - resolvedPaths: string[] - skipped: { - sourcePath: string - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - }[] - failed: { sourcePath: string; reason: string }[] - }> + ) => Promise watchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise unwatchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise onFsChanged: (callback: (payload: FsChangedPayload) => void) => () => void diff --git a/src/preload/api/fs-bridge.ts b/src/preload/api/fs-bridge.ts index 207b34d8519..2f702d9464f 100644 --- a/src/preload/api/fs-bridge.ts +++ b/src/preload/api/fs-bridge.ts @@ -1,8 +1,14 @@ import type { PathExistenceResult } from '../../shared/path-existence-batch' import { ipcRenderer } from 'electron' import type { SshMutationExpectation } from '../../shared/ssh-types' +import type { RuntimeUploadFileStreamRequest } from '../../shared/runtime-upload-staging-contract' import type { SearchResult } from '../../shared/code-search-types' import type { FsChangedPayload } from '../../shared/filesystem-entry-types' +import type { + ImportItemResult, + ResolveDroppedPathsResult, + StagedExternalImportSource +} from '../../shared/filesystem-import-result-types' import type { LocalLogTailChangedPayload, LocalLogTailReadArgs, @@ -151,67 +157,22 @@ export const fsApi = { connectionId?: string ensureDir?: boolean } & SshMutationExpectation - ): Promise<{ - results: ( - | { - sourcePath: string - status: 'imported' - destPath: string - kind: 'file' | 'directory' - renamed: boolean - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> => ipcRenderer.invoke('fs:importExternalPaths', args), + ): Promise<{ results: ImportItemResult[] }> => ipcRenderer.invoke('fs:importExternalPaths', args), stageExternalPathsForRuntimeUpload: (args: { sourcePaths: string[] - }): Promise<{ - sources: ( - | { - sourcePath: string - status: 'staged' - name: string - kind: 'file' | 'directory' - entries: ( - | { relativePath: string; kind: 'directory' } - | { relativePath: string; kind: 'file'; contentBase64: string } - )[] - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> => ipcRenderer.invoke('fs:stageExternalPathsForRuntimeUpload', args), + }): Promise<{ sources: StagedExternalImportSource[] }> => + ipcRenderer.invoke('fs:stageExternalPathsForRuntimeUpload', args), + uploadExternalFileToRuntime: ( + args: RuntimeUploadFileStreamRequest + ): Promise<{ byteLength: number }> => ipcRenderer.invoke('fs:uploadExternalFileToRuntime', args), resolveDroppedPathsForAgent: ( args: { paths: string[] worktreePath: string connectionId?: string } & SshMutationExpectation - ): Promise<{ - resolvedPaths: string[] - skipped: { - sourcePath: string - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - }[] - failed: { sourcePath: string; reason: string }[] - }> => ipcRenderer.invoke('fs:resolveDroppedPathsForAgent', args), + ): Promise => + ipcRenderer.invoke('fs:resolveDroppedPathsForAgent', args), watchWorktree: (args: { worktreePath: string; connectionId?: string }): Promise => ipcRenderer.invoke('fs:watchWorktree', args), unwatchWorktree: (args: { worktreePath: string; connectionId?: string }): Promise => diff --git a/src/relay/relay-runtime-services.ts b/src/relay/relay-runtime-services.ts index 73e03242af6..4295014782f 100644 --- a/src/relay/relay-runtime-services.ts +++ b/src/relay/relay-runtime-services.ts @@ -1,6 +1,10 @@ import { homedir } from 'node:os' +import { join } from 'node:path' import { getRemoteHostPlatform } from '../main/ssh/ssh-remote-platform' -import { parseUnameToRelayPlatform } from '../main/ssh/relay-protocol' +import { parseUnameToRelayPlatform, RELAY_REMOTE_DIR } from '../main/ssh/relay-protocol' +import { DEFAULT_AI_VAULT_SEARCH_SETTINGS } from '../shared/ai-vault-search-settings' +import { LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host' +import { installInProcessSessionSearchService } from '../main/ai-vault-search/session-search-in-process-service' import type { RelayDispatcher } from './dispatcher' import { RelayContext, expandTilde } from './context' import { PtyHandler } from './pty-handler' @@ -29,6 +33,7 @@ export class RelayRuntimeServices { readonly gitHandler: GitHandler readonly skillInstallHandler: SkillInstallHandler private readonly aiVaultService: ReturnType | null + private readonly sessionSearch: { dispose(): void } | null private readonly registeredHandlers: readonly unknown[] constructor( @@ -77,6 +82,22 @@ export class RelayRuntimeServices { const relayPlatform = parseUnameToRelayPlatform(process.platform, process.arch) const hostPlatform = relayPlatform ? getRemoteHostPlatform(relayPlatform) : undefined this.aiVaultService = hostPlatform ? createRelayAiVaultService(homedir(), hostPlatform) : null + // Why beside the AI Vault sidecar and not inside it: that sidecar runs the + // remote scanner, which reads through a filesystem provider and publishes + // nothing to the transcript channel the index consumes. This process is the + // one that would drive the index's own reads, and the only writer on the file. + // Off until something can carry consent to a remote host (see the PR body); + // registering it anyway is what makes this host answer `disabled` and not + // `no-service`, which is the difference between off and too old. + this.sessionSearch = installInProcessSessionSearchService({ + dataRoot: join(homedir(), RELAY_REMOTE_DIR), + roots: { executionHostId: LOCAL_EXECUTION_HOST_ID }, + settings: DEFAULT_AI_VAULT_SEARCH_SETTINGS, + onError: (error) => + relayLogLine( + `[relay] session search: ${error instanceof Error ? error.message : String(error)}` + ) + }) this.registeredHandlers = [ preflightHandler, this.skillInstallHandler, @@ -112,6 +133,7 @@ export class RelayRuntimeServices { } disposeHandlers(): void { + this.sessionSearch?.dispose() this.fsHandler.dispose() this.gitHandler.dispose() void this.registeredHandlers diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 8793b1f17ec..693e7572ddf 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -60,6 +60,7 @@ --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); + --color-editor-surface: var(--editor-surface); --color-agent-question: var(--agent-question); --color-agent-question-text: var(--agent-question-text); --color-chart-1: var(--chart-1); @@ -509,6 +510,18 @@ } } +/* Why @utility, not a plain class: this is a Tailwind-shaped name, so it has to be + one Tailwind generates or `scrollbar-none` silently produces no CSS. */ +@utility scrollbar-none { + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0; + height: 0; + } +} + /* ── Sleek scrollbar (VS Code-like) ─────────────────── */ .scrollbar-sleek { diff --git a/src/renderer/src/assets/terminal-container-geometry.test.ts b/src/renderer/src/assets/terminal-container-geometry.test.ts index db079c9095f..cf17cb00540 100644 --- a/src/renderer/src/assets/terminal-container-geometry.test.ts +++ b/src/renderer/src/assets/terminal-container-geometry.test.ts @@ -15,4 +15,8 @@ describe('terminal container geometry', () => { /\.pane-link-tooltip\s*{[^}]*height:\s*var\(--orca-terminal-link-tooltip-height\);/s ) }) + + it('bounds cursor-blink repaints to the terminal surface (#10481)', () => { + expect(terminalCss).toMatch(/\.xterm-container\s*{[^}]*contain:\s*paint;/s) + }) }) diff --git a/src/renderer/src/assets/terminal.css b/src/renderer/src/assets/terminal.css index 1ee09586892..0d17b1f77df 100644 --- a/src/renderer/src/assets/terminal.css +++ b/src/renderer/src/assets/terminal.css @@ -512,6 +512,10 @@ height: calc(100% - var(--pane-padding-y, 4px)); margin-top: var(--pane-padding-y, 4px); margin-left: var(--pane-padding-x, 4px); + /* Why (#10481): a blinking cursor otherwise invalidates paint all the way up + the pane ancestry. The link tooltip and drag handle are .pane siblings, so + clipping to this box costs no visible chrome. */ + contain: paint; } /* When a pane has a title, shift the terminal content down to make room. diff --git a/src/renderer/src/assets/theme-utility-generation.test.ts b/src/renderer/src/assets/theme-utility-generation.test.ts new file mode 100644 index 00000000000..d17d8a10b4e --- /dev/null +++ b/src/renderer/src/assets/theme-utility-generation.test.ts @@ -0,0 +1,19 @@ +import fs from 'node:fs' +import { describe, expect, it } from 'vitest' + +const mainCss = fs.readFileSync(new URL('./main.css', import.meta.url), 'utf8') +const themeBlock = /@theme inline\s*{([\s\S]*?)\n}/.exec(mainCss)?.[1] ?? '' + +// Why: a token that never reaches `@theme inline`, and a Tailwind-shaped name that is only a +// plain CSS selector, both generate no CSS at all -- the utility silently does nothing. +describe('main.css utility generation', () => { + it('exposes --editor-surface to Tailwind so bg-editor-surface generates', () => { + expect(mainCss).toMatch(/--editor-surface:/) + expect(themeBlock).toMatch(/--color-editor-surface:\s*var\(--editor-surface\)/) + }) + + it('declares scrollbar-none as a utility rather than a plain class', () => { + expect(mainCss).toMatch(/@utility scrollbar-none\s*{/) + expect(mainCss).not.toMatch(/^\.scrollbar-none\b/m) + }) +}) diff --git a/src/renderer/src/components/editor/IpynbCellEditor.tsx b/src/renderer/src/components/editor/IpynbCellEditor.tsx index 77f319f034c..3ec18d46701 100644 --- a/src/renderer/src/components/editor/IpynbCellEditor.tsx +++ b/src/renderer/src/components/editor/IpynbCellEditor.tsx @@ -1,9 +1,10 @@ -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import Editor, { type OnMount } from '@monaco-editor/react' import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' import remarkGfm from 'remark-gfm' +import { cn } from '@/lib/utils' import { monaco } from '@/lib/monaco-setup' import { computeEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { resolveDocumentTheme } from '@/lib/document-theme' @@ -14,11 +15,27 @@ import type { IpynbCell } from './ipynb-parse' import MonacoCodeExcerpt from './MonacoCodeExcerpt' export function IpynbMarkdownCell({ source }: { source: string }): React.JSX.Element { + const settings = useAppStore((s) => s.settings) + const theme = settings?.theme ?? 'system' + const [systemDark, setSystemDark] = useState(() => resolveDocumentTheme('system')) + useEffect(() => { + if (theme !== 'system' || typeof window.matchMedia !== 'function') { + return + } + const media = window.matchMedia('(prefers-color-scheme: dark)') + const onChange = () => setSystemDark(media.matches) + onChange() + media.addEventListener('change', onChange) + return () => media.removeEventListener('change', onChange) + }, [theme]) + const isDark = theme === 'system' ? systemDark : resolveDocumentTheme(theme) return ( -
- - {source || '\u00a0'} - +
+
+ + {source || '\u00a0'} + +
) } diff --git a/src/renderer/src/components/editor/isolated-markdown-extension-for-tests.ts b/src/renderer/src/components/editor/isolated-markdown-extension-for-tests.ts index 46ed021ee2e..2576bb31b21 100644 --- a/src/renderer/src/components/editor/isolated-markdown-extension-for-tests.ts +++ b/src/renderer/src/components/editor/isolated-markdown-extension-for-tests.ts @@ -1,9 +1,10 @@ -import { Markdown } from '@tiptap/markdown' +import { createRichMarkdownExtension } from './rich-markdown-extension' import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport' export function createIsolatedMarkdownExtensionForTests() { - return Markdown.configure({ - marked: createRichMarkdownEditorCodec().marked, + const codec = createRichMarkdownEditorCodec() + return createRichMarkdownExtension(codec).configure({ + marked: codec.marked, markedOptions: { gfm: true } }) } diff --git a/src/renderer/src/components/editor/raw-markdown-html.ts b/src/renderer/src/components/editor/raw-markdown-html.ts index 1262d5254ab..edcf8715808 100644 --- a/src/renderer/src/components/editor/raw-markdown-html.ts +++ b/src/renderer/src/components/editor/raw-markdown-html.ts @@ -7,16 +7,14 @@ import type { RichMarkdownSourceKind, RichMarkdownSourceTransport } from './rich-markdown-source-transport' -import { isReservedRichMarkdownTransportBody } from './rich-markdown-source-transport' +import { + isReservedRichMarkdownTransportBody, + skipInlineTransportStartScan +} from './rich-markdown-source-transport' import { matchHtmlSuperscriptLinkSource } from './rich-markdown-html-superscript-link-source' const INLINE_HTML_PATTERN = /^|^<\/?[A-Za-z][\w.:-]*(?:\s[^<>]*?)?\/?>/ -function matchInlineHtml(src: string): string | null { - const match = src.match(INLINE_HTML_PATTERN) - return match?.[0] ?? null -} - function isEscaped(content: string, index: number): boolean { let backslashCount = 0 for (let i = index - 1; i >= 0 && content[i] === '\\'; i -= 1) { @@ -186,7 +184,7 @@ export function encodeRawMarkdownHtmlForRichEditor( const inlineHtml = normalizedContent.startsWith('B;\n```'} /> diff --git a/src/renderer/src/components/sidebar/CommentMarkdown.tsx b/src/renderer/src/components/sidebar/CommentMarkdown.tsx index 0999c23c8c6..8b862c90c78 100644 --- a/src/renderer/src/components/sidebar/CommentMarkdown.tsx +++ b/src/renderer/src/components/sidebar/CommentMarkdown.tsx @@ -11,7 +11,8 @@ import { createDocumentCommentMarkdownComponents, documentCommentMarkdownComponents, isTrustedCompactImageSrc, - type CommentMarkdownLinkClickHandler + type CommentMarkdownLinkClickHandler, + type DocumentCodeBlockRenderer } from './comment-markdown-element-renderers' import { remarkNativeChatFileLinks } from './comment-markdown-native-chat-file-links' @@ -188,6 +189,7 @@ type CommentMarkdownProps = React.ComponentPropsWithoutRef<'div'> & { allowFileUriLinks?: boolean linkifyFilePaths?: boolean expandImages?: boolean + renderCodeBlock?: DocumentCodeBlockRenderer } // Why forwardRef + rest props: Radix's HoverCardTrigger asChild merges a ref @@ -204,6 +206,7 @@ const CommentMarkdown = React.memo( allowFileUriLinks = false, linkifyFilePaths = false, expandImages = false, + renderCodeBlock, ...rest }, ref @@ -211,15 +214,17 @@ const CommentMarkdown = React.memo( const components = React.useMemo(() => { if (!onLinkClick) { return variant === 'document' - ? documentCommentMarkdownComponents + ? renderCodeBlock + ? createDocumentCommentMarkdownComponents(undefined, renderCodeBlock) + : documentCommentMarkdownComponents : expandImages ? createCompactCommentMarkdownComponents(undefined, true) : compactCommentMarkdownComponents } return variant === 'document' - ? createDocumentCommentMarkdownComponents(onLinkClick) + ? createDocumentCommentMarkdownComponents(onLinkClick, renderCodeBlock) : createCompactCommentMarkdownComponents(onLinkClick, expandImages) - }, [expandImages, variant, onLinkClick]) + }, [expandImages, renderCodeBlock, variant, onLinkClick]) const activeRemarkPlugins = React.useMemo(() => { const plugins = linkifyFilePaths ? [...remarkPlugins, remarkNativeChatFileLinks] diff --git a/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx b/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx index bb82a47356b..7d4e6d1b3bc 100644 --- a/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx +++ b/src/renderer/src/components/sidebar/comment-markdown-element-renderers.tsx @@ -15,6 +15,19 @@ export type CommentMarkdownLinkClickHandler = ( href: string | undefined ) => void +export type DocumentCodeBlockRenderer = (props: { + children?: React.ReactNode + language?: string +}) => React.JSX.Element + +function extractCodeFenceLanguage(children: React.ReactNode): string | undefined { + const child = React.Children.toArray(children)[0] + if (!React.isValidElement<{ className?: string }>(child)) { + return undefined + } + return child.props.className?.match(/(?:^|\s)language-([^\s]+)/)?.[1] +} + export function isTrustedCompactImageSrc(src: string | undefined): src is string { if (!src) { return false @@ -223,7 +236,8 @@ export function createCompactCommentMarkdownComponents( } export function createDocumentCommentMarkdownComponents( - onLinkClick?: CommentMarkdownLinkClickHandler + onLinkClick?: CommentMarkdownLinkClickHandler, + renderCodeBlock?: DocumentCodeBlockRenderer ): Components { return { p: ({ children }) =>

{children}

, @@ -259,6 +273,8 @@ export function createDocumentCommentMarkdownComponents( pre: ({ children }) => isMermaidPre(children) ? ( <>{children} + ) : renderCodeBlock ? ( + renderCodeBlock({ children, language: extractCodeFenceLanguage(children) }) ) : (
           {children}
diff --git a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts
index c5de2eb1813..b0e7340f373 100644
--- a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts
+++ b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts
@@ -90,6 +90,26 @@ describe('buildTitleDerivedAgentRows', () => {
     ])
   })
 
+  it.each([
+    [':', 'working'],
+    ['>', 'idle'],
+    ['!', 'waiting']
+  ])('retains hook-less OMP rows for owner marker %s', (marker, state) => {
+    const title = `OMP ${marker} Run a long task`
+    const rows = buildWorktreeAgentRows({
+      tabs: [makeTab('tab-1', { launchAgent: 'omp' })],
+      entries: [],
+      retained: [],
+      runtimePaneTitlesByTabId: { 'tab-1': { 1: title } },
+      ptyIdsByTabId: { 'tab-1': ['pty-omp'] },
+      terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
+      now: 2000
+    })
+    expect(rows.map((row) => [row.agentType, row.state, row.entry.terminalTitle])).toEqual([
+      ['omp', state, title]
+    ])
+  })
+
   it('keeps Pi-compatible title-derived rows as Pi for launched Pi sessions', () => {
     const rows = buildWorktreeAgentRows({
       tabs: [makeTab('tab-1', { launchAgent: 'pi' })],
diff --git a/src/renderer/src/components/skills/SkillsPage.test.tsx b/src/renderer/src/components/skills/SkillsPage.test.tsx
index 1570f70ed28..bb8296097eb 100644
--- a/src/renderer/src/components/skills/SkillsPage.test.tsx
+++ b/src/renderer/src/components/skills/SkillsPage.test.tsx
@@ -264,6 +264,34 @@ describe('SkillsPage', () => {
     expect(renderedSkillNames()).not.toContain('local-only')
   })
 
+  it("does not show one runtime's skills when the next runtime scan fails", async () => {
+    const discover = vi.fn().mockResolvedValue(discoveryResult(['local-only']))
+    const call = vi.fn(async (args: { method: string; selector?: string }) => {
+      const compatibilityResponse = createCompatibleRuntimeStatusResponseIfNeeded(args)
+      if (compatibilityResponse) {
+        return compatibilityResponse
+      }
+      throw new Error('remote unavailable')
+    })
+    Object.defineProperty(window, 'api', {
+      configurable: true,
+      value: { skills: skillsApi(discover), runtimeEnvironments: { call } }
+    })
+    vi.spyOn(console, 'error').mockImplementation(() => {})
+
+    await renderPage()
+    await flushMicrotasks()
+    expect(renderedSkillNames()).toEqual(['local-only'])
+
+    await act(async () => {
+      setRuntimeOwner('env-1')
+    })
+    await flushMicrotasks()
+
+    expect(container?.textContent).toContain('Could not scan skills')
+    expect(renderedSkillNames()).toEqual([])
+  })
+
   it('keeps scanning rather than listing client skills before the owner is known', async () => {
     const discover = vi.fn().mockResolvedValue(discoveryResult(['local-only']))
     const call = vi.fn()
@@ -451,4 +479,70 @@ describe('SkillsPage', () => {
     expect(container?.textContent).toContain('0 selected')
     expect(renderedSkillNames()).toEqual(['beta'])
   })
+  it('distinguishes a failed scan from empty skill folders', async () => {
+    const discover = vi
+      .fn()
+      .mockRejectedValue(
+        new Error(
+          "Error invoking remote method 'skills:discover': Error: EACCES: permission denied\nSSH host unavailable"
+        )
+      )
+    Object.defineProperty(window, 'api', {
+      configurable: true,
+      value: { skills: skillsApi(discover), runtimeEnvironments: { call: vi.fn() } }
+    })
+    vi.spyOn(console, 'error').mockImplementation(() => {})
+
+    await renderPage()
+    await flushMicrotasks()
+
+    expect(container?.textContent).toContain('Could not scan skills')
+    expect(container?.textContent).toContain('EACCES: permission denied')
+    expect(container?.textContent).toContain('SSH host unavailable')
+    expect(container?.textContent).not.toContain('Error invoking remote method')
+    // Why: nothing was scanned, so "the scanned skill folders are empty" would be a claim we cannot make.
+    expect(container?.textContent).not.toContain('No skills found')
+  })
+
+  it('retries the failed scan from the error band and clears it on success', async () => {
+    const discover = vi
+      .fn()
+      .mockRejectedValueOnce(new Error('EACCES: permission denied'))
+      .mockResolvedValueOnce(discoveryResult(['alpha']))
+    Object.defineProperty(window, 'api', {
+      configurable: true,
+      value: { skills: skillsApi(discover), runtimeEnvironments: { call: vi.fn() } }
+    })
+    vi.spyOn(console, 'error').mockImplementation(() => {})
+
+    await renderPage()
+    await flushMicrotasks()
+    await act(async () => fireEvent.click(buttonNamed('Retry')))
+    await flushMicrotasks()
+
+    expect(container?.textContent).not.toContain('Could not scan skills')
+    expect(renderedSkillNames()).toEqual(['alpha'])
+  })
+
+  it('keeps a previously confirmed empty result visible when a refresh fails', async () => {
+    const discover = vi
+      .fn()
+      .mockResolvedValueOnce(discoveryResult([]))
+      .mockRejectedValueOnce(new Error('host unavailable'))
+    Object.defineProperty(window, 'api', {
+      configurable: true,
+      value: { skills: skillsApi(discover), runtimeEnvironments: { call: vi.fn() } }
+    })
+    vi.spyOn(console, 'error').mockImplementation(() => {})
+
+    await renderPage()
+    await flushMicrotasks()
+    expect(container?.textContent).toContain('No skills found')
+
+    await act(async () => fireEvent.click(buttonNamed('Refresh')))
+    await flushMicrotasks()
+
+    expect(container?.textContent).toContain('Could not scan skills')
+    expect(container?.textContent).toContain('No skills found')
+  })
 })
diff --git a/src/renderer/src/components/skills/SkillsPage.tsx b/src/renderer/src/components/skills/SkillsPage.tsx
index 5d85afc85fc..c733e03ac09 100644
--- a/src/renderer/src/components/skills/SkillsPage.tsx
+++ b/src/renderer/src/components/skills/SkillsPage.tsx
@@ -1,8 +1,10 @@
 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import { Share2, Trash2 } from 'lucide-react'
+import { readIpcErrorDetail } from '@/lib/ipc-error'
 import { cn } from '@/lib/utils'
 import { useAppStore } from '@/store'
 import { discoverSkillsForRuntimeTarget } from '@/runtime/runtime-skills-client'
+import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
 import { useActiveSkillDiscoveryRuntimeTarget } from '@/hooks/use-active-skill-discovery-runtime-target'
 import { useMountedRef } from '@/hooks/useMountedRef'
 import type { DiscoveredSkill, SkillDiscoveryResult } from '../../../../shared/skills'
@@ -58,6 +60,12 @@ const NO_FILTERS: SkillsFilterState = {
   agent: 'all'
 }
 
+type SkillScanState = {
+  runtimeTarget: RuntimeClientTarget
+  result: SkillDiscoveryResult | null
+  error: { detail?: string } | null
+}
+
 export default function SkillsPage(): React.JSX.Element {
   const closeSkillsPage = useAppStore((s) => s.closeSkillsPage)
   const pendingSkillShareId = useAppStore((s) => s.pendingSkillShareId)
@@ -66,9 +74,12 @@ export default function SkillsPage(): React.JSX.Element {
   const clearPendingSkillsSharedView = useAppStore((s) => s.clearPendingSkillsSharedView)
   const runtimeTarget = useActiveSkillDiscoveryRuntimeTarget()
   const hostLabel = useSkillDiscoveryHostLabel(runtimeTarget)
-  const [result, setResult] = useState(null)
+  const [scanState, setScanState] = useState(null)
+  // Target identity changes on host switches and same-ID re-pairs.
+  const currentScan = scanState?.runtimeTarget === runtimeTarget ? scanState : null
+  const result = currentScan?.result ?? null
   const [loading, setLoading] = useState(true)
-  const [scanError, setScanError] = useState(null)
+  const scanError = currentScan?.error ?? null
   const [shareSkills, setShareSkills] = useState([])
   const [selectionMode, setSelectionMode] = useState<'share' | 'delete' | null>(null)
   const [selectedSkillIds, setSelectedSkillIds] = useState>(() => new Set())
@@ -108,8 +119,7 @@ export default function SkillsPage(): React.JSX.Element {
         )
         const local = runtimeTarget.kind === 'local'
         if (isCurrentScan()) {
-          setResult(nextResult)
-          setScanError(null)
+          setScanState({ runtimeTarget, result: nextResult, error: null })
           setSelectedSkillIds((current) =>
             selectionModeRef.current === 'delete'
               ? retainedDeletableSkillSelection(current, nextResult.skills)
@@ -121,9 +131,11 @@ export default function SkillsPage(): React.JSX.Element {
         if (isCurrentScan()) {
           // Why: a failed scan needs to stay on screen with a retry — a toast
           // disappears before the user can act on it.
-          setScanError(
-            translate('auto.components.skills.SkillsPage.ea72d6185b', 'Could not scan skills')
-          )
+          setScanState((current) => ({
+            runtimeTarget,
+            result: current?.runtimeTarget === runtimeTarget ? current.result : null,
+            error: { detail: readIpcErrorDetail(error) }
+          }))
         }
       } finally {
         if (isCurrentScan()) {
@@ -310,7 +322,7 @@ export default function SkillsPage(): React.JSX.Element {
       />
       {scanError ? (
          {
             deleteFlow.reprobe()
@@ -359,7 +371,7 @@ export default function SkillsPage(): React.JSX.Element {
                 />
               ) : skills.length > 0 ? (
                  setFilters(NO_FILTERS)} />
-              ) : (
+              ) : result ? (
                  {
                     deleteFlow.reprobe()
@@ -367,7 +379,7 @@ export default function SkillsPage(): React.JSX.Element {
                   }}
                   onInstallFromLink={openInstallDialog}
                 />
-              )}
+              ) : null}
             
           )}
         
diff --git a/src/renderer/src/components/skills/skills-page-states.tsx b/src/renderer/src/components/skills/skills-page-states.tsx index 1db52379e8f..0a61078e665 100644 --- a/src/renderer/src/components/skills/skills-page-states.tsx +++ b/src/renderer/src/components/skills/skills-page-states.tsx @@ -84,11 +84,11 @@ export function SkillsEmptyState({ } export function SkillsScanErrorBand({ - message, + detail, disabled, onRetry }: { - message: string + detail?: string disabled: boolean onRetry: () => void }): React.JSX.Element { @@ -97,9 +97,18 @@ export function SkillsScanErrorBand({
-

- {message} -

+ {/* Announce the detail with the headline. */} +
+

+ {translate('auto.components.skills.SkillsPage.ea72d6185b', 'Could not scan skills')} +

+ {detail ? ( + // Preserve multi-line git and SSH errors. +

+ {detail} +

+ ) : null} +
diff --git a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts index 019b3e297ee..1269ade41a4 100644 --- a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts +++ b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts @@ -1,10 +1,11 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Tab } from '../../../../shared/tab-types' import { useAppStore } from '../../store' import { applyDragPreviewTab, captureTabDragActivationSnapshot, - restoreTabDragActivationSnapshot + restoreTabDragActivationSnapshot, + restoreSourceGroupActiveTabAfterCrossGroupDrop } from './tab-drag-preview-activation' const WT = 'wt-preview-restore' @@ -59,6 +60,30 @@ describe('restoreTabDragActivationSnapshot', () => { }) }) + it('does not publish repeated preview and restore actions', () => { + const snapshot = captureTabDragActivationSnapshot(WT) + const subscriber = vi.fn() + const unsubscribe = useAppStore.subscribe(subscriber) + try { + applyDragPreviewTab({ + worktreeId: WT, + groupId: 'group-1', + tabId: 'tab-1', + activeGroupId: 'group-1' + }) + restoreTabDragActivationSnapshot(WT, snapshot) + restoreSourceGroupActiveTabAfterCrossGroupDrop({ + worktreeId: WT, + snapshot, + sourceGroupId: 'group-1', + movedTabId: 'tab-2' + }) + expect(subscriber).not.toHaveBeenCalled() + } finally { + unsubscribe() + } + }) + it('restores active-surface fields after a drag preview is cancelled', () => { const snapshot = captureTabDragActivationSnapshot(WT) diff --git a/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts b/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts index a0060271c99..31726672db2 100644 --- a/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts +++ b/src/renderer/src/components/tab-group/tab-drag-preview-activation.ts @@ -30,6 +30,14 @@ function previewActiveSurfacePatch( }) if (unifiedTab.contentType === 'terminal') { + if ( + state.activeTabType === 'terminal' && + state.activeTabTypeByWorktree[worktreeId] === 'terminal' && + state.activeTabId === unifiedTab.entityId && + state.activeTabIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } return { activeTabId: unifiedTab.entityId, activeTabType: 'terminal', @@ -41,6 +49,14 @@ function previewActiveSurfacePatch( } } if (unifiedTab.contentType === 'browser') { + if ( + state.activeTabType === 'browser' && + state.activeTabTypeByWorktree[worktreeId] === 'browser' && + state.activeBrowserTabId === unifiedTab.entityId && + state.activeBrowserTabIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } return { activeBrowserTabId: unifiedTab.entityId, activeTabType: 'browser', @@ -52,11 +68,25 @@ function previewActiveSurfacePatch( } } if (unifiedTab.contentType === 'simulator') { + if ( + state.activeTabType === 'simulator' && + state.activeTabTypeByWorktree[worktreeId] === 'simulator' + ) { + return {} + } return { activeTabType: 'simulator', activeTabTypeByWorktree: nextActiveTabTypeByWorktree('simulator') } } + if ( + state.activeTabType === 'editor' && + state.activeTabTypeByWorktree[worktreeId] === 'editor' && + state.activeFileId === unifiedTab.entityId && + state.activeFileIdByWorktree[worktreeId] === unifiedTab.entityId + ) { + return {} + } return { activeFileId: unifiedTab.entityId, activeTabType: 'editor', @@ -95,7 +125,7 @@ export function applyDragPreviewTab({ const focusUnchanged = (state.activeGroupIdByWorktree[worktreeId] ?? null) === activeGroupId const surfacePatch = previewActiveSurfacePatch(state, worktreeId, groupId, tabId) if (groupUnchanged && focusUnchanged) { - return Object.keys(surfacePatch).length > 0 ? surfacePatch : {} + return Object.keys(surfacePatch).length > 0 ? surfacePatch : state } const next: Partial = { ...surfacePatch } @@ -162,7 +192,7 @@ export function restoreTabDragActivationSnapshot( } if (Object.keys(next).length === 0) { - return {} + return state } return next @@ -191,7 +221,7 @@ export function restoreSourceGroupActiveTabAfterCrossGroupDrop({ const groups = state.groupsByWorktree[worktreeId] ?? [] const sourceGroup = groups.find((group) => group.id === sourceGroupId) if (!sourceGroup || sourceGroup.activeTabId === preDragActiveTabId) { - return {} + return state } return { groupsByWorktree: { diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts index e9d60cabace..9579ea2ba2c 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts @@ -290,7 +290,25 @@ describe('agent completion coordinator', () => { expect(dispatchCompletion).toHaveBeenCalledExactlyOnceWith('done') }) - it('resets exit confirmation across an unavailable inspection', async () => { + it.each([ + { + label: 'client-only uncertainty', + result: { + foregroundProcess: null, + hasChildProcesses: false, + verdict: 'unverifiable', + reason: 'transport_loss' + } satisfies RuntimeTerminalProcessInspection + }, + { + label: 'host child-process uncertainty', + result: { + foregroundProcess: '/bin/zsh', + hasChildProcesses: false, + childProcessEvidence: 'unverifiable' + } satisfies RuntimeTerminalProcessInspection + } + ])('resets exit confirmation across $label', async ({ result: unavailableResult }) => { let result: RuntimeTerminalProcessInspection = processResult('codex') const dispatchCompletion = vi.fn() const coordinator = createAgentCompletionCoordinator({ @@ -306,12 +324,7 @@ describe('agent completion coordinator', () => { await vi.advanceTimersByTimeAsync(2_000) result = processResult(null, false) await vi.advanceTimersByTimeAsync(750) - result = { - foregroundProcess: null, - hasChildProcesses: false, - verdict: 'unverifiable', - reason: 'transport_loss' - } + result = unavailableResult await vi.advanceTimersByTimeAsync(750) result = processResult(null, false) await vi.advanceTimersByTimeAsync(1_500) diff --git a/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts b/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts index f27960361bb..b1a74972311 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts @@ -53,13 +53,16 @@ export function handleAgentCompletionInspectionResult(args: { dispatchCompletion, remoteInspection } = args - if (isClientOnlyUnverifiableInspection(result)) { + const remote = options.isRemotePtyId?.(options.getPtyId() ?? '') === true + if ( + isClientOnlyUnverifiableInspection(result) || + (!remote && result.childProcessEvidence === 'unverifiable') + ) { state.pendingProcessExitAgent = null state.consecutiveInspectionErrors += 1 scheduleNextPoll() return false } - const remote = options.isRemotePtyId?.(options.getPtyId() ?? '') === true if (remote) { const evidence = result.foregroundProcessEvidence // Remote identity is host-authoritative. Compatibility names and unverifiable observations diff --git a/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts b/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts index 09126e91c6b..dac6bbaf326 100644 --- a/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' // Why: Phase 3 slice 1 of terminal-side-effect-authority.md runs a per-PTY // title tracker in main alongside the renderer transport's byte parser. Both // must derive IDENTICAL ordered title/status facts from the same bytes, or @@ -103,6 +104,19 @@ describe('main title tracker parity with the renderer transport processor', () = vi.useRealTimers() }) + it('agrees on captured OMP native frames before and after owner rebranding', () => { + const captured = readFileSync( + new URL('../../../../main/runtime/__fixtures__/omp-native-title-win32.txt', import.meta.url), + 'utf8' + ) + feedBoth(paths, captured) + expect(paths.main.events).toEqual(paths.renderer.events) + expect(paths.main.events.some((event) => event.kind === 'became-working')).toBe(true) + expect(paths.main.events.some((event) => event.kind === 'became-idle')).toBe(true) + feedBoth(paths, captured.replaceAll(']0;π', ']0;OMP')) + expect(paths.main.events).toEqual(paths.renderer.events) + }) + it('derives identical facts from a coalesced spinner+idle chunk (issue #1083)', () => { // One realistic node-pty batch: Pi's 80ms spinner frames plus agent_end's // trailing idle title. A last-title reader sees only the idle title and diff --git a/src/renderer/src/hooks/composer-drop-failure-toast.test.ts b/src/renderer/src/hooks/composer-drop-failure-toast.test.ts new file mode 100644 index 00000000000..ab2945065cb --- /dev/null +++ b/src/renderer/src/hooks/composer-drop-failure-toast.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { toastError } = vi.hoisted(() => ({ + toastError: vi.fn<(title: string, options?: { id?: string; description?: string }) => void>() +})) +vi.mock('sonner', () => ({ toast: { error: toastError } })) + +import { showComposerDropFailureToast } from './composer-drop-failure-toast' +import type { ImportSkipReason } from '../../../shared/filesystem-import-result-types' + +const SKIP_REASON_COPY = [ + ['missing', 'No longer at its original path.'], + ['symlink', 'Symbolic links cannot be attached.'], + ['permission-denied', 'Permission denied.'], + ['unsupported', 'Unsupported file type.'] +] as const satisfies readonly (readonly [ImportSkipReason, string])[] + +function lastToast(): { title: string; id?: string; description?: string } { + const call = toastError.mock.calls.at(-1) + return { + title: String(call?.[0]), + id: call?.[1]?.id, + description: call?.[1]?.description + } +} + +describe('showComposerDropFailureToast', () => { + beforeEach(() => { + toastError.mockClear() + }) + + it('stays neutral about the gesture, and pluralises like its namespace siblings', () => { + showComposerDropFailureToast({ failureCount: 1, total: 1 }) + expect(lastToast().title).toBe('1 of 1 item could not be attached.') + + showComposerDropFailureToast({ failureCount: 2, total: 5 }) + expect(lastToast().title).toBe('2 of 5 items could not be attached.') + }) + + it("turns the import client's skip enum into copy instead of leaking the token", () => { + for (const [reason, expected] of SKIP_REASON_COPY) { + showComposerDropFailureToast({ + failureCount: 1, + total: 3, + commonFailure: { status: 'skipped', reason } + }) + expect(lastToast().description).toBe(expected) + } + }) + + it('passes a free-form failure reason straight through', () => { + showComposerDropFailureToast({ + failureCount: 2, + total: 4, + commonFailure: { status: 'failed', reason: 'EACCES: permission denied' } + }) + expect(lastToast().description).toBe('EACCES: permission denied') + }) + + it('shows no description when nothing explained the failure', () => { + showComposerDropFailureToast({ failureCount: 1, total: 2 }) + expect(lastToast().description).toBeUndefined() + }) + + it('unwraps and clamps a host-minted failure reason before it reaches the row', () => { + showComposerDropFailureToast({ + failureCount: 1, + total: 2, + commonFailure: { + status: 'failed', + reason: + "Error invoking remote method 'runtime:call': Error: EACCES: permission denied\nat Object.upload" + } + }) + expect(lastToast().description).toBe('EACCES: permission denied') + }) + + it('reuses one slot so a second failed drop replaces the first instead of stacking', () => { + showComposerDropFailureToast({ failureCount: 1, total: 2 }) + const first = lastToast().id + showComposerDropFailureToast({ failureCount: 2, total: 3 }) + expect(first).toBeDefined() + expect(lastToast().id).toBe(first) + }) + + it('gives no reason at all when the batch failed for differing reasons', () => { + showComposerDropFailureToast({ failureCount: 3, total: 6 }) + expect(lastToast().title).toBe('3 of 6 items could not be attached.') + expect(lastToast().description).toBeUndefined() + }) +}) diff --git a/src/renderer/src/hooks/composer-drop-failure-toast.ts b/src/renderer/src/hooks/composer-drop-failure-toast.ts new file mode 100644 index 00000000000..a3dde2b7493 --- /dev/null +++ b/src/renderer/src/hooks/composer-drop-failure-toast.ts @@ -0,0 +1,57 @@ +import { toast } from 'sonner' +import { translate } from '@/i18n/i18n' +import { compactIpcErrorMessage } from '@/lib/ipc-error' +import type { ComposerDropFailure } from './composer-drop-result' +import type { ImportSkipReason } from '../../../shared/filesystem-import-result-types' + +// Own slot, not Source Control's: a drop failure must not erase an unread stage/discard failure. +const DROP_FAILURE_TOAST_ID = 'composer-drop-failure' + +const SKIP_REASON_COPY: Record = { + missing: { + key: 'auto.hooks.useComposerState.attachSkipMissing', + fallback: 'No longer at its original path.' + }, + symlink: { + key: 'auto.hooks.useComposerState.attachSkipSymlink', + fallback: 'Symbolic links cannot be attached.' + }, + 'permission-denied': { + key: 'auto.hooks.useComposerState.attachSkipPermissionDenied', + fallback: 'Permission denied.' + }, + unsupported: { + key: 'auto.hooks.useComposerState.attachSkipUnsupported', + fallback: 'Unsupported file type.' + } +} + +function failureDescription(failure: ComposerDropFailure): string | undefined { + if (failure.status === 'failed') { + return failure.reason ? compactIpcErrorMessage(failure.reason) : undefined + } + const copy = SKIP_REASON_COPY[failure.reason] + return copy ? translate(copy.key, copy.fallback) : undefined +} + +export function showComposerDropFailureToast({ + failureCount, + total, + commonFailure +}: { + failureCount: number + total: number + commonFailure?: ComposerDropFailure +}): void { + toast.error( + translate( + 'auto.hooks.useComposerState.dropPartiallyAttached', + '{{failureCount}} of {{count}} items could not be attached.', + { failureCount, count: total } + ), + { + id: DROP_FAILURE_TOAST_ID, + description: commonFailure ? failureDescription(commonFailure) : undefined + } + ) +} diff --git a/src/renderer/src/hooks/composer-drop-result.test.ts b/src/renderer/src/hooks/composer-drop-result.test.ts new file mode 100644 index 00000000000..7909c3b91e0 --- /dev/null +++ b/src/renderer/src/hooks/composer-drop-result.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { collectComposerDropResult, type ComposerDropItemResult } from './composer-drop-result' + +describe('composer drop result', () => { + it('separates imported files and folders while summarizing failures', () => { + const results: ComposerDropItemResult[] = [ + { status: 'imported', kind: 'file', destPath: '/repo/.orca/drops/file.txt' }, + { status: 'imported', kind: 'directory', destPath: '/repo/.orca/drops/folder' }, + { status: 'skipped', reason: 'permission-denied' }, + { status: 'failed', reason: 'disk full' } + ] + + expect(collectComposerDropResult(results)).toEqual({ + filePaths: ['/repo/.orca/drops/file.txt'], + folderPaths: ['/repo/.orca/drops/folder'], + failureCount: 2, + commonFailure: undefined + }) + }) + + it('keeps a failure only when it explains the whole failed subset', () => { + expect( + collectComposerDropResult([ + { status: 'skipped', reason: 'missing' }, + { status: 'skipped', reason: 'missing' } + ]).commonFailure + ).toEqual({ status: 'skipped', reason: 'missing' }) + + expect( + collectComposerDropResult([ + { status: 'imported', kind: 'file', destPath: '/repo/.orca/drops/file.txt' } + ]).commonFailure + ).toBeUndefined() + }) +}) diff --git a/src/renderer/src/hooks/composer-drop-result.ts b/src/renderer/src/hooks/composer-drop-result.ts new file mode 100644 index 00000000000..dd7334b98db --- /dev/null +++ b/src/renderer/src/hooks/composer-drop-result.ts @@ -0,0 +1,58 @@ +import type { ImportSkipReason } from '../../../shared/filesystem-import-result-types' + +export type ComposerDropItemResult = + | { + status: 'imported' + destPath: string + kind: 'file' | 'directory' + } + | { + status: 'skipped' + reason: ImportSkipReason + } + | { + status: 'failed' + reason?: string + } + +export type ComposerDropFailure = Exclude + +export type ComposerDropResult = { + filePaths: string[] + folderPaths: string[] + failureCount: number + commonFailure?: ComposerDropFailure +} + +function sameFailure(left: ComposerDropFailure, right: ComposerDropFailure): boolean { + return left.status === right.status && left.reason === right.reason +} + +export function collectComposerDropResult( + results: readonly ComposerDropItemResult[] +): ComposerDropResult { + const filePaths: string[] = [] + const folderPaths: string[] = [] + const failures: ComposerDropFailure[] = [] + + for (const result of results) { + if (result.status !== 'imported') { + failures.push(result) + } else if (result.kind === 'directory') { + folderPaths.push(result.destPath) + } else { + filePaths.push(result.destPath) + } + } + + const firstFailure = failures[0] + return { + filePaths, + folderPaths, + failureCount: failures.length, + commonFailure: + firstFailure && failures.every((failure) => sameFailure(firstFailure, failure)) + ? firstFailure + : undefined + } +} diff --git a/src/renderer/src/hooks/composer-drop-upload-result.test.ts b/src/renderer/src/hooks/composer-drop-upload-result.test.ts deleted file mode 100644 index fd7633cd9cf..00000000000 --- a/src/renderer/src/hooks/composer-drop-upload-result.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - collectComposerDropUploadResult, - shouldReportComposerDropUploadFailure, - type ComposerDropUploadImportResult -} from './composer-drop-upload-result' - -describe('composer drop upload result', () => { - it('separates imported files and folders while counting skipped or failed paths', () => { - const results: ComposerDropUploadImportResult[] = [ - { status: 'imported', kind: 'file', destPath: '/repo/.orca/drops/file.txt' }, - { status: 'imported', kind: 'directory', destPath: '/repo/.orca/drops/folder' }, - { status: 'skipped' }, - { status: 'failed' } - ] - - expect(collectComposerDropUploadResult(results)).toEqual({ - filePaths: ['/repo/.orca/drops/file.txt'], - folderPaths: ['/repo/.orca/drops/folder'], - skippedOrFailed: 2 - }) - }) - - it('suppresses failed-upload reporting after a composer loses drop ownership', () => { - const uploadResult = { skippedOrFailed: 1 } - - expect(shouldReportComposerDropUploadFailure(uploadResult, () => true)).toBe(true) - expect(shouldReportComposerDropUploadFailure(uploadResult, () => false)).toBe(false) - expect(shouldReportComposerDropUploadFailure({ skippedOrFailed: 0 }, () => true)).toBe(false) - }) -}) diff --git a/src/renderer/src/hooks/composer-drop-upload-result.ts b/src/renderer/src/hooks/composer-drop-upload-result.ts deleted file mode 100644 index a77cbbc9304..00000000000 --- a/src/renderer/src/hooks/composer-drop-upload-result.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type ComposerDropUploadImportResult = - | { - status: 'imported' - destPath: string - kind: 'file' | 'directory' - } - | { - status: 'skipped' | 'failed' - } - -export type ComposerDropUploadResult = { - filePaths: string[] - folderPaths: string[] - skippedOrFailed: number -} - -export function collectComposerDropUploadResult( - results: readonly ComposerDropUploadImportResult[] -): ComposerDropUploadResult { - const filePaths: string[] = [] - const folderPaths: string[] = [] - let skippedOrFailed = 0 - - for (const result of results) { - if (result.status !== 'imported') { - skippedOrFailed += 1 - continue - } - if (result.kind === 'directory') { - folderPaths.push(result.destPath) - } else { - filePaths.push(result.destPath) - } - } - - return { filePaths, folderPaths, skippedOrFailed } -} - -export function shouldReportComposerDropUploadFailure( - uploadResult: Pick, - canReport: () => boolean -): boolean { - return uploadResult.skippedOrFailed > 0 && canReport() -} diff --git a/src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx b/src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx new file mode 100644 index 00000000000..9b72a3d0dba --- /dev/null +++ b/src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx @@ -0,0 +1,205 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { createRef } from 'react' +import type { Dispatch, SetStateAction } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ toastError: vi.fn(), importExternalPaths: vi.fn() })) + +vi.mock('sonner', () => ({ toast: { error: mocks.toastError, message: vi.fn() } })) +vi.mock('@/store', () => ({ + useAppStore: Object.assign(() => undefined, { getState: () => ({}) }) +})) +vi.mock('@/runtime/runtime-file-client', () => ({ + importExternalPathsToRuntime: (...args: unknown[]) => mocks.importExternalPaths(...args) +})) +vi.mock('./composer-drop-listener', () => ({ useComposerDropListener: vi.fn() })) + +import { useAttachmentDropState } from './attachment-drop-state' + +const FAILING_PATHS = new Set(['/drop/bad-1.png', '/drop/bad-2.png', '/drop/bad-3.png']) + +function dropPaths(count: number): string[] { + return [ + ...FAILING_PATHS, + ...Array.from({ length: count - FAILING_PATHS.size }, (_, index) => `/drop/ok-${index}.png`) + ] +} + +function installFsApi(): void { + Object.assign(window, { + api: { + fs: { + authorizeExternalPath: vi.fn(async () => {}), + stat: vi.fn(async ({ filePath }: { filePath: string }) => { + if (FAILING_PATHS.has(filePath)) { + throw new Error( + "Error invoking remote method 'fs:stat': Error: ENOENT: no such file or directory" + ) + } + return { isDirectory: false } + }) + } + } + }) +} + +function renderDropState(setAttachmentPaths: Dispatch>) { + return renderHook(() => + useAttachmentDropState({ + agentPromptRef: { current: '' }, + cancelPromptCaretFrame: () => {}, + connectionId: null, + promptCaretFrameRef: { current: null }, + promptTextareaRef: createRef(), + selectedRepoPath: '/repo', + selectedRepoSettings: null, + setAgentPrompt: () => {}, + setAttachmentPaths + }) + ) +} + +beforeEach(() => { + vi.clearAllMocks() + installFsApi() +}) + +describe('local composer drop failures', () => { + it('reports partially skipped paths in one aggregated toast and still attaches the rest', async () => { + const attached: string[] = [] + const { result } = renderDropState((next) => { + attached.push(...(typeof next === 'function' ? next([]) : next)) + }) + + await act(async () => { + await result.current.applyLocalComposerDrop(dropPaths(12)) + }) + + expect(mocks.toastError).toHaveBeenCalledTimes(1) + const [title, options] = mocks.toastError.mock.calls[0] ?? [] + expect(title).toBe('3 of 12 items could not be attached.') + expect(options.description).toBe('No longer at its original path.') + expect(attached).toHaveLength(9) + expect(attached).not.toContain('/drop/bad-1.png') + }) + + it('stays silent when every dropped path attaches', async () => { + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.applyLocalComposerDrop(['/drop/ok-0.png', '/drop/ok-1.png']) + }) + + expect(mocks.toastError).not.toHaveBeenCalled() + }) + + it('says nothing once the composer that owned the drop is gone', async () => { + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.applyLocalComposerDrop(dropPaths(12), () => false) + }) + + expect(mocks.toastError).not.toHaveBeenCalled() + }) +}) + +// Why: the upload branch returns early unless a runtime environment or connection is resolved. +const RUNTIME_SETTINGS = { activeRuntimeEnvironmentId: 'env-1' } + +describe('composer upload failures', () => { + it('aggregates a mixed runtime import into one toast, and withholds a reason that is not shared', async () => { + mocks.importExternalPaths.mockResolvedValue({ + results: [ + { + sourcePath: '/a.png', + status: 'imported', + destPath: '/repo/.orca/drops/a.png', + kind: 'file', + renamed: false + }, + { sourcePath: '/b.png', status: 'skipped', reason: 'permission-denied' }, + { sourcePath: '/c.png', status: 'failed', reason: 'disk full' } + ] + }) + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.uploadComposerPaths( + ['/a.png', '/b.png', '/c.png'], + RUNTIME_SETTINGS, + null, + '/repo' + ) + }) + + expect(mocks.toastError).toHaveBeenCalledTimes(1) + const [title, options] = mocks.toastError.mock.calls[0] ?? [] + expect(title).toBe('2 of 3 items could not be attached.') + expect(options.description).toBeUndefined() + }) + + it('stays silent when every uploaded path imports', async () => { + mocks.importExternalPaths.mockResolvedValue({ + results: [ + { + sourcePath: '/a.png', + status: 'imported', + destPath: '/repo/.orca/drops/a.png', + kind: 'file', + renamed: false + } + ] + }) + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.uploadComposerPaths(['/a.png'], RUNTIME_SETTINGS, null, '/repo') + }) + + expect(mocks.toastError).not.toHaveBeenCalled() + }) + + it('does not report after the composer that owned the upload is gone', async () => { + mocks.importExternalPaths.mockResolvedValue({ + results: [{ sourcePath: '/b.png', status: 'skipped', reason: 'missing' }] + }) + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.uploadComposerPaths( + ['/b.png'], + RUNTIME_SETTINGS, + null, + '/repo', + () => false + ) + }) + + expect(mocks.toastError).not.toHaveBeenCalled() + }) + + it('does give the shared reason when every uploaded path failed the same way', async () => { + mocks.importExternalPaths.mockResolvedValue({ + results: [ + { sourcePath: '/b.png', status: 'skipped', reason: 'permission-denied' }, + { sourcePath: '/c.png', status: 'skipped', reason: 'permission-denied' } + ] + }) + const { result } = renderDropState(() => {}) + + await act(async () => { + await result.current.uploadComposerPaths( + ['/b.png', '/c.png'], + RUNTIME_SETTINGS, + null, + '/repo' + ) + }) + + const [, options] = mocks.toastError.mock.calls[0] ?? [] + expect(options.description).toBe('Permission denied.') + }) +}) diff --git a/src/renderer/src/hooks/composer-state/attachment-drop-state.ts b/src/renderer/src/hooks/composer-state/attachment-drop-state.ts index 6dcd27e8af5..ace0e0ba4f7 100644 --- a/src/renderer/src/hooks/composer-state/attachment-drop-state.ts +++ b/src/renderer/src/hooks/composer-state/attachment-drop-state.ts @@ -20,13 +20,27 @@ import { joinPath } from '@/lib/path' import { captureDirectSshMutationExpectation } from '@/lib/ssh-mutation-expectation' import { useAppStore } from '@/store' import { importExternalPathsToRuntime } from '@/runtime/runtime-file-client' +import { readIpcErrorMessage } from '@/lib/ipc-error' +import { showComposerDropFailureToast } from '../composer-drop-failure-toast' import { - collectComposerDropUploadResult, - shouldReportComposerDropUploadFailure -} from '../composer-drop-upload-result' + collectComposerDropResult, + type ComposerDropFailure, + type ComposerDropItemResult +} from '../composer-drop-result' import { applyComposerNativeFileDrop } from '../composer-native-file-drop' import { useComposerDropListener } from './composer-drop-listener' +// Local drops bypass the runtime importer's skip classification. +function localDropFailure(detail: string | undefined): ComposerDropFailure { + if (detail?.startsWith('ENOENT')) { + return { status: 'skipped', reason: 'missing' } + } + if (/^(EACCES|EPERM)/.test(detail ?? '')) { + return { status: 'skipped', reason: 'permission-denied' } + } + return { status: 'failed', reason: detail } +} + export function useAttachmentDropState(input: AttachmentDropStateInput) { const { agentPromptRef, @@ -164,14 +178,13 @@ export function useAttachmentDropState(input: AttachmentDropStateInput) { destinationDir, { ensureDestinationDir: true, assertCurrent } ) - const uploadResult = collectComposerDropUploadResult(results) - if (shouldReportComposerDropUploadFailure(uploadResult, canReportFailure)) { - toast.error( - translate( - 'auto.hooks.useComposerState.a9ff236145', - 'Some attachments could not be uploaded.' - ) - ) + const uploadResult = collectComposerDropResult(results) + if (uploadResult.failureCount > 0 && canReportFailure()) { + showComposerDropFailureToast({ + failureCount: uploadResult.failureCount, + total: sourcePaths.length, + commonFailure: uploadResult.commonFailure + }) } return { filePaths: uploadResult.filePaths, folderPaths: uploadResult.folderPaths } }, @@ -199,27 +212,34 @@ export function useAttachmentDropState(input: AttachmentDropStateInput) { const applyLocalComposerDrop = useCallback( async (paths: string[], canApply: () => boolean = () => true): Promise => { - const fileAttachments: string[] = [] - const folderPaths: string[] = [] + const results: ComposerDropItemResult[] = [] for (const filePath of paths) { try { await window.api.fs.authorizeExternalPath({ targetPath: filePath }) const stat = await window.api.fs.stat({ filePath }) - if (stat.isDirectory) { - folderPaths.push(filePath) - } else { - fileAttachments.push(filePath) - } - } catch { - // Skip paths we cannot authorize or stat. + results.push({ + status: 'imported', + destPath: filePath, + kind: stat.isDirectory ? 'directory' : 'file' + }) + } catch (error) { + results.push(localDropFailure(readIpcErrorMessage(error))) } } if (!canApply()) { return } - addComposerAttachments(fileAttachments) - insertComposerFolderPaths(folderPaths) + const dropResult = collectComposerDropResult(results) + addComposerAttachments(dropResult.filePaths) + insertComposerFolderPaths(dropResult.folderPaths) + if (dropResult.failureCount > 0) { + showComposerDropFailureToast({ + failureCount: dropResult.failureCount, + total: paths.length, + commonFailure: dropResult.commonFailure + }) + } }, [addComposerAttachments, insertComposerFolderPaths] ) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index a1c33b790bc..8fb5d19446c 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2535,6 +2535,14 @@ } }, "hooks": { + "useComposerState": { + "attachSkipMissing": "No longer at its original path.", + "attachSkipPermissionDenied": "Permission denied.", + "attachSkipSymlink": "Symbolic links cannot be attached.", + "attachSkipUnsupported": "Unsupported file type.", + "dropPartiallyAttached_one": "{{failureCount}} of {{count}} item could not be attached.", + "dropPartiallyAttached_other": "{{failureCount}} of {{count}} items could not be attached." + }, "useIpcEvents": { "60428567b4": "Local terminal reveal is unavailable while a remote runtime is active", "f6300deb8b": "New Browser Tab" @@ -2610,6 +2618,9 @@ }, "components": { "native-chat": { + "approval": { + "cancel": "Cancel" + }, "composer": { "effort": "Effort" }, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 038e4fe2b5b..2a14bebce64 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -990,14 +990,20 @@ "useComposerState": { "7eb3f44ff7": "Selected agent is disabled. Choose an enabled agent before creating.", "b2ead86962": "Failed to resolve PR base.", - "a9ff236145": "Some attachments could not be uploaded.", "3db83fc58a": "No project path is available on this host for attachments.", "ba6cb77082": "Failed to connect to project.", "chooseOrAddProjectBeforeWorkspace": "Choose or add a project before creating a workspace.", "folderWorkspaceCreateFailedTitle": "Folder workspace creation failed", "folderWorkspaceCreateFailedMessage": "The folder workspace could not be created. Check the error details above, then try again.", "setupAgentStartupPolicySaveFailed": "Failed to save setup startup behavior.", - "5f3d2c8a1b": "Failed to resolve MR base." + "5f3d2c8a1b": "Failed to resolve MR base.", + "dropPartiallyAttached": "{{failureCount}} of {{count}} items could not be attached.", + "dropPartiallyAttached_one": "{{failureCount}} of {{count}} item could not be attached.", + "dropPartiallyAttached_other": "{{failureCount}} of {{count}} items could not be attached.", + "attachSkipMissing": "No longer at its original path.", + "attachSkipSymlink": "Symbolic links cannot be attached.", + "attachSkipPermissionDenied": "Permission denied.", + "attachSkipUnsupported": "Unsupported file type." }, "useGlobalFileDrop": { "38c9f034ff": "Failed to upload dropped files.", @@ -11810,7 +11816,9 @@ "storedAuthFailed": "The saved Bitbucket credential could not authenticate. Edit it, or check that the token still has pull request access.", "storedCredential": "Saved in Orca on this machine. ORCA_BITBUCKET_* environment variables take precedence when set.", "notConfigured": "Connect a Bitbucket Cloud account with an Atlassian API token or an access token. ORCA_BITBUCKET_* environment variables work too and take precedence.", - "disconnectFailed": "Could not remove the saved Bitbucket credential." + "disconnectFailed": "Could not remove the saved Bitbucket credential.", + "statusLoadFailed": "Could not check for a saved Bitbucket credential.", + "replaceCredentials": "Add or replace credentials" } } }, @@ -12372,7 +12380,12 @@ "a4e93c21d7": "Current branch: {{value0}}", "c7d4e2f801": "Change base ref: {{value0}}", "f3a1b8c204": "upstream", - "createPrIntentGenerateDetailsFailed": "Could not generate review details. Retry Create PR." + "createPrIntentGenerateDetailsFailed": "Could not generate review details. Retry Create PR.", + "entryStageFailed": "Failed to stage “{{value0}}”", + "entryUnstageFailed": "Failed to unstage “{{value0}}”", + "entryDiscardFailed": "Failed to discard “{{value0}}”", + "entryDeleteFailed": "Failed to delete “{{value0}}”", + "entryFailedInWorkspace": "{{value0}} in {{value1}}" }, "SourceControlAgentActionDialog": { "8e856842d1": "Could not start the selected agent.", @@ -14298,7 +14311,8 @@ "4e71d72912": "Claude sign-in failed.", "9ddeb558f9": "Claude account added.", "29d0653961": "Sign in", - "945865332e": "Signing in" + "945865332e": "Signing in", + "accountStatusUnknown": "Account status unknown" }, "UsagePage": { "64265cb295": "29% used 5h", @@ -17241,7 +17255,8 @@ "approval": { "title": "Allow {{value0}}?", "allow": "Allow", - "deny": "Deny" + "deny": "Deny", + "cancel": "Cancel" }, "launchPromptNotDelivered": "Not delivered — check the terminal", "structuredSessionCloseFailed": "Could not close this chat session", @@ -17307,7 +17322,8 @@ "drop": { "title": "Drop to attach to this chat", "subtitle": "Files are added to your message as paths the agent can read." - } + }, + "copyCode": "Copy code" }, "tab": { "bar": { @@ -17871,5 +17887,15 @@ "stopped": "stopped", "finished": "finished" } + }, + "aiVault": { + "subagents": { + "loading": "Loading subagents…", + "loadError": "Could not load all subagents.", + "empty": "No subagents found." + } + }, + "common": { + "retry": "Retry" } } diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 61c3f812f5f..2a47a73502d 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -713,7 +713,6 @@ "useComposerState": { "7eb3f44ff7": "El agente seleccionado está deshabilitado. Elige un agente habilitado antes de crear.", "b2ead86962": "No se pudo resolver la base del PR.", - "a9ff236145": "Algunos archivos adjuntos no se pudieron cargar.", "3db83fc58a": "No hay ninguna ruta de proyecto remoto disponible para los archivos adjuntos.", "ba6cb77082": "No se pudo conectar al proyecto.", "chooseOrAddProjectBeforeWorkspace": "Elige o agrega un proyecto antes de crear un espacio de trabajo.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index c21e08e7a5a..3a56fce87a6 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -835,7 +835,6 @@ "useComposerState": { "7eb3f44ff7": "L'agent sélectionné est désactivé. Choisissez un agent activé avant de créer.", "b2ead86962": "Échec de la résolution de la base de la PR.", - "a9ff236145": "Certaines pièces jointes n'ont pas pu être envoyées.", "3db83fc58a": "Aucun chemin de projet n'est disponible sur cet hôte pour les pièces jointes.", "ba6cb77082": "Échec de la connexion au projet.", "chooseOrAddProjectBeforeWorkspace": "Choisissez ou ajoutez un projet avant de créer un espace de travail.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 422e2b39fbc..696135a7669 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -713,7 +713,6 @@ "useComposerState": { "7eb3f44ff7": "選択した Agent は無効です。作成する前に、有効な Agent を選択してください。", "b2ead86962": "PR ベースを解決できませんでした。", - "a9ff236145": "一部の添付ファイルをアップロードできませんでした。", "3db83fc58a": "このホスト上に、添付に使用できるプロジェクトパスがありません。", "ba6cb77082": "プロジェクトへの接続に失敗しました。", "chooseOrAddProjectBeforeWorkspace": "ワークスペースを作成する前に、プロジェクトを選択または追加してください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 45d2d30a225..5fd1e6addb8 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -716,7 +716,6 @@ "useComposerState": { "7eb3f44ff7": "선택한 agent가 비활성화되었습니다. 생성하기 전에 활성화된 agent를 선택하세요.", "b2ead86962": "PR 기반을 해결하지 못했습니다.", - "a9ff236145": "일부 첨부파일을 업로드할 수 없습니다.", "3db83fc58a": "이 호스트에 첨부 파일에 사용할 수 있는 프로젝트 경로가 없습니다.", "ba6cb77082": "프로젝트에 연결하지 못했습니다.", "chooseOrAddProjectBeforeWorkspace": "워크스페이스를 만들기 전에 프로젝트를 선택하거나 추가하세요.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index f64188d7201..125876be1a0 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -716,7 +716,6 @@ "useComposerState": { "7eb3f44ff7": "所选智能体已禁用。创建之前选择启用的智能体。", "b2ead86962": "无法解析 PR 基础引用。", - "a9ff236145": "部分附件无法上传。", "3db83fc58a": "没有可用于附件的远程项目路径。", "ba6cb77082": "无法连接到项目。", "chooseOrAddProjectBeforeWorkspace": "创建工作区前,请选择或添加项目。", diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index b6075183947..cf46fd9aae7 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -21,6 +21,7 @@ export type AgentCatalogEntry = { label: string /** Default CLI binary name used for PATH detection. */ cmd: string + searchAliases?: readonly string[] /** Direct or bundled image URL for agents whose project identity is not represented by a favicon service. */ iconUrl?: string /** Domain for Google's favicon service — used for agents without an SVG icon. */ @@ -123,6 +124,7 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => id: 'omp', label: translate('auto.lib.agent.catalog.09973b4d84', 'OMP'), cmd: 'omp', + searchAliases: ['oh-my-pi', 'oh my pi'], // Why: no faviconDomain — omp renders the hand-authored OmpIcon glyph, so a // favicon fallback would never be reached. homepageUrl: 'https://omp.sh' diff --git a/src/renderer/src/lib/agent-picker-search.test.ts b/src/renderer/src/lib/agent-picker-search.test.ts index fe26fd53dc3..12fe441103f 100644 --- a/src/renderer/src/lib/agent-picker-search.test.ts +++ b/src/renderer/src/lib/agent-picker-search.test.ts @@ -25,6 +25,15 @@ afterEach(() => { }) describe('agent picker search', () => { + it.each(['oh-my-pi', 'oh my pi', 'OH-MY-PI'])('finds OMP by its project name: %s', (query) => { + expect(searchAgentPickerEntries(AGENT_CATALOG, query).map((agent) => agent.id)).toEqual(['omp']) + }) + + it('does not offer unavailable OMP through a search alias', () => { + const available = AGENT_CATALOG.filter((agent) => agent.id !== 'omp') + expect(searchAgentPickerEntries(available, 'oh-my-pi')).toEqual([]) + }) + it('keeps catalog order for an empty query', () => { expect(searchAgentPickerEntries(agents, '').map((agent) => agent.id)).toEqual( agents.map((agent) => agent.id) diff --git a/src/renderer/src/lib/agent-picker-search.ts b/src/renderer/src/lib/agent-picker-search.ts index 6fb980bc8ea..38c368f3d73 100644 --- a/src/renderer/src/lib/agent-picker-search.ts +++ b/src/renderer/src/lib/agent-picker-search.ts @@ -87,7 +87,8 @@ function scoreAgent(agent: AgentCatalogEntry, query: string): number { return Math.min( scoreCandidate(query, agent.label, 0), scoreCandidate(query, agent.id, 600), - scoreCandidate(query, agent.cmd, 650) + scoreCandidate(query, agent.cmd, 650), + ...(agent.searchAliases ?? []).map((alias) => scoreCandidate(query, alias, 650)) ) } diff --git a/src/renderer/src/lib/ipc-error.test.ts b/src/renderer/src/lib/ipc-error.test.ts index 52d789f0027..cf7ba9d6817 100644 --- a/src/renderer/src/lib/ipc-error.test.ts +++ b/src/renderer/src/lib/ipc-error.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { extractIpcErrorMessage, readIpcErrorDetail, readIpcErrorMessage } from './ipc-error' +import { + compactIpcErrorMessage, + extractIpcErrorMessage, + readIpcErrorDetail, + readIpcErrorMessage +} from './ipc-error' describe('readIpcErrorMessage', () => { it('strips the Electron invoke wrapper Electron adds to a rejected handler', () => { @@ -45,6 +50,16 @@ describe('readIpcErrorMessage', () => { }) }) +describe('compactIpcErrorMessage', () => { + it('normalizes string error fields without manufacturing an Error', () => { + expect( + compactIpcErrorMessage( + "Error invoking remote method 'files:import': Error: permission denied\nstack" + ) + ).toBe('permission denied') + }) +}) + describe('extractIpcErrorMessage', () => { it('unwraps the same way readIpcErrorMessage does', () => { expect( diff --git a/src/renderer/src/lib/ipc-error.ts b/src/renderer/src/lib/ipc-error.ts index d9ef8a364eb..020bc7e9629 100644 --- a/src/renderer/src/lib/ipc-error.ts +++ b/src/renderer/src/lib/ipc-error.ts @@ -2,19 +2,20 @@ const IPC_INVOKE_PREFIX = /Error invoking remote method '[^']*':\s*(?:Error:\s*)?/ const IPC_HANDLER_PREFIX = /Error occurred in handler for '[^']*':\s*(?:Error:\s*)?/ +function unwrapIpcErrorMessage(message: string): string | undefined { + const detail = message.replace(IPC_INVOKE_PREFIX, '').replace(IPC_HANDLER_PREFIX, '').trim() + return detail || undefined +} + +export function compactIpcErrorMessage(message: string): string | undefined { + return unwrapIpcErrorMessage(message)?.split('\n')[0]?.trim() || undefined +} export function readIpcErrorDetail(error: unknown): string | undefined { - if (!(error instanceof Error)) { - return undefined - } - const message = error.message - .replace(IPC_INVOKE_PREFIX, '') - .replace(IPC_HANDLER_PREFIX, '') - .trim() - return message || undefined + return error instanceof Error ? unwrapIpcErrorMessage(error.message) : undefined } export function readIpcErrorMessage(error: unknown): string | undefined { - return readIpcErrorDetail(error)?.split('\n')[0]?.trim() || undefined + return error instanceof Error ? compactIpcErrorMessage(error.message) : undefined } // Preserve the legacy contract: wrapped errors are compact, while plain errors retain detail. diff --git a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.ts b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.ts index 13f90af696c..6cdb5dd7508 100644 --- a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.ts +++ b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.ts @@ -12,10 +12,16 @@ import type { Terminal } from '@xterm/xterm' * and the pane blinks — redrawing its whole cursor row through * `WebglRenderer._updateModel` — until the 5-minute idle timeout. * - * `cursorBlink` is the public option that tears the timer down deterministically + * `cursorBlink` is the public option that tears the timer down * (`RenderService.handleOptionsChanged` -> `WebglRenderer._updateCursorBlink`), so * "a hidden pane does not blink" stops depending on which CSS hid it. * + * Not unconditional, though: `_updateCursorBlink` resolves + * `decPrivateModes.cursorBlink ?? options.cursorBlink`, and DECSCUSR with a + * blinking style (`CSI 5 SP q`) pins that DEC mode. On a pane whose shell or agent + * has emitted one, parking the option here has no effect and the hidden pane keeps + * blinking. Making this deterministic means clearing the DEC mode too. + * * Resume restores the parked value rather than the settings value, so a pane that * was not blinking before the hide never comes back blinking. */ diff --git a/src/renderer/src/lib/structured-agent-session-launch.test.ts b/src/renderer/src/lib/structured-agent-session-launch.test.ts index 166382f94ea..458bb08e2e4 100644 --- a/src/renderer/src/lib/structured-agent-session-launch.test.ts +++ b/src/renderer/src/lib/structured-agent-session-launch.test.ts @@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({ callStructuredAgentSession: vi.fn(), createIntent: vi.fn(), retryIntent: vi.fn(), + restoreIntent: vi.fn(), launch: vi.fn(), seedDraft: vi.fn(), clearDraft: vi.fn(), diff --git a/src/renderer/src/runtime/runtime-file-client-external-import.test.ts b/src/renderer/src/runtime/runtime-file-client-external-import.test.ts index fb0f0e8e092..52f0bb83d09 100644 --- a/src/renderer/src/runtime/runtime-file-client-external-import.test.ts +++ b/src/renderer/src/runtime/runtime-file-client-external-import.test.ts @@ -4,6 +4,7 @@ import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revisi import { fsImportExternalPaths, fsStageExternalPathsForRuntimeUpload, + fsUploadExternalFileToRuntime, runtimeEnvironmentCall, runtimeEnvironmentTransportCall, installRuntimeFileClientEnvironment @@ -11,11 +12,65 @@ import { installRuntimeFileClientEnvironment() +const okResponse = (id: string): unknown => ({ + id, + ok: true, + result: { ok: true }, + _meta: { runtimeId: 'remote-runtime' } +}) + +const notFoundResponse = (id: string): unknown => ({ + id, + ok: false, + error: { code: 'not_found', message: 'not found' }, + _meta: { runtimeId: 'remote-runtime' } +}) + +/** Matches what main-process staging now records for a file entry. */ +const stagedFile = ( + relativePath: string, + byteLength: number, + inode: number +): Record => ({ + relativePath, + kind: 'file', + byteLength, + inode, + deviceId: 66, + modifiedAtMs: 1_700_000_000_000 +}) + +/** The upload request main receives; `never[]` mock args widen to it without a cast. */ +type UploadRequest = { + environmentId: string + sourceRootPath: string + entryRelativePath: string + expected: Record + worktree: string + relativePath: string + expectedExecutionHostId?: string + expectedSshTargetId?: string + expectedSshConnectionGeneration?: number + expectedEnvironmentPairingRevision?: number + expectedEnvironmentRuntimeId?: string +} + +function uploadRequests(): UploadRequest[] { + return fsUploadExternalFileToRuntime.mock.calls.flat() +} + +const identityOf = (entry: Record): Record => ({ + byteLength: entry.byteLength, + inode: entry.inode, + deviceId: entry.deviceId, + modifiedAtMs: entry.modifiedAtMs +}) + describe('runtime file client', () => { - it('uploads a staged directory after one ownership and one cold compatibility preflight', async () => { + it('streams staged directory entries through main instead of sending base64 itself', async () => { replaceRuntimeEnvironmentRevisions([{ id: 'env-1', createdAt: 1, pairingRevision: 17 }]) - const firstChunk = 'A'.repeat(512 * 1024) - const secondChunk = 'BBBBBBBB' + const logo = stagedFile('logo.png', 3, 101) + const large = stagedFile('large.bin', 40 * 1024 * 1024, 102) fsStageExternalPathsForRuntimeUpload.mockResolvedValue({ sources: [ { @@ -23,85 +78,19 @@ describe('runtime file client', () => { status: 'staged', name: 'assets', kind: 'directory', - entries: [ - { relativePath: '', kind: 'directory' }, - { relativePath: 'logo.png', kind: 'file', contentBase64: 'cG5n' }, - { - relativePath: 'large.bin', - kind: 'file', - contentBase64: `${firstChunk}${secondChunk}` - } - ] + entries: [{ relativePath: '', kind: 'directory' }, logo, large] } ] }) runtimeEnvironmentCall - .mockResolvedValueOnce({ - id: 'stat-destination-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'create-destination-dir', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'stat-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'create-dir', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-file', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'commit-upload', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-temp', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-1', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-2', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'commit-large-upload', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-large-temp', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) + .mockResolvedValueOnce(notFoundResponse('stat-destination-miss')) + .mockResolvedValueOnce(okResponse('create-destination-dir')) + .mockResolvedValueOnce(notFoundResponse('stat-miss')) + .mockResolvedValueOnce(okResponse('create-dir')) + .mockResolvedValueOnce(okResponse('commit-upload')) + .mockResolvedValueOnce(okResponse('delete-temp')) + .mockResolvedValueOnce(okResponse('commit-large-upload')) + .mockResolvedValueOnce(okResponse('delete-large-temp')) await expect( importExternalPathsToRuntime( @@ -136,75 +125,48 @@ describe('runtime file client', () => { 'files.createDir', 'files.stat', 'files.createDirNoClobber', - 'files.writeBase64', 'files.commitUpload', 'files.delete', - 'files.writeBase64Chunk', - 'files.writeBase64Chunk', 'files.commitUpload', 'files.delete' ]) - expect(transportCalls.filter((args) => args.method === 'status.get')).toHaveLength(2) + // Why: the whole point of the change — no file body crosses this boundary. + expect(transportCalls.some((args) => String(args.method).startsWith('files.writeBase64'))).toBe( + false + ) expect(transportCalls.every((args) => args.expectedEnvironmentPairingRevision === 17)).toBe( true ) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(1, { - selector: 'env-1', - method: 'files.stat', - params: { - worktree: 'id:wt-1', - relativePath: 'uploads' - }, - timeoutMs: 15_000, - expectedEnvironmentPairingRevision: 17 - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(2, { - selector: 'env-1', - method: 'files.createDir', - params: { - worktree: 'id:wt-1', - relativePath: 'uploads', - expectedExecutionHostId: 'local' - }, - timeoutMs: 15_000, + + const uploads = uploadRequests() + expect(uploads).toHaveLength(2) + expect(uploads[0]?.relativePath).toMatch(/^uploads\/assets\/\.logo\.png\.orca-upload-/) + expect(uploads[0]).toEqual({ + environmentId: 'env-1', + sourceRootPath: '/Users/me/assets', + entryRelativePath: 'logo.png', + expected: identityOf(logo), + worktree: 'id:wt-1', + relativePath: uploads[0]?.relativePath, + expectedExecutionHostId: 'local', + expectedSshTargetId: undefined, + expectedSshConnectionGeneration: undefined, expectedEnvironmentPairingRevision: 17, expectedEnvironmentRuntimeId: 'remote-runtime' }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(3, { - selector: 'env-1', - method: 'files.stat', - params: { - worktree: 'id:wt-1', - relativePath: 'uploads/assets' - }, - timeoutMs: 15_000, - expectedEnvironmentPairingRevision: 17 + expect(uploads[1]).toMatchObject({ + entryRelativePath: 'large.bin', + expected: identityOf(large) }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(4, { - selector: 'env-1', - method: 'files.createDirNoClobber', - params: { - worktree: 'id:wt-1', - relativePath: 'uploads/assets', - expectedExecutionHostId: 'local' - }, - timeoutMs: 15_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - const smallWriteCall = runtimeEnvironmentCall.mock.calls[4]?.[0] as { - params: { relativePath: string } - } - expect(smallWriteCall.params.relativePath).toMatch( - /^uploads\/assets\/\.logo\.png\.orca-upload-/ - ) + expect(uploads[1]?.relativePath).toMatch(/^uploads\/assets\/\.large\.bin\.orca-upload-/) + expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(5, { selector: 'env-1', - method: 'files.writeBase64', + method: 'files.commitUpload', params: { worktree: 'id:wt-1', - relativePath: smallWriteCall.params.relativePath, - contentBase64: 'cG5n', + tempRelativePath: uploads[0]?.relativePath, + finalRelativePath: 'uploads/assets/logo.png', expectedExecutionHostId: 'local', expectedSshTargetId: undefined, expectedSshConnectionGeneration: undefined @@ -214,99 +176,11 @@ describe('runtime file client', () => { expectedEnvironmentRuntimeId: 'remote-runtime' }) expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(6, { - selector: 'env-1', - method: 'files.commitUpload', - params: { - worktree: 'id:wt-1', - tempRelativePath: smallWriteCall.params.relativePath, - finalRelativePath: 'uploads/assets/logo.png', - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(7, { selector: 'env-1', method: 'files.delete', params: { worktree: 'id:wt-1', - relativePath: smallWriteCall.params.relativePath, - recursive: false, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 15_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - const largeWriteParams = runtimeEnvironmentCall.mock.calls[7]?.[0].params - if ( - typeof largeWriteParams !== 'object' || - largeWriteParams === null || - !('relativePath' in largeWriteParams) || - typeof largeWriteParams.relativePath !== 'string' - ) { - throw new Error('missing large file write call') - } - const largeWriteRelativePath = largeWriteParams.relativePath - expect(largeWriteRelativePath).toMatch(/^uploads\/assets\/\.large\.bin\.orca-upload-/) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(8, { - selector: 'env-1', - method: 'files.writeBase64Chunk', - params: { - worktree: 'id:wt-1', - relativePath: largeWriteRelativePath, - contentBase64: firstChunk, - append: false, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(9, { - selector: 'env-1', - method: 'files.writeBase64Chunk', - params: { - worktree: 'id:wt-1', - relativePath: largeWriteRelativePath, - contentBase64: secondChunk, - append: true, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(10, { - selector: 'env-1', - method: 'files.commitUpload', - params: { - worktree: 'id:wt-1', - tempRelativePath: largeWriteRelativePath, - finalRelativePath: 'uploads/assets/large.bin', - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: 17, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(11, { - selector: 'env-1', - method: 'files.delete', - params: { - worktree: 'id:wt-1', - relativePath: largeWriteRelativePath, + relativePath: uploads[0]?.relativePath, recursive: false, expectedExecutionHostId: 'local', expectedSshTargetId: undefined, @@ -319,9 +193,8 @@ describe('runtime file client', () => { expect(fsImportExternalPaths).not.toHaveBeenCalled() }) - it('chunks large staged runtime uploads below the WebSocket frame budget', async () => { - const firstChunk = 'A'.repeat(512 * 1024) - const secondChunk = 'AA==' + it('forwards a single staged file with the identity staging measured', async () => { + const entry = stagedFile('', 40 * 1024 * 1024, 55) fsStageExternalPathsForRuntimeUpload.mockResolvedValue({ sources: [ { @@ -329,55 +202,16 @@ describe('runtime file client', () => { status: 'staged', name: 'large.bin', kind: 'file', - entries: [ - { relativePath: '', kind: 'file', contentBase64: `${firstChunk}${secondChunk}` } - ] + entries: [entry] } ] }) runtimeEnvironmentCall - .mockResolvedValueOnce({ - id: 'stat-destination-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'create-destination-dir', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'stat-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-1', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-2', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'commit-upload', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-temp', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) + .mockResolvedValueOnce(notFoundResponse('stat-destination-miss')) + .mockResolvedValueOnce(okResponse('create-destination-dir')) + .mockResolvedValueOnce(notFoundResponse('stat-miss')) + .mockResolvedValueOnce(okResponse('commit-upload')) + .mockResolvedValueOnce(okResponse('delete-temp')) await expect( importExternalPathsToRuntime( @@ -401,79 +235,20 @@ describe('runtime file client', () => { ] }) - const chunkWriteCall = runtimeEnvironmentCall.mock.calls[3]?.[0] as { - params: { relativePath: string } - } - expect(chunkWriteCall.params.relativePath).toMatch(/^uploads\/\.large\.bin\.orca-upload-/) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(4, { - selector: 'env-1', - method: 'files.writeBase64Chunk', - params: { - worktree: 'id:wt-1', - relativePath: chunkWriteCall.params.relativePath, - contentBase64: firstChunk, - append: false, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: undefined, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(5, { - selector: 'env-1', - method: 'files.writeBase64Chunk', - params: { - worktree: 'id:wt-1', - relativePath: chunkWriteCall.params.relativePath, - contentBase64: secondChunk, - append: true, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: undefined, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(6, { - selector: 'env-1', - method: 'files.commitUpload', - params: { - worktree: 'id:wt-1', - tempRelativePath: chunkWriteCall.params.relativePath, - finalRelativePath: 'uploads/large.bin', - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 30_000, - expectedEnvironmentPairingRevision: undefined, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(7, { - selector: 'env-1', - method: 'files.delete', - params: { - worktree: 'id:wt-1', - relativePath: chunkWriteCall.params.relativePath, - recursive: false, - expectedExecutionHostId: 'local', - expectedSshTargetId: undefined, - expectedSshConnectionGeneration: undefined - }, - timeoutMs: 15_000, - expectedEnvironmentPairingRevision: undefined, - expectedEnvironmentRuntimeId: 'remote-runtime' - }) + const upload = uploadRequests()[0] + expect(upload?.relativePath).toMatch(/^uploads\/\.large\.bin\.orca-upload-/) + expect(upload?.sourceRootPath).toBe('/Users/me/large.bin') + expect(upload?.entryRelativePath).toBe('') + expect(upload?.expected).toEqual(identityOf(entry)) expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( expect.objectContaining({ method: 'files.writeBase64' }) ) + expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( + expect.objectContaining({ method: 'files.writeBase64Chunk' }) + ) }) - it('stops a chunked upload when its owner generation changes between writes', async () => { - const firstChunk = 'A'.repeat(512 * 1024) + it('does not commit an upload when the owner generation changes while it streams', async () => { fsStageExternalPathsForRuntimeUpload.mockResolvedValue({ sources: [ { @@ -481,7 +256,7 @@ describe('runtime file client', () => { status: 'staged', name: 'large.bin', kind: 'file', - entries: [{ relativePath: '', kind: 'file', contentBase64: `${firstChunk}BBBBBBBB` }] + entries: [stagedFile('', 40 * 1024 * 1024, 55)] } ] }) @@ -492,22 +267,12 @@ describe('runtime file client', () => { result: { size: 0, isDirectory: true, mtime: 1 }, _meta: { runtimeId: 'remote-runtime' } }) - .mockResolvedValueOnce({ - id: 'stat-file-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockImplementationOnce(async () => { - ownerChanged = true - return { - id: 'write-chunk-1', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - } - }) + .mockResolvedValueOnce(notFoundResponse('stat-file-miss')) let ownerChanged = false + fsUploadExternalFileToRuntime.mockImplementation(async () => { + ownerChanged = true + return { byteLength: 40 * 1024 * 1024 } + }) const assertCurrent = vi.fn(() => { if (ownerChanged) { throw new Error('runtime owner generation changed') @@ -531,20 +296,14 @@ describe('runtime file client', () => { expect(runtimeEnvironmentCall.mock.calls.map((call) => call[0].method)).toEqual([ 'files.stat', - 'files.stat', - 'files.writeBase64Chunk' + 'files.stat' ]) expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( expect.objectContaining({ method: 'files.commitUpload' }) ) - expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( - expect.objectContaining({ method: 'files.delete' }) - ) }) - it('cleans up staged runtime upload temp files when a later chunk fails', async () => { - const firstChunk = 'A'.repeat(512 * 1024) - const secondChunk = 'BBBBBBBB' + it('cleans up the staged temp path when the streamed upload fails', async () => { fsStageExternalPathsForRuntimeUpload.mockResolvedValue({ sources: [ { @@ -552,49 +311,19 @@ describe('runtime file client', () => { status: 'staged', name: 'large.bin', kind: 'file', - entries: [ - { relativePath: '', kind: 'file', contentBase64: `${firstChunk}${secondChunk}` } - ] + entries: [stagedFile('', 40 * 1024 * 1024, 55)] } ] }) runtimeEnvironmentCall - .mockResolvedValueOnce({ - id: 'stat-destination-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'create-destination-dir', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'stat-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-1', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-chunk-2', - ok: false, - error: { code: 'write_failed', message: 'disk full' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-temp', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) + .mockResolvedValueOnce(notFoundResponse('stat-destination-miss')) + .mockResolvedValueOnce(okResponse('create-destination-dir')) + .mockResolvedValueOnce(notFoundResponse('stat-miss')) + .mockResolvedValueOnce(okResponse('delete-temp')) + // Electron wraps a main-process throw; the reason must not leak that. + fsUploadExternalFileToRuntime.mockRejectedValue( + new Error("Error invoking remote method 'fs:uploadExternalFileToRuntime': Error: disk full") + ) await expect( importExternalPathsToRuntime( @@ -610,13 +339,7 @@ describe('runtime file client', () => { results: [{ status: 'failed', reason: 'disk full' }] }) - const chunkCall = runtimeEnvironmentCall.mock.calls[3]?.[0] as - | { params: { relativePath: string } } - | undefined - if (!chunkCall) { - throw new Error('missing first chunk call') - } - const tempRelativePath = chunkCall.params.relativePath + const tempRelativePath = uploadRequests()[0]?.relativePath expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( expect.objectContaining({ method: 'files.commitUpload' }) ) @@ -645,10 +368,7 @@ describe('runtime file client', () => { status: 'staged', name: 'assets', kind: 'directory', - entries: [ - { relativePath: '', kind: 'directory' }, - { relativePath: 'logo.png', kind: 'file', contentBase64: 'cG5n' } - ] + entries: [{ relativePath: '', kind: 'directory' }, stagedFile('logo.png', 3, 101)] } ] }) @@ -659,36 +379,11 @@ describe('runtime file client', () => { result: { size: 0, isDirectory: true, mtime: 1 }, _meta: { runtimeId: 'remote-runtime' } }) - .mockResolvedValueOnce({ - id: 'stat-import-root-miss', - ok: false, - error: { code: 'not_found', message: 'not found' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'create-import-root', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'write-file', - ok: false, - error: { code: 'write_failed', message: 'disk full' }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-temp', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) - .mockResolvedValueOnce({ - id: 'delete-import-root', - ok: true, - result: { ok: true }, - _meta: { runtimeId: 'remote-runtime' } - }) + .mockResolvedValueOnce(notFoundResponse('stat-import-root-miss')) + .mockResolvedValueOnce(okResponse('create-import-root')) + .mockResolvedValueOnce(okResponse('delete-temp')) + .mockResolvedValueOnce(okResponse('delete-import-root')) + fsUploadExternalFileToRuntime.mockRejectedValue(new Error('disk full')) await expect( importExternalPathsToRuntime( @@ -704,13 +399,7 @@ describe('runtime file client', () => { results: [{ status: 'failed', reason: 'disk full' }] }) - const writeCall = runtimeEnvironmentCall.mock.calls[3]?.[0] as - | { params: { relativePath: string } } - | undefined - if (!writeCall) { - throw new Error('missing failed file write call') - } - expect(writeCall.params.relativePath).toMatch(/^uploads\/assets\/\.logo\.png\.orca-upload-/) + expect(uploadRequests()[0]?.relativePath).toMatch(/^uploads\/assets\/\.logo\.png\.orca-upload-/) expect(runtimeEnvironmentCall).toHaveBeenLastCalledWith({ selector: 'env-1', method: 'files.delete', @@ -763,6 +452,7 @@ describe('runtime file client', () => { expectedSshConnectionGeneration: 5 }) expect(fsStageExternalPathsForRuntimeUpload).not.toHaveBeenCalled() + expect(fsUploadExternalFileToRuntime).not.toHaveBeenCalled() expect(runtimeEnvironmentCall).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/runtime/runtime-file-client-test-harness.ts b/src/renderer/src/runtime/runtime-file-client-test-harness.ts index 844080626be..321c6abe6d1 100644 --- a/src/renderer/src/runtime/runtime-file-client-test-harness.ts +++ b/src/renderer/src/runtime/runtime-file-client-test-harness.ts @@ -54,6 +54,7 @@ export const fsFinishDownloadedFile: PreloadStub = vi.fn() export const fsCancelDownloadedFile: PreloadStub = vi.fn() export const fsImportExternalPaths: PreloadStub = vi.fn() export const fsStageExternalPathsForRuntimeUpload: PreloadStub = vi.fn() +export const fsUploadExternalFileToRuntime: PreloadStub = vi.fn() export const runtimeEnvironmentCall: RuntimeRpcStub = vi.fn() export const runtimeEnvironmentTransportCall: RuntimeRpcStub = vi.fn() export const runtimeEnvironmentSubscribe: RuntimeSubscribeStub = vi.fn() @@ -88,6 +89,8 @@ export function installRuntimeFileClientEnvironment(): void { fsCancelDownloadedFile.mockReset() fsImportExternalPaths.mockReset() fsStageExternalPathsForRuntimeUpload.mockReset() + fsUploadExternalFileToRuntime.mockReset() + fsUploadExternalFileToRuntime.mockResolvedValue({ byteLength: 0 }) runtimeEnvironmentCall.mockReset() runtimeEnvironmentTransportCall.mockReset() runtimeEnvironmentSubscribe.mockReset() @@ -131,7 +134,8 @@ export function installRuntimeFileClientEnvironment(): void { finishDownloadedFile: fsFinishDownloadedFile, cancelDownloadedFile: fsCancelDownloadedFile, importExternalPaths: fsImportExternalPaths, - stageExternalPathsForRuntimeUpload: fsStageExternalPathsForRuntimeUpload + stageExternalPathsForRuntimeUpload: fsStageExternalPathsForRuntimeUpload, + uploadExternalFileToRuntime: fsUploadExternalFileToRuntime }, runtime: { call: runtimeCall }, runtimeEnvironments: { diff --git a/src/renderer/src/runtime/runtime-file-import-client.ts b/src/renderer/src/runtime/runtime-file-import-client.ts index 7e274343d0a..2aebfbcfa8c 100644 --- a/src/renderer/src/runtime/runtime-file-import-client.ts +++ b/src/renderer/src/runtime/runtime-file-import-client.ts @@ -1,4 +1,5 @@ import { basename, joinPath } from '@/lib/path' +import type { ImportItemResult } from '../../../shared/filesystem-import-result-types' import { getRuntimeEnvironmentConnectionGeneration } from '@/store/slices/runtime-status' import type { RuntimeFileOperationArgs } from './runtime-file-client-types' import { captureRuntimeEnvironmentRequestRevision } from './runtime-environment-revision' @@ -21,50 +22,12 @@ import { import { getActiveRuntimeTarget } from './runtime-rpc-client' import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' -type StagedRuntimeImportSource = - | { - sourcePath: string - status: 'staged' - name: string - kind: 'file' | 'directory' - entries: StagedRuntimeImportEntry[] - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { sourcePath: string; status: 'failed'; reason: string } - -type StagedRuntimeImportEntry = - | { relativePath: string; kind: 'directory' } - | { relativePath: string; kind: 'file'; contentBase64: string } - -type RuntimeImportResult = - | { - sourcePath: string - status: 'imported' - destPath: string - kind: 'file' | 'directory' - renamed: boolean - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - export async function importExternalPathsToRuntime( context: RuntimeFileOperationArgs, sourcePaths: string[], destinationDir: string, options?: { ensureDestinationDir?: boolean; assertCurrent?: () => void } -): Promise<{ results: RuntimeImportResult[] }> { +): Promise<{ results: ImportItemResult[] }> { const target = getActiveRuntimeTarget(context.settings) if (target.kind !== 'environment' || !context.worktreeId || !context.worktreePath) { return window.api.fs.importExternalPaths( @@ -108,12 +71,12 @@ export async function importExternalPathsToRuntime( importSession.assertCurrent() const staged = await window.api.fs.stageExternalPathsForRuntimeUpload({ sourcePaths }) importSession.assertCurrent() - const results: RuntimeImportResult[] = [] + const results: ImportItemResult[] = [] const reservedNames = new Set() await ensureRuntimeDirectory(context, destinationDir, importSession) - for (const source of staged.sources as StagedRuntimeImportSource[]) { + for (const source of staged.sources) { if (source.status !== 'staged') { results.push(source) continue @@ -150,7 +113,16 @@ export async function importExternalPathsToRuntime( importSession, context.worktreeId, entryRelativePath, - entry.contentBase64, + { + sourceRootPath: source.sourcePath, + entryRelativePath: entry.relativePath, + expected: { + byteLength: entry.byteLength, + inode: entry.inode, + deviceId: entry.deviceId, + modifiedAtMs: entry.modifiedAtMs + } + }, context.expectedSshConnectionGeneration, context.expectedSshTargetId, context.expectedExecutionHostId ?? diff --git a/src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts b/src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts index 34da21c8966..eb8dad53970 100644 --- a/src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts +++ b/src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts @@ -32,6 +32,7 @@ type RuntimeCallArgs = { const runtimeEnvironmentCall = vi.fn<(args: RuntimeCallArgs) => unknown>() const stageExternalPathsForRuntimeUpload = vi.fn() +const uploadExternalFileToRuntime = vi.fn<(args: Record) => unknown>() const importExternalPaths = vi.fn() const nestedSshContext = { @@ -99,7 +100,8 @@ function repairedRuntimeResponse(method: string) { } } -function mockStagedFile(sourcePath: string, name: string, contentBase64: string): void { +/** Staging now hands over identity, not a body; the streamer in main reads the bytes. */ +function mockStagedFile(sourcePath: string, name: string, byteLength: number): void { stageExternalPathsForRuntimeUpload.mockResolvedValue({ sources: [ { @@ -107,12 +109,28 @@ function mockStagedFile(sourcePath: string, name: string, contentBase64: string) status: 'staged', name, kind: 'file', - entries: [{ relativePath: '', kind: 'file', contentBase64 }] + entries: [ + { + relativePath: '', + kind: 'file', + byteLength, + inode: 91, + deviceId: 66, + modifiedAtMs: 1_700_000_000_000 + } + ] } ] }) } +function expectUploadsBoundToCapturedRevision(): void { + for (const [args] of uploadExternalFileToRuntime.mock.calls) { + expect(args.expectedEnvironmentPairingRevision).toBe(CAPTURED_REVISION) + expect(args.expectedEnvironmentRuntimeId).toBe('hub-runtime') + } +} + function expectEveryRuntimeCallBoundToCapturedRevision(ownership: { expectedExecutionHostId: string expectedSshTargetId?: string @@ -146,12 +164,15 @@ beforeEach(() => { markRuntimeEnvironmentCompatible(ENVIRONMENT_ID) runtimeEnvironmentCall.mockReset() stageExternalPathsForRuntimeUpload.mockReset() + uploadExternalFileToRuntime.mockReset() + uploadExternalFileToRuntime.mockResolvedValue({ byteLength: 0 }) importExternalPaths.mockReset() vi.stubGlobal('window', { api: { fs: { importExternalPaths, - stageExternalPathsForRuntimeUpload + stageExternalPathsForRuntimeUpload, + uploadExternalFileToRuntime }, runtimeEnvironments: { call: runtimeEnvironmentCall @@ -183,7 +204,7 @@ describe('runtime file import pairing revision', () => { }) it('stops when the HUB runtime changes without a pairing change', async () => { - mockStagedFile('/client/screenshot.png', 'screenshot.png', `${'A'.repeat(512 * 1024)}BBBBBBBB`) + mockStagedFile('/client/screenshot.png', 'screenshot.png', 40 * 1024 * 1024) runtimeEnvironmentCall.mockImplementation(async (args: RuntimeCallArgs) => { if (args.method === 'status.get') { return runtimeStatusResponse() @@ -191,14 +212,15 @@ describe('runtime file import pairing revision', () => { if (args.method === 'files.stat') { return missingRuntimePathResponse() } - if (args.method === 'files.writeBase64Chunk') { - setRuntimeEnvironmentConnectionGenerationForTests( - ENVIRONMENT_ID, - REPLACEMENT_CONNECTION_GENERATION - ) - } return successfulRuntimeResponse(args.method) }) + uploadExternalFileToRuntime.mockImplementation(async () => { + setRuntimeEnvironmentConnectionGenerationForTests( + ENVIRONMENT_ID, + REPLACEMENT_CONNECTION_GENERATION + ) + return { byteLength: 40 * 1024 * 1024 } + }) await expect( importExternalPathsToRuntime(nestedSshContext, ['/client/screenshot.png'], '/ssh/repo') @@ -208,9 +230,9 @@ describe('runtime file import pairing revision', () => { expect(runtimeEnvironmentCall.mock.calls.map(([args]) => args.method)).toEqual([ 'status.get', - 'files.stat', - 'files.writeBase64Chunk' + 'files.stat' ]) + expectUploadsBoundToCapturedRevision() expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( expect.objectContaining({ method: 'files.commitUpload' }) ) @@ -236,8 +258,8 @@ describe('runtime file import pairing revision', () => { expect(importExternalPaths).not.toHaveBeenCalled() }) - it('stops a rich-markdown upload between chunks without contacting the replacement HUB', async () => { - mockStagedFile('/client/screenshot.png', 'screenshot.png', `${'A'.repeat(512 * 1024)}BBBBBBBB`) + it('never commits a streamed upload against a replacement HUB re-paired mid-stream', async () => { + mockStagedFile('/client/screenshot.png', 'screenshot.png', 40 * 1024 * 1024) runtimeEnvironmentCall.mockImplementation(async (args: RuntimeCallArgs) => { if (args.method === 'status.get') { return runtimeStatusResponse() @@ -245,11 +267,12 @@ describe('runtime file import pairing revision', () => { if (args.method === 'files.stat') { return missingRuntimePathResponse() } - if (args.method === 'files.writeBase64Chunk') { - setEnvironmentRevision(REPLACEMENT_REVISION) - } return successfulRuntimeResponse(args.method) }) + uploadExternalFileToRuntime.mockImplementation(async () => { + setEnvironmentRevision(REPLACEMENT_REVISION) + return { byteLength: 40 * 1024 * 1024 } + }) await expect( importExternalPathsToRuntime(nestedSshContext, ['/client/screenshot.png'], '/ssh/repo') @@ -259,20 +282,9 @@ describe('runtime file import pairing revision', () => { expect(runtimeEnvironmentCall.mock.calls.map(([args]) => args.method)).toEqual([ 'status.get', - 'files.stat', - 'files.writeBase64Chunk' + 'files.stat' ]) - expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ - method: 'files.writeBase64Chunk', - expectedEnvironmentPairingRevision: CAPTURED_REVISION, - params: expect.objectContaining({ - contentBase64: 'A'.repeat(512 * 1024), - append: false - }) - }) - ) + expectUploadsBoundToCapturedRevision() expectEveryRuntimeCallBoundToCapturedRevision(nestedSshContext) expect(runtimeEnvironmentCall).not.toHaveBeenCalledWith( expect.objectContaining({ method: 'files.commitUpload' }) @@ -283,7 +295,7 @@ describe('runtime file import pairing revision', () => { }) it('keeps a HUB-local composer commit on its entry revision when re-paired during commit', async () => { - mockStagedFile('/client/note.txt', 'note.txt', 'bm90ZQ==') + mockStagedFile('/client/note.txt', 'note.txt', 4) runtimeEnvironmentCall.mockImplementation(async (args: RuntimeCallArgs) => { if (args.expectedEnvironmentPairingRevision !== CAPTURED_REVISION) { throw new Error('replacement HUB received an import RPC') @@ -310,14 +322,13 @@ describe('runtime file import pairing revision', () => { expect(runtimeEnvironmentCall.mock.calls.map(([args]) => args.method)).toEqual([ 'status.get', 'files.stat', - 'files.writeBase64', 'files.commitUpload' ]) expectEveryRuntimeCallBoundToCapturedRevision(hubLocalContext) }) it('does not clean up against a replacement HUB after commit', async () => { - mockStagedFile('/client/drop.txt', 'drop.txt', 'ZHJvcA==') + mockStagedFile('/client/drop.txt', 'drop.txt', 4) runtimeEnvironmentCall.mockImplementation(async (args: RuntimeCallArgs) => { if (args.method === 'status.get') { return runtimeStatusResponse() @@ -340,7 +351,6 @@ describe('runtime file import pairing revision', () => { expect(runtimeEnvironmentCall.mock.calls.map(([args]) => args.method)).toEqual([ 'status.get', 'files.stat', - 'files.writeBase64', 'files.commitUpload' ]) expectEveryRuntimeCallBoundToCapturedRevision(nestedSshContext) @@ -356,7 +366,14 @@ describe('runtime file import pairing revision', () => { kind: 'directory', entries: [ { relativePath: '', kind: 'directory' }, - { relativePath: 'broken.txt', kind: 'file', contentBase64: 'YnJva2Vu' } + { + relativePath: 'broken.txt', + kind: 'file', + byteLength: 6, + inode: 92, + deviceId: 66, + modifiedAtMs: 1_700_000_000_000 + } ] } ] @@ -368,16 +385,9 @@ describe('runtime file import pairing revision', () => { if (args.method === 'files.stat') { return missingRuntimePathResponse() } - if (args.method === 'files.writeBase64') { - return { - id: args.method, - ok: false, - error: { code: 'write_failed', message: 'disk full' }, - _meta: { runtimeId: 'hub-runtime' } - } - } return successfulRuntimeResponse(args.method) }) + uploadExternalFileToRuntime.mockRejectedValue(new Error('disk full')) await expect( importExternalPathsToRuntime(nestedSshContext, ['/client/assets'], '/ssh/repo') @@ -387,7 +397,6 @@ describe('runtime file import pairing revision', () => { 'status.get', 'files.stat', 'files.createDirNoClobber', - 'files.writeBase64', 'files.delete', 'files.delete' ]) diff --git a/src/renderer/src/runtime/runtime-file-upload-client.ts b/src/renderer/src/runtime/runtime-file-upload-client.ts index 2f3c1582ec5..fc3d0f83a9a 100644 --- a/src/renderer/src/runtime/runtime-file-upload-client.ts +++ b/src/renderer/src/runtime/runtime-file-upload-client.ts @@ -1,4 +1,6 @@ +import { extractIpcErrorMessage } from '@/lib/ipc-error' import { joinPath, normalizeRelativePath } from '@/lib/path' +import type { StagedRuntimeUploadFileIdentity } from '../../../shared/runtime-upload-staging-contract' import type { RuntimeFileOperationArgs } from './runtime-file-client-types' import { callRuntimeFileImportMutation, @@ -12,28 +14,48 @@ import { import { runtimePathExists } from './runtime-file-metadata-client' import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' -const REMOTE_UPLOAD_BASE64_CHUNK_CHARS = 512 * 1024 +/** Locates a staged file on the client so main can stream it without the renderer reading it. */ +export type RuntimeUploadSource = { + sourceRootPath: string + entryRelativePath: string + /** What staging observed; main refuses the upload if the source no longer matches. */ + expected: StagedRuntimeUploadFileIdentity +} +/** Stream one staged file to a temp path, then commit it; the temp path is always cleaned up. */ export async function uploadRuntimeFileWithoutClobber( session: RuntimeFileImportSession, worktreeId: string, relativePath: string, - contentBase64: string, + source: RuntimeUploadSource, expectedSshConnectionGeneration?: number, expectedSshTargetId?: string, expectedExecutionHostId?: 'local' | `ssh:${string}` ): Promise { const tempRelativePath = makeRuntimeUploadTempPath(relativePath) try { - await writeRuntimeBase64File( - session, - worktreeId, - tempRelativePath, - contentBase64, - expectedSshConnectionGeneration, - expectedSshTargetId, - expectedExecutionHostId - ) + session.assertCurrent() + // Why: main owns the file handle and the runtime socket, so it streams the + // body in slices; the renderer never holds the whole file. + try { + await window.api.fs.uploadExternalFileToRuntime({ + environmentId: session.target.environmentId, + sourceRootPath: source.sourceRootPath, + entryRelativePath: source.entryRelativePath, + expected: source.expected, + worktree: toRuntimeWorktreeSelector(worktreeId), + relativePath: tempRelativePath, + expectedSshTargetId, + expectedSshConnectionGeneration, + expectedExecutionHostId, + expectedEnvironmentPairingRevision: session.expectedEnvironmentPairingRevision, + expectedEnvironmentRuntimeId: session.expectedEnvironmentRuntimeId + }) + } catch (error) { + // Why: this surfaces in the import result as-is, and Electron wraps a + // main-process throw in "Error invoking remote method '…'". + throw new Error(extractIpcErrorMessage(error, 'Upload failed')) + } await callRuntimeFileImportMutation( session, 'files.commitUpload', @@ -64,50 +86,7 @@ export async function uploadRuntimeFileWithoutClobber( } } -async function writeRuntimeBase64File( - session: RuntimeFileImportSession, - worktreeId: string, - relativePath: string, - contentBase64: string, - expectedSshConnectionGeneration?: number, - expectedSshTargetId?: string, - expectedExecutionHostId?: 'local' | `ssh:${string}` -): Promise { - if (contentBase64.length <= REMOTE_UPLOAD_BASE64_CHUNK_CHARS) { - await callRuntimeFileImportMutation( - session, - 'files.writeBase64', - { - worktree: toRuntimeWorktreeSelector(worktreeId), - relativePath, - contentBase64, - expectedSshTargetId, - expectedSshConnectionGeneration, - expectedExecutionHostId - }, - 30_000 - ) - return - } - - for (let offset = 0; offset < contentBase64.length; offset += REMOTE_UPLOAD_BASE64_CHUNK_CHARS) { - await callRuntimeFileImportMutation( - session, - 'files.writeBase64Chunk', - { - worktree: toRuntimeWorktreeSelector(worktreeId), - relativePath, - contentBase64: contentBase64.slice(offset, offset + REMOTE_UPLOAD_BASE64_CHUNK_CHARS), - append: offset > 0, - expectedSshTargetId, - expectedSshConnectionGeneration, - expectedExecutionHostId - }, - 30_000 - ) - } -} - +/** Hidden sibling of the destination, so a failed upload never leaves a plausible-looking file. */ function makeRuntimeUploadTempPath(relativePath: string): string { const normalized = normalizeRelativePath(relativePath) const slashIndex = normalized.lastIndexOf('/') diff --git a/src/renderer/src/runtime/structured-agent-session-client.test.ts b/src/renderer/src/runtime/structured-agent-session-client.test.ts index 4d3ed6960c6..d0f65fd526e 100644 --- a/src/renderer/src/runtime/structured-agent-session-client.test.ts +++ b/src/renderer/src/runtime/structured-agent-session-client.test.ts @@ -1,12 +1,17 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from 'vitest' -import { AGENT_SESSION_REWIND_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' +import { + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY, + AGENT_SESSION_REWIND_RUNTIME_CAPABILITY +} from '../../../shared/protocol-version' const mocks = vi.hoisted(() => ({ subscribe: vi.fn(), call: vi.fn(), - supportsCapability: vi.fn() + supportsCapability: vi.fn(), + readLocalCapabilities: vi.fn(), + ensureLocalCapabilities: vi.fn() })) vi.mock('./runtime-environment-revision', () => ({ @@ -17,12 +22,50 @@ vi.mock('./runtime-rpc-client', () => ({ callRuntimeRpc: mocks.call, runtimeEnvironmentSupportsCapability: mocks.supportsCapability })) +vi.mock('./local-runtime-capabilities', () => ({ + readLocalRuntimeCapabilitiesOrUnknown: mocks.readLocalCapabilities, + ensureLocalRuntimeCapabilities: mocks.ensureLocalCapabilities +})) import { callStructuredAgentSession, - subscribeStructuredAgentSession + subscribeStructuredAgentSession, + supportsStructuredAgentSessionPromptCancel } from './structured-agent-session-client' +describe('structured prompt cancellation capability', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.readLocalCapabilities.mockReturnValue(null) + mocks.ensureLocalCapabilities.mockResolvedValue(null) + }) + + it('uses the local status cache and fails closed until the host answers', async () => { + const target = { kind: 'local' } as const + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(false) + mocks.ensureLocalCapabilities.mockResolvedValue([ + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY + ]) + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(true) + mocks.readLocalCapabilities.mockReturnValue([AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY]) + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(true) + expect(mocks.ensureLocalCapabilities).toHaveBeenCalledTimes(2) + }) + + it('checks the selected remote runtime and downgrades on absent or failed capability', async () => { + const target = { kind: 'environment', environmentId: 'ssh-env-1' } as const + mocks.supportsCapability.mockResolvedValueOnce(true).mockResolvedValueOnce(false) + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(true) + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(false) + mocks.supportsCapability.mockRejectedValue(new Error('Disconnected')) + await expect(supportsStructuredAgentSessionPromptCancel(target)).resolves.toBe(false) + expect(mocks.supportsCapability).toHaveBeenCalledWith( + 'ssh-env-1', + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY + ) + }) +}) + describe('callStructuredAgentSession rewind capability', () => { const target = { kind: 'environment', environmentId: 'env-1' } as const const params = { itemId: 'item-1', expectedEpoch: 'epoch-1' } diff --git a/src/renderer/src/runtime/structured-agent-session-client.ts b/src/renderer/src/runtime/structured-agent-session-client.ts index c769ec302c1..5ccbe6360f3 100644 --- a/src/renderer/src/runtime/structured-agent-session-client.ts +++ b/src/renderer/src/runtime/structured-agent-session-client.ts @@ -4,12 +4,39 @@ import type { AgentSessionSubscribeEvent } from '../../../shared/agent-session-wire' import { getRuntimeEnvironmentRevision } from './runtime-environment-revision' -import { AGENT_SESSION_REWIND_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' +import { + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY, + AGENT_SESSION_REWIND_RUNTIME_CAPABILITY +} from '../../../shared/protocol-version' import { callRuntimeRpc, runtimeEnvironmentSupportsCapability, type RuntimeClientTarget } from './runtime-rpc-client' +import { + ensureLocalRuntimeCapabilities, + readLocalRuntimeCapabilitiesOrUnknown +} from './local-runtime-capabilities' +/** Read the prompt-cancel capability through the runtime's existing status cache. + * A failed/unknown probe is treated as legacy so strict prompt fields are never + * sent before the host has proved it understands them. */ +export async function supportsStructuredAgentSessionPromptCancel( + target: RuntimeClientTarget +): Promise { + try { + if (target.kind === 'local') { + const known = readLocalRuntimeCapabilitiesOrUnknown() + const capabilities = known ?? (await ensureLocalRuntimeCapabilities()) + return capabilities?.includes(AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY) === true + } + return await runtimeEnvironmentSupportsCapability( + target.environmentId, + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY + ) + } catch { + return false + } +} export async function callStructuredAgentSession( target: RuntimeClientTarget, diff --git a/src/renderer/src/store/github/project-cache.ts b/src/renderer/src/store/github/project-cache.ts index df8ac361025..eb296633201 100644 --- a/src/renderer/src/store/github/project-cache.ts +++ b/src/renderer/src/store/github/project-cache.ts @@ -76,11 +76,11 @@ export function applyRowPatch( set((s) => { const entry = s.projectViewCache[cacheKey] if (!entry?.data) { - return {} + return s } const rowIndex = entry.data.rows.findIndex((r) => r.id === rowId) if (rowIndex === -1) { - return {} + return s } const rows = [...entry.data.rows] rows[rowIndex] = nextRow diff --git a/src/renderer/src/store/github/pull-request-execution.ts b/src/renderer/src/store/github/pull-request-execution.ts index 72069802064..553ef5c1e13 100644 --- a/src/renderer/src/store/github/pull-request-execution.ts +++ b/src/renderer/src/store/github/pull-request-execution.ts @@ -153,7 +153,7 @@ export function startPullRequestLookup(args: { // Why: unlinking a PR mid exact-linked-PR-lookup must stop the older result from restoring the manual link UI. if (isStaleExactLinkedPRLookup(s, options?.worktreeId, linkedPRNumber)) { skippedStaleLinkedPRLookup = true - return {} + return s } const updates = setGitHubPRResultCaches(s, { prCacheKey: cacheKey, @@ -174,7 +174,7 @@ export function startPullRequestLookup(args: { requestStartedEntry: requestStartedHostedReviewEntry }) didUpdatePRCache = updates.prCache !== undefined - return updates + return updates.prCache || updates.hostedReviewCache ? updates : s }) if (skippedStaleLinkedPRLookup) { return null diff --git a/src/renderer/src/store/github/refresh-event-actions.ts b/src/renderer/src/store/github/refresh-event-actions.ts index 1e4bccbebd6..803d0168c21 100644 --- a/src/renderer/src/store/github/refresh-event-actions.ts +++ b/src/renderer/src/store/github/refresh-event-actions.ts @@ -229,6 +229,7 @@ export const createRefreshEventActions = ( } } + // Preserve root identity so no-op writes do not notify every store subscriber. return changed ? { prRefreshSequences: capPrRefreshSequences(nextSequences), @@ -237,7 +238,7 @@ export const createRefreshEventActions = ( prCache: nextPRCache, hostedReviewCache: nextHostedReviewCache } - : {} + : s }) if (didUpdatePRCache && event.outcome && event.outcome.kind !== 'upstream-error') { debouncedSaveCache(get()) diff --git a/src/renderer/src/store/github/refresh-event-noop-notification.test.ts b/src/renderer/src/store/github/refresh-event-noop-notification.test.ts new file mode 100644 index 00000000000..ccc94c30b30 --- /dev/null +++ b/src/renderer/src/store/github/refresh-event-noop-notification.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest' +import { create } from 'zustand' +import { useAppStore } from '../index' +import { createGitHubSlice } from '../slices/github' +import { createHostedReviewSlice } from '../slices/hosted-review' +import type { AppState } from '../types' + +// @ts-expect-error test window mock +globalThis.window = { api: { gh: { prChecks: vi.fn() }, cache: { setGitHub: vi.fn() } } } + +function createTestStore() { + return create()((...a) => ({ + ...useAppStore.getInitialState(), + ...createGitHubSlice(...a), + ...createHostedReviewSlice(...a) + })) +} + +const inFlightEvent = (sequence: number) => ({ + sequence, + aliases: [{ cacheKey: 'repo-1::main', repoId: 'repo-1', repoPath: '/repo', branch: 'main' }], + reason: 'visible' as const, + status: 'in-flight' as const +}) + +describe('applyGitHubPRRefreshEvent no-op updates', () => { + // Why a listener count: zustand bails out only on Object.is(next, state), so a + // `return {}` no-op branch still rebuilds the root and wakes every selector in the + // app. The state looks unchanged afterwards, which is exactly why it goes unnoticed. + it('does not notify subscribers when a stale sequence changes nothing', () => { + const store = createTestStore() + store.getState().applyGitHubPRRefreshEvent(inFlightEvent(4)) + + let notifications = 0 + const unsubscribe = store.subscribe(() => { + notifications += 1 + }) + try { + store.getState().applyGitHubPRRefreshEvent(inFlightEvent(4)) + store.getState().applyGitHubPRRefreshEvent(inFlightEvent(3)) + } finally { + unsubscribe() + } + + expect(notifications).toBe(0) + }) + + it('still notifies when the event advances the sequence', () => { + const store = createTestStore() + store.getState().applyGitHubPRRefreshEvent(inFlightEvent(1)) + + let notifications = 0 + const unsubscribe = store.subscribe(() => { + notifications += 1 + }) + try { + store.getState().applyGitHubPRRefreshEvent(inFlightEvent(2)) + } finally { + unsubscribe() + } + + expect(notifications).toBe(1) + }) +}) diff --git a/src/renderer/src/store/github/work-item-mutation-actions.ts b/src/renderer/src/store/github/work-item-mutation-actions.ts index e1d02a09d3b..97a6d3b66f3 100644 --- a/src/renderer/src/store/github/work-item-mutation-actions.ts +++ b/src/renderer/src/store/github/work-item-mutation-actions.ts @@ -45,7 +45,7 @@ export const createWorkItemMutationActions = ( nextCache[key] = { ...entry, data: updatedItems } changed = true } - return changed ? { workItemsCache: nextCache } : {} + return changed ? { workItemsCache: nextCache } : s }) }, diff --git a/src/renderer/src/store/slices/browser/browser-host-actions.ts b/src/renderer/src/store/slices/browser/browser-host-actions.ts index 2977717d38c..193dfdafe60 100644 --- a/src/renderer/src/store/slices/browser/browser-host-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-host-actions.ts @@ -24,7 +24,7 @@ export function createBrowserHostActions( closes, Date.now() ) - return next ? { clientHostedBrowserCloseIntentsByEnvironment: next } : {} + return next ? { clientHostedBrowserCloseIntentsByEnvironment: next } : s }) }, @@ -34,7 +34,7 @@ export function createBrowserHostActions( s.clientHostedBrowserCloseIntentsByEnvironment, { environmentId, browserPageIds, now: Date.now() } ) - return next ? { clientHostedBrowserCloseIntentsByEnvironment: next } : {} + return next ? { clientHostedBrowserCloseIntentsByEnvironment: next } : s }) }, diff --git a/src/renderer/src/store/slices/browser/browser-host-state.ts b/src/renderer/src/store/slices/browser/browser-host-state.ts index 1b4b3a70e9a..7ee75ac54a1 100644 --- a/src/renderer/src/store/slices/browser/browser-host-state.ts +++ b/src/renderer/src/store/slices/browser/browser-host-state.ts @@ -153,7 +153,7 @@ export function browserImportStateForHostUpdate( hostId: ExecutionHostId, browserSessionImportState: BrowserSlice['browserSessionImportState'] ): Partial { - return getBrowserSettingsHostId(state) === hostId ? { browserSessionImportState } : {} + return getBrowserSettingsHostId(state) === hostId ? { browserSessionImportState } : state } export function getFallbackTabTypeForWorktree( diff --git a/src/renderer/src/store/slices/browser/browser-hydration-actions.ts b/src/renderer/src/store/slices/browser/browser-hydration-actions.ts index a2ed3f5bbb1..13beee05872 100644 --- a/src/renderer/src/store/slices/browser/browser-hydration-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-hydration-actions.ts @@ -257,7 +257,7 @@ export function createBrowserHydrationActions( } } } - return {} + return s }) } } diff --git a/src/renderer/src/store/slices/browser/browser-profile-import-actions.ts b/src/renderer/src/store/slices/browser/browser-profile-import-actions.ts index ec03690bf8e..9b79ec9adbc 100644 --- a/src/renderer/src/store/slices/browser/browser-profile-import-actions.ts +++ b/src/renderer/src/store/slices/browser/browser-profile-import-actions.ts @@ -133,13 +133,13 @@ export function createBrowserProfileImportActions( set((s) => getBrowserSettingsHostId(s) === hostId ? { detectedBrowsers: browsers, detectedBrowsersLoaded: true, detectedBrowsersHost } - : {} + : s ) } catch { set((s) => getBrowserSettingsHostId(s) === hostId ? { detectedBrowsers: [], detectedBrowsersLoaded: true, detectedBrowsersHost: null } - : {} + : s ) } return @@ -161,11 +161,11 @@ export function createBrowserProfileImportActions( detectedBrowsersLoaded: true, detectedBrowsersHost: null } - : {} + : s ) } catch { /* best-effort — empty list is acceptable fallback */ - set((s) => (getBrowserSettingsHostId(s) === hostId ? { detectedBrowsersLoaded: true } : {})) + set((s) => (getBrowserSettingsHostId(s) === hostId ? { detectedBrowsersLoaded: true } : s)) } } } diff --git a/src/renderer/src/store/slices/commit-message-generation.ts b/src/renderer/src/store/slices/commit-message-generation.ts index e361958932c..4e7ef3e722d 100644 --- a/src/renderer/src/store/slices/commit-message-generation.ts +++ b/src/renderer/src/store/slices/commit-message-generation.ts @@ -160,7 +160,7 @@ export const createCommitMessageGenerationSlice: StateCreator< set((state) => { const nextRecord = updater(state.commitMessageGenerationRecords[key] ?? null) if (!nextRecord) { - return {} + return state } return { commitMessageGenerationRecords: { @@ -184,6 +184,6 @@ export const createCommitMessageGenerationSlice: StateCreator< changed = true } } - return changed ? { commitMessageGenerationRecords: nextRecords } : {} + return changed ? { commitMessageGenerationRecords: nextRecords } : state }) }) diff --git a/src/renderer/src/store/slices/diff-comment-persistence.ts b/src/renderer/src/store/slices/diff-comment-persistence.ts index 92e1eb9ea9f..4d28d7b7f1d 100644 --- a/src/renderer/src/store/slices/diff-comment-persistence.ts +++ b/src/renderer/src/store/slices/diff-comment-persistence.ts @@ -239,13 +239,13 @@ export function mutateDiffComments( if (scope?.type === 'folder') { const target = findFolderWorkspaceOwner(s, scope.folderWorkspaceId) if (!target) { - return {} + return s } folderExecutionHostId = getExecutionHostIdForFolderWorkspace(s, scope.folderWorkspaceId) previous = target.diffComments const computed = mutate(previous ?? []) if (computed === null) { - return {} + return s } next = computed return { @@ -256,16 +256,16 @@ export function mutateDiffComments( } const repoList = s.worktreesByRepo[repoId] if (!repoList) { - return {} + return s } const target = repoList.find((w) => w.id === worktreeId) if (!target) { - return {} + return s } previous = target.diffComments const computed = mutate(previous ?? []) if (computed === null) { - return {} + return s } next = computed const nextList: Worktree[] = repoList.map((w) => @@ -293,7 +293,7 @@ function rollback( if (scope?.type === 'folder') { const target = findFolderWorkspaceOwner(s, scope.folderWorkspaceId, folderExecutionHostId) if (!target || target.diffComments !== expectedCurrent) { - return {} + return s } return { folderWorkspaces: s.folderWorkspaces.map((workspace) => @@ -303,16 +303,16 @@ function rollback( } const repoList = s.worktreesByRepo[repoId] if (!repoList) { - return {} + return s } const target = repoList.find((w) => w.id === worktreeId) // Why: worktree gone since the mutation; bail before remapping so we don't allocate a new array identity and fire spurious notifications. if (!target) { - return {} + return s } // Why: only roll back if no later mutation replaced the array, else our stale `previous` would erase newer state. if (target.diffComments !== expectedCurrent) { - return {} + return s } const nextList: Worktree[] = repoList.map((w) => w.id === worktreeId ? { ...w, diffComments: previous } : w diff --git a/src/renderer/src/store/slices/empty-update-notifications.test.ts b/src/renderer/src/store/slices/empty-update-notifications.test.ts new file mode 100644 index 00000000000..692fa50b7d4 --- /dev/null +++ b/src/renderer/src/store/slices/empty-update-notifications.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest' +import { createTestStore } from './store-test-helpers' +import { createTabsSliceMockApi } from './tabs-slice-test-harness' +import { browserImportStateForHostUpdate } from './browser/browser-host-state' +import { mutateDiffComments } from './diff-comment-persistence' + +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +createTabsSliceMockApi() + +describe('empty store updates', () => { + it('does not notify for missing tab actions', () => { + const store = createTestStore() + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.setTabLabel('missing', 'label') + before.setTabCustomLabel('missing', 'label') + before.setUnifiedTabColor('missing', null) + before.setTabViewMode('missing', 'chat') + before.toggleTabViewMode('missing') + before.pinTab('missing') + before.unpinTab('missing') + before.reorderUnifiedTabs('missing', []) + before.moveUnifiedTabToGroup('missing', 'missing') + + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + }) + + it('does not notify for unchanged labels but publishes changed labels', () => { + const store = createTestStore() + const tab = store + .getState() + .createUnifiedTab('folder-workspace', 'terminal', { label: 'label' }) + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.setTabLabel(tab.id, 'label') + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + + before.setTabLabel(tab.id, 'new label') + expect(listener).toHaveBeenCalledTimes(1) + expect(store.getState().getTab(tab.id)?.label).toBe('new label') + }) + + it('does not notify for rejected generation updates or empty pruning', () => { + const store = createTestStore() + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.updateCommitMessageGenerationRecord('missing', () => null) + before.updatePullRequestGenerationRecord('missing', () => null) + before.pruneCommitMessageGenerationRecords(new Set()) + before.prunePullRequestGenerationRecords(new Set()) + + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + }) + + it('does not notify for absent Jira issues, browser pages or diff comments', () => { + const store = createTestStore() + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.patchJiraIssue('MISSING-1', {}) + before.patchLinearIssue('missing', {}) + before.switchBrowserTabProfile('missing', null, 'persist:missing') + before.recordClientHostedBrowserCloseIntents([]) + before.clearClientHostedBrowserCloseIntents('missing', []) + mutateDiffComments(store.setState, 'missing', () => null) + store.setState((state) => browserImportStateForHostUpdate(state, 'runtime:other', null)) + + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts b/src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts index 418b6648fa0..8b42bf3e56f 100644 --- a/src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts +++ b/src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts @@ -86,6 +86,8 @@ describe('createGitHubSlice.fetchPRForBranch', () => { hostedReviewCache: {}, prCache: {} } as unknown as Partial) + const subscriber = vi.fn() + const unsubscribe = store.subscribe(subscriber) resolveRefresh({ kind: 'found', pr: makePR({ number: 12, title: 'Stale exact linked PR' }), @@ -93,6 +95,8 @@ describe('createGitHubSlice.fetchPRForBranch', () => { }) await expect(request).resolves.toBeNull() + unsubscribe() + expect(subscriber).not.toHaveBeenCalled() expect(store.getState().prCache[`${repoId}::${branch}`]).toBeUndefined() expect(store.getState().hostedReviewCache[hostedReviewCacheKey]).toBeUndefined() }) diff --git a/src/renderer/src/store/slices/github-work-item-cache-identity.test.ts b/src/renderer/src/store/slices/github-work-item-cache-identity.test.ts index ef22da072d1..ba741de5cfb 100644 --- a/src/renderer/src/store/slices/github-work-item-cache-identity.test.ts +++ b/src/renderer/src/store/slices/github-work-item-cache-identity.test.ts @@ -15,6 +15,15 @@ describe('createGitHubSlice.patchWorkItem', () => { resetRemoteRuntimeMocks() }) + it('does not notify when a patch has no matching cached work item', () => { + const store = createTestStore() + const subscriber = vi.fn() + const unsubscribe = store.subscribe(subscriber) + store.getState().patchWorkItem('pr:missing', { title: 'Missing' }, 'repo-1') + unsubscribe() + expect(subscriber).not.toHaveBeenCalled() + }) + it('can scope patches to one repo when different repos have the same work-item id', () => { const store = createTestStore() const repoOneItem = { diff --git a/src/renderer/src/store/slices/hosted-review-cache-race.test.ts b/src/renderer/src/store/slices/hosted-review-cache-race.test.ts index 59e253e1823..16d765fe0d5 100644 --- a/src/renderer/src/store/slices/hosted-review-cache-race.test.ts +++ b/src/renderer/src/store/slices/hosted-review-cache-race.test.ts @@ -112,10 +112,14 @@ describe('hosted review cache race protection', () => { } } }) + const subscriber = vi.fn() + const unsubscribe = store.subscribe(subscriber) vi.setSystemTime(300) resolveFetch(olderReview) await expect(request).resolves.toEqual(olderReview) + unsubscribe() + expect(subscriber).not.toHaveBeenCalled() expect(store.getState().hostedReviewCache[cacheKey]).toEqual({ data: newerReview, fetchedAt: 200, diff --git a/src/renderer/src/store/slices/hosted-review.ts b/src/renderer/src/store/slices/hosted-review.ts index 53cdb00db55..cad350d986b 100644 --- a/src/renderer/src/store/slices/hosted-review.ts +++ b/src/renderer/src/store/slices/hosted-review.ts @@ -234,7 +234,7 @@ export const createHostedReviewSlice: StateCreator { const nextRecord = updater(state.pullRequestGenerationRecords[key] ?? null) if (!nextRecord) { - return {} + return state } return { pullRequestGenerationRecords: { @@ -312,6 +312,6 @@ export const createPullRequestGenerationSlice: StateCreator< changed = true } } - return changed ? { pullRequestGenerationRecords: nextRecords } : {} + return changed ? { pullRequestGenerationRecords: nextRecords } : state }) }) diff --git a/src/renderer/src/store/slices/sparse-presets.ts b/src/renderer/src/store/slices/sparse-presets.ts index b9e763bf72e..6a4a2d68245 100644 --- a/src/renderer/src/store/slices/sparse-presets.ts +++ b/src/renderer/src/store/slices/sparse-presets.ts @@ -145,7 +145,7 @@ export const createSparsePresetsSlice: StateCreator { const existing = s.sparsePresetsByRepo[args.repoId] if (existing === undefined) { - return {} + return s } const without = existing.filter((preset) => preset.id !== saved.id) return { diff --git a/src/renderer/src/store/slices/tabs/tabs-create-actions.ts b/src/renderer/src/store/slices/tabs/tabs-create-actions.ts index b804b31036e..bc3f08558a1 100644 --- a/src/renderer/src/store/slices/tabs/tabs-create-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-create-actions.ts @@ -121,7 +121,7 @@ export function createTabsCreateActions( target.sourceGroupId ) if (!sourceGroup) { - return {} + return state } const existingTabs = state.unifiedTabsByWorktree[worktreeId] ?? [] const currentGroups = state.groupsByWorktree[worktreeId] ?? [] diff --git a/src/renderer/src/store/slices/tabs/tabs-drop-actions.ts b/src/renderer/src/store/slices/tabs/tabs-drop-actions.ts index 2cadf76365e..612bee0b538 100644 --- a/src/renderer/src/store/slices/tabs/tabs-drop-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-drop-actions.ts @@ -25,19 +25,19 @@ export function createTabsDropActions( const foundTab = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) const foundTarget = findGroupAndWorktree(state.groupsByWorktree, target.groupId) if (!foundTab || !foundTarget || foundTab.worktreeId !== foundTarget.worktreeId) { - return {} + return state } const { tab, worktreeId } = foundTab const sourceGroup = findGroupForTab(state.groupsByWorktree, worktreeId, tab.groupId) const targetGroup = foundTarget.group if (!sourceGroup) { - return {} + return state } const isSplitDrop = Boolean(target.splitDirection) if (!isSplitDrop && tab.groupId === target.groupId) { - return {} + return state } const layout = state.layoutByWorktree[worktreeId] if ( @@ -51,7 +51,7 @@ export function createTabsDropActions( }) ) { // Why: dropping a group's last tab on its own/sibling matching edge only makes a transient column that immediately collapses. - return {} + return state } moved = true diff --git a/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts b/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts index 39a0dd38858..6e4134c6a1b 100644 --- a/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-focus-actions.ts @@ -53,7 +53,7 @@ export function createTabsFocusActions( found = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) } if (!found) { - return {} + return state } const { tab, worktreeId } = found // Why: activating a terminal tab dismisses its tab-level bell — the user has moved their eyes here. diff --git a/src/renderer/src/store/slices/tabs/tabs-label-actions.ts b/src/renderer/src/store/slices/tabs/tabs-label-actions.ts index 42b8e3d7163..3ee06aa105c 100644 --- a/src/renderer/src/store/slices/tabs/tabs-label-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-label-actions.ts @@ -50,7 +50,7 @@ export function createTabsLabelActions( } } } - return {} + return state }) if (reordered && opts?.recordInteraction !== false) { get().recordFeatureInteraction?.('terminal-tabs') @@ -58,17 +58,24 @@ export function createTabsLabelActions( }, setTabLabel: (tabId, label) => { - set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { label }) ?? {}) + set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { label }) ?? state) }, setTabViewMode: (tabId, mode) => { - set((state) => ({ - ...patchTab(state.unifiedTabsByWorktree, tabId, { viewMode: mode }), - // Why the row too: viewMode is declared on both types and host-sync - // already writes it to the row. Only these local toggles skipped it, so - // readers had to OR the two indices to find out who owns the surface. - ...patchTerminalTabRow(state.tabsByWorktree, tabId, { viewMode: mode }) - })) + set((state) => { + const tabPatch = patchTab(state.unifiedTabsByWorktree, tabId, { viewMode: mode }) + const rowPatch = patchTerminalTabRow(state.tabsByWorktree, tabId, { viewMode: mode }) + if (!tabPatch && !rowPatch.tabsByWorktree) { + return state + } + return { + ...tabPatch, + // Why the row too: viewMode is declared on both types and host-sync + // already writes it to the row. Only these local toggles skipped it, so + // readers had to OR the two indices to find out who owns the surface. + ...rowPatch + } + }) mirrorTabViewModeToHost(get(), tabId, mode) }, @@ -81,7 +88,7 @@ export function createTabsLabelActions( set((state) => { const found = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) if (!found) { - return {} + return state } // Why: viewMode defaults to 'terminal' for legacy/missing, so the first toggle flips to 'chat'. const fromMode: 'terminal' | 'chat' = found.tab.viewMode === 'chat' ? 'chat' : 'terminal' @@ -111,7 +118,7 @@ export function createTabsLabelActions( setTabCustomLabel: (tabId, label, opts) => { const exists = get().getTab(tabId) !== null - set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { customLabel: label }) ?? {}) + set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { customLabel: label }) ?? state) if (exists && opts?.recordInteraction !== false) { get().recordFeatureInteraction?.('terminal-tabs') } @@ -119,7 +126,7 @@ export function createTabsLabelActions( setUnifiedTabColor: (tabId, color) => { const exists = get().getTab(tabId) !== null - set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { color }) ?? {}) + set((state) => patchTab(state.unifiedTabsByWorktree, tabId, { color }) ?? state) if (exists) { get().recordFeatureInteraction?.('terminal-tabs') } @@ -130,7 +137,7 @@ export function createTabsLabelActions( set((state) => { const found = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) if (!found) { - return {} + return state } const { tab, worktreeId } = found const tabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).map((candidate) => @@ -168,7 +175,7 @@ export function createTabsLabelActions( set((state) => { const found = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) if (!found) { - return {} + return state } const { tab, worktreeId } = found const tabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).map((candidate) => diff --git a/src/renderer/src/store/slices/tabs/tabs-move-actions.ts b/src/renderer/src/store/slices/tabs/tabs-move-actions.ts index 4c651f613ac..8e8a51f5c0c 100644 --- a/src/renderer/src/store/slices/tabs/tabs-move-actions.ts +++ b/src/renderer/src/store/slices/tabs/tabs-move-actions.ts @@ -22,16 +22,16 @@ export function createTabsMoveActions( const foundTab = findTabAndWorktree(state.unifiedTabsByWorktree, tabId) const foundTarget = findGroupAndWorktree(state.groupsByWorktree, targetGroupId) if (!foundTab || !foundTarget || foundTab.worktreeId !== foundTarget.worktreeId) { - return {} + return state } const { tab, worktreeId } = foundTab if (tab.groupId === targetGroupId) { - return {} + return state } const sourceGroup = findGroupForTab(state.groupsByWorktree, worktreeId, tab.groupId) const targetGroup = foundTarget.group if (!sourceGroup) { - return {} + return state } moved = true diff --git a/src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts b/src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts new file mode 100644 index 00000000000..811b97f3aec --- /dev/null +++ b/src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, beforeEach } from 'vitest' +import { useAppStore } from '../../index' + +describe('workspace port-scan surface actions', () => { + beforeEach(() => { + useAppStore.setState({ workspacePortScanRefreshing: false }) + }) + + // Why a listener count and not a state assertion: zustand notifies on identity, so an + // unconditional `set` is invisible in the resulting state yet re-runs every selector. + const countNotifications = (run: () => void): number => { + let notifications = 0 + const unsubscribe = useAppStore.subscribe(() => { + notifications += 1 + }) + try { + run() + } finally { + unsubscribe() + } + return notifications + } + + it('does not notify subscribers when the refreshing flag is unchanged', () => { + const setRefreshing = useAppStore.getState().setWorkspacePortScanRefreshing + + expect(countNotifications(() => setRefreshing(false))).toBe(0) + expect(countNotifications(() => setRefreshing(false))).toBe(0) + expect(useAppStore.getState().workspacePortScanRefreshing).toBe(false) + }) + + it('still notifies once on a real transition, in both directions', () => { + const setRefreshing = useAppStore.getState().setWorkspacePortScanRefreshing + + expect(countNotifications(() => setRefreshing(true))).toBe(1) + expect(useAppStore.getState().workspacePortScanRefreshing).toBe(true) + expect(countNotifications(() => setRefreshing(false))).toBe(1) + expect(useAppStore.getState().workspacePortScanRefreshing).toBe(false) + }) +}) diff --git a/src/renderer/src/store/slices/ui/ui-slice-surface-actions.ts b/src/renderer/src/store/slices/ui/ui-slice-surface-actions.ts index 26aab77d0cb..7157a243d7a 100644 --- a/src/renderer/src/store/slices/ui/ui-slice-surface-actions.ts +++ b/src/renderer/src/store/slices/ui/ui-slice-surface-actions.ts @@ -79,8 +79,13 @@ export function createUiSurfaceActions(set: UISliceSet, _get: UISliceGet): Parti : state.workspacePortScan } }), + // Preserve root identity so no-op writes do not notify every store subscriber. setWorkspacePortScanRefreshing: (refreshing) => - set({ workspacePortScanRefreshing: refreshing }), + set((state) => + state.workspacePortScanRefreshing === refreshing + ? state + : { workspacePortScanRefreshing: refreshing } + ), // Why: default true so enabling experimentalPet shows the pet immediately (persisted; "Hide pet" flips it false). petVisible: true, diff --git a/src/renderer/src/store/slices/worktrees/create/pending-worktree-creation.ts b/src/renderer/src/store/slices/worktrees/create/pending-worktree-creation.ts index 28530219cce..435eadec4ec 100644 --- a/src/renderer/src/store/slices/worktrees/create/pending-worktree-creation.ts +++ b/src/renderer/src/store/slices/worktrees/create/pending-worktree-creation.ts @@ -23,14 +23,14 @@ export function createUpdatePendingWorktreeCreation( set((s) => { const entry = s.pendingWorktreeCreations[creationId] if (!entry) { - return {} + return s } // Why: the main process re-emits the same phase; skip no-op writes so the strip and panel don't re-render. const hasChange = (Object.keys(patch) as (keyof typeof patch)[]).some( (key) => patch[key] !== entry[key] ) if (!hasChange) { - return {} + return s } return { pendingWorktreeCreations: { @@ -51,7 +51,7 @@ export function createRemovePendingWorktreeCreation( set((s) => { const entry = s.pendingWorktreeCreations[creationId] if (!entry) { - return {} + return s } removedEntry = entry const { [creationId]: _removed, ...rest } = s.pendingWorktreeCreations @@ -90,7 +90,7 @@ export function createSetActivePendingWorktreeCreation( return (creationId) => { set((s) => { if (creationId !== null && !s.pendingWorktreeCreations[creationId]) { - return {} + return s } return { activePendingCreationId: creationId } }) diff --git a/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts b/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts index fdf17d38f8f..35464523ba5 100644 --- a/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts +++ b/src/renderer/src/store/slices/worktrees/metadata/hosted-review-link-mutation.ts @@ -261,7 +261,7 @@ export function applyHostedReviewLinkClear( nextWorktrees === s.worktreesByRepo && nextDetectedWorktrees === s.detectedWorktreesByRepo ) { - return {} + return s } return { ...(nextWorktrees !== s.worktreesByRepo diff --git a/src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts b/src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts index 5e5c38a71e2..414a2ef206a 100644 --- a/src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts +++ b/src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts @@ -149,7 +149,7 @@ export function createUpdateWorktreeMeta( shouldApplyUpdate && !shouldApplyUpdate(findKnownWorktreeById(s, worktreeId, executionHostId)) ) { - return {} + return s } didApply = true const nextWorktrees = applyWorktreeUpdates( @@ -204,7 +204,7 @@ export function createUpdateWorktreeMeta( !cacheKey && !prCacheKey ) { - return {} + return s } const nextHostedReviewCache = diff --git a/src/renderer/src/store/slices/worktrees/metadata/update-worktrees-meta.ts b/src/renderer/src/store/slices/worktrees/metadata/update-worktrees-meta.ts index 78254535106..8ae7be3ea8b 100644 --- a/src/renderer/src/store/slices/worktrees/metadata/update-worktrees-meta.ts +++ b/src/renderer/src/store/slices/worktrees/metadata/update-worktrees-meta.ts @@ -73,7 +73,7 @@ export function createUpdateWorktreesMeta( } return nextWorktrees === s.worktreesByRepo && nextDetectedWorktrees === s.detectedWorktreesByRepo - ? {} + ? s : { ...(nextWorktrees !== s.worktreesByRepo ? { worktreesByRepo: nextWorktrees, sortEpoch: s.sortEpoch + 1 } diff --git a/src/renderer/src/store/slices/worktrees/session/migrate-worktree-identity.ts b/src/renderer/src/store/slices/worktrees/session/migrate-worktree-identity.ts index 75d6059f309..d14392e72d9 100644 --- a/src/renderer/src/store/slices/worktrees/session/migrate-worktree-identity.ts +++ b/src/renderer/src/store/slices/worktrees/session/migrate-worktree-identity.ts @@ -14,7 +14,10 @@ export function createMigrateWorktreeIdentity( } // Why: invalidate pre-rename toast actions before publishing the new path, carrying the dismissal forward. migrateHugeRepoWarningDismissal(oldWorktreeId, newWorktreeId) - set((s) => buildWorktreeRenameState(s, oldWorktreeId, newWorktreeId)) + set((s) => { + const patch = buildWorktreeRenameState(s, oldWorktreeId, newWorktreeId) + return Object.keys(patch).length > 0 ? patch : s + }) migrateHostedReviewLinkMutationGeneration(oldWorktreeId, newWorktreeId) } } diff --git a/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts b/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts index 31c7e8bbb37..84c85c45a3c 100644 --- a/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts @@ -217,15 +217,15 @@ export function createSetActiveWorktree( pendingActivationTerminalPrepCancels.delete(worktreeId) set((s) => { if (s.activeWorktreeId !== worktreeId) { - return {} + return s } const tabs = s.tabsByWorktree[worktreeId] ?? [] if (tabs.length === 0) { - return {} + return s } const allDead = tabs.every((tab) => !tabHasLivePty(s.ptyIdsByTabId, tab.id)) if (!allDead && !shouldTagTerminalTabs) { - return {} + return s } return { tabsByWorktree: { diff --git a/src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts b/src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts new file mode 100644 index 00000000000..7fdaea7999f --- /dev/null +++ b/src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest' +import { createTestStore } from '../../worktrees-slice-test-harness' +import { makeWorktree } from '../../worktrees-slice-test-fixtures' + +vi.mock('sonner', () => ({ + toast: { warning: vi.fn(), info: vi.fn(), success: vi.fn(), error: vi.fn(), dismiss: vi.fn() } +})) +vi.mock('@/components/worktree-base-fallback-notice', () => ({ + requestWorktreeBaseFallbackNotice: vi.fn() +})) + +describe('worktree no-op notifications', () => { + it('keeps missing creation, recovery, activity, deletion and visit updates silent', () => { + const store = createTestStore() + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.updatePendingWorktreeCreation('missing', { phase: 'fetching' }) + before.removePendingWorktreeCreation('missing') + before.setActivePendingWorktreeCreation('missing') + before.remountTerminalTabForRecovery('missing') + before.settleTerminalTabRecovery('missing', 1, 'success') + before.markWorktreeUnread('missing') + before.bumpWorktreeActivity('missing') + before.clearWorktreeDeleteState('missing') + before.seedActiveWorktreeLastVisitedIfMissing() + before.pruneLastVisitedTimestamps() + before.migrateWorktreeIdentity('missing-old', 'missing-new') + + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + }) + + it.each(['local', 'ssh:test'] as const)( + 'keeps repeated %s deletion and visit updates silent', + (hostId) => { + const store = createTestStore() + const worktree = makeWorktree({ id: 'repo1::/path/wt', repoId: 'repo1', hostId }) + store.setState({ worktreesByRepo: { repo1: [worktree] } }) + const target = { id: worktree.id, hostId } + store.getState().markWorktreesQueuedForDeletion([target]) + store.getState().markWorktreeVisited(worktree.id, 100, hostId) + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + before.markWorktreesQueuedForDeletion([target]) + before.markWorktreeVisited(worktree.id, 100, hostId) + before.markWorktreeVisited(worktree.id, 99, hostId) + expect(store.getState()).toBe(before) + expect(listener).not.toHaveBeenCalled() + + before.markWorktreesDeleting([target]) + expect(listener).toHaveBeenCalledTimes(1) + const deleting = store.getState() + deleting.markWorktreesDeleting([target]) + expect(store.getState()).toBe(deleting) + expect(listener).toHaveBeenCalledTimes(1) + deleting.clearWorktreeDeleteState(worktree.id, hostId) + expect(listener).toHaveBeenCalledTimes(2) + } + ) +}) diff --git a/src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts b/src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts index 78e0e397bca..66e6ddf5b51 100644 --- a/src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts +++ b/src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts @@ -55,7 +55,7 @@ export function createRemountTerminalTabForRecovery( const { admitted: _admitted, ...decline } = admission result = { remounted: false, ...decline } } - return {} + return s } const { worktreeId, index, tab } = location const nextTabs = s.tabsByWorktree[worktreeId].slice() @@ -110,12 +110,12 @@ export function createSettleTerminalTabRecovery( set((s) => { const location = locateTerminalTab(s.tabsByWorktree, tabId) if (!location) { - return {} + return s } const { worktreeId, index, tab } = location const recovery = settledTerminalRecoveryLedger(tab, generation, outcome) if (!recovery) { - return {} + return s } const nextTabs = s.tabsByWorktree[worktreeId].slice() nextTabs[index] = { ...tab, recovery } diff --git a/src/renderer/src/store/slices/worktrees/session/worktree-unread-activity.ts b/src/renderer/src/store/slices/worktrees/session/worktree-unread-activity.ts index 72d95b6a0a4..bea375b8292 100644 --- a/src/renderer/src/store/slices/worktrees/session/worktree-unread-activity.ts +++ b/src/renderer/src/store/slices/worktrees/session/worktree-unread-activity.ts @@ -59,7 +59,7 @@ export function createMarkWorktreeUnread( set((s) => { const worktree = findKnownWorktreeById(s, worktreeId) if (!worktree || worktree.isUnread) { - return {} + return s } shouldPersist = true const nextWorktrees = applyWorktreeUpdates(s.worktreesByRepo, worktreeId, { @@ -266,7 +266,7 @@ export function createBumpWorktreeActivity( set((s) => { const worktree = findKnownWorktreeById(s, worktreeId) if (!worktree) { - return {} + return s } shouldPersist = true // Why: skip sortEpoch bump for the active worktree — its PTY events are click side-effects (reorder-on-click bug, PR #209). diff --git a/src/renderer/src/store/slices/worktrees/session/worktree-visit-recency.ts b/src/renderer/src/store/slices/worktrees/session/worktree-visit-recency.ts index 5cfc5eef3d2..7a9d94c89fd 100644 --- a/src/renderer/src/store/slices/worktrees/session/worktree-visit-recency.ts +++ b/src/renderer/src/store/slices/worktrees/session/worktree-visit-recency.ts @@ -29,7 +29,7 @@ export function createMarkWorktreeVisited( hostId: ownerHostId }) ?? 0 if (!(now > prev)) { - return {} + return s } return { lastVisitedAtByWorktreeId: { @@ -124,7 +124,7 @@ export function createPruneLastVisitedTimestamps( patch.activeWorkspaceExecutionHostId = null } } - return Object.keys(patch).length > 0 ? patch : {} + return Object.keys(patch).length > 0 ? patch : s }) } } @@ -137,12 +137,12 @@ export function createSeedActiveWorktreeLastVisitedIfMissing( set((s) => { const id = s.activeWorktreeId if (!id) { - return {} + return s } const hostId = s.activeWorkspaceExecutionHostId ?? s.getKnownWorktreeById(id)?.hostId const key = getWorktreeVisitKey(id, hostId) if (getWorktreeVisitTimestamp(s.lastVisitedAtByWorktreeId, { id, hostId }) != null) { - return {} + return s } return { lastVisitedAtByWorktreeId: { diff --git a/src/renderer/src/store/slices/worktrees/teardown/worktree-delete-state.ts b/src/renderer/src/store/slices/worktrees/teardown/worktree-delete-state.ts index 30975b958e6..7e89bb2e34b 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/worktree-delete-state.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/worktree-delete-state.ts @@ -78,7 +78,7 @@ export function createMarkWorktreesDeleting( } changed = true } - return changed ? { deleteStateByWorktreeId: nextDeleteState } : {} + return changed ? { deleteStateByWorktreeId: nextDeleteState } : s }) } } @@ -113,7 +113,7 @@ export function createMarkWorktreesQueuedForDeletion( } changed = true } - return changed ? { deleteStateByWorktreeId: nextDeleteState } : {} + return changed ? { deleteStateByWorktreeId: nextDeleteState } : s }) } } @@ -128,7 +128,7 @@ export function createClearWorktreeDeleteState( : worktreeId set((s) => { if (!s.deleteStateByWorktreeId[key]) { - return {} + return s } const next = { ...s.deleteStateByWorktreeId } delete next[key] diff --git a/src/renderer/src/store/terminals/terminal-disowned-pty-sources.ts b/src/renderer/src/store/terminals/terminal-disowned-pty-sources.ts index d67a382a421..fc3abb5edd6 100644 --- a/src/renderer/src/store/terminals/terminal-disowned-pty-sources.ts +++ b/src/renderer/src/store/terminals/terminal-disowned-pty-sources.ts @@ -16,7 +16,7 @@ export function createTerminalDisownedPtySourceActions( markPtySourceDisowned: (ptyId) => { set((state) => state.disownedPtyIds[ptyId] - ? {} + ? state : { disownedPtyIds: { ...state.disownedPtyIds, [ptyId]: true } } ) } diff --git a/src/renderer/src/store/terminals/terminal-ephemeral-state.ts b/src/renderer/src/store/terminals/terminal-ephemeral-state.ts index 89a67bd72ae..80de6b6899a 100644 --- a/src/renderer/src/store/terminals/terminal-ephemeral-state.ts +++ b/src/renderer/src/store/terminals/terminal-ephemeral-state.ts @@ -30,7 +30,7 @@ export function createTerminalEphemeralActions( markDefaultTerminalTabsApplied: (worktreeId) => set((s) => { if (s.defaultTerminalTabsAppliedByWorktreeId[worktreeId]) { - return {} + return s } return { defaultTerminalTabsAppliedByWorktreeId: { @@ -70,7 +70,7 @@ export function createTerminalEphemeralActions( set((s) => { const current = s.nativeChatLaunchPromptByTabId[tabId] if (!current || current.failed) { - return {} + return s } return { nativeChatLaunchPromptByTabId: { @@ -83,7 +83,7 @@ export function createTerminalEphemeralActions( clearNativeChatLaunchPrompt: (tabId) => { set((s) => { if (!s.nativeChatLaunchPromptByTabId[tabId]) { - return {} + return s } const next = { ...s.nativeChatLaunchPromptByTabId } delete next[tabId] @@ -102,7 +102,7 @@ export function createTerminalEphemeralActions( set((s) => { const current = s.nativeChatLaunchDraftByTabId[tabId] if (!current || current.adopted) { - return {} + return s } return { nativeChatLaunchDraftByTabId: { @@ -121,7 +121,7 @@ export function createTerminalEphemeralActions( current.createdAt !== resolution.createdAt || current.text !== resolution.text ) { - return {} + return s } return { nativeChatLaunchDraftByTabId: { @@ -134,7 +134,7 @@ export function createTerminalEphemeralActions( clearNativeChatLaunchDraft: (tabId) => { set((s) => { if (!s.nativeChatLaunchDraftByTabId[tabId]) { - return {} + return s } const next = { ...s.nativeChatLaunchDraftByTabId } delete next[tabId] @@ -168,7 +168,7 @@ export function createTerminalEphemeralActions( next ??= { ...s.lastTerminalInputAtByPaneKey } next[key] = at } - return next ? { lastTerminalInputAtByPaneKey: next } : {} + return next ? { lastTerminalInputAtByPaneKey: next } : s }) } }) @@ -227,7 +227,7 @@ export function createTerminalEphemeralActions( removeDeferredSshSessionId: (tabId) => set((s) => { if (!s.deferredSshSessionIdsByTabId[tabId]) { - return {} + return s } const next = { ...s.deferredSshSessionIdsByTabId } delete next[tabId] diff --git a/src/renderer/src/store/terminals/terminal-layout-state.ts b/src/renderer/src/store/terminals/terminal-layout-state.ts index 9b5cf8323c2..42ad3dbc659 100644 --- a/src/renderer/src/store/terminals/terminal-layout-state.ts +++ b/src/renderer/src/store/terminals/terminal-layout-state.ts @@ -30,7 +30,7 @@ export function createTerminalLayoutActions( set((s) => { const layout = s.terminalLayoutsByTabId[tabId] if (!layout || layout.ptyIdsByLeafId?.[leafId] === ptyId) { - return {} + return s } return { terminalLayoutsByTabId: { diff --git a/src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts b/src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts new file mode 100644 index 00000000000..4d68e867770 --- /dev/null +++ b/src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts @@ -0,0 +1,146 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + flushTerminalInputActivity, + resetTerminalInputActivityCoalescingForTests +} from '@/lib/terminal-input-activity-coalescing' +import { createTestStore, makeLayout } from '../slices/store-test-helpers' + +afterEach(resetTerminalInputActivityCoalescingForTests) + +describe('terminal no-op subscriber budget', () => { + it('does not publish missing-entry cleanup and restart actions', () => { + const store = createTestStore() + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + for (let i = 0; i < 25; i += 1) { + const s = store.getState() + s.replaceTerminalLayoutPanePtyId('missing', 'leaf', 'pty') + expect(s.consumeSuppressedPtyExit('missing')).toBe(false) + expect(s.consumePendingCodexPaneRestart('missing')).toBe(false) + s.clearCodexRestartNotice('missing') + s.dismissCodexRestartNotices(['missing']) + s.reopenCodexRestartPrompt('missing') + s.markNativeChatLaunchPromptFailed('missing') + s.clearNativeChatLaunchPrompt('missing') + s.markNativeChatLaunchDraftAdopted('missing') + s.resolveNativeChatLaunchDraft('missing', { text: 'draft', createdAt: 1 }) + s.clearNativeChatLaunchDraft('missing') + s.removeDeferredSshSessionId('missing') + } + + expect(listener).not.toHaveBeenCalled() + expect(store.getState()).toBe(before) + }) + + it('publishes real mutations once and keeps repeated actions silent', () => { + const store = createTestStore() + const draft = { tabId: 'tab', agent: 'codex', text: 'draft', createdAt: 1 } as const + store.getState().seedNativeChatLaunchPrompt(draft) + store.getState().seedNativeChatLaunchDraft(draft) + store.getState().setTabLayout('tab', makeLayout()) + const listener = vi.fn() + store.subscribe(listener) + + const actions = [ + () => store.getState().markDefaultTerminalTabsApplied('folder-workspace'), + () => store.getState().markUnverifiedPtyLoss('tab'), + () => store.getState().markPtySourceDisowned('pty'), + () => store.getState().markNativeChatLaunchPromptFailed('tab'), + () => store.getState().markNativeChatLaunchDraftAdopted('tab'), + () => store.getState().resolveNativeChatLaunchDraft('tab', draft), + () => store.getState().replaceTerminalLayoutPanePtyId('tab', 'leaf', 'pty'), + () => store.getState().clearNativeChatLaunchPrompt('tab'), + () => store.getState().clearNativeChatLaunchDraft('tab') + ] + for (const action of actions) { + listener.mockClear() + action() + expect(listener).toHaveBeenCalledTimes(1) + const before = store.getState() + action() + expect(listener).toHaveBeenCalledTimes(1) + expect(store.getState()).toBe(before) + } + }) + + it('retains draft generations when stale resolutions arrive without notifying', () => { + const store = createTestStore() + const draft = { tabId: 'tab', agent: 'codex', text: 'new draft', createdAt: 2 } as const + store.getState().seedNativeChatLaunchDraft(draft) + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + store.getState().resolveNativeChatLaunchDraft('tab', { ...draft, createdAt: 1 }) + store.getState().resolveNativeChatLaunchDraft('tab', { ...draft, text: 'old draft' }) + + expect(listener).not.toHaveBeenCalled() + expect(store.getState()).toBe(before) + expect(store.getState().nativeChatLaunchDraftByTabId.tab).toBe(draft) + }) + + it('consumes real restart entries and leaves repeated consumes silent', () => { + const store = createTestStore() + store.getState().suppressPtyExit('pty') + store.getState().queueCodexPaneRestarts(['pty']) + const listener = vi.fn() + store.subscribe(listener) + + expect(store.getState().consumeSuppressedPtyExit('pty')).toBe(true) + expect(store.getState().consumePendingCodexPaneRestart('pty')).toBe(true) + expect(listener).toHaveBeenCalledTimes(2) + const before = store.getState() + expect(store.getState().consumeSuppressedPtyExit('pty')).toBe(false) + expect(store.getState().consumePendingCodexPaneRestart('pty')).toBe(false) + expect(listener).toHaveBeenCalledTimes(2) + expect(store.getState()).toBe(before) + }) + + it('drops a trailing input flush after pane teardown without publishing', () => { + const store = createTestStore() + store.getState().recordTerminalInput('tab:leaf', 1000) + store.getState().recordTerminalInput('tab:leaf', 1001) + store.setState({ lastTerminalInputAtByPaneKey: {} }) + const before = store.getState() + const listener = vi.fn() + store.subscribe(listener) + + flushTerminalInputActivity() + + expect(listener).not.toHaveBeenCalled() + expect(store.getState()).toBe(before) + expect(store.getState().lastTerminalInputAtByPaneKey['tab:leaf']).toBeUndefined() + }) + + it('dismisses, reopens and clears restart notices without replaying no-op notifications', () => { + const store = createTestStore() + store + .getState() + .markCodexRestartNotices([ + { ptyId: 'pty', previousAccountLabel: 'old', nextAccountLabel: 'new' } + ]) + const listener = vi.fn() + store.subscribe(listener) + const actions = [ + () => store.getState().dismissCodexRestartNotices(['pty']), + () => store.getState().reopenCodexRestartPrompt('pty'), + () => store.getState().clearCodexRestartNotice('pty') + ] + for (const [index, action] of actions.entries()) { + if (index === 1) { + store.getState().queueCodexPaneRestarts(['pty']) + } + listener.mockClear() + action() + expect(listener).toHaveBeenCalledTimes(1) + const before = store.getState() + action() + expect(listener).toHaveBeenCalledTimes(1) + expect(store.getState()).toBe(before) + } + expect(store.getState().codexRestartNoticeByPtyId.pty).toBeUndefined() + expect(store.getState().pendingCodexPaneRestartIds.pty).toBeUndefined() + }) +}) diff --git a/src/renderer/src/store/terminals/terminal-restart-state.ts b/src/renderer/src/store/terminals/terminal-restart-state.ts index f1075bdb698..2b712b6ccdb 100644 --- a/src/renderer/src/store/terminals/terminal-restart-state.ts +++ b/src/renderer/src/store/terminals/terminal-restart-state.ts @@ -21,7 +21,7 @@ export function createTerminalRestartActions( let wasSuppressed = false set((s) => { if (!s.suppressedPtyExitIds[ptyId]) { - return {} + return s } wasSuppressed = true const next = { ...s.suppressedPtyExitIds } @@ -68,7 +68,7 @@ export function createTerminalRestartActions( let wasQueued = false set((s) => { if (!s.pendingCodexPaneRestartIds[ptyId]) { - return {} + return s } wasQueued = true const next = { ...s.pendingCodexPaneRestartIds } @@ -144,7 +144,7 @@ export function createTerminalRestartActions( clearCodexRestartNotice: (ptyId) => { set((s) => { if (!s.codexRestartNoticeByPtyId[ptyId]) { - return {} + return s } const next = { ...s.codexRestartNoticeByPtyId } const nextPendingCodexPaneRestartIds = { ...s.pendingCodexPaneRestartIds } @@ -175,7 +175,7 @@ export function createTerminalRestartActions( changed = true } if (!changed) { - return {} + return s } return { codexRestartNoticeByPtyId: next, @@ -187,7 +187,7 @@ export function createTerminalRestartActions( set((s) => { const notice = s.codexRestartNoticeByPtyId[ptyId] if (!notice?.restartRequested) { - return {} + return s } const { restartRequested: _restartRequested, ...kept } = notice const nextPendingCodexPaneRestartIds = { ...s.pendingCodexPaneRestartIds } diff --git a/src/renderer/src/store/terminals/terminal-startup-queues.ts b/src/renderer/src/store/terminals/terminal-startup-queues.ts index 0f684e3f8b7..af56050ef23 100644 --- a/src/renderer/src/store/terminals/terminal-startup-queues.ts +++ b/src/renderer/src/store/terminals/terminal-startup-queues.ts @@ -62,7 +62,7 @@ export function createTerminalStartupQueueActions( } set((s) => { if (s.pendingStartupByTabId[tabId] !== pending) { - return {} + return s } const next = { ...s.pendingStartupByTabId } delete next[tabId] diff --git a/src/renderer/src/store/terminals/terminal-unverified-pty-loss.ts b/src/renderer/src/store/terminals/terminal-unverified-pty-loss.ts index 9f381a02c4e..075d349a528 100644 --- a/src/renderer/src/store/terminals/terminal-unverified-pty-loss.ts +++ b/src/renderer/src/store/terminals/terminal-unverified-pty-loss.ts @@ -8,7 +8,7 @@ export function createTerminalUnverifiedPtyLossActions( markUnverifiedPtyLoss: (tabId) => { set((state) => state.unverifiedPtyLossTabIds[tabId] - ? {} + ? state : { unverifiedPtyLossTabIds: { ...state.unverifiedPtyLossTabIds, [tabId]: true } } ) } diff --git a/src/renderer/src/web/preload-api/web-filesystem-api.ts b/src/renderer/src/web/preload-api/web-filesystem-api.ts index dcf93839828..889e89b5103 100644 --- a/src/renderer/src/web/preload-api/web-filesystem-api.ts +++ b/src/renderer/src/web/preload-api/web-filesystem-api.ts @@ -126,6 +126,11 @@ export function createFileApi(): NonNullable['fs']> { }, importExternalPaths: async () => ({ results: [] }), stageExternalPathsForRuntimeUpload: async () => ({ sources: [] }), + // Why: the web client has no local filesystem to stream from, so staging + // never yields a source for this to upload. + uploadExternalFileToRuntime: async () => { + throw new Error('Uploading local files is not supported in the web client') + }, resolveDroppedPathsForAgent: async () => ({ resolvedPaths: [], skipped: [], failed: [] }), watchWorktree: () => Promise.resolve(), unwatchWorktree: () => Promise.resolve(), diff --git a/src/shared/agent-session-fence-mint-boundary.test.ts b/src/shared/agent-session-fence-mint-boundary.test.ts new file mode 100644 index 00000000000..c38d17c4b23 --- /dev/null +++ b/src/shared/agent-session-fence-mint-boundary.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { resolve } from 'node:path' +import { scanSourceTree, stripComments } from './source-scan/source-tree-scan' + +const BARE_FENCE_MINT = /runtimeFence\s*:\s*[^\n]*\.runtimeFence\s*\+\s*1\b/ + +describe('agent-session fence mint boundary', () => { + it('rejects direct runtimeFence increments in shipped source', () => { + const offenders = scanSourceTree(resolve(__dirname, '..', '..', 'src')) + .filter(({ source }) => BARE_FENCE_MINT.test(stripComments(source))) + .map(({ relativePath }) => relativePath) + + expect( + offenders, + 'New bare runtimeFence mint. Route the assignment through nextAgentSessionFence(...).' + ).toEqual([]) + }) +}) diff --git a/src/shared/agent-session-next-fence.ts b/src/shared/agent-session-next-fence.ts index bf2eb9f50c7..2c05aa5004d 100644 --- a/src/shared/agent-session-next-fence.ts +++ b/src/shared/agent-session-next-fence.ts @@ -7,8 +7,8 @@ // // Recovery records the floor instead of rewriting the current fence, because `live` means a handle // proven at exactly the current fence — moving it would invalidate the very records recovery exists -// to save. Every mint site routes through here so a new transition cannot quietly reintroduce a -// bare `+ 1`; the floor is pinned by a test that drives each transition. +// to save. A source-level ratchet rejects direct `+ 1` mints, while acquisition-transition tests +// pin the one-step bound and floor; an indirected mint is not caught. import type { AgentSessionLease } from './agent-session-record' diff --git a/src/shared/agent-session-wire.ts b/src/shared/agent-session-wire.ts index fb495f7cb8c..b1ad68f0fa3 100644 --- a/src/shared/agent-session-wire.ts +++ b/src/shared/agent-session-wire.ts @@ -75,6 +75,8 @@ export type AgentSessionTurnActivity = { text: string } +export const AGENT_SESSION_ID_MAX_LENGTH = 512 + /** Backward paging is the client's normal read; 40 matches the page size the * mobile list renders without a visible fill-in. */ export const AGENT_SESSION_HISTORY_DEFAULT_LIMIT = 40 diff --git a/src/shared/agent-title-identity.ts b/src/shared/agent-title-identity.ts index 2b5194bfda8..949768e03b5 100644 --- a/src/shared/agent-title-identity.ts +++ b/src/shared/agent-title-identity.ts @@ -1,3 +1,4 @@ +import { getPiStateTitleBrand } from './pi-state-title-marker' import { AGY_AGENT_NAME_RE, CLAUDE_IDLE, @@ -67,6 +68,10 @@ function computeAgentLabel(title: string): string | null { ) { return 'Claude Code' } + const piStateBrand = getPiStateTitleBrand(title) + if (piStateBrand) { + return piStateBrand + } if (isGeminiTerminalTitle(title)) { return 'Gemini CLI' } diff --git a/src/shared/agent-title-owner.ts b/src/shared/agent-title-owner.ts index 2526c94572f..df0a668621a 100644 --- a/src/shared/agent-title-owner.ts +++ b/src/shared/agent-title-owner.ts @@ -1,3 +1,4 @@ +import { rebrandPiStateTitle } from './pi-state-title-marker' import { detectAgentStatusFromTitle, getAgentLabel } from './agent-detection' import type { AgentStatusEntry, AgentType } from './agent-status-types' import { @@ -157,6 +158,10 @@ export function normalizeCompatibleAgentTitleForOwner( ) { return title } + const stateTitle = rebrandPiStateTitle(title, ownerProfile.workingLabel) + if (stateTitle !== null) { + return stateTitle + } // Why: a π-branded title is the agent's own semantic session title (`π > - `; // Orca's injected extension writes the same shape). Swap only the BRAND for the owner's label // so the pane still reads as its launch owner (#6689, #7633, #9077) without discarding the diff --git a/src/shared/ai-vault-search-settings.test.ts b/src/shared/ai-vault-search-settings.test.ts new file mode 100644 index 00000000000..78ff95fa2f3 --- /dev/null +++ b/src/shared/ai-vault-search-settings.test.ts @@ -0,0 +1,81 @@ +import { expect, it } from 'vitest' +import { + AiVaultSearchSettingsSchema, + DEFAULT_AI_VAULT_SEARCH_SETTINGS, + resolveAiVaultSearchSettings, + sameAiVaultSearchSettings +} from './ai-vault-search-settings' + +// Off is the only safe default: building the index reads every transcript on the +// machine, so a profile that has never answered must read as "no". +it('reads anything that is not an explicit opt-in as off', () => { + expect(resolveAiVaultSearchSettings(undefined)).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS) + expect(resolveAiVaultSearchSettings({})).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual( + DEFAULT_AI_VAULT_SEARCH_SETTINGS + ) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual( + DEFAULT_AI_VAULT_SEARCH_SETTINGS + ) + expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual( + DEFAULT_AI_VAULT_SEARCH_SETTINGS + ) +}) + +it('normalizes a history bound and drops anything that is not one', () => { + expect( + resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays: 30.7 } }) + ).toEqual({ enabled: true, historyDays: 30 }) + // A fractional day floors to zero, which would read as "all history" on one + // side and "cutoff is now" on the other. + for (const historyDays of [0.4, 0, -30, Number.NaN] as const) { + expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays } })).toEqual( + { enabled: true, historyDays: null } + ) + } + expect( + resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays: 999_999 } }) + ).toEqual({ enabled: true, historyDays: 3_650 }) +}) + +// There is no `paused`: the indexer is immutable, so a pause would be a second +// lifetime for one object's store, queue and sweep flag. +it('keeps only the two fields the indexer is constructed from', () => { + expect( + resolveAiVaultSearchSettings({ + aiVaultSearch: { enabled: true, historyDays: 90, paused: true } + }) + ).toEqual({ enabled: true, historyDays: 90 }) +}) + +it('accepts what it produces and refuses what it does not', () => { + expect(AiVaultSearchSettingsSchema.parse({ enabled: true, historyDays: 90 })).toEqual({ + enabled: true, + historyDays: 90 + }) + expect(AiVaultSearchSettingsSchema.safeParse({ enabled: true, historyDays: 0 }).success).toBe( + false + ) + expect(AiVaultSearchSettingsSchema.safeParse({ historyDays: null }).success).toBe(false) +}) + +it('treats an unchanged policy as unchanged so a re-save never restarts the index', () => { + expect( + sameAiVaultSearchSettings( + { enabled: true, historyDays: 30 }, + { enabled: true, historyDays: 30 } + ) + ).toBe(true) + expect( + sameAiVaultSearchSettings( + { enabled: true, historyDays: 30 }, + { enabled: true, historyDays: 90 } + ) + ).toBe(false) + expect( + sameAiVaultSearchSettings( + { enabled: true, historyDays: null }, + { enabled: false, historyDays: null } + ) + ).toBe(false) +}) diff --git a/src/shared/ai-vault-search-settings.ts b/src/shared/ai-vault-search-settings.ts new file mode 100644 index 00000000000..8b6b8e1b07c --- /dev/null +++ b/src/shared/ai-vault-search-settings.ts @@ -0,0 +1,65 @@ +import { z } from 'zod' + +/** + * Consent and retention for the agent-session transcript index. + * + * Off until the user turns it on: building the index reads every transcript on + * the machine, so nothing constructs an indexer, opens the database or reads a + * transcript for it before that choice is recorded. + * + * There is no `paused`. The indexer is immutable after construction, so every + * change here is close-and-construct (see session-search-instance.ts). + */ +export type AiVaultSearchSettings = { + enabled: boolean + /** null = all history; otherwise only transcripts modified within this many days. */ + historyDays: number | null +} + +export const DEFAULT_AI_VAULT_SEARCH_SETTINGS: AiVaultSearchSettings = { + enabled: false, + historyDays: null +} + +const HISTORY_DAYS_MAX = 3_650 + +export const AiVaultSearchSettingsSchema: z.ZodType = z.object({ + enabled: z.boolean(), + historyDays: z.number().int().positive().max(HISTORY_DAYS_MAX).nullable() +}) + +export function normalizeAiVaultSearchHistoryDays(value: unknown): number | null { + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { + return null + } + // A fractional day floors to 0, which reads as "all history" on one side and + // "now" on the other; make the two agree. + const days = Math.floor(value) + return days <= 0 ? null : Math.min(HISTORY_DAYS_MAX, days) +} + +/** + * The persisted shape, from whatever a settings write or an old profile left behind. + * + * The input is `unknown` on purpose: this is the sanitizer, and what it reads is a + * JSON profile that may predate either field or hold a value no version wrote. + */ +export function resolveAiVaultSearchSettings( + settings: { aiVaultSearch?: unknown } | null | undefined +): AiVaultSearchSettings { + const raw = settings?.aiVaultSearch + if (typeof raw !== 'object' || raw === null) { + return { ...DEFAULT_AI_VAULT_SEARCH_SETTINGS } + } + return { + enabled: 'enabled' in raw && raw.enabled === true, + historyDays: normalizeAiVaultSearchHistoryDays('historyDays' in raw ? raw.historyDays : null) + } +} + +export function sameAiVaultSearchSettings( + a: AiVaultSearchSettings, + b: AiVaultSearchSettings +): boolean { + return a.enabled === b.enabled && a.historyDays === b.historyDays +} diff --git a/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt b/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt index 068f9a8a96f..a59e3be4d7c 100644 --- a/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt +++ b/src/shared/child-process/__fixtures__/windows-console-visibility-allowlist.txt @@ -23,7 +23,6 @@ main/emulator/serve-sim-runtime-materializer.ts main/emulator/simctl-simulator-devices.ts main/emulator/simulator-app-visibility.ts main/external-editor-launch.ts -main/hooks.ts main/ipc/app.ts main/ipc/developer-permissions.ts main/ipc/macos-keyboard-layout-snapshot.ts diff --git a/src/shared/child-process/windows-console-visibility.test.ts b/src/shared/child-process/windows-console-visibility.test.ts index 69b0a99eb5b..a15f98b288b 100644 --- a/src/shared/child-process/windows-console-visibility.test.ts +++ b/src/shared/child-process/windows-console-visibility.test.ts @@ -34,7 +34,7 @@ const ALLOWLIST: readonly string[] = readAllowlist( * the allowlist does not bound this: a swap (one file fixed and delisted, one * new file added with its entry) satisfies both membership assertions. */ -const UNHIDDEN_SPAWNER_PIN = 65 +const UNHIDDEN_SPAWNER_PIN = 64 const CHILD_PROCESS_IMPORT = /from\s+['"](?:node:)?child_process['"]|require\(\s*['"](?:node:)?child_process['"]/ diff --git a/src/shared/filesystem-import-result-types.ts b/src/shared/filesystem-import-result-types.ts new file mode 100644 index 00000000000..206570e9a33 --- /dev/null +++ b/src/shared/filesystem-import-result-types.ts @@ -0,0 +1,36 @@ +import type { + StagedRuntimeUploadEntry, + StagedRuntimeUploadSource +} from './runtime-upload-staging-contract' + +export type ImportSkipReason = 'missing' | 'symlink' | 'permission-denied' | 'unsupported' + +export type ResolveDroppedPathsResult = { + resolvedPaths: string[] + skipped: { sourcePath: string; reason: ImportSkipReason }[] + failed: { sourcePath: string; reason: string }[] +} + +export type ImportItemResult = + | { + sourcePath: string + status: 'imported' + destPath: string + kind: 'file' | 'directory' + renamed: boolean + } + | { + sourcePath: string + status: 'skipped' + reason: ImportSkipReason + } + | { + sourcePath: string + status: 'failed' + reason: string + } + +// Why: staging crosses IPC to the renderer and back into the streamer, so the +// shape lives in shared and every layer names the same type. +export type StagedExternalImportSource = StagedRuntimeUploadSource +export type StagedExternalImportEntry = StagedRuntimeUploadEntry diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index e65fec28c39..6369033c892 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -1,6 +1,7 @@ import type { ExecutionHostId } from './execution-host' import type { GitHubProjectSettings } from './github/project-types' import type { VoiceSettings } from './speech-types' +import type { AiVaultSearchSettings } from './ai-vault-search-settings' import type { GitLabProjectSettings } from './gitlab-types' import type { TaskProvider } from './task-providers' import type { KeybindingOverrides, TerminalShortcutPolicy } from './keybindings' @@ -488,6 +489,8 @@ export type GlobalSettings = { tabSwitchKeybindingSeed?: 'pending' | 'done' /** Local voice/dictation config. Optional for pre-voice profiles; getDefaultSettings() hydrates defaults via the persistence merge. */ voice?: VoiceSettings + /** Transcript full-text search consent + retention. Absent means off; nothing indexes until the user opts in. */ + aiVaultSearch?: AiVaultSearchSettings } export type OrcaWorkspaceLayout = { diff --git a/src/shared/omp-owner-state-title.test.ts b/src/shared/omp-owner-state-title.test.ts new file mode 100644 index 00000000000..c06e4dbfe78 --- /dev/null +++ b/src/shared/omp-owner-state-title.test.ts @@ -0,0 +1,51 @@ +import { getPiCompatibleTitleSeparatorStatus } from './pi-compatible-synthetic-title' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { detectAgentStatusFromTitle, getAgentLabel } from './agent-detection' +import { normalizeCompatibleAgentTitleForOwner } from './agent-title-owner' +import { clearPiStateWorkingMarker } from './pi-state-title-marker' + +const transcript = readFileSync( + join(__dirname, '..', 'main', 'runtime', '__fixtures__', 'omp-native-title-win32.txt'), + 'utf8' +) +// oxlint-disable-next-line no-control-regex -- The fixture retains actual OSC control bytes. +const titles = [...transcript.matchAll(/\x1b\]0;([^\x07]+)\x07/g)].map((match) => match[1]) + +describe('owner-rewritten OMP titles from captured upstream output', () => { + it('contains the six upstream state frames', () => expect(titles).toHaveLength(6)) + it.each( + titles.map((title, index) => ({ + title, + state: index < 2 ? 'working' : index < 4 ? 'idle' : 'permission' + })) + )('preserves $state and label for $title', ({ title, state }) => { + for (const prefix of ['', 'zsh | ', 'tmux: ']) { + const wrapped = prefix + title + expect(detectAgentStatusFromTitle(wrapped)).toBe(state) + const owned = normalizeCompatibleAgentTitleForOwner(wrapped, 'omp', { ownerIsLaunch: true }) + expect(owned).toBe(prefix + title.replace('π', 'OMP')) + expect(getAgentLabel(owned)).toBe('OMP') + expect(detectAgentStatusFromTitle(owned)).toBe(state) + expect(getPiCompatibleTitleSeparatorStatus(owned)).toBe(state) + expect(normalizeCompatibleAgentTitleForOwner(owned, 'omp')).toBe(owned) + expect(normalizeCompatibleAgentTitleForOwner(owned, 'pi')).toBe( + prefix + title.replace('π', 'Pi') + ) + if (state === 'working') { + expect(detectAgentStatusFromTitle(clearPiStateWorkingMarker(owned) ?? '')).toBe('idle') + } + } + }) + it.each([ + 'omp-harness ready', + '/tmp/OMP : file', + 'lowercase omp : note', + 'Pi: legacy', + 'OMP ready' + ])('does not rewrite neutral or legacy title %s as a working marker', (title) => { + expect(clearPiStateWorkingMarker(title)).toBeNull() + expect(detectAgentStatusFromTitle(title)).not.toBe('working') + }) +}) diff --git a/src/shared/pi-compatible-synthetic-title.ts b/src/shared/pi-compatible-synthetic-title.ts index 7b99235811d..0e9e5d2b196 100644 --- a/src/shared/pi-compatible-synthetic-title.ts +++ b/src/shared/pi-compatible-synthetic-title.ts @@ -1,3 +1,5 @@ +import { getPiStateTitleStatus } from './pi-state-title-marker' + export type PiCompatibleSyntheticAgentLabel = 'Pi' | 'OMP' export type PiCompatibleSyntheticAgentStatus = 'working' | 'permission' | 'idle' @@ -71,6 +73,10 @@ export function isLegacyPiCompatibleTitle(title: string): boolean { export function getPiCompatibleTitleSeparatorStatus( title: string ): PiCompatibleSyntheticAgentStatus | null { + const nativeState = getPiStateTitleStatus(title) + if (nativeState) { + return nativeState + } // Why: a spinner anywhere means the agent is working, and that outranks the separator — // the frame is drawn over the idle separator position while a turn runs. if (containsBrailleSpinner(title)) { diff --git a/src/shared/pi-state-title-marker.ts b/src/shared/pi-state-title-marker.ts index 7f6ece71bd6..3d2ed19a759 100644 --- a/src/shared/pi-state-title-marker.ts +++ b/src/shared/pi-state-title-marker.ts @@ -27,15 +27,17 @@ function escapeForCharacterClass(marker: string): string { return marker.replace(/[\\\]^-]/g, '\\$&') } -// Why: `π` must sit at a token boundary so wrapper prefixes of any shape (`zsh | π : cwd`, +// Why: the brand must sit at a token boundary so wrapper prefixes (`zsh | OMP : cwd`, // `tmux: π : cwd`) still expose the marker, and whitespace must separate the marker so the // legacy no-space `π: cwd` disabled title keeps its historical idle classification. const PI_STATE_TITLE_RE = new RegExp( - `(?:^|[\\s|])π[ \\t]+([${PI_STATE_MARKERS.map(escapeForCharacterClass).join('')}])(?=\\s|$)`, + `(?:^|[\\s|])(π|Pi|OMP)[ \\t]+([${PI_STATE_MARKERS.map(escapeForCharacterClass).join('')}])(?=\\s|$)`, 'u' ) type PiStateTitleMatch = { + brand: string + brandIndex: number marker: PiStateMarker markerIndex: number } @@ -49,8 +51,14 @@ function matchPiStateTitle(title: string): PiStateTitleMatch | null { if (!match) { return null } + const marker = match[2] + if (marker !== ':' && marker !== '!' && marker !== '>') { + return null + } return { - marker: match[1] as PiStateMarker, + brand: match[1], + brandIndex: match.index + match[0].indexOf(match[1]), + marker, markerIndex: match.index + match[0].length - 1 } } @@ -73,3 +81,20 @@ export function clearPiStateWorkingMarker(title: string): string | null { } return `${title.slice(0, match.markerIndex)}${PI_IDLE_MARKER}${title.slice(match.markerIndex + 1)}` } + +/** The state marker owns identity too; its label may mention another agent. */ +export function getPiStateTitleBrand(title: string): 'Pi' | 'OMP' | null { + const match = matchPiStateTitle(title) + return match ? (match.brand === 'OMP' ? 'OMP' : 'Pi') : null +} + +/** Rebrand only the protocol prefix, preserving wrappers and the opaque session label. */ +export function rebrandPiStateTitle(title: string, brand: string): string | null { + const match = matchPiStateTitle(title) + if (!match) { + return null + } + return ( + title.slice(0, match.brandIndex) + brand + title.slice(match.brandIndex + match.brand.length) + ) +} diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 99908de9b84..6afdf0b3b62 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -175,6 +175,10 @@ export const AGENT_SESSION_REWIND_RUNTIME_CAPABILITY = 'agent-session.rewind.v1' export const AGENT_SESSION_TURN_ITEM_CAPABILITY = 'agent-session.turn-item.v1' as const export const AGENT_SESSION_BACKGROUND_TASK_STOP_CAPABILITY = 'agent-session.background-task-stop.v1' as const +// Why: agentSession.cancel has a strict schema, so clients must not send prompt identity to an +// older host that would reject the whole cancellation instead of falling back to turn stop. +export const AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY = + 'agent-session.prompt-cancel.v1' as const // Why: the host now publishes rows for work that is live inside a turn, and such // a row carries `stoppable: false` because no targeted stop can reach it. A // reader that predates the field draws a per-row Stop on every row it is given, @@ -294,6 +298,7 @@ export const RUNTIME_CAPABILITIES = [ AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY, AGENT_SESSION_REWIND_RUNTIME_CAPABILITY, AGENT_SESSION_BACKGROUND_TASK_STOP_CAPABILITY, + AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY, AGENT_SESSION_TURN_ITEM_CAPABILITY, AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY, AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY, diff --git a/src/shared/repo-types.ts b/src/shared/repo-types.ts index a6980b45d54..9e3e53cf7a3 100644 --- a/src/shared/repo-types.ts +++ b/src/shared/repo-types.ts @@ -52,6 +52,8 @@ export type Repo = { upstream?: GitHubRepositoryIdentity | null addedAt: number kind?: RepoKind + /** Git root proven during folder upgrade; keeps the original checkout locator stable. */ + folderUpgradeGitRootPath?: string gitUsername?: string worktreeBaseRef?: string /** Optional repo-scoped workspace root override. Relative paths resolve from `path`. */ diff --git a/src/shared/rpc-contract/structured-agent-session-params.ts b/src/shared/rpc-contract/structured-agent-session-params.ts index 7d2c73afb66..edbda12d6d9 100644 --- a/src/shared/rpc-contract/structured-agent-session-params.ts +++ b/src/shared/rpc-contract/structured-agent-session-params.ts @@ -2,11 +2,12 @@ import { z } from 'zod' import { isAgentSessionId } from '../agent-session-record' import { normalizeExecutionHostId } from '../execution-host' import { + AGENT_SESSION_ID_MAX_LENGTH, AGENT_SESSION_HISTORY_DIRECTIONS, AGENT_SESSION_HISTORY_MAX_LIMIT } from '../agent-session-wire' -export const MAX_ID_LENGTH = 512 +export const MAX_ID_LENGTH = AGENT_SESSION_ID_MAX_LENGTH // Four Claude questions with all four generated choices occupy 610 chars when fully percent-encoded. export const MAX_RESPONSE_OPTION_ID_LENGTH = 1024 @@ -164,11 +165,23 @@ export const CancelParams = z envelope: MutationEnvelope, turnId: Identifier('Invalid turn id'), scope: z.literal('background-tasks').optional(), - taskId: Identifier('Invalid task id').optional() + taskId: Identifier('Invalid task id').optional(), + prompt: z + .object({ + itemId: Identifier('Invalid item id'), + expectedRevision: z.number().int().positive() + }) + .strict() + .optional() }) .strict() - .refine((value) => value.taskId === undefined || value.scope === 'background-tasks', { - message: 'A task id requires background-task scope' + .superRefine((value, ctx) => { + if (value.taskId !== undefined && value.scope !== 'background-tasks') { + ctx.addIssue({ code: 'custom', message: 'A task id requires background-task scope' }) + } + if (value.prompt !== undefined && value.scope === 'background-tasks') { + ctx.addIssue({ code: 'custom', message: 'A prompt cannot use background-task scope' }) + } }) export const RespondParams = z diff --git a/src/shared/runtime-upload-staging-contract.ts b/src/shared/runtime-upload-staging-contract.ts new file mode 100644 index 00000000000..19a3d2f6261 --- /dev/null +++ b/src/shared/runtime-upload-staging-contract.ts @@ -0,0 +1,50 @@ +import type { SshMutationExpectation } from './ssh-types' + +export type RuntimeUploadSkipReason = 'missing' | 'symlink' | 'permission-denied' | 'unsupported' + +/** + * What staging observed about a file, so the uploader can refuse a source that + * was swapped between the two calls. Size alone misses a same-size replacement. + */ +export type StagedRuntimeUploadFileIdentity = { + byteLength: number + /** 0 when the filesystem does not report one; compared only when both sides have it. */ + inode: number + deviceId: number + modifiedAtMs: number +} + +export type StagedRuntimeUploadEntry = + | { relativePath: string; kind: 'directory' } + // Why: file bodies are streamed in slices at upload time, so staging carries + // identity the uploader re-checks against the handle it actually reads. + | ({ relativePath: string; kind: 'file' } & StagedRuntimeUploadFileIdentity) + +export type StagedRuntimeUploadSource = + | { + sourcePath: string + status: 'staged' + name: string + kind: 'file' | 'directory' + entries: StagedRuntimeUploadEntry[] + } + | { sourcePath: string; status: 'skipped'; reason: RuntimeUploadSkipReason } + | { sourcePath: string; status: 'failed'; reason: string } + +export type StageRuntimeUploadResult = { sources: StagedRuntimeUploadSource[] } + +/** Renderer → main request to pump one staged file's bytes to the runtime. */ +export type RuntimeUploadFileStreamRequest = { + environmentId: string + /** Client-local path of the dropped source (file, or root of a dropped directory). */ + sourceRootPath: string + /** Path of this file within the dropped directory; empty when the source is a file. */ + entryRelativePath: string + /** Identity staging recorded; a source that no longer matches is refused, not streamed. */ + expected: StagedRuntimeUploadFileIdentity + worktree: string + /** Destination path on the runtime, relative to the worktree. */ + relativePath: string + expectedEnvironmentPairingRevision?: number + expectedEnvironmentRuntimeId?: string +} & SshMutationExpectation diff --git a/src/shared/structured-agent-session-dispatch-rejection.ts b/src/shared/structured-agent-session-dispatch-rejection.ts index 9ea733e5832..14e197325d8 100644 --- a/src/shared/structured-agent-session-dispatch-rejection.ts +++ b/src/shared/structured-agent-session-dispatch-rejection.ts @@ -20,8 +20,14 @@ export const DISPATCH_REJECTED_WRITE_FAILED = 'provider_write_failed' -/** Local admission refused the frame before any transport was involved. */ +/** Local admission refused the frame before any transport was involved. Two + * strings rather than one provider-neutral marker because both are already + * durable journal reasons; rewording either would relabel rows on disk. */ export const DISPATCH_REJECTED_QUEUE_FULL = 'claude structured dispatch queue is full' +export const DISPATCH_REJECTED_CODEX_QUEUE_FULL = 'codex structured dispatch queue is full' + +/** The provider confirmed a queued frame was withdrawn before execution. */ +export const DISPATCH_REJECTED_CANCELLED = 'provider_cancelled_before_start' export function dispatchWriteFailureReason(error: unknown): string { const detail = error instanceof Error ? error.message : String(error) @@ -45,6 +51,9 @@ export function dispatchRejectionWasTransportWriteFailure( */ export function dispatchRejectionReasonIsInternal(reason: string | null | undefined): boolean { return ( - dispatchRejectionWasTransportWriteFailure(reason) || reason === DISPATCH_REJECTED_QUEUE_FULL + dispatchRejectionWasTransportWriteFailure(reason) || + reason === DISPATCH_REJECTED_QUEUE_FULL || + reason === DISPATCH_REJECTED_CODEX_QUEUE_FULL || + reason === DISPATCH_REJECTED_CANCELLED ) } diff --git a/src/shared/structured-agent-session-outbox.ts b/src/shared/structured-agent-session-outbox.ts index 0030f17a5b0..80c862f9f02 100644 --- a/src/shared/structured-agent-session-outbox.ts +++ b/src/shared/structured-agent-session-outbox.ts @@ -2,6 +2,7 @@ import type { AgentJournalMessageItem, AgentJournalSubmission } from './agent-se import { agentSessionRefusalOperationState } from './agent-session-refusal-retry' import type { AgentSessionWireRefusalCode } from './agent-session-wire' import { structuredAgentSessionPayloadFingerprint } from './structured-agent-session-mutation' +import { DISPATCH_REJECTED_CANCELLED } from './structured-agent-session-dispatch-rejection' export type StructuredAgentSessionOutboxState = 'queued' | 'dispatching' | 'unconfirmed' @@ -102,6 +103,12 @@ export function reconcileStructuredAgentSessionOutbox( if (submission?.dispatchState === 'accepted') { return [] } + if ( + submission?.dispatchState === 'rejected' && + submission.reason === DISPATCH_REJECTED_CANCELLED + ) { + return [] + } if (submission?.dispatchState === 'pending') { return entry.state === 'dispatching' ? [entry] : [{ ...entry, state: 'dispatching' as const }] } diff --git a/src/shared/structured-agent-session-send-disposition.test.ts b/src/shared/structured-agent-session-send-disposition.test.ts index 4ab5c107db0..b567c2a8dd2 100644 --- a/src/shared/structured-agent-session-send-disposition.test.ts +++ b/src/shared/structured-agent-session-send-disposition.test.ts @@ -8,11 +8,13 @@ import type { AgentJournalSubmission } from './agent-session-journal-types' import type { AgentSessionMutationResult, AgentSessionSendResult } from './agent-session-wire' import { dispatchWriteFailureReason, + DISPATCH_REJECTED_CANCELLED, DISPATCH_REJECTED_QUEUE_FULL } from './structured-agent-session-dispatch-rejection' import { disposeStructuredAgentSessionSendResult } from './structured-agent-session-send-disposition' import { createStructuredAgentSessionOutboxEntry, + reconcileStructuredAgentSessionOutbox, type StructuredAgentSessionOutboxEntry } from './structured-agent-session-outbox' @@ -55,6 +57,15 @@ function notice(reason: string | null): string | null { } describe('what a rejection shows the user', () => { + it('removes a queued message the provider confirms Stop cancelled', () => { + const result = rejectedWith(DISPATCH_REJECTED_CANCELLED) + if (!result.ok) { + throw new Error('expected rejected submission fixture') + } + + expect(reconcileStructuredAgentSessionOutbox([entry], [result.value.submission])).toEqual([]) + }) + it('never puts the transport marker on screen', () => { const shown = notice(dispatchWriteFailureReason(new Error('broken pipe'))) // `provider_write_failed: broken pipe` names nothing a person can act on. diff --git a/tests/e2e/folder-upgrade-identity-persistence.unit.test.ts b/tests/e2e/folder-upgrade-identity-persistence.unit.test.ts new file mode 100644 index 00000000000..417133b7c43 --- /dev/null +++ b/tests/e2e/folder-upgrade-identity-persistence.unit.test.ts @@ -0,0 +1,101 @@ +import { getDefaultWorkspaceSession } from '../../src/shared/constants' +import type { AppState } from '../../src/renderer/src/store/types' +import { getRemovedWorktreeIdsAfterAuthoritativeScan } from '../../src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership' +import { mergeWorktree } from '../../src/main/ipc/worktree-metadata-merge' +import { createFolderWorktree } from '../../src/main/repo-worktrees' +import { afterEach, beforeEach, expect, it } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + createStore, + makeRepo, + makeTerminalTab, + testState +} from '../../src/main/persistence-test-harness' +import { buildDetectedGitWorktrees } from '../../src/main/ipc/worktrees/listing/ssh-worktree-fallback' +import { resolveRepoWorktreeRows } from '../../src/main/runtime/repo-worktree-row-resolution' + +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-folder-upgrade-store-')) +}) +afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) +}) + +it('retains the folder instance and metadata through upgrade, listing, and Store reload', async () => { + const store = createStore() + const owner = makeRepo({ id: 'folder', path: 'C:\\projects\\draft', kind: 'folder' }) + store.addRepo(owner) + const id = `folder::${owner.path}` + const before = store.setWorktreeMetaForHost(id, 'local', { comment: 'ongoing OMP work' }) + store.setWorktreeMetaForHost(id, 'ssh:builder', { comment: 'other host' }) + store.setWorkspaceSession({ + ...getDefaultWorkspaceSession(), + activeRepoId: owner.id, + activeWorktreeId: id, + activeTabId: 'omp-tab', + tabsByWorktree: { [id]: [makeTerminalTab({ id: 'omp-tab', worktreeId: id })] } + }) + store.updateRepo(owner.id, { kind: 'git', folderUpgradeGitRootPath: 'C:/projects/draft' }) + store.flush() + const reloaded = createStore() + const repo = reloaded.getRepo(owner.id) + expect(repo?.folderUpgradeGitRootPath).toBe('C:/projects/draft') + if (!repo) { + throw new Error('registered repo missing') + } + const worktrees = [ + { path: 'C:/projects/draft', branch: 'main', head: 'abc', isMainWorktree: true, isBare: false } + ] + const detected = buildDetectedGitWorktrees(reloaded, repo, worktrees) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The purge reader only reads these catalog fields when session hydration is complete. + const state = { + repos: [owner], + worktreesByRepo: { [owner.id]: [mergeWorktree(owner.id, createFolderWorktree(owner), before)] }, + detectedWorktreesByRepo: {}, + hasHydratedWorktreePurge: true + } as unknown as AppState + expect( + getRemovedWorktreeIdsAfterAuthoritativeScan( + state, + owner.id, + { repoId: owner.id, authoritative: true, source: 'git', worktrees: detected }, + 'local' + ) + ).toEqual([]) + expect(reloaded.getWorkspaceSession()).toMatchObject({ + activeWorktreeId: id, + activeTabId: 'omp-tab', + tabsByWorktree: { [id]: [{ id: 'omp-tab', worktreeId: id }] } + }) + const runtime = await resolveRepoWorktreeRows( + { + store: reloaded, + scanRepo: async () => ({ ok: true, worktrees }), + listFolderWorkspaces: () => [] + }, + repo, + reloaded.getAllWorktreeMeta(), + new Map() + ) + for (const rows of [detected, runtime]) { + expect(rows[0]).toMatchObject({ + id, + instanceId: before.instanceId, + comment: 'ongoing OMP work', + hostId: 'local' + }) + } + expect(reloaded.getWorktreeMetaForHost(id, 'ssh:builder')?.comment).toBe('other host') + reloaded.flush() + expect(createStore().getWorktreeMetaForHost(id, 'local')?.instanceId).toBe(before.instanceId) +}) + +it('drops upgrade path evidence when execution ownership changes', () => { + const store = createStore() + store.addRepo(makeRepo({ id: 'folder', path: 'C:\\projects\\draft', kind: 'git' })) + store.updateRepo('folder', { folderUpgradeGitRootPath: 'C:/projects/draft' }) + store.updateRepo('folder', { executionHostId: 'ssh:builder' }) + expect(store.getRepo('folder')?.folderUpgradeGitRootPath).toBeUndefined() +}) diff --git a/tests/e2e/markdown-literal-save-reopen.spec.ts b/tests/e2e/markdown-literal-save-reopen.spec.ts new file mode 100644 index 00000000000..745396d8771 --- /dev/null +++ b/tests/e2e/markdown-literal-save-reopen.spec.ts @@ -0,0 +1,119 @@ +import { mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { test, expect } from './helpers/orca-app' +import { switchToWorktree, waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { + cleanupMarkdownFixture, + closeActiveEditorTab, + createMarkdownFixture, + getActiveWorktreeContext, + openMarkdownFixture, + waitForRichMarkdownEditor +} from './helpers/markdown-editor-fixture' +import { + createRuntimeDesktopPairingOffer, + launchPairedElectronClient, + type PairedElectronClient +} from './helpers/paired-electron-client' +import { waitForPairedClientWorktree } from './helpers/paired-client-host-session' + +const SOURCE = '# Compatibility\n\n[[]] [[a|]]\n\n[**Bold**](https://example.com)\n\nEnd\n' +const TYPED = '[typed](./target.md)' + +for (const workspace of ['git', 'folder', 'paired remote'] as const) { + test(`preserves literal Markdown and formatted links when saving in ${workspace}`, async ({ + orcaPage, + registerPostElectronShutdownCleanup + }, testInfo) => { + test.setTimeout(180_000) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + if (workspace === 'folder') { + const folder = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'orca-markdown-folder-'))) + registerPostElectronShutdownCleanup(async () => + rmSync(folder, { recursive: true, force: true }) + ) + await orcaPage.evaluate(async (folderPath) => { + const repo = await window.__store!.getState().addNonGitFolder(folderPath) + if (!repo) { + throw new Error('Could not add folder workspace') + } + }, folder) + await expect + .poll(async () => (await getActiveWorktreeContext(orcaPage)).rootPath) + .toBe(folder) + } + const context = await getActiveWorktreeContext(orcaPage) + const filePath = await createMarkdownFixture( + context, + 'markdown-compatibility', + workspace.replaceAll(' ', '-'), + testInfo.workerIndex, + SOURCE + ) + let client: PairedElectronClient | undefined + try { + if (workspace === 'paired remote') { + client = await launchPairedElectronClient( + await createRuntimeDesktopPairingOffer(orcaPage), + testInfo, + 'Markdown save compatibility' + ) + await waitForPairedClientWorktree(client.page, context.worktreeId) + await client.page.evaluate( + ({ worktreeId, environmentId }) => { + window.__store!.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`) + }, + { worktreeId: context.worktreeId, environmentId: client.environmentId } + ) + } + const page = client?.page ?? orcaPage + await openMarkdownFixture(page, context, filePath) + if (client) { + expect( + await page.evaluate(() => { + const state = window.__store!.getState() + return state.openFiles.find((file) => file.id === state.activeFileId) + ?.runtimeEnvironmentId + }) + ).toBe(client.environmentId) + } + const editor = await waitForRichMarkdownEditor(page) + await expect(editor).toContainText('[[]] [[a|]]') + await expect(editor.locator('a strong')).toHaveText('Bold') + await editor.click() + await page.keyboard.press('ControlOrMeta+End') + await page.keyboard.press('Enter') + await page.keyboard.insertText(TYPED) + await expect(editor.locator('a').filter({ hasText: 'typed' })).toHaveCount(0) + await page.keyboard.press('ControlOrMeta+S') + await expect + .poll(() => readFileSync(filePath, 'utf8'), { timeout: 15_000 }) + .toContain('typed') + const saved = readFileSync(filePath, 'utf8') + expect(saved).toContain('[[]] [[a|]]') + expect(saved).toContain('[**Bold**](https://example.com)') + await testInfo.attach('saved-markdown', { body: saved, contentType: 'text/markdown' }) + await closeActiveEditorTab(page, filePath) + // Closing a folder's only tab intentionally returns to the landing screen. + if (workspace === 'folder') { + await switchToWorktree(page, context.worktreeId) + } + await openMarkdownFixture(page, context, filePath) + const reopened = await waitForRichMarkdownEditor(page) + await expect(reopened).toContainText(TYPED) + await expect(reopened).toContainText('[[]] [[a|]]') + await expect(reopened.locator('a strong')).toHaveText('Bold') + await expect(reopened.locator('a').filter({ hasText: 'typed' })).toHaveCount(0) + await testInfo.attach('reopened-editor', { + body: await page.screenshot(), + contentType: 'image/png' + }) + await closeActiveEditorTab(page, filePath) + } finally { + await client?.dispose() + await cleanupMarkdownFixture(filePath) + } + }) +} diff --git a/tests/e2e/omp-title-marker.spec.ts b/tests/e2e/omp-title-marker.spec.ts new file mode 100644 index 00000000000..4f2db0ddbd9 --- /dev/null +++ b/tests/e2e/omp-title-marker.spec.ts @@ -0,0 +1,43 @@ +import { writeFile } from 'node:fs/promises' +import { buildShellCommandFromArgv } from '../../src/shared/tui-agent-startup-shell' +import { test, expect } from './helpers/orca-app' +import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { + execInTerminal, + sendToTerminal, + waitForActivePanePtyId, + waitForActiveTerminalManager +} from './helpers/terminal' + +test('OMP spaced-colon title renders working and clears on idle', async ({ + orcaPage +}, testInfo) => { + test.skip( + process.platform === 'win32', + 'POSIX title replay; Windows formatter bytes have separate coverage' + ) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + await waitForActiveTerminalManager(orcaPage) + const ptyId = await waitForActivePanePtyId(orcaPage) + const script = testInfo.outputPath('title-replay.cjs') + await writeFile( + script, + ` +process.stdout.write('\\x1b]0;OMP : Image review\\x07') +process.stdin.on('data', () => process.stdout.write('\\x1b]0;OMP > Image review\\x07')) +` + ) + await execInTerminal( + orcaPage, + ptyId, + buildShellCommandFromArgv([process.execPath, script], 'posix') + ) + const working = orcaPage.locator('[aria-label="Working"]') + await expect(working.first()).toBeVisible({ timeout: 15000 }) + await orcaPage.screenshot({ path: testInfo.outputPath('omp-title-working.png') }) + await sendToTerminal(orcaPage, ptyId, '\r') + await expect(working).toHaveCount(0) + await orcaPage.screenshot({ path: testInfo.outputPath('omp-title-idle.png') }) +}) diff --git a/tests/tools/omp-child-history-rendered/README.md b/tests/tools/omp-child-history-rendered/README.md new file mode 100644 index 00000000000..52cd99a8371 --- /dev/null +++ b/tests/tools/omp-child-history-rendered/README.md @@ -0,0 +1,15 @@ +# Child history resume proof + +Run `ORCA_BACKGROUND_LAUNCH=1 node tests/tools/omp-child-history-rendered/run.mjs`. +The hidden Electron fixture renders the production virtual history list and styles with +injected OMP, Claude and empty OMP transcripts. It opens eight generations lazily, +resumes the deepest child into a folder target, and checks that Claude remains view-only. +It verifies parent-row measurement avoids overlap, indentation stops growing, scrolling +away/back restores expansion, and collapse removes descendants. CDP screenshots and +native hidden/unfocused window assertions are saved in `.bench-fixtures`. +This exercises production rendering and callbacks, not the complete terminal launch UI. + +Run `ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-child-session-resume-smoke.mjs /path/to/oh-my-pi` +for a zero-model-call check against real OMP session storage and CLI parsing. The smoke +builds Orca's path-based resume command, creates parent, child and grandchild transcripts, +and verifies OMP selects each descendant's distinct identity in a folder workspace. diff --git a/tests/tools/omp-child-history-rendered/fixture.css b/tests/tools/omp-child-history-rendered/fixture.css new file mode 100644 index 00000000000..30978a0b8da --- /dev/null +++ b/tests/tools/omp-child-history-rendered/fixture.css @@ -0,0 +1,5 @@ +@import '../../../src/renderer/src/assets/main.css'; +@source './fixture.tsx'; +@source '../../../src/renderer/src/components/right-sidebar'; +@source '../../../src/renderer/src/components/ui'; +@source '../../../src/renderer/src/components/AgentStateDot.tsx'; diff --git a/tests/tools/omp-child-history-rendered/fixture.tsx b/tests/tools/omp-child-history-rendered/fixture.tsx new file mode 100644 index 00000000000..b8fb1b54707 --- /dev/null +++ b/tests/tools/omp-child-history-rendered/fixture.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react' +import { createRoot } from 'react-dom/client' +import { TooltipProvider } from '../../../src/renderer/src/components/ui/tooltip' +import { AiVaultSessionVirtualList } from '../../../src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList' +import type { AiVaultSession } from '../../../src/shared/ai-vault-types' +import './fixture.css' + +const parent: AiVaultSession = { + id: 'parent', + agent: 'omp', + executionHostId: 'local', + sessionId: 'parent', + title: 'Coordinate the change', + cwd: '/project', + branch: null, + model: null, + filePath: '/sessions/parent.jsonl', + codexHome: null, + createdAt: null, + updatedAt: null, + modifiedAt: '2026-09-14T00:00:00Z', + messageCount: 2, + totalTokens: 0, + previewMessages: [], + queuedMessageCount: 0, + subagentTranscriptCount: 3, + resumeCommand: 'omp --resume parent', + subagent: null +} +const children: AiVaultSession[] = [ + { + ...parent, + id: 'child', + sessionId: 'child', + title: 'OMP worker with saved conversation', + filePath: '/sessions/parent/child.jsonl', + subagentTranscriptCount: 1, + subagent: { parentSessionId: 'parent', agentType: 'worker', status: 'completed' } + }, + { + ...parent, + id: 'claude', + agent: 'claude', + filePath: '/sessions/parent/claude.jsonl', + subagentTranscriptCount: 0, + title: 'Claude worker (view only)', + subagent: { parentSessionId: 'parent', agentType: 'worker', status: 'completed' } + }, + { + ...parent, + id: 'empty', + sessionId: 'empty', + filePath: '/sessions/parent/empty.jsonl', + subagentTranscriptCount: 0, + messageCount: 0, + title: 'OMP worker without saved turns', + subagent: { parentSessionId: 'parent', agentType: 'worker', status: 'stopped' } + } +] + +const descendants = Array.from({ length: 7 }, (_, index): AiVaultSession => ({ + ...parent, + id: `depth-${index}`, + sessionId: `depth-${index}`, + filePath: `/sessions/parent/child/${Array.from({ length: index + 1 }, () => 'nested').join('/')}.jsonl`, + title: `Research depth ${index + 2}`, + subagentTranscriptCount: index === 6 ? 0 : 1, + subagent: { parentSessionId: 'child', agentType: 'researcher', status: 'completed' } +})) +const requests: string[] = [] +Object.defineProperty(window, 'nestedRequests', { value: requests }) +Object.defineProperty(window, 'api', { + value: { + aiVault: { + listSubagentSessions: async ({ parentFilePath }: { parentFilePath: string }) => { + requests.push(parentFilePath) + await new Promise((resolve) => setTimeout(resolve, 250)) + const index = descendants.findIndex((session) => session.filePath === parentFilePath) + return { + sessions: + parentFilePath === parent.filePath + ? children + : parentFilePath === children[0].filePath + ? [descendants[0]] + : index !== -1 + ? descendants.slice(index + 1, index + 2) + : [], + issues: [] + } + } + } + } +}) +const sessions = [ + parent, + ...Array.from({ length: 100 }, (_, index) => ({ + ...parent, + id: `row-${index}`, + sessionId: `row-${index}`, + title: `Other session ${index}`, + filePath: `/sessions/other-${index}.jsonl`, + subagentTranscriptCount: 0 + })) +] +const ignore = () => {} +function App() { + const [result, setResult] = useState('No resume requested') + return ( + +
+

Agent Session History

+
+ ({ command: session.resumeCommand })} + getOriginalPaneTarget={() => null} + getSessionLiveState={() => null} + getWorktreeInfo={() => null} + getSessionResumeState={() => ({ + blocked: false, + worktreeId: 'folder:project', + usesSessionWorktree: true + })} + getSessionResumeActions={() => ({ + worktree: { worktreeId: 'folder:project', disabled: false }, + newTab: { worktreeId: 'folder:project', disabled: false } + })} + getSessionResumeInChat={() => ({ available: false, reason: 'agent' })} + onToggleGroup={ignore} + onJumpToOriginalPane={ignore} + onJumpToWorktree={ignore} + onResume={(session, target) => setResult(`Resume ${session.sessionId} in ${target}`)} + onContinueInNewSession={ignore} + onResumeInNewChat={ignore} + onCopyResume={ignore} + onCopyId={ignore} + onCopyPath={ignore} + onOpenLog={ignore} + onRevealLog={ignore} + onOpenCwd={ignore} + onRequestDelete={ignore} + /> +
+ {result} +
+
+ ) +} +createRoot(document.getElementById('root')!).render() diff --git a/tests/tools/omp-child-history-rendered/index.html b/tests/tools/omp-child-history-rendered/index.html new file mode 100644 index 00000000000..44793ff9830 --- /dev/null +++ b/tests/tools/omp-child-history-rendered/index.html @@ -0,0 +1,10 @@ + + + + + + +
+ + + diff --git a/tests/tools/omp-child-history-rendered/run.mjs b/tests/tools/omp-child-history-rendered/run.mjs new file mode 100644 index 00000000000..67866ffc985 --- /dev/null +++ b/tests/tools/omp-child-history-rendered/run.mjs @@ -0,0 +1,194 @@ +import { _electron as electron, expect } from '@stablyai/playwright-test' +import { build as buildMain } from 'esbuild' +import { build as buildRenderer } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { + throw new Error('Requires ORCA_BACKGROUND_LAUNCH=1') +} +const root = fileURLToPath(new URL('../../../', import.meta.url)) +const parent = path.join(root, '.bench-fixtures') +mkdirSync(parent, { recursive: true }) +const output = mkdtempSync(path.join(parent, 'omp-child-history-')) +const main = path.join(output, 'main.cjs') +await buildMain({ + entryPoints: [path.join(root, 'tests/tools/benchmarks/spinner-rendering/main.ts')], + outfile: main, + bundle: true, + platform: 'node', + format: 'cjs', + external: ['electron'] +}) +await buildRenderer({ + configFile: false, + root: import.meta.dirname, + base: './', + logLevel: 'silent', + plugins: [react(), tailwindcss()], + resolve: { alias: { '@': path.join(root, 'src/renderer/src') } }, + build: { outDir: path.join(output, 'renderer'), emptyOutDir: true } +}) +const { ELECTRON_RUN_AS_NODE: _runAsNode, ...env } = process.env +const app = await electron.launch({ args: [main], env: { ...env, ORCA_BACKGROUND_LAUNCH: '1' } }) +const report = { + scope: + 'Production virtual history list and nested rows with injected records; hidden Electron/CDP layout and action targeting, not full launch UI.' +} +try { + const page = await app.firstWindow() + const errors = [] + page.on('pageerror', (error) => { + errors.push(error.message) + console.error(error) + }) + await page.goto(pathToFileURL(path.join(output, 'renderer/index.html')).href) + await page.getByTestId('ai-vault-session-toggle-details').first().click() + await expect(page.getByText('OMP worker with saved conversation')).toBeVisible() + expect(await page.evaluate(() => window.nestedRequests.length)).toBe(1) + const cdp = await page.context().newCDPSession(page) + const capture = async (name) => { + await page.evaluate(async () => { + await Promise.all( + document + .getAnimations() + .filter((animation) => animation.effect?.getComputedTiming().iterations !== Infinity) + .map((animation) => animation.finished.catch(() => {})) + ) + }) + const { data } = await cdp.send('Page.captureScreenshot', { format: 'png' }) + writeFileSync(path.join(output, `${name}.png`), Buffer.from(data, 'base64')) + } + await capture('child-resume-affordance') + await page + .getByText('OMP worker with saved conversation') + .locator('..') + .getByRole('button', { name: 'Resume in Worktree' }) + .click() + await expect(page.getByText('Resume child in folder:project')).toBeVisible() + await capture('child-resume-callback') + for (let depth = 2; depth <= 8; depth++) { + const row = + depth === 2 + ? page.getByText('OMP worker with saved conversation') + : page.getByText(`Research depth ${depth - 1}`, { exact: true }) + await row.locator('..').getByRole('button', { name: 'Subagents (1)' }).click() + await expect(page.getByText(`Research depth ${depth}`, { exact: true })).toBeVisible() + if (depth === 2) { + await capture('grandchild-disclosure-dark') + await page.evaluate(() => document.documentElement.classList.remove('dark')) + await capture('grandchild-disclosure-light') + await page.evaluate(() => document.documentElement.classList.add('dark')) + } + } + await page + .getByText('Research depth 8', { exact: true }) + .locator('..') + .getByRole('button', { name: 'Resume in Worktree' }) + .click() + await expect(page.getByText('Resume depth-6 in folder:project')).toBeVisible() + const scroll = page.locator('.overflow-y-auto').first() + const checkLayout = async () => { + const layout = await page.locator('[data-index="1"]').evaluate((element) => { + const next = document.querySelector('[data-index="2"]') + return { + height: element.getBoundingClientRect().height, + bottom: element.getBoundingClientRect().bottom, + nextTop: next?.getBoundingClientRect().top + } + }) + expect(layout.nextTop).toBeGreaterThanOrEqual(layout.bottom - 1) + return layout + } + await expect + .poll(async () => { + const bounds = await page.locator('[data-index="1"]').boundingBox() + const next = await page.locator('[data-index="2"]').boundingBox() + return next.y - bounds.y - bounds.height + }) + .toBeGreaterThanOrEqual(-1) + report.expandedLayout = await checkLayout() + const lefts = await page + .getByText(/^Research depth /) + .evaluateAll((elements) => + elements.map((element) => element.parentElement.getBoundingClientRect().left) + ) + expect(lefts.at(-1)).toBe(lefts.at(-2)) + report.depthLefts = lefts + await capture('nested-expanded') + report.sidebarWidths = [] + for (const width of [280, 350]) { + await page.getByTestId('history-panel').evaluate((element, value) => { + element.style.width = `${value}px` + }, width) + const measurements = await page.getByText(/^Research depth /).evaluateAll((elements) => + elements.map((element) => { + const row = element.parentElement + const bounds = row.getBoundingClientRect() + return { + titleWidth: element.getBoundingClientRect().width, + rowRight: bounds.right, + buttonsRight: Math.max( + ...[...row.querySelectorAll('button')].map( + (button) => button.getBoundingClientRect().right + ) + ) + } + }) + ) + expect( + measurements.every((row) => row.titleWidth >= 40 && row.buttonsRight <= row.rowRight + 1) + ).toBe(true) + report.sidebarWidths.push({ width, measurements }) + await page.getByText('Research depth 4', { exact: true }).scrollIntoViewIfNeeded() + await capture(`nested-width-${width}`) + } + await page.getByTestId('history-panel').evaluate((element) => { + element.style.width = '' + }) + + await scroll.evaluate((element) => { + element.scrollTop = element.scrollHeight + }) + await expect(page.getByText('OMP worker with saved conversation')).toHaveCount(0) + await scroll.evaluate((element) => { + element.scrollTop = 0 + }) + await expect(page.getByText('Research depth 8', { exact: true })).toBeVisible() + await page + .getByText('OMP worker with saved conversation') + .locator('..') + .getByRole('button', { name: 'Subagents (1)' }) + .click() + await expect(page.getByText('Research depth 8', { exact: true })).toHaveCount(0) + await expect + .poll(async () => { + const bounds = await page.locator('[data-index="1"]').boundingBox() + return bounds.height + }) + .toBeLessThan(report.expandedLayout.height) + await expect + .poll(async () => { + const bounds = await page.locator('[data-index="1"]').boundingBox() + const next = await page.locator('[data-index="2"]').boundingBox() + return Math.abs(next.y - bounds.y - bounds.height) + }) + .toBeLessThanOrEqual(1) + report.collapsedLayout = await checkLayout() + report.requests = await page.evaluate(() => window.nestedRequests) + await capture('nested-collapsed') + expect(errors).toEqual([]) + report.windows = await app.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().map((window) => ({ + visible: window.isVisible(), + focused: window.isFocused() + })) + ) + expect(report.windows.every((window) => !window.visible && !window.focused)).toBe(true) +} finally { + writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`) + console.log(`OMP child history evidence: ${output}`) + await app.close() +} diff --git a/tests/tools/omp-child-session-resume-smoke.mjs b/tests/tools/omp-child-session-resume-smoke.mjs new file mode 100644 index 00000000000..baa7c7d4f19 --- /dev/null +++ b/tests/tools/omp-child-session-resume-smoke.mjs @@ -0,0 +1,82 @@ +// Bun; argv[2] is a read-only OMP checkout. No model requests. +import assert from 'node:assert/strict' +import { mkdtemp, mkdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { buildAiVaultResumeCommand } from '../../src/shared/ai-vault-resume-command.ts' +import { tokenizeStartupCommand } from '../../src/shared/tui-agent-startup-shell.ts' + +assert.ok(process.argv[2], 'Pass a read-only OMP checkout path') +const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-child-resume-')) +process.env.HOME = join(scratch, 'home') +process.env.USERPROFILE = process.env.HOME +process.env.XDG_CONFIG_HOME = join(scratch, 'config') +process.env.XDG_DATA_HOME = join(scratch, 'data') +process.env.XDG_STATE_HOME = join(scratch, 'state') +process.env.PI_CODING_AGENT_DIR = join(scratch, 'agent') +delete process.env.OMP_CODING_AGENT_DIR +delete process.env.PI_CONFIG_DIR +delete process.env.OMP_PROFILE +delete process.env.PI_PROFILE +delete process.env.PI_CONFIG_FILES +const source = (name) => + pathToFileURL(join(resolve(process.argv[2]), 'packages/coding-agent/src', name)).href +const managers = [] +try { + await mkdir(process.env.HOME, { recursive: true }) + const { SessionManager } = await import(source('session/session-manager.ts')) + const { Settings } = await import(source('config/settings.ts')) + const { createSessionManager } = await import(source('main.ts')) + const { parseArgs } = await import(source('cli/args.ts')) + const cwd = join(scratch, 'folder workspace') + await mkdir(cwd) + const parent = SessionManager.create(cwd) + managers.push(parent) + parent.appendMessage({ role: 'user', content: 'coordinate work', timestamp: Date.now() }) + await parent.ensureOnDisk() + await parent.flush() + const child = SessionManager.create(cwd, parent.getSessionFile().replace(/\.jsonl$/, '')) + managers.push(child) + child.appendMessage({ role: 'user', content: 'child task', timestamp: Date.now() }) + await child.ensureOnDisk() + await child.flush() + const grandchild = SessionManager.create(cwd, child.getSessionFile().replace(/\.jsonl$/, '')) + managers.push(grandchild) + grandchild.appendMessage({ role: 'user', content: 'grandchild research', timestamp: Date.now() }) + await grandchild.ensureOnDisk() + await grandchild.flush() + for (const target of [child, grandchild]) { + const command = buildAiVaultResumeCommand({ + agent: 'omp', + sessionId: target.getSessionId(), + resumeFilePath: target.getSessionFile(), + cwd: null, + platform: process.platform, + shell: 'posix' + }) + const tokens = tokenizeStartupCommand(command, 'posix') + assert.ok(tokens.ok) + const args = parseArgs(tokens.tokens.slice(1)) + assert.equal(args.resume, target.getSessionFile()) + const settings = await Settings.init({ cwd }) + const resumed = await createSessionManager(args, cwd, settings) + managers.push(resumed) + assert.equal(resumed.getSessionId(), target.getSessionId()) + assert.notEqual(resumed.getSessionId(), parent.getSessionId()) + } + console.log( + JSON.stringify({ + childPathResumed: true, + grandchildPathResumed: true, + distinctFromParent: true, + folderWorkspace: true, + modelCalls: 0 + }) + ) +} finally { + for (const manager of managers) { + await manager?.close() + } + await rm(scratch, { recursive: true, force: true }) +} diff --git a/tests/tools/omp-history-title-smoke.mjs b/tests/tools/omp-history-title-smoke.mjs new file mode 100644 index 00000000000..7c5d84aaa2c --- /dev/null +++ b/tests/tools/omp-history-title-smoke.mjs @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict' +import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +assert.ok(process.argv[2], 'Pass a read-only OMP checkout') +const orcaRoot = fileURLToPath(new URL('../../', import.meta.url)) +const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-history-title-')) +process.env.HOME = join(scratch, 'home') +process.env.USERPROFILE = process.env.HOME +for (const [key, value] of Object.entries({ + XDG_CONFIG_HOME: 'config', + XDG_DATA_HOME: 'data', + XDG_STATE_HOME: 'state', + XDG_CACHE_HOME: 'cache' +})) { + process.env[key] = join(scratch, value) +} +for (const key of [ + 'OMP_CODING_AGENT_DIR', + 'PI_CODING_AGENT_DIR', + 'OMP_PROFILE', + 'PI_PROFILE', + 'PI_CONFIG_DIR', + 'PI_CONFIG_FILES' +]) { + delete process.env[key] +} +await mkdir(process.env.HOME, { recursive: true }) +const source = (root, path) => pathToFileURL(join(resolve(root), path)).href +const { SessionManager } = await import( + source(process.argv[2], 'packages/coding-agent/src/session/session-manager.ts') +) +const { parseMessageGraphSessionFile } = await import( + source(orcaRoot, 'src/main/ai-vault/session-scanner-graph-parsers.ts') +) +const { createSessionParseStats, parseAgentSessionFileCached } = await import( + source(orcaRoot, 'src/main/ai-vault/session-scanner-parse-cache.ts') +) +const stats = createSessionParseStats() +const manager = SessionManager.create(scratch, join(scratch, 'sessions')) +try { + manager.appendMessage({ role: 'user', content: 'Original first prompt', timestamp: Date.now() }) + await manager.ensureOnDisk() + await manager.flush() + const candidate = async () => { + const details = await stat(manager.getSessionFile()) + return { + agent: 'omp', + codexHome: null, + file: { + path: manager.getSessionFile(), + mtimeMs: details.mtimeMs, + modifiedAt: details.mtime.toISOString(), + sizeBytes: details.size + } + } + } + const initial = await parseAgentSessionFileCached(await candidate(), process.platform, stats) + assert.equal(initial.title, 'Original first prompt') + await manager.setSessionName('Explicit renamed conversation', 'user') + await manager.flush() + const path = manager.getSessionFile() + const details = await stat(path) + const parsed = await parseMessageGraphSessionFile( + 'omp', + { path, mtimeMs: details.mtimeMs, modifiedAt: details.mtime.toISOString() }, + process.platform + ) + const refreshed = await parseAgentSessionFileCached(await candidate(), process.platform, stats) + assert.equal(parsed?.title, manager.getSessionName()) + assert.equal(refreshed?.title, manager.getSessionName()) + assert.equal(stats.fullParses, 1) + assert.equal(stats.incremental, 1) + assert.equal(initial.title, 'Original first prompt') + const reused = await parseAgentSessionFileCached(await candidate(), process.platform, stats) + assert.equal(reused, refreshed) + console.log( + JSON.stringify({ + actualOmpPersistence: true, + renamedTitlePreserved: true, + cachedRenamePreserved: true, + unchangedSnapshotReused: true, + fullParses: stats.fullParses, + incrementalParses: stats.incremental, + modelCalls: 0 + }) + ) +} finally { + await manager.close() + await rm(scratch, { recursive: true, force: true }) +} diff --git a/tests/tools/omp-native-title-capture.mjs b/tests/tools/omp-native-title-capture.mjs new file mode 100644 index 00000000000..9f4be2cc7c0 --- /dev/null +++ b/tests/tools/omp-native-title-capture.mjs @@ -0,0 +1,18 @@ +// Run under Bun through capture-agent-pty-transcript.mjs; sourceRoot is read-only. +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +const sourceRoot = process.argv[2] +if (!sourceRoot) { + throw new Error('Expected path to the read-only oh-my-pi checkout') +} +const { buildTerminalTitleWithState } = await import( + pathToFileURL(resolve(sourceRoot, 'packages/coding-agent/src/utils/title-generator.ts')).href +) +for (const state of ['working', 'idle', 'attention']) { + for (const label of ['Run a long task', 'release | π : note | OMP ! action required ✦']) { + // Exercise upstream's explicit Windows argument, independently of the capture host OS. + const title = buildTerminalTitleWithState(label, state, 0, true, 'win32') + process.stdout.write(`\x1b]0;${title}\x07`) + } +} diff --git a/tests/tools/omp-picker-search-rendered/README.md b/tests/tools/omp-picker-search-rendered/README.md new file mode 100644 index 00000000000..653a7bd1c33 --- /dev/null +++ b/tests/tools/omp-picker-search-rendered/README.md @@ -0,0 +1,17 @@ +# OMP picker project-name search (#14319) + +Run `ORCA_BACKGROUND_LAUNCH=1 node tests/tools/omp-picker-search-rendered/run.mjs`. +Uses production AgentCombobox, the production catalog and canonical CSS in a hidden +Electron renderer. Rebuilds the existing background-launch harness before execution; +all windows must remain invisible and unfocused. No dependency install is needed. + +The probe types `oh-my-pi`, captures the resulting OMP row over CDP, selects it, +asserts the callback receives `omp`, and checks `oh my pi` too. Reports/screenshots +are local under `.bench-fixtures/omp-picker-search-*`. Before proof uses the baseline +catalog without search aliases and `ORCA_OMP_PICKER_BASELINE=1`, expecting no match. + +Available agents are supplied by the fixture. This does not exercise local/SSH/WSL +PATH detection, disabled-agent settings, terminal launch, or a full workspace form. +Search only ranks entries supplied by each caller; aliases cannot introduce an +agent absent from that list. The broader missing-agent explanation in #14319 is +separate from this reproduced project-name search defect. diff --git a/tests/tools/omp-picker-search-rendered/fixture.css b/tests/tools/omp-picker-search-rendered/fixture.css new file mode 100644 index 00000000000..9c08bc0261c --- /dev/null +++ b/tests/tools/omp-picker-search-rendered/fixture.css @@ -0,0 +1,4 @@ +@import '../../../src/renderer/src/assets/main.css'; +@source './fixture.tsx'; +@source '../../../src/renderer/src/components/agent/AgentCombobox.tsx'; +@source '../../../src/renderer/src/components/ui'; diff --git a/tests/tools/omp-picker-search-rendered/fixture.tsx b/tests/tools/omp-picker-search-rendered/fixture.tsx new file mode 100644 index 00000000000..b318cdee827 --- /dev/null +++ b/tests/tools/omp-picker-search-rendered/fixture.tsx @@ -0,0 +1,29 @@ +import React, { useState } from 'react' +import { createRoot } from 'react-dom/client' +import AgentCombobox from '../../../src/renderer/src/components/agent/AgentCombobox' +import { getAgentCatalog } from '../../../src/renderer/src/lib/agent-catalog' +import type { TuiAgent } from '../../../src/shared/tui-agent' +import './fixture.css' +const baseline = new URLSearchParams(window.location.search).get('baseline') === '1' +const agents = getAgentCatalog().map((agent) => + baseline && agent.id === 'omp' ? { ...agent, searchAliases: [] } : agent +) +function App() { + const [selected, setSelected] = useState(null) + return ( +
+

Agent picker

+ +

Selected agent: {selected ?? 'none'}

+
+ ) +} +const root = document.getElementById('root') +if (root) { + createRoot(root).render() +} diff --git a/tests/tools/omp-picker-search-rendered/index.html b/tests/tools/omp-picker-search-rendered/index.html new file mode 100644 index 00000000000..44793ff9830 --- /dev/null +++ b/tests/tools/omp-picker-search-rendered/index.html @@ -0,0 +1,10 @@ + + + + + + +
+ + + diff --git a/tests/tools/omp-picker-search-rendered/run.mjs b/tests/tools/omp-picker-search-rendered/run.mjs new file mode 100644 index 00000000000..14bee4a6752 --- /dev/null +++ b/tests/tools/omp-picker-search-rendered/run.mjs @@ -0,0 +1,93 @@ +import { _electron as electron, expect } from '@stablyai/playwright-test' +import { build as buildMain } from 'esbuild' +import { build as buildRenderer } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') { + throw new Error('Requires ORCA_BACKGROUND_LAUNCH=1') +} +const root = fileURLToPath(new URL('../../../', import.meta.url)) +const parent = path.join(root, '.bench-fixtures') +mkdirSync(parent, { recursive: true }) +const output = mkdtempSync(path.join(parent, 'omp-picker-search-')) +const main = path.join(output, 'main.cjs') +await buildMain({ + entryPoints: [path.join(root, 'tests/tools/benchmarks/spinner-rendering/main.ts')], + outfile: main, + bundle: true, + platform: 'node', + format: 'cjs', + external: ['electron'] +}) +await buildRenderer({ + configFile: false, + root: import.meta.dirname, + base: './', + logLevel: 'silent', + plugins: [react(), tailwindcss()], + resolve: { alias: { '@': path.join(root, 'src/renderer/src') } }, + build: { outDir: path.join(output, 'renderer'), emptyOutDir: true } +}) +const { ELECTRON_RUN_AS_NODE: _runAsNode, ...env } = process.env +const app = await electron.launch({ args: [main], env: { ...env, ORCA_BACKGROUND_LAUNCH: '1' } }) +const report = { + scope: + 'Production AgentCombobox and agent catalog in hidden Electron; supplied available agents, no PATH detection or terminal launch.' +} +try { + const page = await app.firstWindow() + const errors = [] + page.on('pageerror', (error) => { + errors.push(error.message) + console.error(error) + }) + const baseline = process.env.ORCA_OMP_PICKER_BASELINE === '1' + const fixtureUrl = pathToFileURL(path.join(output, 'renderer/index.html')) + if (baseline) { + fixtureUrl.searchParams.set('baseline', '1') + } + await page.goto(fixtureUrl.href) + await page.locator('button[role=combobox]').click() + const search = page.getByPlaceholder('Search agents...') + await search.fill('oh-my-pi') + await expect( + baseline + ? page.getByText('No agents match your search.') + : page.getByRole('option', { name: 'OMP', exact: true }) + ).toBeVisible() + await page.evaluate(async () => { + await Promise.all( + document.getAnimations().map((animation) => animation.finished.catch(() => {})) + ) + }) + const cdp = await page.context().newCDPSession(page) + const { data } = await cdp.send('Page.captureScreenshot', { format: 'png' }) + writeFileSync( + path.join(output, baseline ? 'before.png' : 'after.png'), + Buffer.from(data, 'base64') + ) + if (!baseline) { + await page.getByRole('option', { name: 'OMP', exact: true }).click() + await expect(page.getByText('Selected agent: omp', { exact: true })).toBeVisible() + await expect(search).toBeHidden() + await page.locator('button[role=combobox]').click() + await search.fill('oh my pi') + await expect(page.getByRole('option', { name: 'OMP', exact: true })).toBeVisible() + } + report.baseline = baseline + expect(errors).toEqual([]) + report.windows = await app.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().map((window) => ({ + visible: window.isVisible(), + focused: window.isFocused() + })) + ) + expect(report.windows.every((window) => !window.visible && !window.focused)).toBe(true) +} finally { + writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`) + console.log(`OMP picker search evidence: ${output}`) + await app.close() +}