From ffc331212c38e9d94af09df74f03afa6e63a0717 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:13:25 -0700 Subject: [PATCH 01/34] Fix PTY child process verdict to preserve unverifiable state (#20729) * fix(pty): preserve unverifiable local child reads * fix(pty): make child-process inspection synchronous Separate foreground and child-process sampling. Sample child processes synchronously after confirming foreground availability, returning unverifiable verdicts when pty reads fail. Handle both transport loss and local read failures uniformly in the completion coordinator. --- .../local-pty-child-process-verdict.test.ts | 142 ++++++++++++++++++ .../local-pty-foreground-inspection.ts | 16 +- src/main/providers/local-pty-provider.ts | 14 +- ...letion-coordinator-process-cadence.test.ts | 27 +++- .../agent-completion-inspection-result.ts | 7 +- 5 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 src/main/providers/local-pty-child-process-verdict.test.ts diff --git a/src/main/providers/local-pty-child-process-verdict.test.ts b/src/main/providers/local-pty-child-process-verdict.test.ts new file mode 100644 index 00000000000..b36f19fef50 --- /dev/null +++ b/src/main/providers/local-pty-child-process-verdict.test.ts @@ -0,0 +1,142 @@ +import type * as pty from 'node-pty' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { resolveForegroundMock } = vi.hoisted(() => ({ 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/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 From 20794ee785b826480a927d06f2d134d806ddeae4 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:32:33 -0700 Subject: [PATCH 02/34] ci: keep the baseline build off the compatibility matrix lanes (#20733) The compatibility gate started the pinned 2.25.5 source build inside the same step that runs the three measured lanes, so `make -j$(nproc)` competed with two container lanes whose wall clock is container starts, not Git. A boundary case that costs ~1.5s stretched past Vitest's 30s timeout and failed the job. Build the binary in its own step before the matrix, and pull both images before any lane starts so a lazy pull cannot stall whichever test its sibling is timing. --- .github/workflows/pr.yml | 57 ++++++++----- ...git-binary-compatibility-workflow.test.mjs | 79 ++++++++++++------- docs/reference/git-compatibility.md | 6 ++ 3 files changed, 94 insertions(+), 48 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 398bd61bd04..2f54f2ae785 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -326,40 +326,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/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/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. From 2b34255d9657dc63830978f4e4433a422ec30765 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:47:11 -0700 Subject: [PATCH 03/34] fix(ci): stop defining pilot mutant tests inside a conditional (#20755) `vitest/no-conditional-tests` fires on the `if (mutation) { it(...) }` inside the pilot loop, and `audit:code-quality:native` runs oxlint with `--deny-warnings`, so main's "Enforce focused code-quality plugins" step exits 1 and blocks every open PR. Pair each pilot with its pinned mutant and reference state before the loops, so every iteration defines exactly one test unconditionally. Same 14 tests, same names: 11 mutant-kill tests and the 3 reference tests that `skipIf` still gates on RPC_FOUNDATION_REFERENCE_ROOT. --- .../mutants/pilot-mutants.test.ts | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) 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 index a81a48238f5..acd9840e4f0 100644 --- a/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts +++ b/mobile/src/test-support/rpc-recording/mutants/pilot-mutants.test.ts @@ -59,30 +59,35 @@ 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 pilot of pilotGoldens(input.scenarios)) { - const { id, scenario } = pilot - const mutation = mutants[id] - if (mutation) { - 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, mutation } of mutantPilots) { + it(`${id}: kills ${mutation}`, async () => { + const { adapters, assertMutationApplied } = pilotMountAdapters(root, { + mutation: operationMutation(mutation) }) - } - const reference = referenceStates[id] - if (!reference) { - continue - } + 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 From db09a7bd508fded16cd44a1925d432cdfc22131c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:47:19 -0700 Subject: [PATCH 04/34] fix(native-chat): let a reader park just above the latest message (#20709) * fix(native-chat): let a reader park just above the latest message A reader who scrolled up by less than the bottom threshold was still classified as being at the end, so follow stayed armed and the next chunk of stream carried them back down. One constant was answering two different questions: how close to the end still counts as pinned, and whether a reader's own scroll meant to stay there. The first wants slack, because a streaming last message jitters in height by tens of pixels. The second wants almost none, because it is a statement of intent. Give it its own, far stricter band, and move the choice of band into the decision rather than leaving it to the call site, which is where the two got conflated. Re-arming follow now requires the reader to be within 4px of the end: enough for fractional-pixel and zoom rounding, well inside one line of prose. The pin and the jump-to-latest affordance keep their 48px band. * fix(native-chat): make transcript intent own end following --- .../native-chat/NativeChatMessageList.tsx | 5 +- .../NativeChatMessageList.windowing.test.tsx | 185 +++++++++++++++++- .../native-chat-autoscroll.test.ts | 52 ++++- .../native-chat/native-chat-autoscroll.ts | 17 +- .../use-native-chat-transcript-scroll.ts | 3 +- ...ve-chat-transcript-window.options.test.tsx | 6 +- .../use-native-chat-transcript-window.ts | 17 +- 7 files changed, 248 insertions(+), 37 deletions(-) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index 108a188c4ab..f7462e07434 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -256,10 +256,7 @@ export function NativeChatMessageList({ // Named so measurement can find the scroll root without depending on // which utility class happens to make it scroll. data-native-chat-scroll - // `overflow-anchor:none`: the transcript decides whether an offset - // it did not write is the reader moving, so the engine adjusting - // scrollTop under a settling row would read as a departure. The - // virtualizer does its own end anchoring, so this is redundant here. + // Browser anchoring would add unattributed movement beside the virtualizer's anchor. className="scrollbar-sleek relative h-full overflow-y-auto [overflow-anchor:none] [scrollbar-gutter:stable_both-edges]" // Why: `zoom` scales the chat transcript's text and layout together, // scoped to this pane so the rest of the app is untouched. It sits on diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx index 8d9f3ffa83b..e4debc8e676 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx @@ -12,7 +12,10 @@ import { projectStructuredItemsToNativeChat } from '../../../../shared/structure import type { NativeChatMessage } from '../../../../shared/native-chat-types' import type { NativeChatLiveSession } from './use-native-chat-live-session' import { NativeChatMessageList } from './NativeChatMessageList' -import { NATIVE_CHAT_BOTTOM_THRESHOLD_PX } from './native-chat-autoscroll' +import { + NATIVE_CHAT_BOTTOM_THRESHOLD_PX, + NATIVE_CHAT_FOLLOW_REARM_PX +} from './native-chat-autoscroll' import { estimateNativeChatRowHeight, NATIVE_CHAT_ROW_GAP_PX, @@ -469,11 +472,8 @@ describe('transcript with a hidden scroll root', () => { // arrive at their final height and are a different case; this is the one where // the row the reader is looking at keeps changing size underneath them. // -// Two mechanisms are supposed to hold the pin, and both are exercised here: the -// list's own resize observer on the transcript column (which re-runs -// `scrollToBottom` against the document) and the virtualizer's end anchor (which -// compensates `scrollTop` by the growth when the view was already at the end). -describe('a row growing in place while the view is pinned to the bottom', () => { +// Exercise the real virtualizer together with the transcript's follow owner. +describe('transcript follow ownership across growth and appends', () => { const TAIL_INDEX = TRANSCRIPT_LENGTH - 1 const GROWTH_STEPS = 24 const LINES_PER_STEP = 12 @@ -490,6 +490,13 @@ describe('a row growing in place while the view is pinned to the bottom', () => const transcript = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => marker(index)) + function appendedTranscript(count: number): NativeChatMessage[] { + return [ + ...transcript, + ...Array.from({ length: count }, (_, index) => marker(TRANSCRIPT_LENGTH + index)) + ] + } + function tailHeightAt(step: number): number { return Math.max(ROW_PX, (1 + step * LINES_PER_STEP) * STREAM_LINE_PX) } @@ -642,6 +649,170 @@ describe('a row growing in place while the view is pinned to the bottom', () => expect(screen.getByRole('button', { name: /jump to latest/i })).toBeInTheDocument() }) + it.each([0, 100])( + 'keeps a reader parked above a growing row with a %i px initial measurement delta', + (measurementDelta) => { + setMeasuredTail(4) + measuredRowHeights = measuredRowHeights.map((height, index) => + index === TAIL_INDEX ? height + measurementDelta : height + ) + const { container, rerender } = render(streamingList(4)) + paint(container) + const scroller = scrollRoot(container) + + const parkGapPx = NATIVE_CHAT_BOTTOM_THRESHOLD_PX - 8 + const parkedAt = scroller.scrollHeight - scroller.clientHeight - parkGapPx + scrollTranscript(container, parkedAt) + expect(distanceFromBottom(container)).toBe(parkGapPx) + // Not the "scrolled far away" case above: the latest message is still on + // screen, so there is nothing to offer a way back to yet. + expect(screen.queryByRole('button', { name: /jump to latest/i })).toBeNull() + + setMeasuredTail(5) + rerender(streamingList(5)) + paint(container) + expect(scroller.scrollTop).toBe(parkedAt) + + let previousDistance = distanceFromBottom(container) + for (let step = 6; step <= GROWTH_STEPS; step += 1) { + setMeasuredTail(step) + rerender(streamingList(step)) + paint(container) + + // The offset stops moving at all... + expect(scroller.scrollTop).toBe(parkedAt) + // ...so the end runs away from the reader instead of carrying them along. + const distance = distanceFromBottom(container) + expect(distance).toBeGreaterThan(previousDistance) + previousDistance = distance + } + + expect(previousDistance).toBeGreaterThan(VIEWPORT_PX) + expect(screen.getByRole('button', { name: /jump to latest/i })).toBeInTheDocument() + } + ) + + it('leaves a parked reader in place through repeated appends', () => { + const { container, rerender } = render(list(transcript)) + paint(container) + const scroller = scrollRoot(container) + const parkedAt = scroller.scrollHeight - scroller.clientHeight - 40 + scrollTranscript(container, parkedAt) + + for (let count = 1; count <= 8; count += 1) { + rerender(list(appendedTranscript(count))) + paint(container) + expect(scroller.scrollTop).toBe(parkedAt) + expect(windowState(container).indexes.length).toBeLessThan(TRANSCRIPT_LENGTH / 4) + } + expect(screen.getByRole('button', { name: /jump to latest/i })).toBeInTheDocument() + }) + + it('follows repeated appends until the reader detaches', () => { + const { container, rerender } = render(list(transcript)) + paint(container) + const scroller = scrollRoot(container) + for (let count = 1; count <= 8; count += 1) { + rerender(list(appendedTranscript(count))) + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) + fireEvent.scroll(scroller) + } + + const parkedAt = scroller.scrollTop - 22 + scrollTranscript(container, parkedAt) + rerender(list(appendedTranscript(9))) + paint(container) + expect(scroller.scrollTop).toBe(parkedAt) + }) + + it('follows an empty transcript through underflow into scrollable output', () => { + const { container, rerender } = render(list([])) + paint(container) + expect(scrollRoot(container).scrollTop).toBe(0) + rerender(list(transcript.slice(0, 1))) + paint(container) + expect(scrollRoot(container).scrollTop).toBe(0) + fireEvent.scroll(scrollRoot(container)) + rerender(list(transcript)) + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) + expect(windowState(container).indexes.length).toBeLessThan(TRANSCRIPT_LENGTH / 4) + }) + + it.each(['reader', 'jump'] as const)('rearms growth and append following via %s', (rearm) => { + setMeasuredTail(4) + const { container, rerender } = render(streamingList(4)) + paint(container) + const scroller = scrollRoot(container) + fireEvent.scroll(scroller) + const parkedAt = scroller.scrollTop - 22 + scrollTranscript(container, parkedAt) + setMeasuredTail(5) + rerender(streamingList(5)) + paint(container) + expect(scroller.scrollTop).toBe(parkedAt) + + if (rearm === 'reader') { + scrollTranscript( + container, + scroller.scrollHeight - scroller.clientHeight - NATIVE_CHAT_FOLLOW_REARM_PX + ) + } else { + fireEvent.click(screen.getByRole('button', { name: /jump to latest/i })) + } + paint(container) + expect(screen.queryByRole('button', { name: /jump to latest/i })).toBeNull() + for (let step = 6; step <= 8; step += 1) { + setMeasuredTail(step) + rerender(streamingList(step)) + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) + expect(windowState(container).indexes.length).toBeLessThan(TRANSCRIPT_LENGTH / 4) + } + rerender(list([...transcriptAt(8), marker(TRANSCRIPT_LENGTH)])) + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) + }) + + it('preserves the visible row anchor across prepends while detached', () => { + const { container, rerender } = render(list(transcript)) + paint(container) + const readingAt = 2000 + scrollTranscript(container, readingAt) + paint(container) + + const earlier = Array.from({ length: 10 }, (_, index) => marker(index - 10)) + rerender(list([...earlier, ...transcript])) + paint(container) + expect(scrollRoot(container).scrollTop).toBe(readingAt + earlier.length * ROW_PITCH_PX) + expect(windowState(container).indexes.length).toBeLessThan(TRANSCRIPT_LENGTH / 4) + expect(screen.getByRole('button', { name: /jump to latest/i })).toBeInTheDocument() + }) + + it('compensates a measurement entirely above the viewport without reattaching', () => { + const { container, rerender } = render(list(transcript)) + paint(container) + const scroller = scrollRoot(container) + fireEvent.scroll(scroller) + const readingAt = 2000 + scrollTranscript(container, readingAt) + paint(container) + const aboveIndex = windowState(container).indexes[0]! + expect((aboveIndex + 1) * ROW_PITCH_PX).toBeLessThan(readingAt) + for (const growth of [100, 200]) { + measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + index === aboveIndex ? ROW_PX + growth : ROW_PX + ) + paint(container) + expect(scroller.scrollTop).toBe(readingAt + growth) + } + rerender(list(appendedTranscript(1))) + paint(container) + expect(scroller.scrollTop).toBe(readingAt + 200) + expect(windowState(container).indexes.length).toBeLessThan(TRANSCRIPT_LENGTH / 4) + }) + it('keeps following when a pin echo arrives after the document grows', () => { setMeasuredTail(0) const { container } = render(streamingList(0)) @@ -657,6 +828,8 @@ describe('a row growing in place while the view is pinned to the bottom', () => expect(scroller.scrollTop).toBe(pinnedAt) expect(screen.queryByRole('button', { name: /jump to latest/i })).toBeNull() + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) }) it('settles a pending end reconcile after the reader keeps scrolling away', async () => { diff --git a/src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts b/src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts index fa23669e931..0dc34b7eb30 100644 --- a/src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts @@ -5,13 +5,23 @@ import { nextFollowingEnd, shouldLoadEarlier, shouldShowJumpToLatest, - NATIVE_CHAT_BOTTOM_THRESHOLD_PX + NATIVE_CHAT_BOTTOM_THRESHOLD_PX, + NATIVE_CHAT_FOLLOW_REARM_PX } from './native-chat-autoscroll' const atBottom = { scrollTop: 952, scrollHeight: 1000, clientHeight: 48 } const scrolledUp = { scrollTop: 0, scrollHeight: 1000, clientHeight: 48 } const noOverflow = { scrollTop: 0, scrollHeight: 48, clientHeight: 48 } +/** A view parked exactly `distance` px above the end of the same document. */ +function parkedAbove(distance: number): { + scrollTop: number + scrollHeight: number + clientHeight: number +} { + return { scrollTop: 952 - distance, scrollHeight: 1000, clientHeight: 48 } +} + describe('distanceFromBottom', () => { it('is zero at the exact bottom and never negative', () => { expect(distanceFromBottom(atBottom)).toBe(0) @@ -48,7 +58,8 @@ describe('shouldShowJumpToLatest', () => { // The browser reports application writes as ordinary scroll events. Explicit // marks distinguish their delayed echoes from reader movement after growth. describe('nextFollowingEnd', () => { - const following = { following: true, programmatic: false, atEnd: true } + const following = { following: true, programmatic: false, geometry: parkedAbove(0) } + const wellAway = parkedAbove(400) it('follows when the reader reaches the end', () => { expect(nextFollowingEnd(following)).toBe(true) @@ -58,15 +69,44 @@ describe('nextFollowingEnd', () => { // the end runs away from an offset the transcript itself pinned. That is not a // reader leaving, and treating it as one strands them mid-transcript. it('keeps following when a delayed application scroll arrives after growth', () => { - expect(nextFollowingEnd({ ...following, programmatic: true, atEnd: false })).toBe(true) + expect(nextFollowingEnd({ ...following, programmatic: true, geometry: wellAway })).toBe(true) }) it('treats an unmarked offset away from the end as the reader leaving', () => { - expect(nextFollowingEnd({ ...following, atEnd: false })).toBe(false) + expect(nextFollowingEnd({ ...following, geometry: wellAway })).toBe(false) }) - it('does not re-attach a detached reader from an application write', () => { - expect(nextFollowingEnd({ following: false, programmatic: true, atEnd: false })).toBe(false) + it.each([0, NATIVE_CHAT_FOLLOW_REARM_PX, 400])( + 'does not reattach a detached reader from an application write %i px from the end', + (distance) => { + expect( + nextFollowingEnd({ following: false, programmatic: true, geometry: parkedAbove(distance) }) + ).toBe(false) + } + ) + + // The jump affordance's wider band must not decide whether a reader follows. + it('lets the reader park just inside the near-bottom band', () => { + expect(NATIVE_CHAT_FOLLOW_REARM_PX).toBeLessThan(NATIVE_CHAT_BOTTOM_THRESHOLD_PX) + const parked = parkedAbove(NATIVE_CHAT_BOTTOM_THRESHOLD_PX - 1) + expect(nextFollowingEnd({ ...following, geometry: parked })).toBe(false) + expect(isNearBottom(parked)).toBe(true) + expect(shouldShowJumpToLatest(false, parked)).toBe(false) + }) + + it('re-arms at the band and not one pixel past it', () => { + const detached = { following: false, programmatic: false } + expect( + nextFollowingEnd({ ...detached, geometry: parkedAbove(NATIVE_CHAT_FOLLOW_REARM_PX) }) + ).toBe(true) + expect( + nextFollowingEnd({ ...detached, geometry: parkedAbove(NATIVE_CHAT_FOLLOW_REARM_PX + 1) }) + ).toBe(false) + }) + + // Sub-pixel and zoom rounding put the true end a fraction short of exact. + it('holds follow through rounding noise at the end', () => { + expect(nextFollowingEnd({ ...following, geometry: parkedAbove(1.5) })).toBe(true) }) }) diff --git a/src/renderer/src/components/native-chat/native-chat-autoscroll.ts b/src/renderer/src/components/native-chat/native-chat-autoscroll.ts index f07aeeb6671..a8f2e54b22f 100644 --- a/src/renderer/src/components/native-chat/native-chat-autoscroll.ts +++ b/src/renderer/src/components/native-chat/native-chat-autoscroll.ts @@ -11,9 +11,7 @@ export type ScrollGeometry = { clientHeight: number } -/** Pixels from the bottom within which we treat the view as "at the bottom" and - * keep it pinned as content arrives. A small slack absorbs sub-pixel rounding - * and the height jitter of a streaming last message. */ +/** Hide the jump affordance while the latest output is still nearby. */ export const NATIVE_CHAT_BOTTOM_THRESHOLD_PX = 48 /** Distance in px from the bottom edge of the scroll range. */ @@ -21,8 +19,7 @@ export function distanceFromBottom(geometry: ScrollGeometry): number { return Math.max(0, geometry.scrollHeight - geometry.clientHeight - geometry.scrollTop) } -/** True when the viewport is close enough to the bottom that new content should - * keep it pinned (auto-scroll "attached"). */ +/** Whether the viewport is inside the requested distance from the bottom. */ export function isNearBottom( geometry: ScrollGeometry, threshold: number = NATIVE_CHAT_BOTTOM_THRESHOLD_PX @@ -43,22 +40,26 @@ export function shouldShowJumpToLatest( return distanceFromBottom(geometry) > threshold } +/** Allow bottom rounding noise without following a reader who moved up a line. */ +export const NATIVE_CHAT_FOLLOW_REARM_PX = 4 + export type FollowIntent = { following: boolean /** Whether the scroll event matches an offset the application registered. */ programmatic: boolean - atEnd: boolean + geometry: ScrollGeometry } /** Whether the transcript should still follow the end after this offset. * * Application writes preserve intent even when their delayed events arrive - * after the end moved. Reader events detach away from the end and reattach at it. */ + * after the end moved. Reader events detach away from the end and reattach at + * it — against the re-arm band, never the wider near-bottom one. */ export function nextFollowingEnd(intent: FollowIntent): boolean { if (intent.programmatic) { return intent.following } - return intent.atEnd + return isNearBottom(intent.geometry, NATIVE_CHAT_FOLLOW_REARM_PX) } /** Distance from the top within which the transcript pages in older history. */ diff --git a/src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts b/src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts index 0f690bce57e..c54cbf54e85 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.ts @@ -21,7 +21,6 @@ import { type UIEventHandler } from 'react' import { - isNearBottom, nextFollowingEnd, shouldLoadEarlier, shouldShowJumpToLatest, @@ -89,7 +88,7 @@ export function useNativeChatTranscriptScroll({ const following = nextFollowingEnd({ following: followingRef.current, programmatic, - atEnd: isNearBottom(geometry) + geometry }) followingRef.current = following if (!programmatic) { diff --git a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx index da19a51a9c3..82166952670 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx @@ -67,7 +67,7 @@ afterEach(() => { }) describe('native chat transcript virtualizer contract', () => { - it('configures prepend anchoring and matching bottom-follow behavior', () => { + it('retains prepend anchoring without independently following the end', () => { renderHook(() => useNativeChatTranscriptWindow({ scrollRef: { current: null }, @@ -78,8 +78,8 @@ describe('native chat transcript virtualizer contract', () => { expect(virtualizerMock.options.current).toMatchObject({ anchorTo: 'end', - followOnAppend: true, - scrollEndThreshold: 48 + followOnAppend: false, + scrollEndThreshold: -1 }) }) diff --git a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts index 24b63e1fbae..30227bf47c5 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts @@ -1,11 +1,8 @@ // DOM windowing for the transcript: only the rows near the viewport are mounted, // the rest are reserved as estimated height. // -// Anchoring is the library's, not ours. `anchorTo: 'end'` captures the row at the -// current offset before a count change and re-resolves its position afterwards, -// which is what keeps a "load earlier" prepend from yanking the view; -// `followOnAppend` + `scrollEndThreshold` keep a reader who is already at the -// bottom pinned there as a turn streams. +// The virtualizer owns visible-row anchoring; the transcript scroll hook owns +// end-follow intent. Geometry alone must never reattach a parked reader. // // Every measurement here ends up in the scroll container's own coordinate space, // which means `offsetTop` / `offsetHeight` rather than a bounding rect. The @@ -16,7 +13,6 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { elementScroll, useVirtualizer, type VirtualItem } from '@tanstack/react-virtual' import { createProgrammaticScrollMarks } from '@/hooks/programmatic-scroll-marks' -import { NATIVE_CHAT_BOTTOM_THRESHOLD_PX } from './native-chat-autoscroll' import { NATIVE_CHAT_ROW_GAP_PX } from './native-chat-row-height-estimate' import { nativeChatPinnedRowIndexes, nativeChatTranscriptRange } from './native-chat-pinned-rows' import type { NativeChatTranscriptSlot } from './native-chat-transcript-slots' @@ -135,8 +131,9 @@ export function useNativeChatTranscriptWindow({ gap: NATIVE_CHAT_ROW_GAP_PX, scrollMargin, anchorTo: 'end', - followOnAppend: true, - scrollEndThreshold: NATIVE_CHAT_BOTTOM_THRESHOLD_PX, + followOnAppend: false, + // Distances are nonnegative: disable geometry-only resize pinning, retaining prepend anchoring. + scrollEndThreshold: -1, // Every virtualizer write uses this public adapter, including measurement // adjustments and prepend anchoring, so scroll events have one provenance. scrollToFn: (offset, options, instance) => { @@ -164,6 +161,10 @@ export function useNativeChatTranscriptWindow({ } }) + // Growing a row that spans the viewport changes content below the reader's anchor. + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => + item.end <= (instance.scrollOffset ?? 0) + const finishReaderTakeover = useCallback(() => { if (readerTakeoverFrameRef.current !== null) { window.cancelAnimationFrame(readerTakeoverFrameRef.current) From b61a2347b99cc5d6c001473108c621811a35e33b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:52:21 -0700 Subject: [PATCH 05/34] feat(design-system): gate renderer UI with @shadcn/lint (#20731) * feat(design-system): gate renderer UI with @shadcn/lint Wires shadcn-ui/lint's Oxlint plugin into the two places this repo already ratchets: the changed-lines PR gate for rules the renderer can't satisfy today, and `pnpm lint` for the one that is already at zero. - config/oxlint-design-system.json: no-restyle (layout allowed), no-raw-colors, require-static-classes -- scoped to src/renderer/**/*.tsx, run over added lines only. Measured at 10 findings across the last 60 commits (771 changed files), so it holds the line without a migration. - config/oxlint-dead-classes.json: no-unknown-classes repo-wide, with the renderer's plain-CSS hook namespaces allow-listed. Now at zero. - no-inline-styles and no-arbitrary-values stay off; STYLEGUIDE says why. Fixes the three live bugs the linter found: - `--editor-surface` never reached `@theme inline`, so `bg-editor-surface` generated no CSS -- 12 editor/artifact/notebook panes fell through to the page background instead of #1e1e1e in dark mode. - `scrollbar-none` is not a Tailwind utility and was declared nowhere, so the remote file browser breadcrumbs showed the scrollbar they meant to hide. Declared as a real `@utility`. - Notebook markdown cells used `markdown-preview-body`, which no stylesheet defines; the styled class is `markdown-body`. They rendered unstyled. * ci: run the dead-class gate in PR CI `pnpm lint` gained check:dead-classes, and pr-workflow-lint-parity requires every `pnpm lint` step to have a matching step in pr.yml. * fix(notebook): keep markdown theme selectors working --- .github/workflows/pr.yml | 3 + AGENTS.md | 3 +- config/oxlint-dead-classes.json | 77 ++ config/oxlint-design-system.json | 54 ++ config/scripts/check-changed-code-quality.mjs | 6 + package.json | 5 +- pnpm-lock.yaml | 776 +++++++++++++++++- pnpm-workspace.yaml | 1 + src/renderer/src/assets/main.css | 13 + .../assets/theme-utility-generation.test.ts | 19 + .../src/components/editor/IpynbCellEditor.tsx | 27 +- 11 files changed, 975 insertions(+), 9 deletions(-) create mode 100644 config/oxlint-dead-classes.json create mode 100644 config/oxlint-design-system.json create mode 100644 src/renderer/src/assets/theme-utility-generation.test.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2f54f2ae785..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 }} 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/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/package.json b/package.json index a9f12759f74..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,6 +201,7 @@ "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2f6425aafd..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) @@ -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'} @@ -3282,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==} @@ -3353,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'} @@ -3575,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'} @@ -3596,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==} @@ -3771,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'} @@ -3882,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==} @@ -4194,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'} @@ -4447,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'} @@ -4470,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'} @@ -4524,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==} @@ -4561,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==} @@ -4576,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'} @@ -4685,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} @@ -4738,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'} @@ -4798,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'} @@ -4883,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'} @@ -5077,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==} @@ -5111,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==} @@ -5131,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'} @@ -5224,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==} @@ -5576,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'} @@ -5672,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'} @@ -5690,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} @@ -5740,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'} @@ -5892,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} @@ -5971,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==} @@ -5978,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'} @@ -6650,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'} @@ -6673,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'} @@ -6767,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'} @@ -6907,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'} @@ -7278,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': @@ -7524,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 @@ -7549,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': @@ -7699,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) @@ -7887,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 @@ -7942,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 @@ -8988,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': {} @@ -9621,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': @@ -9696,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 @@ -9867,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: {} @@ -9877,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 @@ -10103,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 @@ -10201,6 +10805,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cn@0.2.6: {} + code-block-writer@13.0.3: {} color-convert@2.0.1: @@ -10500,6 +11106,8 @@ snapshots: dedent@1.7.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -10791,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: {} @@ -10803,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 @@ -10819,6 +11475,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} eventemitter3@5.0.4: {} @@ -10912,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: {} @@ -10946,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 @@ -10970,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 @@ -11080,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 @@ -11159,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 @@ -11293,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 @@ -11396,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: @@ -11531,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 @@ -11564,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: {} @@ -11576,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 @@ -11651,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: {} @@ -12248,6 +12958,8 @@ snapshots: nanoid@3.3.18: {} + natural-compare@1.4.0: {} + negotiator@1.0.0: {} node-abi@4.33.0: @@ -12339,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 @@ -12392,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 @@ -12469,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 @@ -12607,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 @@ -12725,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 @@ -13528,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: @@ -13549,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 @@ -13668,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 @@ -13782,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/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/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'} + +
) } From 6f4e4bfa223dfec5804441c81c66d292c9b247f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:00:51 +0000 Subject: [PATCH 06/34] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index bd7a18f8488..a4191395e41 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 54m + + downloads: 56m @@ -15,7 +15,7 @@ downloads downloads - 54m - 54m + 56m + 56m From ff5b1a5a05e68981ebad0bbe62cdd12b7c8f49c2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:24:04 -0700 Subject: [PATCH 07/34] fix(native-chat): preserve detached transcript position during growth (#20710) * fix(native-chat): stop the transcript following an end it measured short The virtualizer compensates a row's measured size change by moving scrollTop whenever it believes the view was already at the end. It decides that from the spacer's own height minus a container-absolute offset, so the distance it computes is short by everything in the document outside the spacer: the transcript's top gutter, the "load earlier" block while older history is still pageable, and the trailing chrome. A reader sitting ~100px above the bottom therefore measured as "at the end", and every row that settled below them dragged them down to it. Measured in the windowing harness with a 92px gutter and 24px of trailing chrome: a reader parked 96px above the end is pulled to the end on the first growth frame, scrollTop 9261 to 9357. The same option gates following an append, but that path measures the true document distance, so it was never wrong, only redundant. The transcript already decides whether to follow the end from the scroll container's real geometry, and it re-pins once the growth is in the document rather than before it, where the library's own write is clamped. Both library end behaviours are retired by a threshold no finite distance can meet; the prepend anchoring that shares the option is kept. overflow-anchor:none is restated as structural: the engine's anchoring writes never pass through the scrollToFn adapter that attributes this pane's own scrolls, so they would arrive unmarked and read as the reader leaving. * fix(native-chat): preserve visible rows on first measurement --- .../NativeChatMessageList.windowing.test.tsx | 154 +++++++++++++++++- .../use-native-chat-transcript-window.ts | 7 +- 2 files changed, 155 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx index e4debc8e676..8bbed27e75c 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx @@ -35,6 +35,12 @@ const TRANSCRIPT_LENGTH = 200 const BELOW_TRANSCRIPT_PX = 24 let belowTranscriptPx = BELOW_TRANSCRIPT_PX +/** Everything the document holds above the spacer: the scroll root's top gutter, + * and the "load earlier" block whenever there is older history to page in. This + * is the virtualizer's `scrollMargin`, and it is the larger half of the gap + * between the document's end and the end the virtualizer computes. */ +let aboveTranscriptPx = 0 + /** Heights the stubbed layout reports per row index, when a case wants a row to * measure as something other than its estimate. Empty means "every row at its * estimate", which is what every non-growth case wants. */ @@ -91,9 +97,13 @@ function reservedTranscriptHeight(root: ParentNode): number { // bottom and the cases above are about where the window sits, not where it lands. function stubLayout({ scrollGeometry = false, + offsetChain = false, viewportHeight = () => VIEWPORT_PX }: { scrollGeometry?: boolean + /** Give the spacer an `offsetTop` and a chain to walk up to the scroll root, + * so `scrollMargin` can be something other than zero. */ + offsetChain?: boolean viewportHeight?: () => number } = {}): () => void { const scrollTops = new WeakMap() @@ -128,7 +138,7 @@ function stubLayout({ overrideLayoutProperty('scrollHeight', { get(this: HTMLElement): number { return this.hasAttribute('data-native-chat-scroll') - ? reservedTranscriptHeight(this) + belowTranscriptPx + ? aboveTranscriptPx + reservedTranscriptHeight(this) + belowTranscriptPx : 0 } }), @@ -145,6 +155,22 @@ function stubLayout({ }) ) } + if (offsetChain) { + restores.push( + overrideLayoutProperty('offsetTop', { + get(this: HTMLElement): number { + return this.hasAttribute('data-native-chat-window') ? aboveTranscriptPx : 0 + } + }), + // happy-dom has no `offsetParent` at all, so production's walk to the + // scroll root ends before it starts and every margin reads zero. + overrideLayoutProperty('offsetParent', { + get(this: HTMLElement): HTMLElement | null { + return this.parentElement?.closest('[data-native-chat-scroll]') ?? null + } + }) + ) + } return () => { for (const restore of restores.toReversed()) { restore() @@ -572,9 +598,10 @@ describe('transcript follow ownership across growth and appends', () => { let restoreLayout = (): void => {} let restoreResizeObserver = (): void => {} beforeEach(() => { - restoreLayout = stubLayout({ scrollGeometry: true }) + restoreLayout = stubLayout({ scrollGeometry: true, offsetChain: true }) restoreResizeObserver = stubResizeObserver() belowTranscriptPx = BELOW_TRANSCRIPT_PX + aboveTranscriptPx = 0 setMeasuredTail(0) }) afterEach(() => { @@ -582,6 +609,7 @@ describe('transcript follow ownership across growth and appends', () => { restoreLayout() measuredRowHeights = [] belowTranscriptPx = BELOW_TRANSCRIPT_PX + aboveTranscriptPx = 0 vi.restoreAllMocks() }) @@ -794,7 +822,11 @@ describe('transcript follow ownership across growth and appends', () => { const { container, rerender } = render(list(transcript)) paint(container) const scroller = scrollRoot(container) - fireEvent.scroll(scroller) + // Establish a forward scroll direction before reading at this offset. The + // backward-scroll suppression below covers the separate case where a reader + // is still moving upward while overscan rows settle. + scrollTranscript(container, 0) + paint(container) const readingAt = 2000 scrollTranscript(container, readingAt) paint(container) @@ -832,6 +864,56 @@ describe('transcript follow ownership across growth and appends', () => { expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_FOLLOW_REARM_PX) }) + it('does not counter upward scrolling when measured overscan rows settle', () => { + const readingAt = 2000 + const aboveIndex = Math.floor(readingAt / ROW_PITCH_PX) - 1 + const { container } = render(list(transcript)) + paint(container) + scrollTranscript(container, readingAt + 100) + paint(container) + measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + index === aboveIndex ? ROW_PX + 10 : ROW_PX + ) + paint(container) + scrollTranscript(container, readingAt) + paint(container) + const scroller = scrollRoot(container) + const scrollTo = vi.spyOn(scroller, 'scrollTo') + + measuredRowHeights = measuredRowHeights.map((height, index) => + index === aboveIndex ? height + 20 : height + ) + paint(container) + + expect(scroller.scrollTop).toBe(readingAt) + expect(scrollTo).not.toHaveBeenCalled() + }) + + it('keeps the offset when a visible row shrinks past the viewport top', () => { + const focusedIndex = 45 + const { container } = render(list(transcript)) + paint(container) + scrollTranscript(container, focusedIndex * ROW_PITCH_PX) + paint(container) + measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + index === focusedIndex ? 100 : ROW_PX + ) + paint(container) + const readingAt = focusedIndex * ROW_PITCH_PX + 60 + scrollTranscript(container, readingAt) + paint(container) + const scroller = scrollRoot(container) + const scrollTo = vi.spyOn(scroller, 'scrollTo') + + measuredRowHeights = measuredRowHeights.map((height, index) => + index === focusedIndex ? 30 : height + ) + paint(container) + + expect(scroller.scrollTop).toBe(readingAt) + expect(scrollTo).not.toHaveBeenCalled() + }) + it('settles a pending end reconcile after the reader keeps scrolling away', async () => { setMeasuredTail(0) const { container } = render(streamingList(0)) @@ -863,4 +945,70 @@ describe('transcript follow ownership across growth and appends', () => { expect(scroller.scrollTop).toBe(1800) expect(screen.getByRole('button', { name: /jump to latest/i })).toBeInTheDocument() }) + + // With something above the spacer, the two parties stop agreeing on where the + // end is: the transcript measures it from the document, the virtualizer from + // the spacer's own height against a container-absolute offset. The second is + // short by everything outside the spacer, so it reads a reader who is clearly + // above the end as sitting on it. + describe('with a gutter above the transcript', () => { + /** `pt-10` plus the "Load earlier" block and its gap — what sits above the + * spacer once a resumed session still has older history to page in. */ + const GUTTER_PX = 92 + /** Far enough up that the transcript itself calls the reader detached, and + * still inside the band the virtualizer computes (48 + 92 + 24). */ + const READING_ABOVE_END_PX = 96 + // A nonzero delta seeds the size cache; zero exercises first-measure growth. + const MEASURE_SKEW_PX = 7 + + function setSkewedTail(step: number, skew = MEASURE_SKEW_PX): void { + const heights = Array.from({ length: TRANSCRIPT_LENGTH }, () => ROW_PX) + heights[TAIL_INDEX] = tailHeightAt(step) + skew + measuredRowHeights = heights + } + + beforeEach(() => { + aboveTranscriptPx = GUTTER_PX + }) + + it.each([0, MEASURE_SKEW_PX])( + 'leaves a reader just above the end while the row grows (skew %i)', + (skew) => { + setSkewedTail(4, skew) + const { container, rerender } = render(streamingList(4)) + paint(container) + const scroller = scrollRoot(container) + + const readingAt = scroller.scrollHeight - scroller.clientHeight - READING_ABOVE_END_PX + scrollTranscript(container, readingAt) + paint(container) + expect(distanceFromBottom(container)).toBe(READING_ABOVE_END_PX) + + for (let step = 5; step <= 10; step += 1) { + setSkewedTail(step, skew) + rerender(streamingList(step)) + paint(container) + + // Not dragged along: the offset the reader chose is the offset they keep, + // however much the row below them grows. + expect(scroller.scrollTop).toBe(readingAt) + } + } + ) + + it('still pins a reader who is at the end, with the gutter in the document', () => { + setSkewedTail(4) + const { container, rerender } = render(streamingList(4)) + paint(container) + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_BOTTOM_THRESHOLD_PX) + + for (let step = 5; step <= 10; step += 1) { + setSkewedTail(step) + rerender(streamingList(step)) + paint(container) + + expect(distanceFromBottom(container)).toBeLessThanOrEqual(NATIVE_CHAT_BOTTOM_THRESHOLD_PX) + } + }) + }) }) diff --git a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts index 30227bf47c5..15604bcb9a2 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-transcript-window.ts @@ -160,10 +160,11 @@ export function useNativeChatTranscriptWindow({ } } }) - - // Growing a row that spans the viewport changes content below the reader's anchor. + // Preserve rows above the reader, never compensate growth within the visible + // row — including its first measurement, which may follow an exact estimate. virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => - item.end <= (instance.scrollOffset ?? 0) + item.end <= (instance.scrollOffset ?? 0) && + (instance.scrollDirection !== 'backward' || !instance.itemSizeCache.has(item.key)) const finishReaderTakeover = useCallback(() => { if (readerTakeoverFrameRef.current !== null) { From 438603f9e723ae0e16bbd34d3f0d932b417790df Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:31:40 -0700 Subject: [PATCH 08/34] feat(native-chat): add a message rail for jumping between your prompts (#20719) * feat(native-chat): add a message rail for jumping between your prompts A vertical rail down the right edge of the transcript, one bar per user message, with the bar for the turn you are reading highlighted once scrolling settles. Hovering the rail opens a panel that previews every prompt and jumps to it on click. Bars are capped at 20 and sampled evenly across the thread, always keeping both ends and the active bar, so the rail stays readable at a glance on a long conversation. The active bar is resolved from virtualizer offsets rather than by scanning rendered rows: the transcript is windowed, so an off-window row has no element to measure. The row at the scroll fold resolves to its owning prompt through turnKey, which is what keeps your own message lit while you read a long reply instead of going dark. Jumps reuse the existing reveal/pin path and scrollMessageToTop, which releases the bottom pin. Scrolling through the virtualizer directly would leave a reader snapped back down by the next streamed token. Ticks cover loaded history only; older prompts gain a bar once "Load earlier messages" pages them in. * fix(native-chat): service a rail jump once and give its pin back The rail borrowed the diff reveal's pin to reach a row the window had left behind, but copied only its state shape, not its consumption. The request was never cleared and the effect depended on `slots`, which is rebuilt on every render, so three things went wrong at once: - every later render re-scrolled to the jumped message, dragging a reader back there for the rest of the pane's life, and forcing the bottom pin off each time; - the standing request outranked `revealedDiff` in the shared pin, so revealing a diff outside the window silently stopped mounting its row; - the pinned row stayed mounted and measured indefinitely. The request now carries a monotonic id, is serviced once, and is released as soon as the scroll is issued, which hands the pin back. The rail's scroll listener had the same churn: it listed `items` in its deps, so a streaming turn tore the listener down and cancelled the pending idle timer on every frame and the highlight never settled. It now subscribes once and re-reads on a key built from the prompt ids. Also: the hover trigger is a real button, because `asChild` discards the primitive's focusable trigger and the panel is the only way to reach these messages; the wheel forwarder honours line and page delta modes rather than treating every delta as pixels; and the e2e panel assertion is exact, since a loose bound passed at 20 rows against 20 ticks. * fix(native-chat): make prompt rail accessible and reuse previews * fix(native-chat): supersede prior navigation when selecting a prompt --- .../native-chat/NativeChatMessageList.tsx | 62 +++++- .../NativeChatMessageList.windowing.test.tsx | 119 +++++++++++ .../NativeChatMessageRail.test.tsx | 85 ++++++++ .../native-chat/NativeChatMessageRail.tsx | 187 ++++++++++++++++++ .../native-chat-active-rail-item.test.ts | 158 +++++++++++++++ .../native-chat-active-rail-item.ts | 76 +++++++ .../native-chat-message-rail-items.test.ts | 150 ++++++++++++++ .../native-chat-message-rail-items.ts | 114 +++++++++++ .../use-native-chat-message-rail.test.ts | 137 +++++++++++++ .../use-native-chat-message-rail.ts | 130 ++++++++++++ src/renderer/src/i18n/locales/en.json | 3 + tests/e2e/native-chat-message-rail.spec.ts | 175 ++++++++++++++++ 12 files changed, 1392 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/components/native-chat/NativeChatMessageRail.test.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatMessageRail.tsx create mode 100644 src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts create mode 100644 src/renderer/src/components/native-chat/native-chat-active-rail-item.ts create mode 100644 src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts create mode 100644 src/renderer/src/components/native-chat/native-chat-message-rail-items.ts create mode 100644 src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts create mode 100644 src/renderer/src/components/native-chat/use-native-chat-message-rail.ts create mode 100644 tests/e2e/native-chat-message-rail.spec.ts diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index f7462e07434..0cfb9cbe869 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' import { ArrowDown } from 'lucide-react' import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown' import { translate } from '@/i18n/i18n' @@ -26,6 +26,9 @@ import { } from './native-chat-transcript-slots' import { useNativeChatTranscriptWindow } from './use-native-chat-transcript-window' import { useNativeChatTranscriptScroll } from './use-native-chat-transcript-scroll' +import { useNativeChatMessageRail } from './use-native-chat-message-rail' +import { NativeChatMessageRail } from './NativeChatMessageRail' +import type { NativeChatRailItem } from './native-chat-message-rail-items' import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types' import { isStructuredAgentSessionThinking } from '../../../../shared/structured-agent-session-live-turn' @@ -41,6 +44,10 @@ export { ProviderFrameRow } from './NativeChatTranscriptChrome' const MAX_EXPANDED_TURNS = 128 +type NativeChatNavigationRequest = + | { kind: 'diff'; target: NativeChatDiffReveal } + | { kind: 'rail'; messageId: string; requestId: number } + export function NativeChatMessageList({ session, journalItems, @@ -77,9 +84,18 @@ export function NativeChatMessageList({ turnActivity?: NativeChatTurnActivity | null runtimeContext?: RuntimeFileOperationArgs | null }): React.JSX.Element { - const [revealedDiff, setRevealedDiff] = useState(null) + const [navigationRequest, setNavigationRequest] = useState( + null + ) + const navigationSequence = useRef(0) + const revealedDiff = navigationRequest?.kind === 'diff' ? navigationRequest.target : null + const railJump = navigationRequest?.kind === 'rail' ? navigationRequest : null const revealDiff = useCallback((target: NativeChatDiffTarget) => { - setRevealedDiff((current) => ({ ...target, requestId: (current?.requestId ?? 0) + 1 })) + navigationSequence.current += 1 + setNavigationRequest({ + kind: 'diff', + target: { ...target, requestId: navigationSequence.current } + }) }, []) const receipts = useMemo( () => @@ -198,7 +214,9 @@ export function NativeChatMessageList({ const transcriptWindow = useNativeChatTranscriptWindow({ scrollRef, slots, - revealIndex: nativeChatSlotIndexOf(slots, revealedDiff?.messageId) + // One pin serves both: revealing a diff and jumping from the rail are + // mutually exclusive things to be doing. + revealIndex: nativeChatSlotIndexOf(slots, railJump?.messageId ?? revealedDiff?.messageId) }) const { showJump, onScroll, scrollToBottom, scrollMessageToTop } = useNativeChatTranscriptScroll({ scrollRef, @@ -214,6 +232,41 @@ export function NativeChatMessageList({ consumeProgrammaticScroll: transcriptWindow.consumeProgrammaticScroll, reconcileReaderScroll: transcriptWindow.reconcileReaderScroll }) + const rail = useNativeChatMessageRail({ + scrollRef, + slots, + virtualItems: transcriptWindow.virtualItems + }) + const servicedRailJumpRef = useRef(0) + const selectRailItem = useCallback((item: NativeChatRailItem) => { + navigationSequence.current += 1 + setNavigationRequest({ + kind: 'rail', + messageId: item.id, + requestId: navigationSequence.current + }) + }, []) + // Pinning the target mounts it in the same commit, so the row exists by the time + // layout runs. Routed through `scrollMessageToTop` rather than the virtualizer + // because that is what releases the bottom pin — without it the next streamed + // token snaps the reader straight back down. + // + // Serviced once per request, then released. `slots` takes a new identity on + // every render, so an effect that merely depended on it would re-scroll to this + // row forever; and a request left standing would keep its pin, which outranks + // the diff reveal that shares it. + useLayoutEffect(() => { + if (railJump === null || servicedRailJumpRef.current === railJump.requestId) { + return + } + servicedRailJumpRef.current = railJump.requestId + const index = nativeChatSlotIndexOf(slots, railJump.messageId) + const row = scrollRef.current?.querySelector(`[data-index="${index}"]`) + if (row) { + scrollMessageToTop(row) + } + setNavigationRequest(null) + }, [railJump, scrollMessageToTop, slots]) const rowContext = useMemo( () => ({ @@ -302,6 +355,7 @@ export function NativeChatMessageList({
+ {showJump ? ( + + { + cancelClose() + restoreFocus.current = true + setMode('interactive') + }} + onOpenAutoFocus={(event) => { + if (mode === 'hover') { + event.preventDefault() + } + }} + onCloseAutoFocus={(event) => { + if (!restoreFocus.current) { + event.preventDefault() + } + }} + > +
    + {rail.items.map((item) => ( +
  • + +
  • + ))} +
+
+ + ) +}) diff --git a/src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts b/src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts new file mode 100644 index 00000000000..42295f85457 --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from 'vitest' +import { + findActiveNativeChatRailItem, + type NativeChatRailSlot +} from './native-chat-active-rail-item' + +/** Rows 100px tall, laid end to end, as the virtualizer would report them. */ +function rows(count: number, height = 100) { + return Array.from({ length: count }, (_unused, index) => ({ + index, + start: index * height, + end: index * height + height + })) +} + +/** Turn shape: `u1` opens, three agent rows answer, `u2` opens the next. */ +const TURNS: NativeChatRailSlot[] = [ + { turnKey: 'u1' }, + { turnKey: 'u1' }, + { turnKey: 'u1' }, + { turnKey: 'u1' }, + { turnKey: 'u2' }, + { turnKey: 'u2' }, + { turnKey: 'u2' }, + { turnKey: 'u2' }, + { turnKey: 'u2' }, + { turnKey: 'u2' } +] + +const VIEWPORT = 300 +/** Ten 100px rows against a 300px viewport, scrolled off the bottom. */ +const MID_SCROLL = { clientHeight: VIEWPORT, scrollHeight: 1000, previousActiveId: null } + +describe('active rail item', () => { + // The whole point of resolving through `turnKey`: most of a transcript is reply, + // and a rule that needs a user row on screen goes dark for the length of one. + it('keeps the owning prompt lit while an agent reply fills the viewport', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: rows(10), + scrollTop: 250, + ...MID_SCROLL + }) + ).toBe('u1') + }) + + it('moves to the next prompt once its turn reaches the fold', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: rows(10), + scrollTop: 450, + ...MID_SCROLL + }) + ).toBe('u2') + }) + + it('selects the row the fold sits exactly on', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: rows(10), + scrollTop: 400, + ...MID_SCROLL + }) + ).toBe('u2') + }) + + // A short last turn would otherwise light its predecessor while the reader is + // staring at the newest prompt. + it('lights the newest turn when pinned to the bottom', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: rows(10), + scrollTop: 700, + clientHeight: VIEWPORT, + scrollHeight: 1000, + previousActiveId: null + }) + ).toBe('u2') + }) + + it('lights nothing above the first prompt', () => { + expect( + findActiveNativeChatRailItem({ + slots: [{ turnKey: undefined }, { turnKey: undefined }, ...TURNS], + virtualItems: rows(12), + scrollTop: 50, + clientHeight: VIEWPORT, + scrollHeight: 1200, + previousActiveId: null + }) + ).toBeNull() + }) + + // The window reflects the last committed render, so it can lag a scroll by a + // commit. Holding the previous tick beats blanking one. + it('holds the previous tick when the window is stale', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: [{ index: 0, start: 0, end: 100 }], + scrollTop: 600, + clientHeight: VIEWPORT, + scrollHeight: 4000, + previousActiveId: 'u1' + }) + ).toBe('u1') + }) + + it('holds the previous tick when nothing is windowed', () => { + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: [], + scrollTop: 0, + clientHeight: VIEWPORT, + scrollHeight: 1000, + previousActiveId: 'u2' + }) + ).toBe('u2') + }) + + it('keeps a row taller than the viewport active while it spans it', () => { + expect( + findActiveNativeChatRailItem({ + slots: [{ turnKey: 'u1' }], + virtualItems: [{ index: 0, start: 0, end: 2000 }], + scrollTop: 800, + clientHeight: VIEWPORT, + scrollHeight: 4000, + previousActiveId: null + }) + ).toBe('u1') + }) + + // `start` already carries `scrollMargin`, so subtracting it again would shift + // every row and select the wrong turn. + it('reads offsets in container space, margin included', () => { + const margin = 500 + expect( + findActiveNativeChatRailItem({ + slots: TURNS, + virtualItems: rows(10).map((row) => ({ + ...row, + start: row.start + margin, + end: row.end + margin + })), + scrollTop: margin + 450, + clientHeight: VIEWPORT, + scrollHeight: 1500, + previousActiveId: null + }) + ).toBe('u2') + }) +}) diff --git a/src/renderer/src/components/native-chat/native-chat-active-rail-item.ts b/src/renderer/src/components/native-chat/native-chat-active-rail-item.ts new file mode 100644 index 00000000000..f9604978159 --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-active-rail-item.ts @@ -0,0 +1,76 @@ +// Which rail tick is lit, decided from virtualizer offsets rather than rendered rows. +// +// A DOM scan is the obvious way to answer "what is on screen", and it is the wrong +// one here: the transcript is windowed, so an off-window row has no element to +// measure. Virtual items carry the same answer without that hole. +// +// The row at the fold is usually the agent's, not the reader's — most of a long +// transcript is reply. Resolving it through `turnKey`, which every row carries and +// which holds the id of the user message that opened its turn, is what keeps the +// reader's own prompt lit while they read the answer to it. Asking instead which +// *user* row is on screen goes dark for the whole length of a long reply. +// +// Every offset below is in the scroll container's own pixels (rows are placed at +// `item.start - scrollMargin` inside a sizer sitting `scrollMargin` down), which is +// the same space as `scrollTop`. That keeps the comparison honest under the +// transcript's `zoom`, where a bounding rect would be off by exactly the zoom factor. + +import { NATIVE_CHAT_BOTTOM_THRESHOLD_PX } from './native-chat-autoscroll' + +/** The virtualizer's item, restated so this module needs nothing from the lib. */ +export type NativeChatRailVirtualItem = { + index: number + start: number + end: number +} + +/** Only the field the rail reads, so a test needs no slot builder. */ +export type NativeChatRailSlot = { + turnKey: string | undefined +} + +export function findActiveNativeChatRailItem({ + slots, + virtualItems, + scrollTop, + clientHeight, + scrollHeight, + previousActiveId +}: { + slots: readonly NativeChatRailSlot[] + virtualItems: readonly NativeChatRailVirtualItem[] + scrollTop: number + clientHeight: number + scrollHeight: number + previousActiveId: string | null +}): string | null { + if (virtualItems.length === 0) { + return previousActiveId + } + + // Pinned to the bottom the newest turn is what is being read, whatever happens + // to sit at the top edge — a short last turn would otherwise light its predecessor. + const atBottom = scrollHeight - clientHeight - scrollTop <= NATIVE_CHAT_BOTTOM_THRESHOLD_PX + if (atBottom) { + const last = virtualItems.at(-1) + return last === undefined ? previousActiveId : (slots[last.index]?.turnKey ?? null) + } + + let fold: NativeChatRailVirtualItem | undefined + for (const item of virtualItems) { + if (item.start <= scrollTop && (fold === undefined || item.start > fold.start)) { + fold = item + } + } + // Scrolled above everything the window holds: the first windowed row is the + // nearest thing to the fold. + if (fold === undefined) { + return slots[virtualItems[0]?.index ?? -1]?.turnKey ?? null + } + // The window lags the scroll by a commit, so a fold past every row it holds is + // a stale read, not an answer. Holding the previous tick beats blanking one. + if (fold.end <= scrollTop) { + return previousActiveId + } + return slots[fold.index]?.turnKey ?? null +} diff --git a/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts b/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts new file mode 100644 index 00000000000..dc6ab6d3714 --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import type { NativeChatResolvedPrompt } from './native-chat-resolution-receipt' +import type { NativeChatTurnDiff } from './native-chat-turn-diffs' +import { buildNativeChatTranscriptSlots } from './native-chat-transcript-slots' +import { + buildNativeChatRailItems, + selectNativeChatRailTicks, + NATIVE_CHAT_RAIL_MAX_TICKS, + type NativeChatRailItem +} from './native-chat-message-rail-items' + +function text(id: string, body: string, role: NativeChatMessage['role'] = 'assistant') { + return { + id, + role, + blocks: [{ type: 'text' as const, text: body }], + timestamp: 1, + source: 'transcript' as const + } +} + +function image(id: string): NativeChatMessage { + return { + id, + role: 'user', + blocks: [{ type: 'image-ref' as const, path: '/tmp/shot.png' }], + timestamp: 1, + source: 'transcript' as const + } +} + +function slotsOf(messages: NativeChatMessage[]) { + let turn: string | undefined + const turnKeys = messages.map((message) => { + if (message.role === 'user') { + turn = message.id + } + return turn + }) + return buildNativeChatTranscriptSlots({ + messages, + turnKeys, + latestUserIndex: messages.findLastIndex((message) => message.role === 'user'), + currentTurnKey: undefined, + receipts: new Map(), + turnStatuses: { active: null, completedByTurn: {} }, + turnDiffs: new Map(), + showTurnStatus: false, + isWorking: false, + lifecycleWorking: false + }) +} + +function railItems(count: number): NativeChatRailItem[] { + return Array.from({ length: count }, (_unused, index) => ({ + id: `m${index}`, + slotIndex: index, + text: `m${index}`, + hasImages: false + })) +} + +describe('rail items', () => { + it('invalidates cached previews and positions after edits, prepends and removals', () => { + const prompt = text('u1', 'original prompt', 'user') + const first = buildNativeChatRailItems(slotsOf([prompt])) + const prepended = buildNativeChatRailItems( + slotsOf([text('a0', 'earlier reply'), prompt]), + first + ) + expect(prepended[0]).toEqual({ ...first[0], slotIndex: 1 }) + const edited = buildNativeChatRailItems( + slotsOf([text('u1', 'edited prompt', 'user')]), + prepended + ) + expect(edited[0]).toEqual({ ...first[0], text: 'edited prompt' }) + expect(buildNativeChatRailItems([], edited)).toEqual([]) + }) + + it('ticks only the user messages', () => { + const items = buildNativeChatRailItems( + slotsOf([ + text('u1', 'first ask', 'user'), + text('a1', 'agent reply'), + text('u2', 'second ask', 'user') + ]) + ) + expect(items.map((item) => item.id)).toEqual(['u1', 'u2']) + }) + + // The rail points at a row, and the virtualizer counts slots — so an entry has + // to carry the slot index. A message that draws nothing takes no slot, which + // is exactly where a message index would start lying. + it('indexes by slot, not by message position', () => { + const items = buildNativeChatRailItems(slotsOf([text('blank', ''), text('u1', 'ask', 'user')])) + expect(items).toHaveLength(1) + expect(items[0]?.slotIndex).toBe(0) + }) + + it('collapses whitespace in the preview', () => { + const items = buildNativeChatRailItems(slotsOf([text('u1', ' a\n\n b ', 'user')])) + expect(items[0]?.text).toBe('a b') + }) + + it('reports an image-only message as having no prose', () => { + const items = buildNativeChatRailItems(slotsOf([image('u1')])) + expect(items[0]?.text).toBe('') + expect(items[0]?.hasImages).toBe(true) + }) +}) + +describe('rail tick sampling', () => { + it('keeps every tick while the thread fits', () => { + const items = railItems(NATIVE_CHAT_RAIL_MAX_TICKS) + expect(selectNativeChatRailTicks({ items, activeId: null })).toBe(items) + }) + + it('caps a long thread and keeps both ends', () => { + const items = railItems(120) + const ticks = selectNativeChatRailTicks({ items, activeId: null }) + expect(ticks).toHaveLength(NATIVE_CHAT_RAIL_MAX_TICKS) + expect(ticks[0]?.id).toBe('m0') + expect(ticks.at(-1)?.id).toBe('m119') + }) + + it('always includes the active tick', () => { + const items = railItems(120) + const ticks = selectNativeChatRailTicks({ items, activeId: 'm7' }) + expect(ticks.map((tick) => tick.id)).toContain('m7') + expect(ticks).toHaveLength(NATIVE_CHAT_RAIL_MAX_TICKS) + }) + + // Losing an end would make the rail claim the conversation starts or stops + // somewhere it doesn't, so the eviction has to fall on a neighbour instead. + it('evicts a neighbour rather than an end when the active tick is near one', () => { + const items = railItems(120) + const ticks = selectNativeChatRailTicks({ items, activeId: 'm1' }) + const ids = ticks.map((tick) => tick.id) + expect(ids).toContain('m0') + expect(ids).toContain('m1') + expect(ids).toContain('m119') + }) + + it('returns ticks in thread order', () => { + const ticks = selectNativeChatRailTicks({ items: railItems(120), activeId: 'm63' }) + const indexes = ticks.map((tick) => tick.slotIndex) + expect(indexes).toEqual([...indexes].sort((left, right) => left - right)) + }) +}) diff --git a/src/renderer/src/components/native-chat/native-chat-message-rail-items.ts b/src/renderer/src/components/native-chat/native-chat-message-rail-items.ts new file mode 100644 index 00000000000..0f6faf95899 --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-message-rail-items.ts @@ -0,0 +1,114 @@ +// The rail's tick set: one entry per user message the transcript actually draws. +// +// Built from slots rather than messages because the rail's whole job is to point +// at a row, and a message that takes no slot has no row to point at. Slot indexes +// are also what the virtualizer counts, so an entry can be compared against a +// virtual item without a second lookup table. + +import { deriveNativeChatRowContent } from './native-chat-row-content' +import type { NativeChatBlock } from '../../../../shared/native-chat-types' +import type { NativeChatTranscriptSlot } from './native-chat-transcript-slots' + +/** Ticks past this are sampled away: a taller rail than the viewport cannot be + * read at a glance, which is the only thing the rail is for. */ +export const NATIVE_CHAT_RAIL_MAX_TICKS = 20 + +/** Below this a rail is noise — two ticks say nothing a scrollbar doesn't. */ +export const NATIVE_CHAT_RAIL_MIN_ITEMS = 3 + +export type NativeChatRailItem = { + id: string + /** Index into the slot list, i.e. the virtualizer's own index. */ + slotIndex: number + /** Preview prose, whitespace collapsed. Empty when the message is images only. */ + text: string + hasImages: boolean +} + +const previews = new WeakMap() + +export function buildNativeChatRailItems( + slots: readonly NativeChatTranscriptSlot[], + previous: readonly NativeChatRailItem[] = [] +): readonly NativeChatRailItem[] { + const items: NativeChatRailItem[] = [] + for (const [slotIndex, slot] of slots.entries()) { + if (slot.message.role !== 'user') { + continue + } + let preview = previews.get(slot.message.blocks) + if (!preview) { + const content = deriveNativeChatRowContent(slot.message.blocks) + preview = { text: content.markdown.replace(/\s+/g, ' ').trim(), hasImages: content.hasImages } + previews.set(slot.message.blocks, preview) + } + const prior = previous[items.length] + items.push( + prior?.id === slot.message.id && + prior.slotIndex === slotIndex && + prior.text === preview.text && + prior.hasImages === preview.hasImages + ? prior + : { + id: slot.message.id, + slotIndex, + ...preview + } + ) + } + return items.length === previous.length && items.every((item, index) => item === previous[index]) + ? previous + : items +} + +/** Evenly spaced ticks across the whole thread, always including both ends and + * the active one. Keeping the ends fixed is what makes the rail read as a map + * of the conversation rather than a window onto part of it. */ +export function selectNativeChatRailTicks({ + items, + activeId +}: { + items: readonly NativeChatRailItem[] + activeId: string | null +}): readonly NativeChatRailItem[] { + if (items.length <= NATIVE_CHAT_RAIL_MAX_TICKS) { + return items + } + + const maxIndex = items.length - 1 + const sampled = new Set() + for (let slot = 0; slot < NATIVE_CHAT_RAIL_MAX_TICKS; slot += 1) { + sampled.add(Math.round((slot * maxIndex) / (NATIVE_CHAT_RAIL_MAX_TICKS - 1))) + } + + const activeIndex = activeId === null ? -1 : items.findIndex((item) => item.id === activeId) + if (activeIndex >= 0 && !sampled.has(activeIndex)) { + sampled.add(activeIndex) + // Drop the neighbour nearest the active tick, never an end: losing an end + // would make the rail claim the thread starts or stops somewhere it doesn't. + let evict: number | null = null + let evictDistance = Number.POSITIVE_INFINITY + for (const index of sampled) { + if (index === activeIndex || index === 0 || index === maxIndex) { + continue + } + const distance = Math.abs(index - activeIndex) + if (distance < evictDistance) { + evict = index + evictDistance = distance + } + } + if (evict !== null) { + sampled.delete(evict) + } + } + + const ordered: NativeChatRailItem[] = [] + for (const index of Array.from(sampled).sort((left, right) => left - right)) { + const item = items[index] + if (item) { + ordered.push(item) + } + } + return ordered +} diff --git a/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts b/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts new file mode 100644 index 00000000000..962b4250750 --- /dev/null +++ b/src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts @@ -0,0 +1,137 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import * as rowContent from './native-chat-row-content' +import { describe, expect, it, vi } from 'vitest' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import type { NativeChatResolvedPrompt } from './native-chat-resolution-receipt' +import type { NativeChatTurnDiff } from './native-chat-turn-diffs' +import { buildNativeChatTranscriptSlots } from './native-chat-transcript-slots' +import { useNativeChatMessageRail } from './use-native-chat-message-rail' + +function message(id: string, role: NativeChatMessage['role']): NativeChatMessage { + return { + id, + role, + blocks: [{ type: 'text', text: `body of ${id}` }], + timestamp: 1, + source: 'transcript' + } +} + +/** A fresh slot array each call, the way the list rebuilds it every render. */ +function slotsOf(messages: NativeChatMessage[]) { + let turn: string | undefined + const turnKeys = messages.map((entry) => { + if (entry.role === 'user') { + turn = entry.id + } + return turn + }) + return buildNativeChatTranscriptSlots({ + messages, + turnKeys, + latestUserIndex: messages.findLastIndex((entry) => entry.role === 'user'), + currentTurnKey: undefined, + receipts: new Map(), + turnStatuses: { active: null, completedByTurn: {} }, + turnDiffs: new Map(), + showTurnStatus: false, + isWorking: false, + lifecycleWorking: false + }) +} + +const CONVERSATION = [ + message('u1', 'user'), + message('a1', 'assistant'), + message('u2', 'user'), + message('a2', 'assistant'), + message('u3', 'user') +] + +describe('message rail hook', () => { + it('reuses previews and rail state during long-history streamed renders', () => { + const conversation = Array.from({ length: 2000 }, (_, index) => + message(`history-${index}`, index % 2 === 0 ? 'user' : 'assistant') + ) + const scrollRef = { current: document.createElement('div') } + const { result, rerender, unmount } = renderHook( + ({ slots }) => useNativeChatMessageRail({ scrollRef, slots, virtualItems: [] }), + { initialProps: { slots: slotsOf(conversation) } } + ) + const initial = result.current + const derive = vi.spyOn(rowContent, 'deriveNativeChatRowContent') + for (let revision = 0; revision < 20; revision += 1) { + const slots = slotsOf([ + ...conversation.slice(0, -1), + message(`tail-${revision}`, 'assistant') + ]) + derive.mockClear() + rerender({ slots }) + expect(derive.mock.calls.length).toBe(0) + expect(result.current).toBe(initial) + } + unmount() + derive.mockRestore() + }) + + it('removes its scroll listener and pending idle read on unmount', () => { + vi.useFakeTimers() + const element = document.createElement('div') + const remove = vi.spyOn(element, 'removeEventListener') + const { unmount } = renderHook(() => + useNativeChatMessageRail({ + scrollRef: { current: element }, + slots: slotsOf(CONVERSATION), + virtualItems: [] + }) + ) + act(() => element.dispatchEvent(new Event('scroll'))) + expect(vi.getTimerCount()).toBe(1) + unmount() + expect(remove).toHaveBeenCalledWith('scroll', expect.any(Function)) + expect(vi.getTimerCount()).toBe(0) + vi.useRealTimers() + }) + + // `slots` is rebuilt on every render, so a listener effect that depended on it + // would unsubscribe and cancel its pending idle timer on every frame of a + // streaming turn — and the highlight would never settle. + it('subscribes to scroll once across renders that rebuild the slots', () => { + const element = document.createElement('div') + const scrollRef = { current: element } + const addListener = vi.spyOn(element, 'addEventListener') + + const { rerender } = renderHook( + ({ slots }) => useNativeChatMessageRail({ scrollRef, slots, virtualItems: [] }), + { initialProps: { slots: slotsOf(CONVERSATION) } } + ) + // Same prompts, new array identity — exactly what a re-render produces. + rerender({ slots: slotsOf(CONVERSATION) }) + rerender({ slots: slotsOf(CONVERSATION) }) + + const scrollSubscriptions = addListener.mock.calls.filter(([type]) => type === 'scroll') + expect(scrollSubscriptions).toHaveLength(1) + }) + + it('ticks every user message and hides below the minimum', () => { + const element = document.createElement('div') + const scrollRef = { current: element } + + const { result } = renderHook(() => + useNativeChatMessageRail({ scrollRef, slots: slotsOf(CONVERSATION), virtualItems: [] }) + ) + expect(result.current.items.map((item) => item.id)).toEqual(['u1', 'u2', 'u3']) + expect(result.current.visible).toBe(true) + + const { result: short } = renderHook(() => + useNativeChatMessageRail({ + scrollRef, + slots: slotsOf([message('u1', 'user'), message('a1', 'assistant')]), + virtualItems: [] + }) + ) + expect(short.current.visible).toBe(false) + }) +}) diff --git a/src/renderer/src/components/native-chat/use-native-chat-message-rail.ts b/src/renderer/src/components/native-chat/use-native-chat-message-rail.ts new file mode 100644 index 00000000000..53778d00286 --- /dev/null +++ b/src/renderer/src/components/native-chat/use-native-chat-message-rail.ts @@ -0,0 +1,130 @@ +// Rail state: which user messages get a tick, and which tick is lit. +// +// The lit tick is recomputed once scrolling settles rather than per scroll event. +// Mid-scroll the answer is both expensive and useless — nobody reads a rail that +// is itself moving — and settling on it is what makes the highlight feel like a +// position report instead of a flicker. + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { findActiveNativeChatRailItem } from './native-chat-active-rail-item' +import { + buildNativeChatRailItems, + selectNativeChatRailTicks, + NATIVE_CHAT_RAIL_MIN_ITEMS, + type NativeChatRailItem +} from './native-chat-message-rail-items' +import type { NativeChatTranscriptSlot } from './native-chat-transcript-slots' +import type { NativeChatTranscriptWindow } from './use-native-chat-transcript-window' + +/** Quiet period that counts as "stopped scrolling". */ +export const NATIVE_CHAT_RAIL_IDLE_MS = 120 + +/** Narrower than this the panel would cover the message it previews, so the whole + * rail stands down rather than half-working in a split pane. */ +export const NATIVE_CHAT_RAIL_MIN_WIDTH_PX = 512 + +export type NativeChatMessageRailState = { + ticks: readonly NativeChatRailItem[] + items: readonly NativeChatRailItem[] + activeId: string | null + visible: boolean +} + +export function useNativeChatMessageRail({ + scrollRef, + slots, + virtualItems +}: { + scrollRef: React.RefObject + slots: readonly NativeChatTranscriptSlot[] + virtualItems: NativeChatTranscriptWindow['virtualItems'] +}): NativeChatMessageRailState { + const [activeId, setActiveId] = useState(null) + const [wideEnough, setWideEnough] = useState(true) + + const previousItemsRef = useRef([]) + const items = buildNativeChatRailItems(slots, previousItemsRef.current) + previousItemsRef.current = items + + // Read through refs so a settling scroll never re-subscribes the listener: + // `virtualItems` is a fresh array on every frame of a scroll. + const virtualItemsRef = useRef(virtualItems) + virtualItemsRef.current = virtualItems + const slotsRef = useRef(slots) + slotsRef.current = slots + + const readActiveId = useCallback(() => { + const element = scrollRef.current + if (!element) { + return + } + setActiveId((previous) => + findActiveNativeChatRailItem({ + slots: slotsRef.current, + virtualItems: virtualItemsRef.current, + scrollTop: element.scrollTop, + clientHeight: element.clientHeight, + scrollHeight: element.scrollHeight, + previousActiveId: previous + }) + ) + }, [scrollRef]) + + useEffect(() => { + const element = scrollRef.current + if (!element) { + return + } + let idleTimer: number | null = null + const scheduleRead = (): void => { + if (idleTimer !== null) { + window.clearTimeout(idleTimer) + } + idleTimer = window.setTimeout(() => { + idleTimer = null + readActiveId() + }, NATIVE_CHAT_RAIL_IDLE_MS) + } + scheduleRead() + element.addEventListener('scroll', scheduleRead, { passive: true }) + return () => { + element.removeEventListener('scroll', scheduleRead) + if (idleTimer !== null) { + window.clearTimeout(idleTimer) + } + } + // Subscribed once. Depending on anything that changes per render would tear + // the listener down and cancel the pending idle timer on every frame of a + // streaming turn, so the highlight would never settle. + }, [readActiveId, scrollRef]) + + // Re-read when the set of prompts actually changes, so a transcript that grew + // updates without waiting for the next scroll. + useEffect(() => { + readActiveId() + }, [items, readActiveId]) + + useEffect(() => { + const element = scrollRef.current + if (!element || typeof ResizeObserver === 'undefined') { + return + } + const observer = new ResizeObserver(() => { + setWideEnough(element.clientWidth >= NATIVE_CHAT_RAIL_MIN_WIDTH_PX) + }) + observer.observe(element) + return () => observer.disconnect() + }, [scrollRef]) + + const ticks = useMemo(() => selectNativeChatRailTicks({ items, activeId }), [items, activeId]) + + return useMemo( + () => ({ + ticks, + items, + activeId, + visible: wideEnough && items.length >= NATIVE_CHAT_RAIL_MIN_ITEMS + }), + [ticks, items, activeId, wideEnough] + ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index ebca3398176..44f74db10cb 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17242,6 +17242,9 @@ "scrollMessageToTop": "Scroll this message to top", "loadingEarlier": "Loading…", "loadEarlier": "Load earlier messages", + "railLabel": "Your messages", + "railImageMessage": "Image attachment", + "railEmptyMessage": "Message", "question": { "step": "Step {{value0}}", "other": "Other…", diff --git a/tests/e2e/native-chat-message-rail.spec.ts b/tests/e2e/native-chat-message-rail.spec.ts new file mode 100644 index 00000000000..25ca0a4745b --- /dev/null +++ b/tests/e2e/native-chat-message-rail.spec.ts @@ -0,0 +1,175 @@ +// Exercise the prompt picker and an off-window jump against a real transcript. + +import { randomUUID } from 'node:crypto' +import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import type { Page } from '@stablyai/playwright-test' +import { test, expect } from './helpers/orca-app' +import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { waitForActivePaneHookDescriptor, waitForActiveTerminalManager } from './helpers/terminal' + +/** 30 user turns, so the rail is well past its 20-tick sampling cap. */ +const TRANSCRIPT_ROWS = 60 +const SHOT_DIR = path.join(os.tmpdir(), 'orca-rail-validation-larvacean', 'shots') + +async function enableNativeChatSetting(page: Page): Promise { + await page.evaluate(async () => { + const nextSettings = await window.api.settings.set({ experimentalNativeChat: true }) + window.__store?.setState({ settings: nextSettings }) + }) +} + +async function seedClaudeProviderSession( + page: Page, + args: { paneKey: string; worktreeId: string; sessionId: string; transcriptPath: string } +): Promise { + await page.evaluate(({ paneKey, worktreeId, sessionId, transcriptPath }) => { + window.__store + ?.getState() + .setAgentStatus( + paneKey, + { state: 'working', prompt: 'e2e message rail probe', agentType: 'claude' }, + 'Claude', + undefined, + { worktreeId }, + { providerSession: { key: 'session_id', id: sessionId, transcriptPath } } + ) + }, args) +} + +async function toggleTerminalTabToChatView( + page: Page, + args: { tabId: string; worktreeId: string } +): Promise { + await page.evaluate(({ tabId, worktreeId }) => { + const store = window.__store + if (!store) { + throw new Error('Store unavailable') + } + const state = store.getState() + const unifiedTab = (state.unifiedTabsByWorktree[worktreeId] ?? []).find( + (tab) => tab.contentType === 'terminal' && tab.entityId === tabId + ) + if (!unifiedTab) { + throw new Error('Unified terminal tab not found for chat toggle') + } + state.toggleTabViewMode(unifiedTab.id) + }, args) +} + +function claudeTranscript(rowCount: number, sessionId: string): string { + const startedAt = Date.now() - rowCount * 1_000 + return `${Array.from({ length: rowCount }, (_, index) => { + const isUser = index % 2 === 0 + const turn = Math.floor(index / 2) + const body = isUser + ? `Question ${turn}: what does the rail do when I scroll a long reply?` + : Array.from( + { length: 6 + (turn % 7) * 3 }, + (_unused, line) => `Answer paragraph ${line + 1} for turn ${turn}.` + ).join('\n\n') + return JSON.stringify({ + sessionId, + uuid: `${sessionId}-${index}`, + timestamp: new Date(startedAt + index * 1_000).toISOString(), + type: isUser ? 'user' : 'assistant', + message: { + role: isUser ? 'user' : 'assistant', + model: 'claude-opus-4', + content: [{ type: 'text', text: body }] + } + }) + }).join('\n')}\n` +} + +test.describe('Native chat message rail', () => { + test('previews prompts and jumps without following later output', async ({ orcaPage }) => { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + await waitForActiveTerminalManager(orcaPage, 30_000) + + const descriptor = await waitForActivePaneHookDescriptor(orcaPage) + const [tabId] = descriptor.paneKey.split(':') + const sessionId = `e2e-message-rail-${randomUUID()}` + const scratchDir = mkdtempSync(path.join(os.tmpdir(), 'orca-e2e-native-chat-rail-')) + const transcriptPath = path.join(scratchDir, `${sessionId}.jsonl`) + writeFileSync(transcriptPath, claudeTranscript(TRANSCRIPT_ROWS, sessionId)) + mkdirSync(SHOT_DIR, { recursive: true }) + + await enableNativeChatSetting(orcaPage) + await seedClaudeProviderSession(orcaPage, { + paneKey: descriptor.paneKey, + worktreeId: descriptor.worktreeId, + sessionId, + transcriptPath + }) + await toggleTerminalTabToChatView(orcaPage, { tabId, worktreeId: descriptor.worktreeId }) + + await expect(orcaPage.locator('[data-native-chat-root="true"]')).toBeVisible({ + timeout: 15_000 + }) + const transcriptWindow = orcaPage.locator('[data-native-chat-window]') + await expect(transcriptWindow).toBeVisible({ timeout: 30_000 }) + + const rail = orcaPage.locator('[data-native-chat-rail]') + await expect(rail).toBeVisible({ timeout: 30_000 }) + + // Sampling cap: 30 user turns must not render 30 bars. + const tickCount = await rail.locator(':scope > span').count() + expect(tickCount).toBeGreaterThan(2) + expect(tickCount).toBeLessThanOrEqual(20) + + await orcaPage.screenshot({ + path: path.join(SHOT_DIR, 'rail-01-app.png'), + animations: 'disabled' + }) + + await rail.hover() + const panel = orcaPage.getByRole('dialog', { name: 'Your messages' }) + await expect(panel).toBeVisible({ timeout: 10_000 }) + // The panel lists every user message, not the sampled ticks. + await expect(panel.getByRole('button').first()).toBeVisible() + await orcaPage.screenshot({ + path: path.join(SHOT_DIR, 'rail-02-panel.png'), + animations: 'disabled' + }) + + // Exact, not `> ticks`: a panel that listed only the sampled ticks would + // still satisfy a loose bound at 20 vs 20. + const panelCount = await panel.getByRole('button').count() + expect(panelCount).toBe(TRANSCRIPT_ROWS / 2) + + // Activating the hover preview transfers focus into the prompt picker. + await rail.press('Enter') + await expect(panel.getByRole('button').first()).toBeFocused() + await panel.getByRole('button', { name: 'Question 5:', exact: false }).click() + await expect(panel).not.toBeVisible() + const target = transcriptWindow.locator('[data-index="10"]') + const scroller = orcaPage.locator('[data-native-chat-scroll]') + const targetOffset = async (): Promise => { + const [row, viewport] = await Promise.all([target.boundingBox(), scroller.boundingBox()]) + return row && viewport ? Math.abs(row.y - viewport.y) : Number.POSITIVE_INFINITY + } + await expect.poll(targetOffset).toBeLessThan(4) + + for (let revision = 0; revision < 3; revision += 1) { + const body = `Later streamed output ${revision}` + appendFileSync( + transcriptPath, + `${JSON.stringify({ + sessionId, + uuid: `${sessionId}-stream-${revision}`, + type: 'assistant', + timestamp: new Date().toISOString(), + message: { role: 'assistant', content: [{ type: 'text', text: body }] } + })}\n` + ) + await expect(transcriptWindow.getByText(body, { exact: true })).toBeAttached() + await expect.poll(targetOffset).toBeLessThan(4) + } + + console.log(`[rail] ticks=${tickCount} panelRows=${panelCount} shots=${SHOT_DIR}`) + }) +}) From 99062ed80ba45826d767a860158da8ec0231cee2 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:03:51 -0700 Subject: [PATCH 09/34] fix(worktrees): preserve unverifiable disk witness (#20713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(worktrees): preserve unverifiable disk witness * fix(worktrees): follow gitdir/commondir markers in disk witness The disk witness validates created worktrees by reading the repo's common directory from disk. Previously it only checked for a direct .git directory and returned a status object that conflated different failure modes. Now it properly follows .gitdir and commondir pointer files to locate the true common directory, fixing detection on repos with linked git directories (worktrees, submodules) and WSL scenarios. Error handling is simplified: definitive absence returns undefined, other read failures throw with proper cause chains, eliminating the ambiguous "unverifiable" state that would mask real errors. * fix: validate gitdir marker targets are directories When a .git marker points to a missing or non-directory path, that's unverifiable—not the same as an absent .git file (bare repo). Validate accessibility before reading commondir to catch these errors clearly. --- ...ktree-created-description-real-git.test.ts | 8 +- .../git/worktree-created-disk-witness.test.ts | 187 ++++++++++++++++++ ...tree-listing-created-sparse-distro.test.ts | 23 +++ src/main/git/worktree-listing.ts | 83 ++++++-- .../created-worktree-reconciliation.test.ts | 30 ++- .../ipc/created-worktree-reconciliation.ts | 39 ++-- 6 files changed, 329 insertions(+), 41 deletions(-) create mode 100644 src/main/git/worktree-created-disk-witness.test.ts diff --git a/src/main/git/worktree-created-description-real-git.test.ts b/src/main/git/worktree-created-description-real-git.test.ts index 4a5545dd4ba..e54da7e4783 100644 --- a/src/main/git/worktree-created-description-real-git.test.ts +++ b/src/main/git/worktree-created-description-real-git.test.ts @@ -112,16 +112,20 @@ describe('describeCreatedWorktree against the real Git binary', () => { // `mkfifo` stands in for a `.git` on a hung mount: the read never rejects on its own. it.skipIf(process.platform === 'win32')( - "still settles when the repo's .git blocks forever", + "settles with the unread witness named when the repo's .git blocks forever", async () => { const stalledRepo = join(scratchDir, 'stalled') await mkdir(stalledRepo, { recursive: true }) const stalledDotGit = join(stalledRepo, '.git') await execFileAsync('mkfifo', [stalledDotGit]) try { + // Rejecting, not resolving undefined: undefined becomes a bare "created worktree not found", + // which claims Git put the worktree somewhere else. A stalled mount proves no such thing. + const settledBy = Date.now() + 5_000 await expect( describeCreatedWorktree(stalledRepo, worktreePath, 'feature', { timeout: 250 }) - ).resolves.toBeUndefined() + ).rejects.toThrow(/^repo common dir unverifiable: could not read .*\.git: /) + expect(Date.now()).toBeLessThan(settledBy) } finally { // Release the pending read so the fifo does not pin a threadpool thread for the whole run. await writeFile(stalledDotGit, '') diff --git a/src/main/git/worktree-created-disk-witness.test.ts b/src/main/git/worktree-created-disk-witness.test.ts new file mode 100644 index 00000000000..e3f7d16b3fc --- /dev/null +++ b/src/main/git/worktree-created-disk-witness.test.ts @@ -0,0 +1,187 @@ +import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('./worktree-list-reader', () => ({ + readRepoLocation: vi.fn(), + readRepoCommonDirFromGit: vi.fn(), + readCheckedOutBranchRef: vi.fn(), + readWorktreeHeadOid: vi.fn(), + readTranslatedWorktreeGraph: vi.fn(), + readWorktreeList: vi.fn() +})) +vi.mock('./worktree-sparse-checkout-cache', () => ({ + detectSparseCheckoutCached: vi.fn(async () => false) +})) + +import { describeCreatedWorktree } from './worktree-listing' +import { + readCheckedOutBranchRef, + readRepoCommonDirFromGit, + readRepoLocation, + readWorktreeHeadOid +} from './worktree-list-reader' + +const readRepoLocationMock = vi.mocked(readRepoLocation) +const readRepoCommonDirFromGitMock = vi.mocked(readRepoCommonDirFromGit) +const readCheckedOutBranchRefMock = vi.mocked(readCheckedOutBranchRef) +const readWorktreeHeadOidMock = vi.mocked(readWorktreeHeadOid) + +/** Repo convention: root bypasses the mode bits, so `chmod 000` denies nothing there. */ +const CAN_DENY_READ = process.platform !== 'win32' && process.getuid?.() !== 0 + +let scratchDir = '' +let repoPath = '' +let worktreePath = '' + +/** realpath: the witness canonicalizes, and macOS `tmpdir()` is a symlink (`/var` -> `/private/var`). */ +beforeEach(() => { + scratchDir = realpathSync(mkdtempSync(join(tmpdir(), 'orca-created-witness-'))) + repoPath = join(scratchDir, 'repo') + worktreePath = join(scratchDir, 'workspaces', 'feature') + mkdirSync(repoPath, { recursive: true }) + readRepoLocationMock.mockResolvedValue({ + topLevel: worktreePath, + // Deliberately not the repo's store, so every case below reaches the disk witness. + commonDir: join(scratchDir, 'elsewhere', '.git') + }) + // Git's own reading disagrees; only the witness can break the tie. + readRepoCommonDirFromGitMock.mockResolvedValue(join(scratchDir, 'other-repo', '.git')) + readCheckedOutBranchRefMock.mockResolvedValue('refs/heads/feature') + readWorktreeHeadOidMock.mockResolvedValue('a'.repeat(40)) +}) + +afterEach(() => { + vi.clearAllMocks() + chmodSync(repoPath, 0o700) + rmSync(scratchDir, { recursive: true, force: true }) +}) + +describe('describeCreatedWorktree when Git and the repo disagree', () => { + it('reports nothing when the witness proves a different object store', async () => { + // A real `.git` file pointing somewhere else: the worktree genuinely is not this repo's. + const otherGitDir = join(scratchDir, 'other-repo', '.git') + mkdirSync(otherGitDir, { recursive: true }) + writeFileSync(join(repoPath, '.git'), `gitdir: ${otherGitDir}\n`) + await expect( + describeCreatedWorktree(repoPath, worktreePath, 'feature') + ).resolves.toBeUndefined() + }) + + it('throws when the .git marker points at a path that does not exist', async () => { + // Nothing is there to prove a store either way: a fabricated candidate would decide the create. + writeFileSync(join(repoPath, '.git'), `gitdir: ${join(scratchDir, 'gone', '.git')}\n`) + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).rejects.toMatchObject({ + message: expect.stringContaining('gitdir marker target unreadable') + }) + }) + + it('throws when the .git marker points at a file', async () => { + const notAGitDir = join(scratchDir, 'not-a-git-dir') + writeFileSync(notAGitDir, 'not a git dir\n') + writeFileSync(join(repoPath, '.git'), `gitdir: ${notAGitDir}\n`) + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).rejects.toMatchObject({ + message: expect.stringContaining('gitdir marker target is not a directory') + }) + }) + + it('reports nothing for a bare repo, whose missing .git is a real answer', async () => { + // No `.git` at all is definitive absence, not an unreadable witness. + await expect( + describeCreatedWorktree(repoPath, worktreePath, 'feature') + ).resolves.toBeUndefined() + }) + + it('reports nothing when .git is a path under a file, not a directory', async () => { + // ENOTDIR, the other spelling of absence: `repo` is a file, so `repo/.git` cannot exist. + const filePath = join(scratchDir, 'plain-file') + writeFileSync(filePath, 'not a repo\n') + await expect( + describeCreatedWorktree(filePath, worktreePath, 'feature') + ).resolves.toBeUndefined() + }) + + it('follows gitdir and commondir markers', async () => { + const commonDir = join(scratchDir, 'main', '.git') + const linkedGitDir = join(commonDir, 'worktrees', 'source') + mkdirSync(linkedGitDir, { recursive: true }) + writeFileSync(join(repoPath, '.git'), `gitdir: ${linkedGitDir}\n`) + writeFileSync(join(linkedGitDir, 'commondir'), '../..\n') + readRepoLocationMock.mockResolvedValue({ topLevel: worktreePath, commonDir }) + + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).resolves.toMatchObject( + { + branch: 'refs/heads/feature' + } + ) + }) + + it.skipIf(!CAN_DENY_READ)('throws when the .git marker exists but cannot be read', async () => { + const dotGit = join(repoPath, '.git') + writeFileSync(dotGit, 'gitdir: /somewhere\n') + chmodSync(dotGit, 0o000) + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).rejects.toMatchObject({ + message: expect.stringMatching(/^repo common dir unverifiable: could not read .*\.git: /), + cause: expect.objectContaining({ code: 'EACCES' }) + }) + }) + + // The other unverifiable branch -- the deadline firing on a `.git` that never answers -- needs a + // read that really blocks, so it lives in worktree-created-description-real-git.test.ts behind a + // fifo. A short timeout here would only race the filesystem. + + it('accepts the create when the witness agrees with the worktree', async () => { + const commonDir = join(repoPath, '.git') + mkdirSync(commonDir, { recursive: true }) + writeFileSync(join(commonDir, 'HEAD'), 'ref: refs/heads/main\n') + readRepoLocationMock.mockResolvedValue({ topLevel: worktreePath, commonDir }) + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).resolves.toEqual({ + path: worktreePath, + head: 'a'.repeat(40), + branch: 'refs/heads/feature', + isBare: false, + isMainWorktree: false + }) + }) +}) + +describe('describeCreatedWorktree before the witness is reached', () => { + it('never pays for the disk read when Git already agreed', async () => { + const commonDir = join(repoPath, '.git') + mkdirSync(commonDir, { recursive: true }) + readRepoLocationMock.mockResolvedValue({ topLevel: worktreePath, commonDir }) + readRepoCommonDirFromGitMock.mockResolvedValue(commonDir) + // chmod 000 would make the witness unverifiable; agreement means it is never opened. + if (CAN_DENY_READ) { + chmodSync(repoPath, 0o000) + } + await expect(describeCreatedWorktree(repoPath, worktreePath, 'feature')).resolves.toMatchObject( + { + branch: 'refs/heads/feature' + } + ) + }) + + it('reports nothing when Git could not confirm the worktree at all', async () => { + readRepoLocationMock.mockResolvedValue(undefined) + // An unconfirmed worktree is not an unverifiable common dir: resolving undefined under a repo + // whose witness cannot be read is how we know the witness was never consulted. + if (CAN_DENY_READ) { + chmodSync(repoPath, 0o000) + } + await expect( + describeCreatedWorktree(repoPath, worktreePath, 'feature') + ).resolves.toBeUndefined() + }) + + it('reports nothing when the worktree has the wrong branch checked out', async () => { + readCheckedOutBranchRefMock.mockResolvedValue('refs/heads/other') + if (CAN_DENY_READ) { + chmodSync(repoPath, 0o000) + } + await expect( + describeCreatedWorktree(repoPath, worktreePath, 'feature') + ).resolves.toBeUndefined() + }) +}) diff --git a/src/main/git/worktree-listing-created-sparse-distro.test.ts b/src/main/git/worktree-listing-created-sparse-distro.test.ts index 2b3922d5b9b..ab0326dcee2 100644 --- a/src/main/git/worktree-listing-created-sparse-distro.test.ts +++ b/src/main/git/worktree-listing-created-sparse-distro.test.ts @@ -102,4 +102,27 @@ describe('describeCreatedWorktree on a drvfs-spelled WSL worktree', () => { platformSpy.mockRestore() } }) + + it('uses the repo disk witness in the WSL execution namespace', async () => { + const platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') + readRepoCommonDirFromGitMock.mockResolvedValue('/other/.git') + statMock.mockImplementation(async (target: string) => { + const value = slashed(target) + if (value === `${slashed(REPO)}/.git`) { + return { isDirectory: () => true } + } + if (value === `${HOST_GIT_DIR}/info/sparse-checkout`) { + return { isFile: () => true, size: 12 } + } + throw missing() + }) + + try { + await expect( + describeCreatedWorktree(REPO, 'C:\\wt\\x', 'feature', { wslDistro: 'Ubuntu' }) + ).resolves.toMatchObject({ branch: 'refs/heads/feature' }) + } finally { + platformSpy.mockRestore() + } + }) }) diff --git a/src/main/git/worktree-listing.ts b/src/main/git/worktree-listing.ts index f027bac4bc1..e902a89a957 100644 --- a/src/main/git/worktree-listing.ts +++ b/src/main/git/worktree-listing.ts @@ -1,5 +1,8 @@ -import { realpath, stat } from 'node:fs/promises' +import { readFile, realpath, stat } from 'node:fs/promises' import { join, posix } from 'node:path' +import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence' +import { resolveGitMetadataPath } from '../../shared/git-metadata-path' +import { parseGitdirMarkerPayload } from '../../shared/gitdir-marker-payload' import { isWorktreeCreatePreparation } from '../../shared/worktree/create-preparation' import { toWslExecutionSpace } from '../../shared/wsl-paths' import type { GitWorktreeInfo } from '../../shared/worktree/types' @@ -20,8 +23,6 @@ import { } from './worktree-operation-options' import { areWorktreePathsEqual, translateWorktreePath } from './worktree-path-comparison' import { detectSparseCheckoutCached } from './worktree-sparse-checkout-cache' -import { resolveGitCommonDir } from './worktree-sparse-state' -import { resolveGitDir } from './source-control/resolve-git-dir' const SPARSE_CHECKOUT_DETECTION_CONCURRENCY = 8 @@ -151,25 +152,75 @@ export async function annotateSparseCheckoutStatus( * * Deadlined because a `.git` on a hung mount (dead NFS/SSHFS, stalled WSL 9p) never rejects, and an * unbounded read here would leave the whole create IPC pending instead of failing like it used to. + * + * A missing `.git` is a real "no candidate"; every other read failure is unverifiable and rejects. */ async function readRepoCommonDirFromDisk( repoPath: string, timeoutMs: number ): Promise { + const dotGit = join(repoPath, '.git') try { - const dotGit = join(repoPath, '.git') - // A bare repo has no `.git`, and resolveGitDir would fabricate one; offer no candidate instead. - await withDeadline(stat(dotGit), timeoutMs) - const commonDir = await withDeadline( - resolveGitDir(repoPath).then(resolveGitCommonDir), - timeoutMs - ) - // Node answers in the caller's space, Git in the distro's. Without this the WSL candidate is a UNC - // path that can never equal Git's `/home/...`, leaving this witness inert on exactly the fallback - // path that needs it (realpath cannot bridge the two: a Linux path has no local inode). - return toWslExecutionSpace(commonDir) - } catch { - return undefined + const commonDir = await withDeadline(resolveRepoCommonDirFromDisk(repoPath, dotGit), timeoutMs) + return commonDir ? toWslExecutionSpace(commonDir) : undefined + } catch (error) { + // A bare repo has no `.git`; do not fabricate a candidate for it. + if (isDefinitiveAbsence(error)) { + return undefined + } + const reason = error instanceof Error ? error.message : String(error) + throw new Error(`repo common dir unverifiable: could not read ${dotGit}: ${reason}`, { + cause: error + }) + } +} + +async function resolveRepoCommonDirFromDisk( + repoPath: string, + dotGit: string +): Promise { + // The general metadata resolvers are intentionally best effort; a witness must preserve read failures. + const dotGitStats = await stat(dotGit) + let gitDir = dotGit + if (!dotGitStats.isDirectory()) { + const pointer = parseGitdirMarkerPayload(await readFile(dotGit, 'utf8')) + if (!pointer) { + return undefined + } + gitDir = resolveGitMetadataPath(repoPath, pointer) ?? dotGit + await assertGitDirIsDirectory(gitDir) + } + + return readCommonDirMarker(gitDir) +} + +/** + * A marker target that is missing or is not a directory is unverifiable, not an absent `.git`: + * without this, `commondir`'s own ENOENT/ENOTDIR would pass as absence and hand the caller the + * pointer target as a common dir it never proved exists. + */ +async function assertGitDirIsDirectory(gitDir: string): Promise { + let gitDirStats + try { + gitDirStats = await stat(gitDir) + } catch (error) { + // Rewrapped so the outer absence check cannot read this errno as a bare repo's missing `.git`. + throw new Error(`gitdir marker target unreadable: ${gitDir}`, { cause: error }) + } + if (!gitDirStats.isDirectory()) { + throw new Error(`gitdir marker target is not a directory: ${gitDir}`) + } +} + +async function readCommonDirMarker(gitDir: string): Promise { + try { + const pointer = await readFile(join(gitDir, 'commondir'), 'utf8') + return resolveGitMetadataPath(gitDir, pointer) ?? gitDir + } catch (error) { + if (!isDefinitiveAbsence(error)) { + throw error + } + return gitDir } } diff --git a/src/main/ipc/created-worktree-reconciliation.test.ts b/src/main/ipc/created-worktree-reconciliation.test.ts index 22912394d84..b62088ef105 100644 --- a/src/main/ipc/created-worktree-reconciliation.test.ts +++ b/src/main/ipc/created-worktree-reconciliation.test.ts @@ -139,14 +139,29 @@ describe('resolveCreatedWorktree', () => { ) }) + it('does not mistake a falsy rejection for a successful listing', async () => { + vi.mocked(listWorktreesSharedStrict).mockRejectedValue(undefined) + + await expect(resolveCreatedWorktree('/repo', '/workspaces/feature', 'feature')).rejects.toThrow( + 'undefined' + ) + }) + it('keeps the listing failure when the direct read itself throws', async () => { const failure = new Error('fatal: not a git repository') + const recoveryFailure = new Error('repo common dir unverifiable: deadline exceeded') + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) vi.mocked(listWorktreesSharedStrict).mockRejectedValue(failure) - vi.mocked(describeCreatedWorktree).mockRejectedValue(new Error('rev-parse exploded')) + vi.mocked(describeCreatedWorktree).mockRejectedValue(recoveryFailure) await expect(resolveCreatedWorktree('/repo', '/workspaces/feature', 'feature')).rejects.toBe( failure ) + expect(warn).toHaveBeenCalledWith('[worktrees:create] created-worktree recovery also failed', { + err: recoveryFailure, + worktreePath: '/workspaces/feature' + }) + warn.mockRestore() }) it('names the path and branch when the listing succeeded without the row', async () => { @@ -159,11 +174,16 @@ describe('resolveCreatedWorktree', () => { it("adds the direct read's failure when the listing merely omitted the row", async () => { vi.mocked(listWorktreesSharedStrict).mockResolvedValue([MAIN]) - vi.mocked(describeCreatedWorktree).mockRejectedValue(new Error('rev-parse exploded')) + const recoveryFailure = new Error('rev-parse exploded') + vi.mocked(describeCreatedWorktree).mockRejectedValue(recoveryFailure) - await expect(resolveCreatedWorktree('/repo', '/workspaces/feature', 'feature')).rejects.toThrow( - 'Worktree created but not found in listing: /workspaces/feature (branch feature): rev-parse exploded' - ) + await expect( + resolveCreatedWorktree('/repo', '/workspaces/feature', 'feature') + ).rejects.toMatchObject({ + message: + 'Worktree created but not found in listing: /workspaces/feature (branch feature): rev-parse exploded', + cause: recoveryFailure + }) }) it('charges the recovery what the listing left of the budget, not a fresh one', async () => { diff --git a/src/main/ipc/created-worktree-reconciliation.ts b/src/main/ipc/created-worktree-reconciliation.ts index 0f9b5cdbdb1..ac5b79954c2 100644 --- a/src/main/ipc/created-worktree-reconciliation.ts +++ b/src/main/ipc/created-worktree-reconciliation.ts @@ -53,7 +53,7 @@ export async function resolveCreatedWorktree( options?: GitWorktreeExecOptions ): Promise { const startedAt = Date.now() - let listingError: unknown + let listingError: Error | undefined try { const worktrees = options ? await listWorktreesSharedStrict(repoPath, options) @@ -63,11 +63,9 @@ export async function resolveCreatedWorktree( return { created, worktrees, listingComplete: true } } } catch (err) { - listingError = err + listingError = err instanceof Error ? err : new Error(String(err)) } - let described: GitWorktreeInfo | undefined - let describeError: unknown try { // One budget for verifying the create, not one per attempt: a hung Git already spent the // listing's deadline, and charging the recovery a fresh one doubles the wait before the error. @@ -75,26 +73,31 @@ export async function resolveCreatedWorktree( WORKTREE_LIST_TIMEOUT_MS - (Date.now() - startedAt), MIN_CREATED_WORKTREE_RECOVERY_MS ) - described = await describeCreatedWorktree(repoPath, worktreePath, branchName, { + const described = await describeCreatedWorktree(repoPath, worktreePath, branchName, { ...options, timeout: options?.timeout ?? remainingMs }) + if (described) { + return { created: described, worktrees: [], listingComplete: false } + } } catch (err) { - // Why keep, not rethrow: the recovery must not replace the listing's own, more informative failure. - describeError = err - } - if (described) { - return { created: described, worktrees: [], listingComplete: false } + if (listingError) { + // The listing's failure stays the thrown one, but the recovery's reason -- often + // `repo common dir unverifiable: ...` -- would otherwise vanish from the record entirely. + console.warn('[worktrees:create] created-worktree recovery also failed', { + err, + worktreePath + }) + throw listingError + } + // The listing simply omitted the row, so the direct read holds the only actionable failure. + const notFound = createdWorktreeNotFoundError(worktreePath, branchName) + throw new Error(`${notFound.message}: ${err instanceof Error ? err.message : String(err)}`, { + cause: err + }) } if (listingError) { throw listingError } - const notFound = createdWorktreeNotFoundError(worktreePath, branchName) - if (describeError) { - // The listing simply omitted the row, so the direct read holds the only actionable failure. - throw new Error( - `${notFound.message}: ${describeError instanceof Error ? describeError.message : String(describeError)}` - ) - } - throw notFound + throw createdWorktreeNotFoundError(worktreePath, branchName) } From ef39f32d4fe537611af5c477b90bcdc2a757dc70 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:35:48 -0700 Subject: [PATCH 10/34] test(native-chat): split the windowing test harness out of the suite (#20773) #20719 grew NativeChatMessageList.windowing.test.tsx to 897 effective lines, past the 800 ceiling for test files, so oxlint fails on main. Moves the shared layout/ResizeObserver stubs into native-chat-windowing-test-harness.tsx. No test was changed, split or dropped: still 5 describes and 23 it() blocks, 29 assertions passing. The stubs' mutable knobs become one exported `layout` object because an imported binding cannot be reassigned across modules. AGENTS.md forbids a max-lines disable, so extraction is the fix. --- .../NativeChatMessageList.windowing.test.tsx | 322 ++---------------- .../native-chat-windowing-test-harness.tsx | 289 ++++++++++++++++ 2 files changed, 320 insertions(+), 291 deletions(-) create mode 100644 src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx index 5421e023bd2..0caadd20c21 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx @@ -10,291 +10,31 @@ import type { } from '../../../../shared/agent-session-journal-types' import { projectStructuredItemsToNativeChat } from '../../../../shared/structured-agent-session-projection' import type { NativeChatMessage } from '../../../../shared/native-chat-types' -import type { NativeChatLiveSession } from './use-native-chat-live-session' import { NativeChatMessageList } from './NativeChatMessageList' import { NATIVE_CHAT_BOTTOM_THRESHOLD_PX, NATIVE_CHAT_FOLLOW_REARM_PX } from './native-chat-autoscroll' +import { NATIVE_CHAT_ROW_GAP_PX } from './native-chat-row-height-estimate' import { - estimateNativeChatRowHeight, - NATIVE_CHAT_ROW_GAP_PX, - nativeChatRowContentMetrics -} from './native-chat-row-height-estimate' + BELOW_TRANSCRIPT_PX, + deliverResizes, + layout, + list, + marker, + ROW_PITCH_PX, + ROW_PX, + scrollTranscript, + session, + stubLayout, + stubResizeObserver, + TRANSCRIPT_LENGTH, + VIEWPORT_PX, + windowState +} from './native-chat-windowing-test-harness' afterEach(cleanup) -const VIEWPORT_PX = 600 -const TRANSCRIPT_LENGTH = 200 - -/** Everything the document holds below the last row: the transcript column's - * trailing chrome and the scroll root's bottom padding. Non-zero on purpose — - * the document's bottom sits past the window's last row, which is exactly where - * a pin computed from the virtualizer's totals and one computed from the - * document disagree. */ -const BELOW_TRANSCRIPT_PX = 24 -let belowTranscriptPx = BELOW_TRANSCRIPT_PX - -/** Everything the document holds above the spacer: the scroll root's top gutter, - * and the "load earlier" block whenever there is older history to page in. This - * is the virtualizer's `scrollMargin`, and it is the larger half of the gap - * between the document's end and the end the virtualizer computes. */ -let aboveTranscriptPx = 0 - -/** Heights the stubbed layout reports per row index, when a case wants a row to - * measure as something other than its estimate. Empty means "every row at its - * estimate", which is what every non-growth case wants. */ -let measuredRowHeights: readonly number[] = [] - -function marker(index: number): NativeChatMessage { - return { - id: `message-${index}`, - role: 'assistant', - blocks: [{ type: 'text', text: `marker-${index}` }], - timestamp: index + 1, - source: 'transcript' - } -} - -const ROW_PX = estimateNativeChatRowHeight(nativeChatRowContentMetrics(marker(0)), { - hasReceipt: false, - hasStatus: false, - hasTurnDiff: false -}) -const ROW_PITCH_PX = ROW_PX + NATIVE_CHAT_ROW_GAP_PX - -/** Replace a layout property on every element, and hand back the undo. */ -function overrideLayoutProperty(name: string, descriptor: PropertyDescriptor): () => void { - const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, name) - Object.defineProperty(HTMLElement.prototype, name, { configurable: true, ...descriptor }) - return () => { - if (original) { - Object.defineProperty(HTMLElement.prototype, name, original) - } else { - Reflect.deleteProperty(HTMLElement.prototype, name) - } - } -} - -/** The spacer's reserved height, which is the transcript's whole rendered height: - * windowed rows are absolutely positioned inside it, so a row growing in place - * reaches the document only through the height the window reserves for it. */ -function reservedTranscriptHeight(root: ParentNode): number { - const spacer = root.querySelector('[data-native-chat-window]') - return spacer ? Number.parseFloat(spacer.style.height) || 0 : 0 -} - -// The virtualizer measures with `offsetHeight` — not `clientHeight`, not a -// bounding rect — so that is the one thing a DOM without layout has to answer -// for windowing to engage at all. Rows report the height their own estimate -// predicted, which keeps the totals exact and independent of which rows happen -// to have been mounted long enough to be measured; `measuredRowHeights` is how a -// case says a row measures as something else. -// -// `scrollGeometry` additionally gives the scroll root a document to scroll: a -// height, a viewport, and a `scrollTop` that clamps the way a real one does. -// Off by default, because a transcript with a real document opens pinned to its -// bottom and the cases above are about where the window sits, not where it lands. -function stubLayout({ - scrollGeometry = false, - offsetChain = false, - viewportHeight = () => VIEWPORT_PX -}: { - scrollGeometry?: boolean - /** Give the spacer an `offsetTop` and a chain to walk up to the scroll root, - * so `scrollMargin` can be something other than zero. */ - offsetChain?: boolean - viewportHeight?: () => number -} = {}): () => void { - const scrollTops = new WeakMap() - const restores = [ - overrideLayoutProperty('offsetHeight', { - get(this: HTMLElement): number { - if (this.hasAttribute('data-native-chat-scroll')) { - return viewportHeight() - } - if (this.hasAttribute('data-native-chat-window')) { - return reservedTranscriptHeight(this.parentElement ?? this) - } - const index = this.dataset.index - if (index !== undefined) { - return measuredRowHeights[Number(index)] ?? ROW_PX - } - // The transcript column: as tall as the window it wraps, plus what sits - // under it. This is the element the list observes for streamed growth. - return this.classList.contains('max-w-4xl') - ? reservedTranscriptHeight(this) + belowTranscriptPx - : 0 - } - }) - ] - if (scrollGeometry) { - restores.push( - overrideLayoutProperty('clientHeight', { - get(this: HTMLElement): number { - return this.hasAttribute('data-native-chat-scroll') ? viewportHeight() : 0 - } - }), - overrideLayoutProperty('scrollHeight', { - get(this: HTMLElement): number { - return this.hasAttribute('data-native-chat-scroll') - ? aboveTranscriptPx + reservedTranscriptHeight(this) + belowTranscriptPx - : 0 - } - }), - overrideLayoutProperty('scrollTop', { - get(this: HTMLElement): number { - return scrollTops.get(this) ?? 0 - }, - set(this: HTMLElement, value: number): void { - // A browser clamps; without this `scrollTop = scrollHeight` would park - // the view past the end and every distance-from-bottom would read 0. - const max = Math.max(0, this.scrollHeight - this.clientHeight) - scrollTops.set(this, Math.min(Math.max(0, value), max)) - } - }) - ) - } - if (offsetChain) { - restores.push( - overrideLayoutProperty('offsetTop', { - get(this: HTMLElement): number { - return this.hasAttribute('data-native-chat-window') ? aboveTranscriptPx : 0 - } - }), - // happy-dom has no `offsetParent` at all, so production's walk to the - // scroll root ends before it starts and every margin reads zero. - overrideLayoutProperty('offsetParent', { - get(this: HTMLElement): HTMLElement | null { - return this.parentElement?.closest('[data-native-chat-scroll]') ?? null - } - }) - ) - } - return () => { - for (const restore of restores.toReversed()) { - restore() - } - } -} - -type FakeResizeObservation = { - callback: ResizeObserverCallback - /** Target -> height last delivered. -1 means "never", so the first flush - * delivers, the way a real observer's initial callback does. */ - observed: Map -} - -const resizeObservations = new Set() - -/** happy-dom's ResizeObserver never fires, so nothing that re-measures ever runs. - * This one records what production observes and delivers only when a target's - * height actually changed — the browser's own rule — and only when a test says - * a frame was painted. Entries carry no `borderBoxSize`, so the virtualizer - * falls back to `offsetHeight`, which is the path being modelled. */ -function stubResizeObserver(): () => void { - const original = window.ResizeObserver - class TestResizeObserver { - private readonly observation: FakeResizeObservation - constructor(callback: ResizeObserverCallback) { - this.observation = { callback, observed: new Map() } - resizeObservations.add(this.observation) - } - observe(target: Element): void { - this.observation.observed.set(target, -1) - } - unobserve(target: Element): void { - this.observation.observed.delete(target) - } - disconnect(): void { - this.observation.observed.clear() - resizeObservations.delete(this.observation) - } - } - window.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver - return () => { - resizeObservations.clear() - window.ResizeObserver = original - } -} - -/** Deliver one round of resize callbacks; true when anything was delivered. */ -function deliverResizes(): boolean { - let delivered = false - // A copy: a callback may disconnect its own observer mid-delivery. - for (const observation of Array.from(resizeObservations)) { - const entries: ResizeObserverEntry[] = [] - for (const [target, lastHeight] of observation.observed) { - const height = (target as HTMLElement).offsetHeight - if (height !== lastHeight) { - observation.observed.set(target, height) - entries.push({ target } as unknown as ResizeObserverEntry) - } - } - if (entries.length > 0) { - delivered = true - observation.callback(entries, undefined as unknown as ResizeObserver) - } - } - return delivered -} - -function session(messages: NativeChatMessage[]): NativeChatLiveSession { - return { - messages, - status: 'ready', - sessionId: 'session-1', - agent: 'codex', - hasMore: false, - loadingEarlier: false, - loadEarlier: vi.fn(), - readPhase: 'ready' - } -} - -function list(messages: NativeChatMessage[]): React.JSX.Element { - return ( - - ) -} - -/** Reads the window, and refuses to pass if there is no window to read. - * - * Without this a change to the usability gate would quietly send every case - * below down the whole-transcript path, where "fewer rows than messages" is - * false but every other assertion still holds. */ -function windowState(container: HTMLElement): { totalSize: number; indexes: number[] } { - const spacer = container.querySelector('[data-native-chat-window]') - if (!spacer) { - throw new Error('transcript is not windowed: no spacer, every row is mounted') - } - const totalSize = Number.parseFloat(spacer.style.height) - if (!(totalSize > 0)) { - throw new Error(`transcript reserved no height (${spacer.style.height})`) - } - return { - totalSize, - indexes: Array.from(container.querySelectorAll('[data-index]')) - .map((row) => Number(row.dataset.index)) - .sort((left, right) => left - right) - } -} - -/** happy-dom fires no scroll event for an assignment to `scrollTop`. */ -function scrollTranscript(container: HTMLElement, top: number): void { - const scroller = container.querySelector('[data-native-chat-scroll]') - if (!scroller) { - throw new Error('no transcript scroll root') - } - scroller.scrollTop = top - fireEvent.scroll(scroller) -} - describe('windowed transcript', () => { let restoreLayout = (): void => {} beforeEach(() => { @@ -711,7 +451,7 @@ describe('transcript follow ownership across growth and appends', () => { function setMeasuredTail(step: number): void { const heights = Array.from({ length: TRANSCRIPT_LENGTH }, () => ROW_PX) heights[TAIL_INDEX] = tailHeightAt(step) - measuredRowHeights = heights + layout.measuredRowHeights = heights } let restoreLayout = (): void => {} @@ -719,16 +459,16 @@ describe('transcript follow ownership across growth and appends', () => { beforeEach(() => { restoreLayout = stubLayout({ scrollGeometry: true, offsetChain: true }) restoreResizeObserver = stubResizeObserver() - belowTranscriptPx = BELOW_TRANSCRIPT_PX - aboveTranscriptPx = 0 + layout.belowTranscriptPx = BELOW_TRANSCRIPT_PX + layout.aboveTranscriptPx = 0 setMeasuredTail(0) }) afterEach(() => { restoreResizeObserver() restoreLayout() - measuredRowHeights = [] - belowTranscriptPx = BELOW_TRANSCRIPT_PX - aboveTranscriptPx = 0 + layout.measuredRowHeights = [] + layout.belowTranscriptPx = BELOW_TRANSCRIPT_PX + layout.aboveTranscriptPx = 0 vi.restoreAllMocks() }) @@ -800,7 +540,7 @@ describe('transcript follow ownership across growth and appends', () => { 'keeps a reader parked above a growing row with a %i px initial measurement delta', (measurementDelta) => { setMeasuredTail(4) - measuredRowHeights = measuredRowHeights.map((height, index) => + layout.measuredRowHeights = layout.measuredRowHeights.map((height, index) => index === TAIL_INDEX ? height + measurementDelta : height ) const { container, rerender } = render(streamingList(4)) @@ -952,7 +692,7 @@ describe('transcript follow ownership across growth and appends', () => { const aboveIndex = windowState(container).indexes[0]! expect((aboveIndex + 1) * ROW_PITCH_PX).toBeLessThan(readingAt) for (const growth of [100, 200]) { - measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + layout.measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => index === aboveIndex ? ROW_PX + growth : ROW_PX ) paint(container) @@ -973,7 +713,7 @@ describe('transcript follow ownership across growth and appends', () => { setMeasuredTail(1) expect(deliverResizes()).toBe(true) const pinnedAt = scroller.scrollTop - belowTranscriptPx += 2_000 + layout.belowTranscriptPx += 2_000 fireEvent.scroll(scroller) @@ -990,7 +730,7 @@ describe('transcript follow ownership across growth and appends', () => { paint(container) scrollTranscript(container, readingAt + 100) paint(container) - measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + layout.measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => index === aboveIndex ? ROW_PX + 10 : ROW_PX ) paint(container) @@ -999,7 +739,7 @@ describe('transcript follow ownership across growth and appends', () => { const scroller = scrollRoot(container) const scrollTo = vi.spyOn(scroller, 'scrollTo') - measuredRowHeights = measuredRowHeights.map((height, index) => + layout.measuredRowHeights = layout.measuredRowHeights.map((height, index) => index === aboveIndex ? height + 20 : height ) paint(container) @@ -1014,7 +754,7 @@ describe('transcript follow ownership across growth and appends', () => { paint(container) scrollTranscript(container, focusedIndex * ROW_PITCH_PX) paint(container) - measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => + layout.measuredRowHeights = Array.from({ length: TRANSCRIPT_LENGTH }, (_, index) => index === focusedIndex ? 100 : ROW_PX ) paint(container) @@ -1024,7 +764,7 @@ describe('transcript follow ownership across growth and appends', () => { const scroller = scrollRoot(container) const scrollTo = vi.spyOn(scroller, 'scrollTo') - measuredRowHeights = measuredRowHeights.map((height, index) => + layout.measuredRowHeights = layout.measuredRowHeights.map((height, index) => index === focusedIndex ? 30 : height ) paint(container) @@ -1083,11 +823,11 @@ describe('transcript follow ownership across growth and appends', () => { function setSkewedTail(step: number, skew = MEASURE_SKEW_PX): void { const heights = Array.from({ length: TRANSCRIPT_LENGTH }, () => ROW_PX) heights[TAIL_INDEX] = tailHeightAt(step) + skew - measuredRowHeights = heights + layout.measuredRowHeights = heights } beforeEach(() => { - aboveTranscriptPx = GUTTER_PX + layout.aboveTranscriptPx = GUTTER_PX }) it.each([0, MEASURE_SKEW_PX])( diff --git a/src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx b/src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx new file mode 100644 index 00000000000..9f204078dcb --- /dev/null +++ b/src/renderer/src/components/native-chat/native-chat-windowing-test-harness.tsx @@ -0,0 +1,289 @@ +// Shared layout/observer stubs for the NativeChatMessageList windowing suites. +// happy-dom has no layout and never fires ResizeObserver, so windowing only +// engages against the stubs below. +import { fireEvent } from '@testing-library/react' +import { vi } from 'vitest' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import type { NativeChatLiveSession } from './use-native-chat-live-session' +import { NativeChatMessageList } from './NativeChatMessageList' +import { + estimateNativeChatRowHeight, + NATIVE_CHAT_ROW_GAP_PX, + nativeChatRowContentMetrics +} from './native-chat-row-height-estimate' + +export const VIEWPORT_PX = 600 +export const TRANSCRIPT_LENGTH = 200 + +/** Everything the document holds below the last row: the transcript column's + * trailing chrome and the scroll root's bottom padding. Non-zero on purpose — + * the document's bottom sits past the window's last row, which is exactly where + * a pin computed from the virtualizer's totals and one computed from the + * document disagree. */ +export const BELOW_TRANSCRIPT_PX = 24 + +/** Everything the document holds above the spacer: the scroll root's top gutter, + * and the "load earlier" block whenever there is older history to page in. This + * is the virtualizer's `scrollMargin`, and it is the larger half of the gap + * between the document's end and the end the virtualizer computes. */ + +/** Heights the stubbed layout reports per row index, when a case wants a row to + * measure as something other than its estimate. Empty means "every row at its + * estimate", which is what every non-growth case wants. */ + +/** Layout knobs the stubs read and a case writes. One shared cell so the test + * module and the stubs below see the same values. */ +export const layout: { + belowTranscriptPx: number + aboveTranscriptPx: number + measuredRowHeights: readonly number[] +} = { belowTranscriptPx: BELOW_TRANSCRIPT_PX, aboveTranscriptPx: 0, measuredRowHeights: [] } + +export function marker(index: number): NativeChatMessage { + return { + id: `message-${index}`, + role: 'assistant', + blocks: [{ type: 'text', text: `marker-${index}` }], + timestamp: index + 1, + source: 'transcript' + } +} + +export const ROW_PX = estimateNativeChatRowHeight(nativeChatRowContentMetrics(marker(0)), { + hasReceipt: false, + hasStatus: false, + hasTurnDiff: false +}) +export const ROW_PITCH_PX = ROW_PX + NATIVE_CHAT_ROW_GAP_PX + +/** Replace a layout property on every element, and hand back the undo. */ +export function overrideLayoutProperty(name: string, descriptor: PropertyDescriptor): () => void { + const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, name) + Object.defineProperty(HTMLElement.prototype, name, { configurable: true, ...descriptor }) + return () => { + if (original) { + Object.defineProperty(HTMLElement.prototype, name, original) + } else { + Reflect.deleteProperty(HTMLElement.prototype, name) + } + } +} + +/** The spacer's reserved height, which is the transcript's whole rendered height: + * windowed rows are absolutely positioned inside it, so a row growing in place + * reaches the document only through the height the window reserves for it. */ +export function reservedTranscriptHeight(root: ParentNode): number { + const spacer = root.querySelector('[data-native-chat-window]') + return spacer ? Number.parseFloat(spacer.style.height) || 0 : 0 +} + +// The virtualizer measures with `offsetHeight` — not `clientHeight`, not a +// bounding rect — so that is the one thing a DOM without layout has to answer +// for windowing to engage at all. Rows report the height their own estimate +// predicted, which keeps the totals exact and independent of which rows happen +// to have been mounted long enough to be measured; `layout.measuredRowHeights` is how a +// case says a row measures as something else. +// +// `scrollGeometry` additionally gives the scroll root a document to scroll: a +// height, a viewport, and a `scrollTop` that clamps the way a real one does. +// Off by default, because a transcript with a real document opens pinned to its +// bottom and the cases above are about where the window sits, not where it lands. +export function stubLayout({ + scrollGeometry = false, + offsetChain = false, + viewportHeight = () => VIEWPORT_PX +}: { + scrollGeometry?: boolean + /** Give the spacer an `offsetTop` and a chain to walk up to the scroll root, + * so `scrollMargin` can be something other than zero. */ + offsetChain?: boolean + viewportHeight?: () => number +} = {}): () => void { + const scrollTops = new WeakMap() + const restores = [ + overrideLayoutProperty('offsetHeight', { + get(this: HTMLElement): number { + if (this.hasAttribute('data-native-chat-scroll')) { + return viewportHeight() + } + if (this.hasAttribute('data-native-chat-window')) { + return reservedTranscriptHeight(this.parentElement ?? this) + } + const index = this.dataset.index + if (index !== undefined) { + return layout.measuredRowHeights[Number(index)] ?? ROW_PX + } + // The transcript column: as tall as the window it wraps, plus what sits + // under it. This is the element the list observes for streamed growth. + return this.classList.contains('max-w-4xl') + ? reservedTranscriptHeight(this) + layout.belowTranscriptPx + : 0 + } + }) + ] + if (scrollGeometry) { + restores.push( + overrideLayoutProperty('clientHeight', { + get(this: HTMLElement): number { + return this.hasAttribute('data-native-chat-scroll') ? viewportHeight() : 0 + } + }), + overrideLayoutProperty('scrollHeight', { + get(this: HTMLElement): number { + return this.hasAttribute('data-native-chat-scroll') + ? layout.aboveTranscriptPx + reservedTranscriptHeight(this) + layout.belowTranscriptPx + : 0 + } + }), + overrideLayoutProperty('scrollTop', { + get(this: HTMLElement): number { + return scrollTops.get(this) ?? 0 + }, + set(this: HTMLElement, value: number): void { + // A browser clamps; without this `scrollTop = scrollHeight` would park + // the view past the end and every distance-from-bottom would read 0. + const max = Math.max(0, this.scrollHeight - this.clientHeight) + scrollTops.set(this, Math.min(Math.max(0, value), max)) + } + }) + ) + } + if (offsetChain) { + restores.push( + overrideLayoutProperty('offsetTop', { + get(this: HTMLElement): number { + return this.hasAttribute('data-native-chat-window') ? layout.aboveTranscriptPx : 0 + } + }), + // happy-dom has no `offsetParent` at all, so production's walk to the + // scroll root ends before it starts and every margin reads zero. + overrideLayoutProperty('offsetParent', { + get(this: HTMLElement): HTMLElement | null { + return this.parentElement?.closest('[data-native-chat-scroll]') ?? null + } + }) + ) + } + return () => { + for (const restore of restores.toReversed()) { + restore() + } + } +} + +type FakeResizeObservation = { + callback: ResizeObserverCallback + /** Target -> height last delivered. -1 means "never", so the first flush + * delivers, the way a real observer's initial callback does. */ + observed: Map +} + +const resizeObservations = new Set() + +/** happy-dom's ResizeObserver never fires, so nothing that re-measures ever runs. + * This one records what production observes and delivers only when a target's + * height actually changed — the browser's own rule — and only when a test says + * a frame was painted. Entries carry no `borderBoxSize`, so the virtualizer + * falls back to `offsetHeight`, which is the path being modelled. */ +export function stubResizeObserver(): () => void { + const original = window.ResizeObserver + class TestResizeObserver { + private readonly observation: FakeResizeObservation + constructor(callback: ResizeObserverCallback) { + this.observation = { callback, observed: new Map() } + resizeObservations.add(this.observation) + } + observe(target: Element): void { + this.observation.observed.set(target, -1) + } + unobserve(target: Element): void { + this.observation.observed.delete(target) + } + disconnect(): void { + this.observation.observed.clear() + resizeObservations.delete(this.observation) + } + } + window.ResizeObserver = TestResizeObserver as unknown as typeof ResizeObserver + return () => { + resizeObservations.clear() + window.ResizeObserver = original + } +} + +/** Deliver one round of resize callbacks; true when anything was delivered. */ +export function deliverResizes(): boolean { + let delivered = false + // A copy: a callback may disconnect its own observer mid-delivery. + for (const observation of Array.from(resizeObservations)) { + const entries: ResizeObserverEntry[] = [] + for (const [target, lastHeight] of observation.observed) { + const height = (target as HTMLElement).offsetHeight + if (height !== lastHeight) { + observation.observed.set(target, height) + entries.push({ target } as unknown as ResizeObserverEntry) + } + } + if (entries.length > 0) { + delivered = true + observation.callback(entries, undefined as unknown as ResizeObserver) + } + } + return delivered +} + +export function session(messages: NativeChatMessage[]): NativeChatLiveSession { + return { + messages, + status: 'ready', + sessionId: 'session-1', + agent: 'codex', + hasMore: false, + loadingEarlier: false, + loadEarlier: vi.fn(), + readPhase: 'ready' + } +} + +export function list(messages: NativeChatMessage[]): React.JSX.Element { + return ( + + ) +} + +/** Reads the window, and refuses to pass if there is no window to read. + * + * Without this a change to the usability gate would quietly send every case + * below down the whole-transcript path, where "fewer rows than messages" is + * false but every other assertion still holds. */ +export function windowState(container: HTMLElement): { totalSize: number; indexes: number[] } { + const spacer = container.querySelector('[data-native-chat-window]') + if (!spacer) { + throw new Error('transcript is not windowed: no spacer, every row is mounted') + } + const totalSize = Number.parseFloat(spacer.style.height) + if (!(totalSize > 0)) { + throw new Error(`transcript reserved no height (${spacer.style.height})`) + } + return { + totalSize, + indexes: Array.from(container.querySelectorAll('[data-index]')) + .map((row) => Number(row.dataset.index)) + .sort((left, right) => left - right) + } +} + +/** happy-dom fires no scroll event for an assignment to `scrollTop`. */ +export function scrollTranscript(container: HTMLElement, top: number): void { + const scroller = container.querySelector('[data-native-chat-scroll]') + if (!scroller) { + throw new Error('no transcript scroll root') + } + scroller.scrollTop = top + fireEvent.scroll(scroller) +} From 11180fa532eb4fb62ebedb19344aa4ffbd2dc089 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:42:37 -0700 Subject: [PATCH 11/34] chore(lint): add anti-slop oxlint plugin (pinned, all rules off) (#20726) * chore(lint): add anti-slop oxlint plugin (all rules off) Vendors dmmulroy/anti-slop (MIT) plus no-call-only-assertions and no-pass-through-type-alias from maharshi365/deslop (MIT). Every rule starts "off"; each follow-up PR fixes one rule's violations and flips it to "error". * fix(lint): actually exclude the vendored plugin from the anti-slop audit oxlint does not honour ignorePatterns supplied via --config, so the config/oxlint-plugins/anti-slop/** entry never matched and the vendored rule source was being linted as first-party code (505 violations). Move the exclusion to the --ignore-pattern CLI flag in audit:anti-slop, which does work, and drop the entry that gave a false sense of coverage. Keeping vendored source unlinted matters because anti-slop is updated by three-way merge against the upstream snapshot; reformatting it locally would conflict on every update. * chore(lint): pin anti-slop instead of vendoring it; drop deslop Replaces the ~5k vendored lines with a git-pinned devDependency: oxlint-plugin-anti-slop: github:dmmulroy/anti-slop#c44ef22 anti-slop ships raw .ts with no build step, and Node refuses to type-strip anything under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so oxlint cannot load it from there -- which is why upstream says to vendor it. A postinstall step copies the pinned package's source to .anti-slop-plugin/ (gitignored), which Node will type-strip because it sits outside node_modules. Upgrading is now a SHA bump rather than a re-vendor and three-way merge. Verified byte-identical rule output to the vendored copy across all 16 rules that fire. Drops maharshi365/deslop and its two rules (no-call-only-assertions, no-pass-through-type-alias). It is not on npm either, so it would need a second git pin and copy step, and it is a 5-star single-maintainer repo that is itself a re-namespaced copy of anti-slop. One upstream is enough. * ci(lint): run audit:anti-slop in PR CI config/scripts/pr-workflow-lint-parity.test.mjs requires every step in `pnpm lint` to have a matching step in .github/workflows/pr.yml; adding audit:anti-slop to lint without the workflow step failed that ratchet. Also makes audit:anti-slop sync the plugin itself before linting. The generated .anti-slop-plugin/ directory is gitignored and otherwise only created by postinstall, so a cached install that skips postinstall would leave oxlint unable to load the plugin. --- .github/workflows/pr.yml | 3 ++ .gitignore | 3 ++ .oxfmtrc.json | 6 ++- config/oxlint-anti-slop.json | 60 ++++++++++++++++++++++++ config/scripts/sync-anti-slop-plugin.mjs | 20 ++++++++ package.json | 10 ++-- pnpm-lock.yaml | 26 ++++++++++ 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 config/oxlint-anti-slop.json create mode 100644 config/scripts/sync-anti-slop-plugin.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7418e8f80aa..b7832ba2d9e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -133,6 +133,9 @@ jobs: - name: Lint run: pnpm exec oxlint --format github + - name: Reject low-evidence patterns + run: pnpm run audit:anti-slop + - name: Enforce focused code-quality plugins run: pnpm run audit:code-quality:native diff --git a/.gitignore b/.gitignore index c132a265c83..3d51edb0009 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,6 @@ tests/e2e/.cross-version-checkouts/ # IS committed). Also keeps oxfmt/oxlint, which honor this file, from walking # vendored gems. /mobile/vendor/ + +# Generated by config/scripts/sync-anti-slop-plugin.mjs from the pinned oxlint-plugin-anti-slop +.anti-slop-plugin/ diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 0f27189d7cb..86931f1f9ec 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -4,5 +4,9 @@ "semi": false, "printWidth": 100, "trailingComma": "none", - "ignorePatterns": ["cloud/**", ".github/actions/cloud-sql-rollout-lease/**"] + "ignorePatterns": [ + "cloud/**", + ".github/actions/cloud-sql-rollout-lease/**", + ".anti-slop-plugin/**" + ] } diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json new file mode 100644 index 00000000000..1f488ff8c56 --- /dev/null +++ b/config/oxlint-anti-slop.json @@ -0,0 +1,60 @@ +{ + "$schema": "../node_modules/oxlint/configuration_schema.json", + "plugins": [], + "jsPlugins": [ + { + "name": "anti-slop", + "specifier": "../.anti-slop-plugin/index.ts" + } + ], + "categories": { + "correctness": "off", + "suspicious": "off", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "ignorePatterns": [ + "**/node_modules", + "**/dist", + "**/out", + "cloud/**", + "src/shared/rpc-contract/rpc-params-catalog.generated.ts", + "tests/e2e/.cross-version-checkouts" + ], + "rules": { + "anti-slop/no-array-filter-map": "off", + "anti-slop/no-chained-type-assertions": "off", + "anti-slop/no-conditional-empty-object-spread": "off", + "anti-slop/no-known-value-widening": "off", + "anti-slop/no-module-mocking": "off", + "anti-slop/no-object-parameters": "off", + "anti-slop/no-reduce-accumulator-copy": "off", + "anti-slop/no-reflect-apply": "off", + "anti-slop/no-reflect-get": "off", + "anti-slop/no-runtime-typeof": "off", + "anti-slop/no-shape-in-symbol-names": "off", + "anti-slop/no-unknown-parameters": "off", + "anti-slop/no-unknown-returns": "off", + "anti-slop/no-unknown-type-aliases": "off", + "anti-slop/no-unsafe-dictionary-type": "off", + "anti-slop/no-widen-then-assert": "off", + "anti-slop/require-readable-spacing": "off", + "anti-slop/require-safety-comment-for-type-assertion": "off" + }, + "overrides": [ + { + "files": [ + "**/*.test.{ts,tsx}", + "**/*.spec.{ts,tsx}", + "tests/**/*.{ts,tsx}", + "**/__mocks__/**" + ], + "rules": { + "anti-slop/no-module-mocking": "off" + } + } + ] +} diff --git a/config/scripts/sync-anti-slop-plugin.mjs b/config/scripts/sync-anti-slop-plugin.mjs new file mode 100644 index 00000000000..66d9ac10a54 --- /dev/null +++ b/config/scripts/sync-anti-slop-plugin.mjs @@ -0,0 +1,20 @@ +// anti-slop ships raw .ts with no build step, and Node refuses to type-strip anything +// under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so oxlint cannot load +// it from there. Copy the pinned package's source out to a gitignored dir it can load. +import { cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const repoRoot = resolve(import.meta.dirname, '../..') +const source = resolve(repoRoot, 'node_modules/oxlint-plugin-anti-slop/src') +const target = resolve(repoRoot, '.anti-slop-plugin') + +rmSync(target, { recursive: true, force: true }) +mkdirSync(target, { recursive: true }) +cpSync(source, target, { recursive: true }) +// Effect rules are opt-in upstream and this repo does not use Effect; tests would be linted. +rmSync(resolve(target, 'effect'), { recursive: true, force: true }) +cpSync( + resolve(repoRoot, 'node_modules/oxlint-plugin-anti-slop/LICENSE'), + resolve(target, 'LICENSE') +) +writeFileSync(resolve(target, 'package.json'), '{ "type": "module" }\n') diff --git a/package.json b/package.json index 174a8718641..96d8e00b97f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "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: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", + "lint": "oxlint && pnpm run audit:anti-slop && 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", @@ -97,7 +97,7 @@ "build": "pnpm run build:desktop && pnpm run build:native", "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", - "postinstall": "node config/scripts/rebuild-native-deps.mjs", + "postinstall": "node config/scripts/rebuild-native-deps.mjs && node config/scripts/sync-anti-slop-plugin.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", "reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs", @@ -164,7 +164,9 @@ "test:e2e:remote-bulk-open-freeze": "pnpm run ensure:electron-runtime && pnpm exec playwright test tests/e2e/remote-session-bulk-open-freeze-repro.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "test:e2e:ssh-docker-bulk-open-freeze": "node config/scripts/run-ssh-docker-bulk-open-freeze-e2e.mjs", "repro:live-remote-bulk-open-freeze": "node config/scripts/live-remote-bulk-open-freeze-repro.mjs", - "repro:live-remote-realistic-freeze": "node config/scripts/live-remote-realistic-freeze-repro.mjs" + "repro:live-remote-realistic-freeze": "node config/scripts/live-remote-realistic-freeze-repro.mjs", + "audit:anti-slop": "node config/scripts/sync-anti-slop-plugin.mjs && oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings", + "sync:anti-slop-plugin": "node config/scripts/sync-anti-slop-plugin.mjs" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "0.3.251", @@ -199,6 +201,7 @@ "@electron-toolkit/tsconfig": "^2.0.0", "@electron/rebuild": "^4.2.0", "@monaco-editor/react": "^4.7.0", + "@oxlint/plugins": "1.80.0", "@playwright/test": "^1.59.1", "@sanity/diff-match-patch": "^3.2.0", "@shadcn/lint": "^0.1.0", @@ -265,6 +268,7 @@ "monaco-editor": "^0.55.1", "oxfmt": "^0.65.0", "oxlint": "^1.80.0", + "oxlint-plugin-anti-slop": "github:dmmulroy/anti-slop#c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b", "oxlint-plugin-react-doctor": "0.9.1", "oxlint-tsgolint": "7.0.2001", "pdfjs-dist": "^6.3.289", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e9de58b228..6c9c98349d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@oxlint/plugins': + specifier: 1.80.0 + version: 1.80.0 '@playwright/test': specifier: ^1.59.1 version: 1.59.1 @@ -411,6 +414,9 @@ importers: oxlint: specifier: ^1.80.0 version: 1.80.0(oxlint-tsgolint@7.0.2001) + oxlint-plugin-anti-slop: + specifier: github:dmmulroy/anti-slop#c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b + version: https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b oxlint-plugin-react-doctor: specifier: 0.9.1 version: 0.9.1 @@ -1940,6 +1946,14 @@ packages: cpu: [x64] os: [win32] + '@oxlint/plugins@1.78.0': + resolution: {integrity: sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@oxlint/plugins@1.80.0': + resolution: {integrity: sha512-QRgH1XqQEYNHa4f1vvPQ5fAdNdncHGIUG1ZWLlGIZHky3qwCEeAKYitZNbZMtaXtAQAAFFTOwqUfzESvimqZNA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -6059,6 +6073,10 @@ packages: vite-plus: optional: true + oxlint-plugin-anti-slop@https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b: + resolution: {gitHosted: true, integrity: sha512-Vj/M0k5Bt1Q2pGdfXZ24wGXycBcvFHwJ/pjHIFl1hV8soaZKHl1lba5UXlQFY5xgwQ5TNfEWiKfsEt0yyRyVUg==, tarball: https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b} + version: 0.1.2 + oxlint-plugin-react-doctor@0.9.1: resolution: {integrity: sha512-yCW8USbiuszbVsUMN4fL1iU7mRu3Ae3w96+k/xqCyWvW6bF6DzCMtLy5N/w6WLRX78iQsWxVqW/SEgzRBXLfsA==} engines: {node: ^20.19.0 || >=22.13.0} @@ -8605,6 +8623,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true + '@oxlint/plugins@1.78.0': {} + + '@oxlint/plugins@1.80.0': {} + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -13162,6 +13184,10 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.65.0 '@oxfmt/binding-win32-x64-msvc': 0.65.0 + oxlint-plugin-anti-slop@https://codeload.github.com/dmmulroy/anti-slop/tar.gz/c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b: + dependencies: + '@oxlint/plugins': 1.78.0 + oxlint-plugin-react-doctor@0.9.1: dependencies: '@typescript-eslint/types': 8.60.0 From b79206533ea983620ea20378b6b1116ce2cd5bde Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:58:56 -0700 Subject: [PATCH 12/34] chore(lint): enable anti-slop no-reduce-accumulator-copy and no-widen-then-assert (#20780) Both rules already report zero violations, so this only locks in the current state as a ratchet. No source changes. --- config/oxlint-anti-slop.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 1f488ff8c56..3e48b189302 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -31,7 +31,7 @@ "anti-slop/no-known-value-widening": "off", "anti-slop/no-module-mocking": "off", "anti-slop/no-object-parameters": "off", - "anti-slop/no-reduce-accumulator-copy": "off", + "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "off", "anti-slop/no-reflect-get": "off", "anti-slop/no-runtime-typeof": "off", @@ -40,7 +40,7 @@ "anti-slop/no-unknown-returns": "off", "anti-slop/no-unknown-type-aliases": "off", "anti-slop/no-unsafe-dictionary-type": "off", - "anti-slop/no-widen-then-assert": "off", + "anti-slop/no-widen-then-assert": "error", "anti-slop/require-readable-spacing": "off", "anti-slop/require-safety-comment-for-type-assertion": "off" }, From 4a5b0583b22654b38472d6d853fa3a420ab65e5f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:43:38 -0700 Subject: [PATCH 13/34] fix(runtime): keep listed handles when graph sync learns a PTY incarnation (#20779) reconcilePtyIncarnationHandles compared a null retained incarnation against the learned one and staled the handle. Daemon-hosted PTYs are recorded from first output before the spawn commit reports an incarnation, so on Windows `orca terminal create` returned a handle that was stale by the next graph publish. Treat null-to-known as un-fenced like every other site; keep the known-to-different and preallocated-handle invalidations. --- ...rca-runtime-bind-pty-incarnation-handle.ts | 14 ++++---- ...untime-terminal-handle-incarnation.test.ts | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/runtime/orca-runtime-bind-pty-incarnation-handle.ts b/src/main/runtime/orca-runtime-bind-pty-incarnation-handle.ts index 3f1a79beb77..6ab9eec8563 100644 --- a/src/main/runtime/orca-runtime-bind-pty-incarnation-handle.ts +++ b/src/main/runtime/orca-runtime-bind-pty-incarnation-handle.ts @@ -54,15 +54,17 @@ export class OrcaRuntimeWithBindPtyIncarnationHandle extends OrcaRuntimeWithBuil for (const [ptyId, retained] of this.handleByPtyIncarnation) { const pty = this.ptysById.get(ptyId) const leaves = this.getLeavesForPty(ptyId) - if ( - !pty || - pty.incarnationId !== retained.incarnationId || - leaves.length !== 1 || - this.handleByPtyId.has(ptyId) - ) { + // Why: a handle issued before the host reported the incarnation is un-fenced, so + // learning it is not a replacement; only a known-to-different incarnation is. + const incarnationReplaced = + retained.incarnationId !== null && + pty !== undefined && + pty.incarnationId !== retained.incarnationId + if (!pty || incarnationReplaced || leaves.length !== 1 || this.handleByPtyId.has(ptyId)) { this.invalidatePtyIncarnationHandle(ptyId) continue } + retained.incarnationId = pty.incarnationId this.bindPtyIncarnationHandle(retained, leaves[0]) } } diff --git a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts index 9765465fdf0..bac59e614b2 100644 --- a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts +++ b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts @@ -107,6 +107,40 @@ describe('runtime terminal handle incarnation fencing', () => { await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ handle, status: 'running' }) }) + it('keeps a listed handle when graph sync learns the incarnation after issue', async () => { + // Daemon-hosted PTYs are recorded from first output before the spawn commit reports an + // incarnation, so the handle is issued un-fenced and must survive learning it. + const { runtime } = makeRuntime() + runtime.registerPty(PTY_ID, WORKTREE_ID, 'target', { tabId: TAB_ID, leafId: LEAF_ID }) + syncGraph(runtime) + const [listed] = (await runtime.listTerminals()).terminals + + register(runtime, 'incarnation-learned') + syncGraph(runtime) + + await expect(runtime.readTerminal(listed.handle)).resolves.toMatchObject({ + handle: listed.handle, + status: 'running' + }) + }) + + it('stales a listed handle when graph sync sees a replaced incarnation', async () => { + const { runtime } = makeRuntime() + register(runtime, 'incarnation-old') + syncGraph(runtime) + const [listed] = (await runtime.listTerminals()).terminals + + // Rotate the record directly so reconcile is the only fence exercised. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test reaches the runtime's protected pty record map to bypass the registerPty fence. + const internals = runtime as unknown as { + ptysById: Map + } + internals.ptysById.get(PTY_ID)!.incarnationId = 'incarnation-new' + syncGraph(runtime) + + await expect(runtime.readTerminal(listed.handle)).rejects.toThrow('terminal_handle_stale') + }) + it('invalidates a direct handle when a reused PTY id gets a new incarnation', async () => { const { runtime, writes } = makeRuntime() const staleHandle = runtime.preAllocateHandleForPty(PTY_ID) From ab6b86dd5c26edea5e2fe1a36c8692796f898074 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:00:35 -0700 Subject: [PATCH 14/34] fix(orchestration): require registered structured worker pane key (#20664) --- .../structured-worker-identity.test.ts | 75 +++++++++++++++++-- .../runtime/structured-worker-identity.ts | 31 ++++++-- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/src/main/runtime/structured-worker-identity.test.ts b/src/main/runtime/structured-worker-identity.test.ts index 9b678ebb0eb..2a66536291a 100644 --- a/src/main/runtime/structured-worker-identity.test.ts +++ b/src/main/runtime/structured-worker-identity.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it, beforeEach } from 'vitest' import { isTerminalLeafId, parsePaneKey } from '../../shared/stable-pane-id' -import { structuredAgentSessionPaneKey } from '../../shared/structured-agent-session-projection' +import { + structuredAgentSessionPaneKey, + structuredAgentSessionTabId +} from '../../shared/structured-agent-session-projection' import { selectExactWorkerProviderSession } from './orchestration/worker-provider-session' import { structuredWorkerChildIdentityEnv } from './structured-worker-child-identity-env' import { @@ -85,12 +88,57 @@ describe('structured worker identity', () => { ) }) - it("accepts a persisted pane key for its own session and rejects another session's", () => { + it('accepts only the registered pane key for its session', () => { + const handle = mintStructuredWorkerHandle() const paneKey = mintStructuredWorkerPaneKey(SESSION_ID) - expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(true) - expect(structuredWorkerPaneKeyBelongsToSession(paneKey, 'another-session-id')).toBe(false) - expect(structuredWorkerPaneKeyBelongsToSession('not-a-pane-key', SESSION_ID)).toBe(false) - expect(structuredWorkerPaneKeyBelongsToSession(null, SESSION_ID)).toBe(false) + structuredWorkerIdentities.register({ + handle, + sessionId: SESSION_ID, + agent: 'claude', + paneKey, + processIncarnation: structuredWorkerProcessIncarnation(SESSION_ID), + worktreeId: 'wt_1', + hostScope: { kind: 'local', hostId: 'local' } + }) + try { + expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(true) + expect( + structuredWorkerPaneKeyBelongsToSession(mintStructuredWorkerPaneKey(SESSION_ID), SESSION_ID) + ).toBe(false) + expect(structuredWorkerPaneKeyBelongsToSession(paneKey, 'another-session-id')).toBe(false) + expect(structuredWorkerPaneKeyBelongsToSession('not-a-pane-key', SESSION_ID)).toBe(false) + expect(structuredWorkerPaneKeyBelongsToSession(null, SESSION_ID)).toBe(false) + } finally { + structuredWorkerIdentities.forget(handle) + } + }) + + it('rejects the deterministic public status key even for a registered worker', () => { + const handle = mintStructuredWorkerHandle() + const paneKey = mintStructuredWorkerPaneKey(SESSION_ID) + structuredWorkerIdentities.register({ + handle, + sessionId: SESSION_ID, + agent: 'claude', + paneKey, + processIncarnation: structuredWorkerProcessIncarnation(SESSION_ID), + worktreeId: 'wt_1', + hostScope: { kind: 'local', hostId: 'local' } + }) + try { + const statusPaneKey = structuredAgentSessionPaneKey( + structuredAgentSessionTabId(SESSION_ID), + SESSION_ID + ) + expect(structuredWorkerPaneKeyBelongsToSession(statusPaneKey, SESSION_ID)).toBe(false) + } finally { + structuredWorkerIdentities.forget(handle) + } + }) + + it('fails closed when the session has no registry record', () => { + const paneKey = mintStructuredWorkerPaneKey(SESSION_ID) + expect(structuredWorkerPaneKeyBelongsToSession(paneKey, SESSION_ID)).toBe(false) }) it('derives a pane key whose leaf passes the terminal leaf check', () => { @@ -169,6 +217,21 @@ describe('structured worker identity registry', () => { ).toBeNull() }) + it('refuses to rehydrate the deterministic public status key as a worker credential', () => { + expect( + registry.rehydrate({ + terminal_handle: mintStructuredWorkerHandle(), + pane_key: structuredAgentSessionPaneKey( + structuredAgentSessionTabId(SESSION_ID), + SESSION_ID + ), + process_incarnation: structuredWorkerProcessIncarnation(SESSION_ID), + worktree_id: 'wt_1', + host_scope: JSON.stringify({ kind: 'local', hostId: 'local' }) + }) + ).toBeNull() + }) + it('forgets both indexes', () => { const handle = mintStructuredWorkerHandle() registry.register({ diff --git a/src/main/runtime/structured-worker-identity.ts b/src/main/runtime/structured-worker-identity.ts index 161ae55dd5d..b29d68c297a 100644 --- a/src/main/runtime/structured-worker-identity.ts +++ b/src/main/runtime/structured-worker-identity.ts @@ -20,7 +20,10 @@ import type { AgentSessionRecord } from '../../shared/agent-session-record' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection' +import { + structuredAgentSessionPaneKey, + structuredAgentSessionTabId +} from '../../shared/structured-agent-session-projection' import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../shared/stable-pane-id' import { parseWorkerTerminalHostScope, @@ -67,13 +70,30 @@ export function mintStructuredWorkerPaneKey(sessionId: string): string { return makePaneKey(structuredAgentSessionTabId(sessionId), randomUUID()) } -/** Integrity check for a persisted pane key: same session's tab, and a real terminal leaf. */ +/** Credential check: only the pane key registered for this session can prove its identity. */ export function structuredWorkerPaneKeyBelongsToSession( paneKey: string | null | undefined, sessionId: string ): boolean { + const registered = structuredWorkerIdentities.getBySessionId(sessionId) const parsed = paneKey ? parsePaneKey(paneKey) : null return Boolean( + registered && + registered.paneKey === paneKey && + parsed && + parsed.tabId === structuredAgentSessionTabId(sessionId) + ) +} + +/** Bootstrap validation for a durable row before its key can enter the registry. */ +function persistedStructuredWorkerPaneKeyIsValid( + paneKey: string | null | undefined, + sessionId: string +): paneKey is string { + const parsed = paneKey ? parsePaneKey(paneKey) : null + return Boolean( + paneKey && + paneKey !== structuredAgentSessionPaneKey(structuredAgentSessionTabId(sessionId), sessionId) && parsed && parsed.tabId === structuredAgentSessionTabId(sessionId) && isTerminalLeafId(parsed.leafId) @@ -176,9 +196,8 @@ export class StructuredWorkerIdentityRegistry { !hostScope || !row.worktree_id || !isStructuredWorkerHandle(row.terminal_handle) || - // The leaf is random, so the row IS the only source for it; verify only that it is a real - // leaf under this session's tab rather than trying to re-derive it. - !structuredWorkerPaneKeyBelongsToSession(row.pane_key, sessionId) + // The durable row bootstraps the registry after restart, so validate it before registration. + !persistedStructuredWorkerPaneKeyIsValid(row.pane_key, sessionId) ) { return null } @@ -187,7 +206,7 @@ export class StructuredWorkerIdentityRegistry { sessionId, // The row does not carry the provider; callers that need it read the durable record. agent: null, - paneKey: row.pane_key as string, + paneKey: row.pane_key, processIncarnation: structuredWorkerProcessIncarnation(sessionId), worktreeId: row.worktree_id, hostScope From bbd808a63d13ced92feffecc5ba41d0322ab0fe0 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:06:05 -0700 Subject: [PATCH 15/34] fix(lint): keep root postinstall as the sole Electron binary install owner (#20788) #20726 appended the anti-slop plugin sync to postinstall, which breaks the contract asserted by package-electron-runtime-contract.test.mjs and is failing on main. The sync is not needed there: audit:anti-slop already runs it before linting, so a cached install that skips postinstall still works. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96d8e00b97f..9d37cb13cba 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "build": "pnpm run build:desktop && pnpm run build:native", "build:release": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", "build:release:parallel": "pnpm run build:relay && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer", - "postinstall": "node config/scripts/rebuild-native-deps.mjs && node config/scripts/sync-anti-slop-plugin.mjs", + "postinstall": "node config/scripts/rebuild-native-deps.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", "reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs", From cf19735a4da18223e87018db073001012b7fc569 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 06:20:16 +0000 Subject: [PATCH 16/34] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index a4191395e41..8a4e9697938 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 56m + + downloads: 58m @@ -15,7 +15,7 @@ downloads downloads - 56m - 56m + 58m + 58m From 18d0afc9183c97eb3de58c1e754e311fc2acc698 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:26:48 -0700 Subject: [PATCH 17/34] test(package): let the postinstall contract allow unrelated chained steps (#20787) --- .../scripts/package-electron-runtime-contract.test.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs index 31d9b7a8d57..b5874a47c03 100644 --- a/config/scripts/package-electron-runtime-contract.test.mjs +++ b/config/scripts/package-electron-runtime-contract.test.mjs @@ -25,7 +25,14 @@ describe('Electron runtime package contract', () => { } it('keeps root postinstall as the single Electron binary install owner', () => { - expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs') + // Why not an exact match: the invariant is that the root postinstall owns the Electron + // binary install, not that nothing else may run after it. Pinning the whole string made + // any unrelated chained step (a lint-plugin sync, say) a CI failure for every open PR. + const postinstall = packageJson.scripts.postinstall + const steps = postinstall.split('&&').map((step) => step.trim()) + expect(steps[0]).toBe('node config/scripts/rebuild-native-deps.mjs') + // No later step may take over the Electron install the first step owns. + expect(steps.slice(1).join(' ')).not.toMatch(/electron/i) expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') }) From 3ec6193e0ffc2c5ffffe11043949d579c8b3f15e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:53:05 -0700 Subject: [PATCH 18/34] fix(pty): preserve child-process inspection uncertainty (#20756) * fix(pty): preserve unverifiable local child reads * fix(pty): make child-process inspection synchronous Separate foreground and child-process sampling. Sample child processes synchronously after confirming foreground availability, returning unverifiable verdicts when pty reads fail. Handle both transport loss and local read failures uniformly in the completion coordinator. * fix(pty): handle retired masters and pane instance swaps Detect when node-pty retires the master fd (fd == -1) and return unverifiable instead of misreading the spawn file as an idle shell. Guard inspectProcess against PTY replacement mid-read to avoid pairing old foreground with replacement's children. * fix test * fix tests --- .../local-pty-child-process-verdict.test.ts | 88 +++++++++++++++++-- .../local-pty-foreground-inspection.ts | 9 ++ src/main/providers/local-pty-provider.ts | 11 +++ src/main/pty/node-pty-master-fd-retirement.ts | 15 ++++ 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/main/pty/node-pty-master-fd-retirement.ts diff --git a/src/main/providers/local-pty-child-process-verdict.test.ts b/src/main/providers/local-pty-child-process-verdict.test.ts index b36f19fef50..d9d063172c5 100644 --- a/src/main/providers/local-pty-child-process-verdict.test.ts +++ b/src/main/providers/local-pty-child-process-verdict.test.ts @@ -1,4 +1,4 @@ -import type * as pty from 'node-pty' +import * as pty from 'node-pty' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { resolveForegroundMock } = vi.hoisted(() => ({ resolveForegroundMock: vi.fn() })) @@ -7,6 +7,7 @@ vi.mock('./agent-foreground-process', () => ({ resolveAgentForegroundProcessWithAvailability: resolveForegroundMock, confirmShellForegroundProcess: vi.fn() })) +import { isRetiredPtyMaster } from '../pty/node-pty-master-fd-retirement' import { hasLocalPtyChildProcesses, inspectLocalPtyChildProcesses @@ -15,6 +16,8 @@ import { LocalPtyProvider } from './local-pty-provider' import { ptyProcesses, ptyShellName } from './local-pty-provider-state' import { inspectPtyProviderProcess } from './pty-process-inspection' +const POSIX_SHELL = '/bin/sh' + function registerPane(id: string, foreground: string | (() => string), shell?: string): void { const pane: pty.IPty = { pid: 4242, @@ -39,6 +42,32 @@ function registerPane(id: string, foreground: string | (() => string), shell?: s } } +/** + * A real node-pty whose master has been given up. The getter does not throw here -- it answers + * `POSIX_SHELL`, which is exactly the recorded shell name, so only the descriptor distinguishes + * this pane from an idle one. + */ +async function registerRetiredPane(id: string): Promise { + const term = pty.spawn(POSIX_SHELL, ['-c', 'exit 0'], { + name: 'xterm-256color', + cols: 80, + rows: 24, + cwd: process.cwd(), + env: { ...process.env } + }) + await new Promise((resolve) => { + term.onExit(() => resolve()) + }) + // `onExit` runs before node-pty's `_close()`, which is where the patch retires `_fd`. + await vi.waitFor(() => expect(isRetiredPtyMaster(term)).toBe(true), { + timeout: 10000, + interval: 10 + }) + ptyProcesses.set(id, term) + ptyShellName.set(id, POSIX_SHELL) + return term +} + beforeEach(() => { resolveForegroundMock.mockReset() resolveForegroundMock.mockResolvedValue({ available: true, processName: '/bin/zsh' }) @@ -49,6 +78,9 @@ afterEach(() => { ptyShellName.clear() }) +// Windows has no master fd to retire, and `WindowsTerminal.process` answers from the spawn name. +const describeOnPosix = process.platform === 'win32' ? describe.skip : describe + describe('inspectLocalPtyChildProcesses', () => { it('reports unverifiable when the pty fd cannot be read', () => { registerPane( @@ -58,8 +90,6 @@ describe('inspectLocalPtyChildProcesses', () => { }, '/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') }) @@ -78,17 +108,37 @@ describe('inspectLocalPtyChildProcesses', () => { }) it('collapses uncertainty to false only in the boolean adapter', async () => { + let reads = 0 registerPane( 'pty-closed', () => { + reads += 1 throw new Error('EBADF: bad file descriptor') }, '/bin/zsh' ) + await expect(hasLocalPtyChildProcesses('pty-closed')).resolves.toBe(false) + // The `false` has to come from the failed read, not from an earlier short-circuit. + expect(reads).toBe(1) + }) +}) + +describeOnPosix('inspectLocalPtyChildProcesses on a retired master', () => { + it('reports unverifiable rather than reading the spawn file as an idle shell', async () => { + const term = await registerRetiredPane('pty-retired') + + // The mechanism is silent: this is the same string an idle pane reports. + expect(term.process).toBe(POSIX_SHELL) + // Not `no-children`: the close guard reads that as "nothing is running here" and kills the pane. + expect(inspectLocalPtyChildProcesses('pty-retired')).toBe('unverifiable') + }, 15000) + + it('collapses uncertainty to false only in the boolean adapter', async () => { + await registerRetiredPane('pty-retired') // The adapter exists for `IPtyProvider.hasChildProcesses`, which has no third slot. - await expect(hasLocalPtyChildProcesses('pty-closed')).resolves.toBe(false) - }) + await expect(hasLocalPtyChildProcesses('pty-retired')).resolves.toBe(false) + }, 15000) }) describe('inspectPtyProviderProcess child-process evidence', () => { @@ -107,7 +157,6 @@ describe('inspectPtyProviderProcess child-process evidence', () => { }, '/bin/zsh' ) - await expect(inspectPtyProviderProcess(provider, 'pty-closing')).resolves.toEqual({ foregroundProcess: '/bin/zsh', hasChildProcesses: false, @@ -139,4 +188,31 @@ describe('inspectPtyProviderProcess child-process evidence', () => { expect(inspection.hasChildProcesses).toBe(true) expect(inspection.childProcessEvidence).toBe('children') }) + + it('refuses to pair one panes foreground with its replacements children', async () => { + registerPane('pty-swapped', '/bin/zsh', '/bin/zsh') + resolveForegroundMock.mockImplementation(async () => { + // Cleanup plus reactivation lands a different IPty under the same id mid-read. + registerPane('pty-swapped', 'vim', '/bin/zsh') + return { available: true, processName: '/bin/zsh' } + }) + + await expect(inspectPtyProviderProcess(provider, 'pty-swapped')).resolves.toEqual({ + foregroundProcess: null, + hasChildProcesses: false, + childProcessEvidence: 'unverifiable' + }) + }) +}) + +describeOnPosix('inspectPtyProviderProcess on a retired master', () => { + const provider = new LocalPtyProvider() + + it('carries unverifiable child evidence beside the foreground it could still read', async () => { + await registerRetiredPane('pty-retired') + + const inspection = await inspectPtyProviderProcess(provider, 'pty-retired') + expect(inspection.hasChildProcesses).toBe(false) + expect(inspection.childProcessEvidence).toBe('unverifiable') + }, 15000) }) diff --git a/src/main/providers/local-pty-foreground-inspection.ts b/src/main/providers/local-pty-foreground-inspection.ts index eec6a19a621..d376124d7db 100644 --- a/src/main/providers/local-pty-foreground-inspection.ts +++ b/src/main/providers/local-pty-foreground-inspection.ts @@ -7,6 +7,7 @@ import { resolveAgentForegroundProcessWithAvailability } from './agent-foreground-process' import { buildPaneProcessFingerprint } from './posix-pane-foreground-fingerprint' +import { isRetiredPtyMaster } from '../pty/node-pty-master-fd-retirement' import { resolveForegroundFallbackProcess } from './local-pty-launch-helpers' import { ptyAgentForegroundContextPaths, @@ -22,11 +23,19 @@ import { import { readWindowsConsoleAttachedProcessIds } from './windows-console-attached-processes' import { isWindowsPtyJobReadable, readWindowsPtyJobProcessIds } from './windows-pty-job-membership' +/** + * A retired master does not fail loudly: the `process` getter answers with the spawn file, which + * equals the recorded shell and would otherwise read as a real "nothing is running here". Ask the + * descriptor before the name, because an unreadable PTY is not evidence that its children exited. + */ export function inspectLocalPtyChildProcesses(id: string): PtyChildProcessVerdict { const proc = ptyProcesses.get(id) if (!proc) { return 'no-children' } + if (isRetiredPtyMaster(proc)) { + return 'unverifiable' + } try { const foreground = proc.process const shell = ptyShellName.get(id) diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 056a829d1dd..8dad9843ab6 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -130,7 +130,18 @@ export class LocalPtyProvider implements IPtyProvider { } async inspectProcess(id: string): Promise { + const proc = ptyProcesses.get(id) const foregroundProcess = await getLocalPtyForegroundProcess(id) + // Both fields have to describe one PTY: cleanup plus reactivation across the await above would + // otherwise pair the old pane's identity with the replacement's children. The child read below + // is synchronous, so this recheck is the last point either answer can drift. + if (ptyProcesses.get(id) !== proc) { + return { + foregroundProcess: null, + hasChildProcesses: false, + childProcessEvidence: 'unverifiable' + } + } const childProcessEvidence = inspectLocalPtyChildProcesses(id) return { foregroundProcess, diff --git a/src/main/pty/node-pty-master-fd-retirement.ts b/src/main/pty/node-pty-master-fd-retirement.ts new file mode 100644 index 00000000000..292578dc907 --- /dev/null +++ b/src/main/pty/node-pty-master-fd-retirement.ts @@ -0,0 +1,15 @@ +/** + * node-pty hands the master fd to libuv, and Orca's patch sets it to -1 in the same block that + * gives up the handle (config/patches/node-pty@1.1.0.patch). Past that point every fd-addressed + * answer is a stand-in rather than an error: the `process` getter names the spawn file instead of + * whatever `tcgetpgrp` would have reported, so callers that need a real observation have to ask + * about the descriptor first. Windows exposes no master fd, so it never reads as retired; an + * unpatched (relay-installed) node-pty never retires the number at all. + */ +export function isRetiredPtyMaster(proc: unknown): boolean { + if (typeof proc !== 'object' || proc === null || !('fd' in proc)) { + return false + } + const fd: unknown = proc.fd + return typeof fd === 'number' && fd < 0 +} From c9ae17fe3d30cbba2b3ab583cb445762e7d51a00 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:02:04 -0700 Subject: [PATCH 19/34] fix(lint): enable anti-slop/no-unknown-type-aliases (#20784) Flips anti-slop/no-unknown-type-aliases from "off" to "error" and fixes the 3 baseline violations. The rule rejects a named type alias whose resolved type is `unknown` (directly, through another alias, through parentheses, or as a member of a union). Such an alias is strictly worse than writing `unknown`: it reads like a real domain type at every use site while accepting anything, so the compiler stops helping and readers are actively misled. `unknown` is fine, but it must stay visible at the boundary that actually parses it. Violations fixed (3 at baseline, 5 source files touched): - src/main/runtime/workspace-session-failed-write-rollback.ts `type RollbackValue = unknown` -> a real recursive JSON-shaped union `RollbackSlot` (primitives | null | undefined | typeof MISSING | readonly RollbackSlot[] | RollbackRecord), with a named `type RollbackRecord = { readonly [key: string]: RollbackSlot }`. The record is a named alias rather than an inline index signature because inline violates typescript/consistent-indexed-object-style, `interface` violates consistent-type-definitions, and `Readonly>` trips TS2456 circular-reference. The named alias satisfies all three. - src/renderer/src/hooks/direct-ssh-reconnect-coordinator-types.ts `type DirectSshReconnectTimer = unknown` -> `ReturnType`, the handle that actually flows. `DirectSshReconnectTargetState.timer` is widened to `DirectSshReconnectTimer | null` to match the state machine, which initializes to null and resets to null in the scheduled callback. - src/renderer/src/hooks/direct-ssh-host-hydration.ts `type HostReadTimer = unknown` -> `ReturnType`. Fix pattern throughout: replace the alias with the type that already flows through the code, never with `any` and never with a relabelled `unknown`. Because the timer aliases are now honest, two pre-existing `as ReturnType` casts at the clearTimeout boundaries could be deleted, a net win under the repo's type-assertion policy. Suppressions added: none. No eslint-disable, oxlint-disable, `any`, or `as` cast was introduced anywhere in this change. The diff is type-annotation-only; no runtime statement changed. --- config/oxlint-anti-slop.json | 2 +- ...workspace-session-failed-write-rollback.ts | 26 ++++++++++++++----- .../src/hooks/direct-ssh-host-hydration.ts | 4 +-- ...ssh-reconnect-coordinator-stabilization.ts | 2 +- .../direct-ssh-reconnect-coordinator-types.ts | 2 +- .../hooks/direct-ssh-reconnect-coordinator.ts | 4 +-- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 3e48b189302..d2ac1d35d75 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -38,7 +38,7 @@ "anti-slop/no-shape-in-symbol-names": "off", "anti-slop/no-unknown-parameters": "off", "anti-slop/no-unknown-returns": "off", - "anti-slop/no-unknown-type-aliases": "off", + "anti-slop/no-unknown-type-aliases": "error", "anti-slop/no-unsafe-dictionary-type": "off", "anti-slop/no-widen-then-assert": "error", "anti-slop/require-readable-spacing": "off", diff --git a/src/main/runtime/workspace-session-failed-write-rollback.ts b/src/main/runtime/workspace-session-failed-write-rollback.ts index 4f9e79a03d7..7234446cb9d 100644 --- a/src/main/runtime/workspace-session-failed-write-rollback.ts +++ b/src/main/runtime/workspace-session-failed-write-rollback.ts @@ -2,9 +2,21 @@ import { isDeepStrictEqual } from 'node:util' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' const MISSING = Symbol('missing') -type RollbackValue = unknown -function isRecord(value: RollbackValue): value is Record { +/** A JSON-shaped slot of persisted session state, or the absent-key sentinel. */ +type RollbackSlot = + | string + | number + | boolean + | null + | undefined + | typeof MISSING + | readonly RollbackSlot[] + | RollbackRecord + +type RollbackRecord = { readonly [key: string]: RollbackSlot } + +function isRecord(value: RollbackSlot): value is RollbackRecord { return ( value !== MISSING && typeof value === 'object' && @@ -15,10 +27,10 @@ function isRecord(value: RollbackValue): value is Record { } function rollbackValue( - original: RollbackValue, - staged: RollbackValue, - current: RollbackValue -): RollbackValue { + original: RollbackSlot, + staged: RollbackSlot, + current: RollbackSlot +): RollbackSlot { if (isDeepStrictEqual(original, staged)) { return current } @@ -29,7 +41,7 @@ function rollbackValue( return current } let changed = false - const next: Record = { ...current } + const next: Record = { ...current } for (const key of new Set([ ...Object.keys(original), ...Object.keys(staged), diff --git a/src/renderer/src/hooks/direct-ssh-host-hydration.ts b/src/renderer/src/hooks/direct-ssh-host-hydration.ts index 419dc134075..2c326501760 100644 --- a/src/renderer/src/hooks/direct-ssh-host-hydration.ts +++ b/src/renderer/src/hooks/direct-ssh-host-hydration.ts @@ -18,7 +18,7 @@ import { directSshAuthoritiesEqual } from './direct-ssh-reconnect-tokens' export const DIRECT_SSH_HOST_READ_TIMEOUT_MS = 5_000 -type HostReadTimer = unknown +type HostReadTimer = ReturnType export type DirectSshHostHydrationDeps = { store: Pick, 'getState' | 'setState'> @@ -121,7 +121,7 @@ export function createDirectSshHostHydration( const setTimer: NonNullable = deps.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs)) const clearTimer: NonNullable = - deps.clearTimer ?? ((timer) => clearTimeout(timer as ReturnType)) + deps.clearTimer ?? ((timer) => clearTimeout(timer)) const catalogRevisionByTarget = new Map() const catalogInFlight = new Map>() const pendingDeadlines = new Set<{ timer: HostReadTimer; settle: () => void }>() diff --git a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-stabilization.ts b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-stabilization.ts index f20ea1e4a30..9924e55d70b 100644 --- a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-stabilization.ts +++ b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-stabilization.ts @@ -5,7 +5,7 @@ export type DirectSshReconnectTargetState = { authority: DirectSshAuthority installedAt: number dampUntil: number | null - timer: DirectSshReconnectTimer + timer: DirectSshReconnectTimer | null } export function createDirectSshReconnectTargetState( diff --git a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-types.ts b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-types.ts index 6cc76174ed1..31dfbe5a8ea 100644 --- a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-types.ts +++ b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator-types.ts @@ -113,7 +113,7 @@ export type DirectSshCoordinatorTelemetry = { damped: boolean } -export type DirectSshReconnectTimer = unknown +export type DirectSshReconnectTimer = ReturnType export type DirectSshReconnectCoordinatorDeps = { scheduler: DirectSshWorktreeRefreshScheduler diff --git a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator.ts b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator.ts index 7ddf3892ff8..01474789f1c 100644 --- a/src/renderer/src/hooks/direct-ssh-reconnect-coordinator.ts +++ b/src/renderer/src/hooks/direct-ssh-reconnect-coordinator.ts @@ -42,9 +42,7 @@ export function createDirectSshReconnectCoordinator( const now = deps.now ?? Date.now const setTimer = deps.setTimer ?? ((callback: () => void, delayMs: number) => setTimeout(callback, delayMs)) - const clearTimer = - deps.clearTimer ?? - ((timer: DirectSshReconnectTimer) => clearTimeout(timer as ReturnType)) + const clearTimer = deps.clearTimer ?? ((timer: DirectSshReconnectTimer) => clearTimeout(timer)) const stabilizationMs = deps.stabilizationMs ?? DIRECT_SSH_RELAY_STABILIZATION_MS const targets = new Map() let stopped = false From 49e5fa597abcb74cb28f77742ebe9646f72e0194 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:10:11 -0700 Subject: [PATCH 20/34] refactor(lint): enable anti-slop/no-reflect-apply (#20782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `anti-slop/no-reflect-apply` rejects `Reflect.apply(fn, thisArg, argsArray)`. It defeats the call-signature checks TypeScript applies to an ordinary call: the args array is checked as an array, not positionally against the callee's parameters, so arity and type errors pass silently. Dynamic dispatch belongs behind a named interface, not behind a reflective call. Flipped the rule from "off" to "error" and cleared all 17 baseline violations across `src config tests mobile` (16 sites; one file had two). Fix pattern: `Reflect.apply(fn, recv, args)` becomes `fn.call(recv, ...args)`, or a direct method call when the implicit receiver is already the right object. The receiver is preserved at every site. Where the callee is a captured built-in whose overloads split on an argument's shape (`String.prototype.split`, `JSON.stringify`), a call-signature capture no longer compiles once the args are passed positionally. Those three sites capture the function through a method-shaped type (`{ split(separator: unknown, limit?: number): string[] }['split']`), which keeps the forwarding call checked rather than asserted. Behaviour notes: - `diff-section-layout.test.ts` drops a `limit === undefined ? [sep] : [sep, limit]` conditional. Equivalent: `String.prototype.split` maps an undefined limit to 2^32-1, and the `Symbol.split` path forwards undefined either way. - `workspace-space-compaction.test.ts` forwards `reduce`'s two arguments unchanged, so the `arguments.length >= 2` initial-value branch is unaffected. - `agent-session-history-byte-accounting.test.ts` is the one site where the receiver is not literally preserved (`JSON` -> undefined). `JSON.stringify` never reads `this` per spec, and restoring `.call(JSON, ...)` would reintroduce the overload failure under strictBindCallApply. No suppression comments added — the rule has zero `oxlint-disable` sites. `Reflect.apply` still appears at electron.vite.config.ts:159, inside a template literal of generated bootstrap source. That is string content, not lintable code. --- config/oxlint-anti-slop.json | 2 +- config/scripts/main-blocking-probe.mjs | 2 +- config/scripts/persistence-call-probe.mjs | 2 +- .../scripts/terminal-stream-byte-length-benchmark.mjs | 2 +- src/main/gitlab/client-mr-auth-rate-limit.test.ts | 2 +- .../agent-session-history-byte-accounting.test.ts | 10 +++++++--- .../orca-runtime-browser-client-hosted.test.ts | 4 +--- src/main/runtime/runtime-linear-command-surface.ts | 2 +- .../runtime/runtime-search-line-fragments.test.ts | 6 ++++-- src/relay/fs-search-line-fragments.test.ts | 6 ++++-- .../agent-map-worktree-lineage-layout.test.ts | 6 +++--- .../agent-map-worktree-packing.test.ts | 4 ++-- .../src/components/editor/diff-section-layout.test.ts | 11 ++++++----- .../src/components/editor/tiptap-marked-facade.ts | 3 ++- .../task-page-mutation-page-allocation.test.ts | 7 ++++++- src/shared/git-history-message-allocation.test.ts | 8 +++++--- src/shared/workspace-space-compaction.test.ts | 2 +- 17 files changed, 47 insertions(+), 32 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index d2ac1d35d75..f0c779bd7e2 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -32,7 +32,7 @@ "anti-slop/no-module-mocking": "off", "anti-slop/no-object-parameters": "off", "anti-slop/no-reduce-accumulator-copy": "error", - "anti-slop/no-reflect-apply": "off", + "anti-slop/no-reflect-apply": "error", "anti-slop/no-reflect-get": "off", "anti-slop/no-runtime-typeof": "off", "anti-slop/no-shape-in-symbol-names": "off", diff --git a/config/scripts/main-blocking-probe.mjs b/config/scripts/main-blocking-probe.mjs index 93a5372a765..e8b038abcaa 100644 --- a/config/scripts/main-blocking-probe.mjs +++ b/config/scripts/main-blocking-probe.mjs @@ -12,7 +12,7 @@ export function installMainBlockingProbe() { const epoch = Date.now() let result try { - result = Reflect.apply(original, this, args) + result = original.call(this, ...args) return result } finally { const durationMs = performance.now() - start diff --git a/config/scripts/persistence-call-probe.mjs b/config/scripts/persistence-call-probe.mjs index d62df17cc0b..dc15462d0cf 100644 --- a/config/scripts/persistence-call-probe.mjs +++ b/config/scripts/persistence-call-probe.mjs @@ -21,7 +21,7 @@ export function installPersistenceCallProbe() { const epoch = Date.now() let result try { - result = Reflect.apply(original, this, args) + result = original.call(this, ...args) return result } finally { const durationMs = performance.now() - start diff --git a/config/scripts/terminal-stream-byte-length-benchmark.mjs b/config/scripts/terminal-stream-byte-length-benchmark.mjs index 4ea2548b393..7ad53e74dce 100644 --- a/config/scripts/terminal-stream-byte-length-benchmark.mjs +++ b/config/scripts/terminal-stream-byte-length-benchmark.mjs @@ -88,7 +88,7 @@ function runWithNativeCallCount(fn) { let calls = 0 Buffer.byteLength = (...args) => { calls += 1 - return Reflect.apply(nativeByteLength, Buffer, args) + return nativeByteLength.call(Buffer, ...args) } try { return { output: fn(), calls } diff --git a/src/main/gitlab/client-mr-auth-rate-limit.test.ts b/src/main/gitlab/client-mr-auth-rate-limit.test.ts index abf928a7c9f..6d76bea52a5 100644 --- a/src/main/gitlab/client-mr-auth-rate-limit.test.ts +++ b/src/main/gitlab/client-mr-auth-rate-limit.test.ts @@ -95,7 +95,7 @@ describe('gitlab client — MR operations', () => { if (this[0] === 'gitlab.com' && this.every((value) => typeof value === 'string')) { knownHostCacheScans += 1 } - return Reflect.apply(originalMap, this, [callback, thisArg]) + return originalMap.call(this, callback, thisArg) }) try { diff --git a/src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts b/src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts index 757e7e44084..17bdf710490 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts @@ -69,13 +69,17 @@ it.each([1, 100, 200])('serializes each of %i unchanged forward page items once' await appendItems(count, 'x'.repeat(8_000)) const snapshot = journal.snapshot() const stringify = JSON.stringify + // Method-shaped type: the JSON.stringify overloads split on replacer shape and reject a forwarded one. + const forwardStringify: { + stringify(value: unknown, replacer?: unknown, space?: unknown): string + }['stringify'] = stringify let itemSerializations = 0 - JSON.stringify = ((value: unknown, ...args: unknown[]) => { + JSON.stringify = (value: unknown, replacer?: unknown, space?: unknown): string => { if (value && typeof value === 'object' && 'itemId' in value && 'body' in value) { itemSerializations++ } - return Reflect.apply(stringify, JSON, [value, ...args]) - }) as typeof JSON.stringify + return forwardStringify(value, replacer, space) + } try { const result = readAgentSessionHistory( journal, diff --git a/src/main/runtime/orca-runtime-browser-client-hosted.test.ts b/src/main/runtime/orca-runtime-browser-client-hosted.test.ts index e0a901a0f96..f9f69834463 100644 --- a/src/main/runtime/orca-runtime-browser-client-hosted.test.ts +++ b/src/main/runtime/orca-runtime-browser-client-hosted.test.ts @@ -312,9 +312,7 @@ describe('RuntimeBrowserCommands client-hosted routing', () => { .spyOn(registry, 'publishClientPage') .mockImplementation((input) => { order.push('publish') - return Reflect.apply(RuntimeBrowserPageRegistry.prototype.publishClientPage, registry, [ - input - ]) + return RuntimeBrowserPageRegistry.prototype.publishClientPage.call(registry, input) }) const notifyHeadlessBrowserSessionTabsChanged = vi.fn(() => order.push('notify')) const issueClientPageCommand = vi.fn(() => { diff --git a/src/main/runtime/runtime-linear-command-surface.ts b/src/main/runtime/runtime-linear-command-surface.ts index 63b053f45df..0f234768203 100644 --- a/src/main/runtime/runtime-linear-command-surface.ts +++ b/src/main/runtime/runtime-linear-command-surface.ts @@ -66,7 +66,7 @@ export function installRuntimeLinearCommandSurface(target: object): void { const method = { [name](this: LinearFacadeInstance, ...args: unknown[]): unknown { const commands = this.linearCommands as unknown as LinearMethodBag - return Reflect.apply(commands[name], overrideAwareReceiver(this, commands, names), args) + return commands[name].call(overrideAwareReceiver(this, commands, names), ...args) } }[name] delegators.add(method) diff --git a/src/main/runtime/runtime-search-line-fragments.test.ts b/src/main/runtime/runtime-search-line-fragments.test.ts index 17c873efdd8..701abd1327e 100644 --- a/src/main/runtime/runtime-search-line-fragments.test.ts +++ b/src/main/runtime/runtime-search-line-fragments.test.ts @@ -90,7 +90,9 @@ describe('RuntimeFileCommands', () => { submatches: [{ start: 0, end: 6 }] } }) - const originalSplit = String.prototype.split + // Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload. + const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] = + String.prototype.split let scanned = 0 const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function ( this: string, @@ -100,7 +102,7 @@ describe('RuntimeFileCommands', () => { if (separator === '\n') { scanned += this.length } - return Reflect.apply(originalSplit, this, [separator, limit]) + return originalSplit.call(this, separator, limit) }) try { for (let offset = 0; offset < line.length; offset += 1024) { diff --git a/src/relay/fs-search-line-fragments.test.ts b/src/relay/fs-search-line-fragments.test.ts index 53b8902ddfd..74d17c428fe 100644 --- a/src/relay/fs-search-line-fragments.test.ts +++ b/src/relay/fs-search-line-fragments.test.ts @@ -83,7 +83,9 @@ describe.each(searchCases)('relay $name line fragments', ({ search, encode }) => for (let offset = 0; offset < wire.length; offset += 4096) { chunks.push(wire.slice(offset, offset + 4096)) } - const originalSplit = String.prototype.split + // Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload. + const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] = + String.prototype.split let scannedCharacters = 0 const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function ( this: string, @@ -93,7 +95,7 @@ describe.each(searchCases)('relay $name line fragments', ({ search, encode }) => if (separator === '\n') { scannedCharacters += this.length } - return Reflect.apply(originalSplit, this, [separator, limit]) + return originalSplit.call(this, separator, limit) }) let fragmented try { diff --git a/src/renderer/src/components/dashboard-popout/agent-map-worktree-lineage-layout.test.ts b/src/renderer/src/components/dashboard-popout/agent-map-worktree-lineage-layout.test.ts index 8bfb9fc28d4..3025aecad77 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-worktree-lineage-layout.test.ts +++ b/src/renderer/src/components/dashboard-popout/agent-map-worktree-lineage-layout.test.ts @@ -43,8 +43,8 @@ function layoutWithNumericMapSetCount(worktrees: ReturnType) if (typeof key === 'number') { numericMapSets += 1 } - return Reflect.apply(set, this, [key, value]) - } as typeof Map.prototype.set + return set.call(this, key, value) + } try { return { layout: layoutAgentMapWorktreeLineage(worktrees), numericMapSets } } finally { @@ -64,7 +64,7 @@ function layoutWithWorktreePushCount(count: number) { typeof item.id === 'string' && item.id.startsWith('worktree-') ).length - return Reflect.apply(push, this, items) + return push.call(this, ...items) } try { return { diff --git a/src/renderer/src/components/dashboard-popout/agent-map-worktree-packing.test.ts b/src/renderer/src/components/dashboard-popout/agent-map-worktree-packing.test.ts index a07490dab92..d074c30b2ad 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-worktree-packing.test.ts +++ b/src/renderer/src/components/dashboard-popout/agent-map-worktree-packing.test.ts @@ -145,8 +145,8 @@ describe('packAgentMapWorktrees', () => { if (typeof key === 'number') { numericMapSets += 1 } - return Reflect.apply(set, this, [key, value]) - } as typeof Map.prototype.set + return set.call(this, key, value) + } try { const packed = packAgentMapWorktrees( Array.from({ length: 5 }, (_, index) => ({ diff --git a/src/renderer/src/components/editor/diff-section-layout.test.ts b/src/renderer/src/components/editor/diff-section-layout.test.ts index c1b100a2f33..16a8783ff49 100644 --- a/src/renderer/src/components/editor/diff-section-layout.test.ts +++ b/src/renderer/src/components/editor/diff-section-layout.test.ts @@ -110,8 +110,10 @@ describe('diff section layout', () => { }) it('estimates line-count height without allocating split arrays', () => { - const originalSplit = String.prototype.split - const patchedSplit = function patchedSplit( + // Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload. + const originalSplit: { split(separator: unknown, limit?: number): string[] }['split'] = + String.prototype.split + const patchedSplit: typeof String.prototype.split = function patchedSplit( this: string, separator?: unknown, limit?: number @@ -119,9 +121,8 @@ describe('diff section layout', () => { if (String(this).startsWith('line 0')) { throw new Error('layout should not split full diff content') } - const args = limit === undefined ? [separator] : [separator, limit] - return Reflect.apply(originalSplit, this, args) as string[] - } as typeof String.prototype.split + return originalSplit.call(this, separator, limit) + } String.prototype.split = patchedSplit try { diff --git a/src/renderer/src/components/editor/tiptap-marked-facade.ts b/src/renderer/src/components/editor/tiptap-marked-facade.ts index e34f6755566..823217be6ef 100644 --- a/src/renderer/src/components/editor/tiptap-marked-facade.ts +++ b/src/renderer/src/components/editor/tiptap-marked-facade.ts @@ -32,7 +32,8 @@ export function createTiptapMarkedFacade(): typeof marked { const lexer = (src: string, options?: MarkedOptions): TokensList => new RegistryLexer(options).lex(src) const facade = new Proxy(marked, { - apply: (_target, _thisArg, args) => Reflect.apply(registry.parse, registry, args), + apply: (_target, _thisArg, args: [src: string, options?: MarkedOptions | null]) => + registry.parse(...args), get: (target, property, receiver) => { switch (property) { case 'defaults': diff --git a/src/renderer/src/components/task-page-mutation-page-allocation.test.ts b/src/renderer/src/components/task-page-mutation-page-allocation.test.ts index 08f18926a42..8e1f272afaf 100644 --- a/src/renderer/src/components/task-page-mutation-page-allocation.test.ts +++ b/src/renderer/src/components/task-page-mutation-page-allocation.test.ts @@ -33,7 +33,12 @@ it('avoids allocating copies of unaffected pages during an item mutation', () => if (inputs.has(this)) { allocations++ } - return Reflect.apply(map, this, [callback, thisArg]) as U[] + // Explicit `call` type arguments: inference through `call` erases `map`'s own `U` to `unknown`. + return map.call U, unknown], U[]>( + this, + callback, + thisArg + ) } Array.prototype.slice = function (this: unknown[], ...args: Parameters) { if (inputs.has(this)) { diff --git a/src/shared/git-history-message-allocation.test.ts b/src/shared/git-history-message-allocation.test.ts index cfd8561b0c5..a3bad7d59b3 100644 --- a/src/shared/git-history-message-allocation.test.ts +++ b/src/shared/git-history-message-allocation.test.ts @@ -14,14 +14,16 @@ it('keeps a multiline commit body intact without materializing every message lin '', message ].join('\n') - const original = String.prototype.split + // Method-shaped type: a call-signature capture would reject `split`'s splitter-object overload. + const original: { split(separator: unknown, limit?: number): string[] }['split'] = + String.prototype.split let allocatedFields = 0 const spy = vi.spyOn(String.prototype, 'split').mockImplementation(function ( this: string, - separator: string | RegExp | { [Symbol.split](value: string, limit?: number): string[] }, + separator: unknown, limit?: number ) { - const result = Reflect.apply(original, this, [separator, limit]) as string[] + const result = original.call(this, separator, limit) if (separator === '\n' && String(this).includes('body line')) { allocatedFields += result.length } diff --git a/src/shared/workspace-space-compaction.test.ts b/src/shared/workspace-space-compaction.test.ts index bf774645fbb..7bae49bd73f 100644 --- a/src/shared/workspace-space-compaction.test.ts +++ b/src/shared/workspace-space-compaction.test.ts @@ -19,7 +19,7 @@ it('sums omitted sizes without constructing a replacement object per omitted ite if (initial && typeof initial === 'object' && 'name' in initial && initial.name === 'Other') { objectAccumulators += this.length } - return Reflect.apply(original, this, [callback, initial]) + return original.call(this, callback, initial) }) let result: ReturnType try { From 775a932651fa1cce85b901f3f44f82df0c4f1b6e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:22:15 -0700 Subject: [PATCH 21/34] fix(git): distinguish binary absence from missing cwd on spawn ENOENT (#20798) * fix(repos): preserve unknown Git availability * fix(git): distinguish binary absence from missing cwd on spawn ENOENT Node reports ENOENT for both a missing git binary and a missing working directory during spawn. The fix checks specifically for spawn syscall, then verifies the cwd exists to disambiguate. This prevents reporting "no Git" when the error is actually a missing working directory. Centralizes probe logic in a reusable function; other failures cause rejection so callers preserve the unknown status instead of collapsing to false. --- .../git/command-runner/command-exec-file.ts | 9 +- src/main/git/exec-error.ts | 13 +++ src/main/git/git-availability.ts | 31 +++++++ .../repo-creation-git-availability.test.ts | 82 +++++++++++++++++++ src/main/ipc/repos/repo-creation-handlers.ts | 19 ++--- .../runtime-server-environment-commands.ts | 8 +- .../runtime-server-git-availability.test.ts | 56 +++++++++++++ 7 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 src/main/git/git-availability.ts create mode 100644 src/main/ipc/repos/repo-creation-git-availability.test.ts create mode 100644 src/main/runtime/runtime-server-git-availability.test.ts diff --git a/src/main/git/command-runner/command-exec-file.ts b/src/main/git/command-runner/command-exec-file.ts index aa3e18a3a70..d67fa24f249 100644 --- a/src/main/git/command-runner/command-exec-file.ts +++ b/src/main/git/command-runner/command-exec-file.ts @@ -1,14 +1,9 @@ import { isWindowsBatchScript, resolveWindowsCommand } from '../../win32-utils' +import { isMissingCommandBinaryError } from '../exec-error' import { resolveCommand, type ResolvedCommand } from './wsl-command-resolution' import { execFileCapture } from './exec-file-capture' import { spawnCommandCapture, type CommandExecOptions } from './spawn-command-capture' -function isMissingCommandError(error: unknown): boolean { - return Boolean( - error && typeof error === 'object' && (error as { code?: unknown }).code === 'ENOENT' - ) -} - function hasPathSeparator(command: string): boolean { return command.includes('/') || command.includes('\\') } @@ -17,7 +12,7 @@ function shouldRetryWindowsCommandShim(error: unknown, resolved: ResolvedCommand return ( process.platform === 'win32' && resolved.wsl === null && - isMissingCommandError(error) && + isMissingCommandBinaryError(error) && !hasPathSeparator(resolved.binary) && !/\.[A-Za-z0-9]+$/.test(resolved.binary) ) diff --git a/src/main/git/exec-error.ts b/src/main/git/exec-error.ts index fb44e54917a..6e0385091d5 100644 --- a/src/main/git/exec-error.ts +++ b/src/main/git/exec-error.ts @@ -40,6 +40,19 @@ export function extractExecError(err: unknown): { stderr: string; stdout: string return { stderr: String(err), stdout: '' } } +/** Recognizes spawn ENOENT; callers must separately rule out a missing cwd. */ +export function isMissingCommandBinaryError(err: unknown): boolean { + return Boolean( + err && + typeof err === 'object' && + 'code' in err && + err.code === 'ENOENT' && + 'syscall' in err && + typeof err.syscall === 'string' && + err.syscall.startsWith('spawn ') + ) +} + /** * Detect a Retry-After hint in gh stderr and return the suggested delay in ms, * or null when the response includes no Retry-After. diff --git a/src/main/git/git-availability.ts b/src/main/git/git-availability.ts new file mode 100644 index 00000000000..0fda3933f7a --- /dev/null +++ b/src/main/git/git-availability.ts @@ -0,0 +1,31 @@ +import { access } from 'node:fs/promises' +import { isMissingCommandBinaryError } from './exec-error' + +type GitVersionExec = ( + args: string[], + options: { cwd: string; timeout: number } +) => Promise + +/** + * Resolves `false` only when the spawn proved Git absent; every other failure rejects so callers + * keep an unknown answer instead of reporting a host with no Git. + */ +export async function probeGitAvailability( + exec: GitVersionExec, + options: { cwd: string; timeout: number } +): Promise { + try { + await exec(['--version'], options) + return true + } catch (err) { + if (isMissingCommandBinaryError(err)) { + try { + await access(options.cwd) + return false + } catch { + // Node reports the same spawn ENOENT for a missing binary and a missing cwd. + } + } + throw err + } +} diff --git a/src/main/ipc/repos/repo-creation-git-availability.test.ts b/src/main/ipc/repos/repo-creation-git-availability.test.ts new file mode 100644 index 00000000000..8ffbbdc033e --- /dev/null +++ b/src/main/ipc/repos/repo-creation-git-availability.test.ts @@ -0,0 +1,82 @@ +/** + * `repos:isGitAvailable` gates the create dialog's Git option. Only a spawn that never started may + * answer `false`; everything else rejects so the renderer's existing `unknown` branch is reachable. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() })) + +vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } })) +vi.mock('../../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('../../repo-icon-autodetect', () => ({ + detectRepoIconAndUpstream: vi.fn(async () => ({})) +})) +vi.mock('../../worktree-root-preparation', () => ({ + prepareLocalWorktreeRootForRepo: vi.fn(async () => {}) +})) +vi.mock('../registered-worktree-roots-cache', () => ({ + invalidateAuthorizedRootsCache: vi.fn() +})) +vi.mock('./repo-added-telemetry', () => ({ emitRepoAdded: vi.fn() })) +vi.mock('./repos-changed-notification', () => ({ notifyReposChanged: vi.fn() })) +vi.mock('./local-repo-registration', () => ({ addLocalRepoFromPath: vi.fn() })) +vi.mock('./remote-repo-registration', () => ({ addRemoteRepoFromPath: vi.fn() })) +vi.mock('./remote-repo-creation', () => ({ createRemoteRepo: vi.fn() })) + +import { probeLocalGitAvailability } from './repo-creation-handlers' + +describe('repos:isGitAvailable', () => { + beforeEach(() => { + gitExecFileAsyncMock.mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('answers true when git reports its version', async () => { + gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git version 2.25.1\n', stderr: '' }) + await expect(probeLocalGitAvailability()).resolves.toBe(true) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['--version'], { + cwd: process.cwd(), + timeout: 1500 + }) + }) + + it('answers false only when the spawn itself found no binary', async () => { + gitExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT', syscall: 'spawn git' }) + ) + await expect(probeLocalGitAvailability()).resolves.toBe(false) + }) + + it('rejects an ENOENT when the working directory disappeared', async () => { + const missingCwd = `${process.cwd()}-missing` + vi.spyOn(process, 'cwd').mockReturnValue(missingCwd) + gitExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT', syscall: 'spawn git' }) + ) + + await expect(probeLocalGitAvailability()).rejects.toThrow('spawn git ENOENT') + }) + + it('rejects a non-spawn ENOENT rather than reporting no Git', async () => { + gitExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('open config ENOENT'), { code: 'ENOENT', syscall: 'open' }) + ) + + await expect(probeLocalGitAvailability()).rejects.toThrow('open config ENOENT') + }) + + it('rejects on the timeout rather than reporting no git', async () => { + gitExecFileAsyncMock.mockRejectedValue(new Error('git --version timed out after 1500ms')) + await expect(probeLocalGitAvailability()).rejects.toThrow('timed out') + }) + + it('rejects when git runs and fails', async () => { + gitExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('fatal: detected dubious ownership'), { code: 128 }) + ) + await expect(probeLocalGitAvailability()).rejects.toThrow('dubious ownership') + }) +}) diff --git a/src/main/ipc/repos/repo-creation-handlers.ts b/src/main/ipc/repos/repo-creation-handlers.ts index 90894894253..57bfcf66c79 100644 --- a/src/main/ipc/repos/repo-creation-handlers.ts +++ b/src/main/ipc/repos/repo-creation-handlers.ts @@ -10,6 +10,7 @@ import { DEFAULT_REPO_BADGE_COLOR, getDefaultWorkspaceDir } from '../../../share import { normalizeRuntimePathForComparison } from '../../../shared/cross-platform-path' import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' import { getEffectiveHostSetting } from '../../../shared/host-setting-overrides' +import { probeGitAvailability } from '../../git/git-availability' import { gitExecFileAsync } from '../../git/runner' import { detectRepoIconAndUpstream } from '../../repo-icon-autodetect' import { prepareLocalWorktreeRootForRepo } from '../../worktree-root-preparation' @@ -22,16 +23,12 @@ import { createRemoteRepo } from './remote-repo-creation' const GIT_AVAILABILITY_TIMEOUT_MS = 1500 -async function isGitAvailable(): Promise { - try { - await gitExecFileAsync(['--version'], { - cwd: process.cwd(), - timeout: GIT_AVAILABILITY_TIMEOUT_MS - }) - return true - } catch { - return false - } +// Only ENOENT proves Git absent; rejecting other failures preserves the renderer's unknown state. +export async function probeLocalGitAvailability(): Promise { + return probeGitAvailability(gitExecFileAsync, { + cwd: process.cwd(), + timeout: GIT_AVAILABILITY_TIMEOUT_MS + }) } /** @@ -63,7 +60,7 @@ function getDefaultCreateProjectParent(store: Store): string { } export function registerRepoCreationHandlers(mainWindow: BrowserWindow, store: Store): void { - ipcMain.handle('repos:isGitAvailable', () => isGitAvailable()) + ipcMain.handle('repos:isGitAvailable', () => probeLocalGitAvailability()) ipcMain.handle('repos:getDefaultCreateProjectParent', () => getDefaultCreateProjectParent(store)) ipcMain.handle( diff --git a/src/main/runtime/runtime-server-environment-commands.ts b/src/main/runtime/runtime-server-environment-commands.ts index 54f08d29f0c..7671481b1e5 100644 --- a/src/main/runtime/runtime-server-environment-commands.ts +++ b/src/main/runtime/runtime-server-environment-commands.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os' import { isAbsolute, resolve } from 'node:path' import type { DirEntry, FilesystemPathFlavor } from '../../shared/filesystem-entry-types' import { sortDirEntries } from '../../shared/file-name-sort' +import { probeGitAvailability } from '../git/git-availability' import { gitExecFileAsync } from '../git/runner' import { isServerDriveListRequest, listWindowsDrives } from './windows-drive-listing' @@ -54,11 +55,6 @@ export class RuntimeServerEnvironmentCommands { } async isGitAvailable(): Promise { - try { - await gitExecFileAsync(['--version'], { cwd: process.cwd(), timeout: 3000 }) - return true - } catch { - return false - } + return probeGitAvailability(gitExecFileAsync, { cwd: process.cwd(), timeout: 3000 }) } } diff --git a/src/main/runtime/runtime-server-git-availability.test.ts b/src/main/runtime/runtime-server-git-availability.test.ts new file mode 100644 index 00000000000..2259e1a563a --- /dev/null +++ b/src/main/runtime/runtime-server-git-availability.test.ts @@ -0,0 +1,56 @@ +/** + * `repo.gitAvailable` gates the create dialog's Git option on a runtime/remote host. Only a spawn + * that never started may answer `false`; everything else rejects so the renderer's existing + * `unknown` branch stays reachable instead of collapsing to a false "no Git here". + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() })) + +vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) + +import { RuntimeServerEnvironmentCommands } from './runtime-server-environment-commands' + +function spawnEnoent(): Error { + return Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT', syscall: 'spawn git' }) +} + +describe('RuntimeServerEnvironmentCommands.isGitAvailable', () => { + const commands = new RuntimeServerEnvironmentCommands() + + beforeEach(() => { + gitExecFileAsyncMock.mockReset() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('answers true when git reports its version', async () => { + gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git version 2.25.1\n', stderr: '' }) + await expect(commands.isGitAvailable()).resolves.toBe(true) + }) + + it('answers false only when the spawn itself found no binary', async () => { + gitExecFileAsyncMock.mockRejectedValue(spawnEnoent()) + await expect(commands.isGitAvailable()).resolves.toBe(false) + }) + + it('rejects an ENOENT when the working directory disappeared', async () => { + vi.spyOn(process, 'cwd').mockReturnValue(`${process.cwd()}-missing`) + gitExecFileAsyncMock.mockRejectedValue(spawnEnoent()) + await expect(commands.isGitAvailable()).rejects.toThrow('spawn git ENOENT') + }) + + it('rejects a slow host rather than reporting no Git', async () => { + gitExecFileAsyncMock.mockRejectedValue(new Error('git --version timed out after 3000ms')) + await expect(commands.isGitAvailable()).rejects.toThrow('timed out') + }) + + it('rejects a repository-level git failure rather than reporting no Git', async () => { + gitExecFileAsyncMock.mockRejectedValue( + Object.assign(new Error('detected dubious ownership'), { code: 128 }) + ) + await expect(commands.isGitAvailable()).rejects.toThrow('dubious ownership') + }) +}) From 22ce8d69a1abdbe7c7a8b90c662eac89ed0d1e20 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:41:17 -0700 Subject: [PATCH 22/34] fix(lint): enable anti-slop/no-module-mocking (#20783) The rule rejects `vi.mock` / `vi.doMock` / `vi.unstable_mockModule` and the `jest` equivalents, on the argument that a test which rewrites the module graph asserts against a stand-in the production code never sees. It is already off for `**/*.test.{ts,tsx}`, `**/*.spec.{ts,tsx}`, `tests/**` and `**/__mocks__/**` via the existing override in config/oxlint-anti-slop.json; that override is unchanged here. What the rule actually catches is module mocking that has drifted out of a spec and into a first-party `.ts` support module, where nothing marks it as test-only. 73 violations at baseline, all of them in test-support code. 9 were relocated back into spec files the override already exempts; the remaining 64 sit in 10 files that are test-only but do not match the override globs, and carry a file-level disable naming the rule and the reason. Relocated: - terminal-hydration-store-test-bootstrap.ts: the sonner / sync-runtime-graph / pty-transport `vi.mock` calls moved into the two specs that import it (terminals-hydration-canonical-rows, terminals-hydration-canonical-pty-overlap). Vitest hoists `vi.mock` inside a test file, so registration is strictly earlier than the previous module-eval-time call; the bootstrap keeps only the preload API proxy. Both importers were updated. - ipc-events-ssh-authority-test-fixtures.ts: the 6 direct-ssh `vi.doMock` calls moved into useIpcEvents-agent-status-ssh-authority.test.ts as a local `stubDirectSshModules()` helper, which also de-duplicates the three copies the spec already had inline. The fixture now returns the store state and coordinator doubles it builds, typed via the exported DirectSshReconnectCoordinatorDouble. Suppressed, with justification (each is `/* oxlint-disable anti-slop/no-module-mocking -- ... */`, rule named, no blanket disable): - config/scripts/headless-serve-shutdown-matrix.test.mjs (1) - a genuine Vitest spec that the override misses only because its globs say {ts,tsx}. The script under test is a top-level CLI module; the alternative is spawning real docker. - src/main/codex-accounts/runtime-home-service-test-harness.ts (1) - stubs one probe predicate in ../pty/shell-startup-env, imported directly by several main-process readers; 17 specs share it. - src/main/computer/desktop-script-provider-test-harness.ts (2) - stubs child_process/fs-promises for a provider that shells out; 8 specs share it. - src/main/github/work-item-search-test-harness.ts (4) - one consumer lives in tests/e2e, where the relative mock ids resolve differently, so moving the calls into the specs would silently stop mocking there. - src/renderer/src/components/automations/automations-page-test-harness.tsx (14) - the mount rig for 10 AutomationsPage specs. - src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts (1) - stubs refreshWebRuntimeSessionTabsSnapshot, imported directly by several renderer runtime modules; 18 specs share it. - src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts (7) - stubReactSyncEffect/stubAuxiliaryModules, shared by 11 specs. - src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts (11) - stubs and hook invocation are one unit; 4 specs share it. - src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts (13) - its only spec is at 799 of an 800 max-lines budget. - src/renderer/src/hooks/ipc-events-test-harness.ts (10) - shared by 8 specs. No violation was converted to real dependency injection, and no max-lines disable was added. Verified: the audit command exits 0 with no output (and reports errors on a planted probe, so the rule is live); node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0; 354 spec files / 2506 tests covering every importer of every touched file pass. No mobile/ file was touched. The changed-code quality gate's root Oxlint scan runs without --config so it never loads the anti-slop JS plugin, which made all 10 of those file-level suppressions read as "Unused oxlint-disable directive". check-changed-code-quality.mjs now exempts directives naming an anti-slop rule from that unused-directive warning, the same carve-out isCastingDirectiveUnusedWarning already makes for the casting suppressions the casting config enforces. Such a directive can never suppress a root-config rule, so nothing the root scan would otherwise report is hidden; audit:anti-slop remains the scan that enforces the rule. --- config/oxlint-anti-slop.json | 2 +- config/scripts/check-changed-code-quality.mjs | 17 +++ .../check-changed-code-quality.test.mjs | 44 +++++++ .../headless-serve-shutdown-matrix.test.mjs | 4 + .../runtime-home-service-test-harness.ts | 5 + .../desktop-script-provider-test-harness.ts | 3 + .../github/work-item-search-test-harness.ts | 3 + .../automations-page-test-harness.tsx | 3 + ...mote-runtime-pty-transport-test-harness.ts | 3 + ...vents-agent-status-window-test-fixtures.ts | 3 + .../ipc-events-close-routing-test-harness.ts | 3 + .../ipc-events-ssh-authority-test-fixtures.ts | 53 +++------ ...ipc-events-terminal-create-test-harness.ts | 3 + .../src/hooks/ipc-events-test-harness.ts | 3 + ...cEvents-agent-status-ssh-authority.test.ts | 112 ++++++++---------- ...terminal-hydration-store-test-bootstrap.ts | 16 +-- ...ls-hydration-canonical-pty-overlap.test.ts | 14 ++- ...terminals-hydration-canonical-rows.test.ts | 11 +- 18 files changed, 186 insertions(+), 116 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index f0c779bd7e2..9e5eda11ae0 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -29,7 +29,7 @@ "anti-slop/no-chained-type-assertions": "off", "anti-slop/no-conditional-empty-object-spread": "off", "anti-slop/no-known-value-widening": "off", - "anti-slop/no-module-mocking": "off", + "anti-slop/no-module-mocking": "error", "anti-slop/no-object-parameters": "off", "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs index 31b6d24953a..1b8a0c4f5e9 100644 --- a/config/scripts/check-changed-code-quality.mjs +++ b/config/scripts/check-changed-code-quality.mjs @@ -11,6 +11,8 @@ const ROOT_CODE_QUALITY_IGNORED_PREFIXES = ['cloud/'] const CASTING_RULE = 'typescript/consistent-type-assertions' const CASTING_DISABLE_PATTERN = /\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*typescript\/consistent-type-assertions/ +const ANTI_SLOP_DISABLE_PATTERN = + /\/[/*]\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?\s[^\n]*\banti-slop\// export const OXLINT_SCANS = [ { // Why: no --config, so Oxlint keeps discovering nested configs. Pinning the root @@ -330,6 +332,20 @@ export function isCastingDirectiveUnusedWarning(diagnostic, root) { ) } +// Why: the anti-slop rules live in a JS plugin that only config/oxlint-anti-slop.json loads, so +// the root scan never sees those rule names and reports every anti-slop suppression as unused. +// `audit:anti-slop` is the scan that enforces them. +export function isAntiSlopDirectiveUnusedWarning(diagnostic, root) { + if (!/^Unused (?:oxlint|eslint)-disable/.test(diagnostic.message ?? '')) { + return false + } + return (diagnostic.labels ?? []).some((label) => + diagnosticHighlightedLines(root, diagnostic.filename, label.span).some((line) => + ANTI_SLOP_DISABLE_PATTERN.test(line) + ) + ) +} + // Why: oxlint cannot see the AGENTS.md requirement that every casting suppression carry a // line-specific SAFETY: rationale, so the directive text itself is checked over added lines. export function findCastingDirectivesMissingSafety(root, rangesByFile) { @@ -402,6 +418,7 @@ export function main( (diagnostic) => !isSuppressedDiagnostic(diagnostic, root) && !isCastingDirectiveUnusedWarning(diagnostic, root) && + !isAntiSlopDirectiveUnusedWarning(diagnostic, root) && diagnosticTouchesAddedLines(diagnostic, rangesByFile, root, baseBlocks) ) for (const diagnostic of diagnostics) { diff --git a/config/scripts/check-changed-code-quality.test.mjs b/config/scripts/check-changed-code-quality.test.mjs index 3a88cf1b02e..a0bfcd0ecd9 100644 --- a/config/scripts/check-changed-code-quality.test.mjs +++ b/config/scripts/check-changed-code-quality.test.mjs @@ -1,7 +1,10 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import path from 'node:path' import { describe, expect, it } from 'vitest' import { OXLINT_SCANS, diagnosticTouchesAddedLines, + isAntiSlopDirectiveUnusedWarning, isMovedCode, isRootCodeQualityPath, overlapsAddedLines, @@ -110,3 +113,44 @@ describe('moved-code exemption', () => { expect(isMovedCode(['', ' '], [['a()']])).toBe(false) }) }) + +describe('anti-slop directive unused warning', () => { + const root = path.resolve(import.meta.dirname, '..', '..') + // Assembled so no line here is itself a directive the gate would scan. + const directive = (rule) => `/* oxlint-disable ${rule} -- reason */` + + const withFixture = (firstLine, assert) => { + const directory = mkdtempSync(path.join(root, 'config', 'anti-slop-directive-test-')) + try { + const file = path.join(directory, 'fixture.ts') + writeFileSync(file, [firstLine, 'export const value = 1', ''].join('\n')) + assert({ + message: 'Unused oxlint-disable directive (no problems were reported).', + filename: file, + labels: [{ span: { line: 1 } }] + }) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + + it('exempts a suppression the root scan cannot resolve', () => { + withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => { + expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(true) + }) + }) + + it('still reports an unused directive for a rule the root scan does load', () => { + withFixture(directive('unicorn/no-array-reduce'), (diagnostic) => { + expect(isAntiSlopDirectiveUnusedWarning(diagnostic, root)).toBe(false) + }) + }) + + it('ignores diagnostics that are not unused-directive warnings', () => { + withFixture(directive('anti-slop/no-module-mocking'), (diagnostic) => { + expect( + isAntiSlopDirectiveUnusedWarning({ ...diagnostic, message: 'Unexpected any.' }, root) + ).toBe(false) + }) + }) +}) diff --git a/config/scripts/headless-serve-shutdown-matrix.test.mjs b/config/scripts/headless-serve-shutdown-matrix.test.mjs index 7231cc21e4f..32878b219f5 100644 --- a/config/scripts/headless-serve-shutdown-matrix.test.mjs +++ b/config/scripts/headless-serve-shutdown-matrix.test.mjs @@ -1,3 +1,7 @@ +/* oxlint-disable anti-slop/no-module-mocking -- This IS the Vitest spec for run-headless-serve-shutdown-docker.mjs, but the rule's test-file + override globs only .ts/.tsx, so a .test.mjs spec slips through. The script under test is a + top-level CLI module driven via vi.resetModules() + await import(); the only other way to observe + its docker argv is to spawn real docker. */ import { createHash } from 'node:crypto' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' diff --git a/src/main/codex-accounts/runtime-home-service-test-harness.ts b/src/main/codex-accounts/runtime-home-service-test-harness.ts index 3922823ebd1..86d94de5807 100644 --- a/src/main/codex-accounts/runtime-home-service-test-harness.ts +++ b/src/main/codex-accounts/runtime-home-service-test-harness.ts @@ -1,3 +1,8 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 17 runtime-home specs, not shipped code, and it falls outside the *.test / *.spec / tests glob set. + setupRuntimeHomeTest() overrides one probe predicate in ../pty/shell-startup-env; the production + readers import it directly across several main-process modules, so an injected seam would have to + be threaded through all of them. Inlining the stub into each of the 17 specs would duplicate it 17 + times and push the largest past the max-lines ratchet. */ import { expect, vi } from 'vitest' import { existsSync, diff --git a/src/main/computer/desktop-script-provider-test-harness.ts b/src/main/computer/desktop-script-provider-test-harness.ts index bcb0a4b0118..43d213c855f 100644 --- a/src/main/computer/desktop-script-provider-test-harness.ts +++ b/src/main/computer/desktop-script-provider-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 8 desktop-script-provider specs, not shipped code, and it falls + outside the *.test / *.spec / tests glob set. The stubs replace node builtins (child_process, fs/promises) for a provider + that shells out; inlining them would duplicate the vi.hoisted fixture into all 8 specs. */ import { expect, vi } from 'vitest' import type { DesktopScriptRuntimeHost } from './desktop-script-runtime-host' diff --git a/src/main/github/work-item-search-test-harness.ts b/src/main/github/work-item-search-test-harness.ts index d3783f0afe1..d36732e9eb1 100644 --- a/src/main/github/work-item-search-test-harness.ts +++ b/src/main/github/work-item-search-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 6 work-item-search specs, not shipped code, and it falls outside + the *.test / *.spec / tests glob set. One consumer lives in tests/e2e, where the relative mock ids ('../git/...') resolve to + different modules, so moving these calls into the specs would silently stop mocking there. */ import { afterEach, beforeEach, vi } from 'vitest' import type { Mock } from 'vitest' import { randomUUID } from 'node:crypto' diff --git a/src/renderer/src/components/automations/automations-page-test-harness.tsx b/src/renderer/src/components/automations/automations-page-test-harness.tsx index e34ae0640bc..fc94b173286 100644 --- a/src/renderer/src/components/automations/automations-page-test-harness.tsx +++ b/src/renderer/src/components/automations/automations-page-test-harness.tsx @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 10 AutomationsPage specs, not shipped code, and it falls outside + the *.test / *.spec / tests glob set. Inlining these 13 stubs would duplicate them into all 10 specs and push the largest + past the max-lines ratchet. */ /** * The mount rig for AutomationsPage tests: child stand-ins, the preload API * double, and the per-test store reset. diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts index f5db3036f30..0cc62569b5f 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 18 remote-runtime PTY transport specs, not shipped code, and it + falls outside the *.test / *.spec / tests glob set. refreshWebRuntimeSessionTabsSnapshot is imported directly by several + renderer runtime modules, so an injected seam would have to be threaded through all of them. */ import { vi } from 'vitest' import type { Mock } from 'vitest' import { diff --git a/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts b/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts index e1b5f548996..1f58e87cb99 100644 --- a/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts +++ b/src/renderer/src/hooks/ipc-events-agent-status-window-test-fixtures.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 11 agent-status ipc-events specs, not shipped code, and it falls + outside the *.test / *.spec / tests glob set. Inlining stubReactSyncEffect and stubAuxiliaryModules would duplicate them + into all 11 specs and push several past the max-lines ratchet. */ import type * as ReactModule from 'react' import { vi } from 'vitest' import type { diff --git a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts index 72e9683382c..3883f7ac8af 100644 --- a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 4 close-routing ipc-events specs, not shipped code, and it falls + outside the *.test / *.spec / tests glob set. The stubs and the hook invocation are one unit; splitting the 11 doMock + calls back out would duplicate them into all 4 specs. */ import type * as ReactModule from 'react' import { vi } from 'vitest' diff --git a/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts b/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts index 4c91e898a8b..9490b19a4e8 100644 --- a/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts +++ b/src/renderer/src/hooks/ipc-events-ssh-authority-test-fixtures.ts @@ -1,19 +1,29 @@ import { vi } from 'vitest' import { buildStoreState } from './ipc-events-agent-status-store-test-fixtures' -import { - buildWindowApi, - stubReactSyncEffect, - stubAuxiliaryModules -} from './ipc-events-agent-status-window-test-fixtures' +import type { StoreLike } from './ipc-events-agent-status-store-test-fixtures' +import { buildWindowApi } from './ipc-events-agent-status-window-test-fixtures' +export type DirectSshReconnectCoordinatorDouble = { + requestReconnect: ReturnType + replaceAuthority: ReturnType + prepareOnly: ReturnType + correctUnboundTerminals: ReturnType + finalizeHydratedTerminals: ReturnType + invalidate: ReturnType + stop: ReturnType +} + +/** Store/coordinator doubles for the partial-authority reconciliation path; the spec wires them. */ export function buildSshAuthorityReconciliationHarness(args: { partialAuthority: { providerEpoch?: string; connectionGeneration?: number } latestAuthority: { providerEpoch: string; connectionGeneration: number } }): { + coordinator: DirectSshReconnectCoordinatorDouble emitPartialState: () => void getState: ReturnType requestReconnect: ReturnType setSshConnectionState: ReturnType + storeState: StoreLike storedState: () => Record | undefined } { const targetId = 'target-reconciliation' @@ -55,37 +65,6 @@ export function buildSshAuthorityReconciliationHarness(args: { stop: vi.fn() } - stubReactSyncEffect() - stubAuxiliaryModules() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - vi.doMock('./direct-ssh-reconnect-rollout', () => ({ - isDirectSshReconnectCoordinatorRoutingEnabled: () => true - })) - vi.doMock('./direct-ssh-worktree-refresh-scheduler', () => ({ - createDirectSshWorktreeRefreshScheduler: () => ({ - stop: vi.fn(), - disposeProvider: vi.fn() - }) - })) - vi.doMock('./direct-ssh-host-hydration', () => ({ - createDirectSshHostHydration: () => ({ - capturePreparationInput: vi.fn(), - readHostScopedLineage: vi.fn(), - isPreparationTokenCurrent: vi.fn(() => true), - stop: vi.fn() - }) - })) - vi.doMock('./direct-ssh-reconnect-coordinator', () => ({ - createDirectSshReconnectCoordinator: () => coordinator - })) - vi.doMock('@/lib/direct-ssh-reconnect-product-telemetry', () => ({ - createDirectSshReconnectProductTelemetryAdapter: vi.fn() - })) vi.stubGlobal( 'window', buildWindowApi({ @@ -101,6 +80,7 @@ export function buildSshAuthorityReconciliationHarness(args: { ) return { + coordinator, emitPartialState: () => { if (!sshStateListener) { throw new Error('Expected SSH state listener') @@ -110,6 +90,7 @@ export function buildSshAuthorityReconciliationHarness(args: { getState, requestReconnect, setSshConnectionState, + storeState, storedState: () => sshConnectionStates.get(targetId) } } diff --git a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts index b5c195e4a2d..0cad65374c0 100644 --- a/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for useIpcEvents-terminal-create-surfacing.test.ts, not shipped code, and it + falls outside the *.test / *.spec / tests glob set. That spec already sits at 799 of its 800 max-lines budget, so these 13 + stubs cannot move back into it without a max-lines disable. */ import type * as ReactModule from 'react' import { vi } from 'vitest' import { buildTerminalCreateWindow } from './ipc-events-terminal-create-window-test-fixtures' diff --git a/src/renderer/src/hooks/ipc-events-test-harness.ts b/src/renderer/src/hooks/ipc-events-test-harness.ts index b3d1ef71b98..69ab58bf9d8 100644 --- a/src/renderer/src/hooks/ipc-events-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-test-harness.ts @@ -1,3 +1,6 @@ +/* oxlint-disable anti-slop/no-module-mocking -- Vitest support module for the 8 useIpcEvents specs, not shipped code, and it falls outside + the *.test / *.spec / tests glob set. Inlining these 10 stubs would duplicate them into all 8 specs and push the largest + past the max-lines ratchet. */ import { vi } from 'vitest' import type * as ReactModule from 'react' import type { HarnessStoreState } from './ipc-events-harness-store-state' diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts index bc6ec581586..fc019448ac7 100644 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts @@ -1,11 +1,52 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { buildStoreState } from './ipc-events-agent-status-store-test-fixtures' +import type { StoreLike } from './ipc-events-agent-status-store-test-fixtures' import { buildWindowApi, stubReactSyncEffect, stubAuxiliaryModules } from './ipc-events-agent-status-window-test-fixtures' import { buildSshAuthorityReconciliationHarness } from './ipc-events-ssh-authority-test-fixtures' +import type { DirectSshReconnectCoordinatorDouble } from './ipc-events-ssh-authority-test-fixtures' + +function stubDirectSshModules(args: { + storeState: StoreLike + coordinator: DirectSshReconnectCoordinatorDouble + coordinatorRoutingEnabled?: boolean + capturePreparationInput?: ReturnType +}): void { + stubReactSyncEffect() + stubAuxiliaryModules() + vi.doMock('../store', () => ({ + useAppStore: { + subscribe: vi.fn(() => () => {}), + getState: () => args.storeState + } + })) + vi.doMock('./direct-ssh-reconnect-rollout', () => ({ + isDirectSshReconnectCoordinatorRoutingEnabled: () => args.coordinatorRoutingEnabled ?? true + })) + vi.doMock('./direct-ssh-worktree-refresh-scheduler', () => ({ + createDirectSshWorktreeRefreshScheduler: () => ({ + stop: vi.fn(), + disposeProvider: vi.fn() + }) + })) + vi.doMock('./direct-ssh-host-hydration', () => ({ + createDirectSshHostHydration: () => ({ + capturePreparationInput: args.capturePreparationInput ?? vi.fn(), + readHostScopedLineage: vi.fn(), + isPreparationTokenCurrent: vi.fn(() => true), + stop: vi.fn() + }) + })) + vi.doMock('./direct-ssh-reconnect-coordinator', () => ({ + createDirectSshReconnectCoordinator: () => args.coordinator + })) + vi.doMock('@/lib/direct-ssh-reconnect-product-telemetry', () => ({ + createDirectSshReconnectProductTelemetryAdapter: vi.fn() + })) +} // Why: end-to-end exercise of startup agent-status restoration through // useIpcEvents itself. The main process owns the durable cache; the renderer @@ -71,6 +112,7 @@ describe('useIpcEvents agent status snapshot integration', () => { connectionGeneration: 7 } }) + stubDirectSshModules({ storeState: harness.storeState, coordinator: harness.coordinator }) const { useIpcEvents } = await import('./useIpcEvents') useIpcEvents() @@ -105,6 +147,7 @@ describe('useIpcEvents agent status snapshot integration', () => { connectionGeneration: 7 } }) + stubDirectSshModules({ storeState: harness.storeState, coordinator: harness.coordinator }) const { useIpcEvents } = await import('./useIpcEvents') useIpcEvents() @@ -198,37 +241,12 @@ describe('useIpcEvents agent status snapshot integration', () => { } }) - stubReactSyncEffect() - stubAuxiliaryModules() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - vi.doMock('./direct-ssh-reconnect-rollout', () => ({ - isDirectSshReconnectCoordinatorRoutingEnabled: () => enabled - })) - vi.doMock('./direct-ssh-worktree-refresh-scheduler', () => ({ - createDirectSshWorktreeRefreshScheduler: () => ({ - stop: vi.fn(), - disposeProvider: vi.fn() - }) - })) - vi.doMock('./direct-ssh-host-hydration', () => ({ - createDirectSshHostHydration: () => ({ - capturePreparationInput, - readHostScopedLineage: vi.fn(), - isPreparationTokenCurrent: vi.fn(() => true), - stop: vi.fn() - }) - })) - vi.doMock('./direct-ssh-reconnect-coordinator', () => ({ - createDirectSshReconnectCoordinator: () => coordinator - })) - vi.doMock('@/lib/direct-ssh-reconnect-product-telemetry', () => ({ - createDirectSshReconnectProductTelemetryAdapter: vi.fn() - })) + stubDirectSshModules({ + storeState, + coordinator, + coordinatorRoutingEnabled: enabled, + capturePreparationInput + }) vi.stubGlobal( 'window', buildWindowApi({ @@ -429,37 +447,7 @@ describe('useIpcEvents agent status snapshot integration', () => { } let partialTargetStateCalls = 0 - stubReactSyncEffect() - stubAuxiliaryModules() - vi.doMock('../store', () => ({ - useAppStore: { - subscribe: vi.fn(() => () => {}), - getState: () => storeState - } - })) - vi.doMock('./direct-ssh-reconnect-rollout', () => ({ - isDirectSshReconnectCoordinatorRoutingEnabled: () => true - })) - vi.doMock('./direct-ssh-worktree-refresh-scheduler', () => ({ - createDirectSshWorktreeRefreshScheduler: () => ({ - stop: vi.fn(), - disposeProvider: vi.fn() - }) - })) - vi.doMock('./direct-ssh-host-hydration', () => ({ - createDirectSshHostHydration: () => ({ - capturePreparationInput: vi.fn(), - readHostScopedLineage: vi.fn(), - isPreparationTokenCurrent: vi.fn(() => true), - stop: vi.fn() - }) - })) - vi.doMock('./direct-ssh-reconnect-coordinator', () => ({ - createDirectSshReconnectCoordinator: () => coordinator - })) - vi.doMock('@/lib/direct-ssh-reconnect-product-telemetry', () => ({ - createDirectSshReconnectProductTelemetryAdapter: vi.fn() - })) + stubDirectSshModules({ storeState, coordinator }) vi.stubGlobal( 'window', buildWindowApi({ diff --git a/src/renderer/src/store/slices/terminal-hydration-store-test-bootstrap.ts b/src/renderer/src/store/slices/terminal-hydration-store-test-bootstrap.ts index ed47faad081..52aee154f55 100644 --- a/src/renderer/src/store/slices/terminal-hydration-store-test-bootstrap.ts +++ b/src/renderer/src/store/slices/terminal-hydration-store-test-bootstrap.ts @@ -1,16 +1,6 @@ -import { vi } from 'vitest' - -// Why: import this before the store modules — session hydration reaches for the preload API and the -// runtime/PTY singletons, which don't exist under vitest. -vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) -vi.mock('@/runtime/sync-runtime-graph', () => ({ - scheduleRuntimeGraphSync: vi.fn() -})) -vi.mock('@/components/terminal-pane/pty-transport', () => ({ - registerEagerPtyBuffer: vi.fn(), - ensurePtyDispatcher: vi.fn() -})) - +// Why: import this before the store modules — session hydration reaches for the preload API, which +// doesn't exist under vitest. Module stubs for sonner/runtime-graph/pty-transport live in the test +// files themselves so vitest can hoist them above the store imports. const apiProxy = (): unknown => new Proxy(() => undefined, { get: (_target, prop) => (prop === 'then' ? undefined : apiProxy()), diff --git a/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts b/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts index 170b1ceac81..63daa20874f 100644 --- a/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts @@ -1,7 +1,6 @@ -// Keep this bare import first: its vi.mock calls run at module eval, and vitest only hoists vi.mock -// inside the test file itself — reordering it below the store imports breaks hydration here. +// Keep this bare import first: it installs the preload-API stub the store imports read at eval time. import './terminal-hydration-store-test-bootstrap' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { hydrateWorkspaceTerminalRows } from './terminal-session-row-hydration' import { getOrphanTerminalIds } from './terminal-orphan-helpers' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' @@ -12,6 +11,15 @@ import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { buildWorkspaceSessionPayload } from '@/lib/workspace-session' import { createTestStore, makeLayout, makeTab, makeWorktree, seedStore } from './store-test-helpers' +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +vi.mock('@/runtime/sync-runtime-graph', () => ({ + scheduleRuntimeGraphSync: vi.fn() +})) +vi.mock('@/components/terminal-pane/pty-transport', () => ({ + registerEagerPtyBuffer: vi.fn(), + ensurePtyDispatcher: vi.fn() +})) + const WORKTREE_ID = 'repo1::/wt-1' function makeCanonicalUnifiedTab(entityId: string, sortOrder: number): Tab { diff --git a/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts b/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts index 40eb541cc96..81362f8d50f 100644 --- a/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts @@ -1,11 +1,20 @@ import './terminal-hydration-store-test-bootstrap' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { buildWorkspaceSessionPayload } from '@/lib/workspace-session' import { createTestStore, makeLayout, makeTab, makeWorktree, seedStore } from './store-test-helpers' +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +vi.mock('@/runtime/sync-runtime-graph', () => ({ + scheduleRuntimeGraphSync: vi.fn() +})) +vi.mock('@/components/terminal-pane/pty-transport', () => ({ + registerEagerPtyBuffer: vi.fn(), + ensurePtyDispatcher: vi.fn() +})) + describe('hydrateWorkspaceSession canonical terminal rows', () => { it('drops only legacy rows that duplicate canonical PTY ownership', () => { const store = createTestStore() From 0569ca4cdca7a23d17265631b8ea438a2da24695 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:55:28 -0700 Subject: [PATCH 23/34] Improve microphone permission errors and drop failure reporting (#20801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(renderer): give the IPC error reader a clamped and an unclamped shape * fix(composer): name the attachments a drop could not add, in one toast * fix(composer, source-control): use one stable failure toast slot - Replace per-worktree toast IDs with single slot that replaces on each failure - Remove destructive retry actions; discard must confirm in dialog - Consolidate filesystem import types to shared location - Add compactIpcErrorMessage for string error handling * refactor: centralize filesystem import types and clarify failure naming Move import result types from main/ipc to shared layer so they're available across preload and renderer. Rename uniformFailure → commonFailure and skippedOrFailed → failureCount for clarity. Simplify preload/API type definitions by reusing shared types directly instead of duplicating inlined union shapes. * Reuse single toast slot for composer drop failures Multiple drop failures now replace the previous toast instead of stacking, preventing notification clutter. Uses a dedicated toast ID separate from Source Control's stage/discard notifications. * fix(settings): say when the microphone is blocked and where to grant it * Use generic stream for microphone permission requests - Request generic audio stream instead of saved device to handle stale device IDs (unplugged microphones). This ensures the initial permission grant succeeds even if the previously saved device is no longer available. - Refactor error handling to not require instanceof checks, supporting errors thrown as plain objects and improving robustness across browsers. - Simplify tests with proper typing and add coverage for stale device and permission error edge cases. * fix type check * minor type fix --- .../settings/VoiceMicrophoneSetting.test.tsx | 310 ++++++++++++++++++ .../settings/VoiceMicrophoneSetting.tsx | 150 +++++++-- src/renderer/src/i18n/locales/en.json | 7 +- 3 files changed, 443 insertions(+), 24 deletions(-) create mode 100644 src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx diff --git a/src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx b/src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx new file mode 100644 index 00000000000..4bdd5dfc8b9 --- /dev/null +++ b/src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx @@ -0,0 +1,310 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { DeveloperPermissionRequestResult } from '../../../../shared/developer-permissions-types' +import { getDefaultVoiceSettings } from '../../../../shared/constants' +import type { VoiceSettings } from '../../../../shared/speech-types' + +// Why: repo convention — React only suppresses its act() warning when this global is set. +globalThis.IS_REACT_ACT_ENVIRONMENT = true + +const mocks = vi.hoisted(() => ({ toastSuccess: vi.fn(), toastError: vi.fn() })) + +vi.mock('sonner', () => ({ + toast: { success: mocks.toastSuccess, error: mocks.toastError, message: vi.fn() } +})) + +import { VoiceMicrophoneSetting } from './VoiceMicrophoneSetting' + +const voiceSettings: VoiceSettings = { + ...getDefaultVoiceSettings(), + enabled: true +} + +function namedError(name: string, message = 'boom'): Error { + const error = new Error(message) + error.name = name + return error +} + +function installMediaDevices(getUserMedia: () => Promise>): void { + Object.assign(navigator, { + mediaDevices: { + getUserMedia: vi.fn(getUserMedia), + enumerateDevices: vi.fn(async () => []), + addEventListener: vi.fn(), + removeEventListener: vi.fn() + } + }) +} + +function installPermissionsApi(result: DeveloperPermissionRequestResult | Error): void { + Object.assign(window, { + api: { + developerPermissions: { + request: vi.fn(async () => { + if (result instanceof Error) { + throw result + } + return result + }) + } + } + }) +} + +let container: HTMLDivElement +let root: Root + +async function renderSetting(settings: VoiceSettings = voiceSettings): Promise { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + await act(async () => { + root.render( + {}} /> + ) + }) +} + +async function clickAllowAccess(): Promise { + const button = Array.from(container.querySelectorAll('button')).find( + (candidate) => candidate.textContent === 'Allow access' + ) + if (!button) { + throw new Error('Allow access button not rendered') + } + await act(async () => { + button.click() + }) +} + +function alertText(): string { + return container.querySelector('[role="alert"]')?.textContent ?? '' +} + +describe('VoiceMicrophoneSetting access failures', () => { + beforeEach(() => { + vi.clearAllMocks() + installPermissionsApi({ id: 'microphone', status: 'denied', openedSystemSettings: false }) + }) + + afterEach(() => { + act(() => root.unmount()) + container.remove() + }) + + it('routes a denied getUserMedia to the OS permission request and says where to grant it', async () => { + installMediaDevices(async () => { + throw new DOMException('Permission denied', 'NotAllowedError') + }) + + await renderSetting() + await clickAllowAccess() + + expect(window.api.developerPermissions.request).toHaveBeenCalledWith({ id: 'microphone' }) + expect(alertText()).toBe( + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + }) + + it('points at Privacy & Security once the request opened it', async () => { + installMediaDevices(async () => { + throw namedError('NotAllowedError') + }) + installPermissionsApi({ id: 'microphone', status: 'denied', openedSystemSettings: true }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe( + 'Opened macOS Privacy & Security. Grant microphone access, then try again.' + ) + }) + + it('still reports a block on platforms where the OS request is unsupported', async () => { + installMediaDevices(async () => { + throw namedError('NotAllowedError') + }) + installPermissionsApi({ id: 'microphone', status: 'unsupported', openedSystemSettings: false }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe( + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + }) + + it('names the missing-hardware case instead of a permission instruction', async () => { + installMediaDevices(async () => { + throw namedError('NotFoundError') + }) + + await renderSetting() + await clickAllowAccess() + + expect(window.api.developerPermissions.request).not.toHaveBeenCalled() + expect(alertText()).toBe('No microphone was found. Connect one, then try again.') + }) + + it('keeps the underlying detail for an unclassified failure', async () => { + installMediaDevices(async () => { + throw namedError('AbortError', 'Could not start audio source') + }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe('Could not open the microphone. Could not start audio source') + }) + + it('never renders a literal "undefined" when the error message is absent', async () => { + installMediaDevices(async () => { + throw { name: 'AbortError', message: undefined } + }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe('Could not open the microphone.') + }) + + it('shows the plain hint until something actually fails', async () => { + installMediaDevices(async () => ({ getTracks: () => [] })) + + await renderSetting() + + expect(container.querySelector('[role="alert"]')).toBeNull() + expect(container.textContent).toContain('Allow microphone access to list input devices.') + + await clickAllowAccess() + + expect(container.querySelector('[role="alert"]')).toBeNull() + }) + + it('uses a generic stream when the saved microphone is stale', async () => { + const getUserMedia = vi.fn(async () => ({ getTracks: () => [] })) + installMediaDevices(getUserMedia) + + await renderSetting({ + ...voiceSettings, + microphoneDeviceId: 'unplugged-mic', + microphoneDeviceLabel: 'Old headset' + }) + await clickAllowAccess() + + expect(getUserMedia).toHaveBeenCalledWith({ audio: true }) + }) + + it('classifies browser-shaped permission errors without requiring Error identity', async () => { + installMediaDevices(async () => { + throw { name: 'NotAllowedError', message: 'Permission denied' } + }) + + await renderSetting() + await clickAllowAccess() + + expect(window.api.developerPermissions.request).toHaveBeenCalledWith({ id: 'microphone' }) + }) + + it('opens a stream after the OS grant so the device list is not left empty', async () => { + let calls = 0 + let streamOpened = false + const getUserMedia = vi.fn(async () => { + calls += 1 + // Why: the first attempt is what triggers the OS prompt; the grant must re-open a stream, + // because enumerateDevices hides labels until one has been opened in this renderer. + if (calls === 1) { + throw namedError('NotAllowedError') + } + streamOpened = true + return { getTracks: () => [] } + }) + Object.assign(navigator, { + mediaDevices: { + getUserMedia, + // Why: mirrors the real rule the fix exists for — no labels until a stream has been opened. + enumerateDevices: vi.fn(async () => + streamOpened + ? [{ kind: 'audioinput', deviceId: 'mic-1', label: 'Built-in Microphone' }] + : [] + ), + addEventListener: vi.fn(), + removeEventListener: vi.fn() + } + }) + installPermissionsApi({ id: 'microphone', status: 'granted', openedSystemSettings: false }) + + await renderSetting() + await clickAllowAccess() + + expect(getUserMedia).toHaveBeenCalledTimes(2) + expect(mocks.toastSuccess).toHaveBeenCalledTimes(1) + expect(container.querySelector('[role="alert"]')).toBeNull() + // Why: the grant is only useful if the list it unblocks actually fills in — the hint and its + // Allow access button are what the pane shows while no device is known. + expect(container.textContent).not.toContain('Allow microphone access to list input devices.') + }) + + it('keeps a second browser denial classified as a permission error', async () => { + installMediaDevices(async () => { + throw new DOMException('Permission denied', 'NotAllowedError') + }) + installPermissionsApi({ id: 'microphone', status: 'granted', openedSystemSettings: false }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe( + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + expect(mocks.toastSuccess).not.toHaveBeenCalled() + }) + + it('names the missing-hardware case for the legacy DevicesNotFoundError alias', async () => { + installMediaDevices(async () => { + throw namedError('DevicesNotFoundError') + }) + + await renderSetting() + await clickAllowAccess() + + expect(alertText()).toBe('No microphone was found. Connect one, then try again.') + }) + + it('treats SecurityError as a permission denial, like NotAllowedError', async () => { + installMediaDevices(async () => { + throw namedError('SecurityError') + }) + + await renderSetting() + await clickAllowAccess() + + expect(window.api.developerPermissions.request).toHaveBeenCalledWith({ id: 'microphone' }) + expect(alertText()).toBe( + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + }) + + it('reports a failed permission REQUEST as such, with the IPC wrapper stripped', async () => { + installMediaDevices(async () => { + throw namedError('NotAllowedError') + }) + installPermissionsApi( + new Error( + "Error invoking remote method 'developerPermissions:request': Error: xdg-open not found" + ) + ) + + await renderSetting() + await clickAllowAccess() + + // Why: the microphone was never reopened — calling this a microphone-open failure would invert + // the provenance, and the raw transport prefix must never reach the pane. + expect(alertText()).toBe('xdg-open not found') + expect(alertText()).not.toContain('Error invoking remote method') + }) +}) diff --git a/src/renderer/src/components/settings/VoiceMicrophoneSetting.tsx b/src/renderer/src/components/settings/VoiceMicrophoneSetting.tsx index b5dd245db11..36a74474477 100644 --- a/src/renderer/src/components/settings/VoiceMicrophoneSetting.tsx +++ b/src/renderer/src/components/settings/VoiceMicrophoneSetting.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' import type { VoiceSettings } from '../../../../shared/speech-types' import { Button } from '../ui/button' import { Label } from '../ui/label' @@ -9,13 +10,57 @@ import { microphoneDeviceIdFromSelectValue, type VoiceMicrophoneDevice } from '@/components/dictation/microphone-devices' +import { useMountedRef } from '@/hooks/useMountedRef' import { translate } from '@/i18n/i18n' +import { extractIpcErrorMessage } from '@/lib/ipc-error' type VoiceMicrophoneSettingProps = { voiceSettings: VoiceSettings onUpdateVoiceSettings: (updates: Partial) => void } +function readMediaDeviceError(error: unknown): { name: string; message?: string } { + if (!error || typeof error !== 'object') { + return { name: '' } + } + // Why: an own `name`/`message` key can hold undefined/null; String() would + // turn that into the literal "undefined" and render it to the user. + const name = 'name' in error ? String(error.name ?? '') : '' + const message = 'message' in error ? String(error.message ?? '').trim() || undefined : undefined + return { name, message } +} + +function isMicrophonePermissionDenied(error: unknown): boolean { + const { name } = readMediaDeviceError(error) + return name === 'NotAllowedError' || name === 'SecurityError' +} + +function microphoneAccessErrorMessage(error: unknown): string { + const { name, message } = readMediaDeviceError(error) + if (name === 'NotAllowedError' || name === 'SecurityError') { + return translate( + 'auto.components.settings.VoiceMicrophoneSetting.permissionDenied', + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + } + if (name === 'NotFoundError' || name === 'DevicesNotFoundError') { + return translate( + 'auto.components.settings.VoiceMicrophoneSetting.noMicrophoneFound', + 'No microphone was found. Connect one, then try again.' + ) + } + return message + ? translate( + 'auto.components.settings.VoiceMicrophoneSetting.openFailedDetail', + 'Could not open the microphone. {{value0}}', + { value0: message } + ) + : translate( + 'auto.components.settings.VoiceMicrophoneSetting.openFailed', + 'Could not open the microphone.' + ) +} + function sameDeviceList( a: readonly VoiceMicrophoneDevice[], b: readonly VoiceMicrophoneDevice[] @@ -36,18 +81,12 @@ export function VoiceMicrophoneSetting({ const [devices, setDevices] = useState([]) const [devicesKnown, setDevicesKnown] = useState(false) const [accessPending, setAccessPending] = useState(false) - const mountedRef = useRef(true) + const [accessError, setAccessError] = useState(null) + const mountedRef = useMountedRef() // Why: devicechange fires several times per Bluetooth connect; drop enumerations // that resolve out of order so a stale list cannot land last. const refreshGenerationRef = useRef(0) - useEffect(() => { - mountedRef.current = true - return () => { - mountedRef.current = false - } - }, []) - const refreshDevices = useCallback(async (): Promise => { const generation = refreshGenerationRef.current + 1 refreshGenerationRef.current = generation @@ -65,7 +104,7 @@ export function VoiceMicrophoneSetting({ } setDevicesKnown(next.length > 0) setDevices((current) => (sameDeviceList(current, next) ? current : next)) - }, []) + }, [mountedRef]) // Why: voiceSettings.enabled is a dependency so enabling dictation re-scans — // that toggle is often when mic permission lands and real labels appear. @@ -83,25 +122,84 @@ export function VoiceMicrophoneSetting({ } }, [refreshDevices, voiceSettings.enabled]) - // Why: enumerateDevices hides ids and labels until mic permission is granted, so - // the list stays empty until something opens a stream at least once. + // A generic stream grants discovery even when the saved device is stale. + const openStreamAndRefreshDevices = useCallback(async (): Promise => { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + stream.getTracks().forEach((track) => track.stop()) + await refreshDevices() + }, [refreshDevices]) + const requestMicrophoneAccess = useCallback(async (): Promise => { if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) { return } setAccessPending(true) + setAccessError(null) try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - stream.getTracks().forEach((track) => track.stop()) - await refreshDevices() - } catch { - // Denied or unavailable — the hint stays visible so the user can retry. + try { + await openStreamAndRefreshDevices() + return + } catch (error) { + if (!isMicrophonePermissionDenied(error)) { + throw error + } + } + + let result: Awaited> + try { + result = await window.api.developerPermissions.request({ id: 'microphone' }) + } catch (error) { + // Why separate: this one DID cross IPC, so the wrapper must be stripped — and the microphone + // was never reopened, so reporting it as an open failure would invert the provenance. + if (mountedRef.current) { + setAccessError( + extractIpcErrorMessage( + error, + translate( + 'auto.components.settings.VoicePane.ad5d036ecc', + 'Could not request microphone permission. Voice dictation was not enabled.' + ) + ) + ) + } + return + } + if (!mountedRef.current) { + return + } + if (result.status !== 'granted') { + setAccessError( + result.openedSystemSettings + ? translate( + 'auto.components.settings.VoiceMicrophoneSetting.openedSystemSettings', + 'Opened macOS Privacy & Security. Grant microphone access, then try again.' + ) + : translate( + 'auto.components.settings.VoiceMicrophoneSetting.permissionDenied', + 'Microphone access is blocked. Grant it in your system settings, then try again.' + ) + ) + return + } + await openStreamAndRefreshDevices() + if (mountedRef.current) { + toast.success( + translate( + 'auto.components.settings.VoicePane.cd9fe37556', + 'Microphone permission granted' + ) + ) + } + } catch (error) { + if (mountedRef.current) { + setAccessError(microphoneAccessErrorMessage(error)) + } } finally { if (mountedRef.current) { setAccessPending(false) } } - }, [refreshDevices]) + }, [mountedRef, openStreamAndRefreshDevices]) const { options, selectedValue } = useMemo( () => @@ -138,12 +236,18 @@ export function VoiceMicrophoneSetting({

{showAccessHint && (
-

- {translate( - 'auto.components.settings.VoiceMicrophoneSetting.accessHint', - 'Allow microphone access to list input devices.' - )} -

+ {accessError ? ( +

+ {accessError} +

+ ) : ( +

+ {translate( + 'auto.components.settings.VoiceMicrophoneSetting.accessHint', + 'Allow microphone access to list input devices.' + )} +

+ )} ) : null} + {canWaiveArchiveHook ? ( + + ) : null}
) @@ -74,8 +93,10 @@ export function showDeleteWorktreeFailureToast({ forceDeleteReason, lockReason, hasKnownChanges, + canWaiveArchiveHook, onViewChanges, onForceDelete, + onDeleteAnyway, worktreeId, worktreeName }: DeleteWorktreeFailureToastOptions): void { @@ -96,13 +117,16 @@ export function showDeleteWorktreeFailureToast({ ), - duration: canForceDelete ? Infinity : 10000, + // A toast offering a destructive choice must not expire before the user reads the reason. + duration: canForceDelete || canWaiveArchiveHook === true ? Infinity : 10000, dismissible: true }) } diff --git a/src/renderer/src/components/sidebar/delete-worktree-flow.test.ts b/src/renderer/src/components/sidebar/delete-worktree-flow.test.ts index a84b8323d03..8f4b92a8cd2 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-flow.test.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-flow.test.ts @@ -1,7 +1,20 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ExecutionHostId } from '../../../../shared/execution-host' +type MockWorktreeDeleteState = { + isDeleting?: boolean + error?: string | null + canForceDelete?: boolean + forceDeleteReason?: 'dirty' | null + lockReason?: string | null + canWaiveArchiveHook?: boolean + executionHostId?: ExecutionHostId | null +} + const mocks = vi.hoisted(() => { + // Declared up here so the empty initialisers can be typed rather than asserted. + const gitStatusByWorktree: Record = {} + const deleteStateByWorktreeId: Record = {} const state = { settings: { skipDeleteWorktreeConfirm: false }, worktreeMap: new Map< @@ -35,18 +48,8 @@ const mocks = vi.hoisted(() => { setRightSidebarTab: vi.fn(), setRightSidebarOpen: vi.fn(), removeWorktree: vi.fn().mockResolvedValue({ ok: true }), - gitStatusByWorktree: {} as Record, - deleteStateByWorktreeId: {} as Record< - string, - { - isDeleting?: boolean - error?: string | null - canForceDelete?: boolean - forceDeleteReason?: 'dirty' | null - lockReason?: string | null - executionHostId?: ExecutionHostId | null - } - > + gitStatusByWorktree, + deleteStateByWorktreeId } return { state } }) @@ -631,4 +634,42 @@ describe('delete worktree flow', () => { description: 'Refresh Space and try again if the workspace list looks stale.' }) }) + + // #19334: a waived delete is still a delete — the caller's bookkeeping has to hear about it, or a + // batch/Space-panel list keeps showing the workspace it just removed. + it('reports a Delete Anyway success to the caller like a force retry', async () => { + mocks.state.settings = { skipDeleteWorktreeConfirm: true } + mocks.state.removeWorktree + .mockImplementationOnce(async () => { + mocks.state.deleteStateByWorktreeId['wt-1'] = { + isDeleting: false, + error: 'Archive hook failed for worktree: /w/one — exited 23.', + canForceDelete: false, + forceDeleteReason: null, + canWaiveArchiveHook: true + } + return { ok: false, error: 'Archive hook failed for worktree: /w/one — exited 23.' } + }) + .mockResolvedValueOnce({ ok: true }) + setWorktrees([{ id: 'wt-1', displayName: 'one' }]) + const onDeleted = vi.fn() + + expect(runWorktreeBatchDelete(['wt-1'], { onDeleted })).toBe(true) + + await vi.waitFor(() => expect(showDeleteWorktreeFailureToast).toHaveBeenCalled()) + const toastOptions = vi.mocked(showDeleteWorktreeFailureToast).mock.calls[0]?.[0] + expect(toastOptions?.canWaiveArchiveHook).toBe(true) + toastOptions?.onDeleteAnyway() + + await vi.waitFor(() => { + // The waiver rides its own option; force stays whatever the original attempt used. + expect(mocks.state.removeWorktree).toHaveBeenNthCalledWith( + 2, + { id: 'wt-1', executionHostId: null }, + false, + { allowFailedArchiveHook: true } + ) + expect(onDeleted).toHaveBeenCalledWith([{ id: 'wt-1', executionHostId: null }]) + }) + }) }) diff --git a/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts b/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts index 3f844d6f09d..82e3a1ed048 100644 --- a/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts +++ b/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts @@ -38,6 +38,101 @@ export function runWorktreeDeleteWithToast( ...(options.suppressPreservedBranchToast ? { suppressPreservedBranchToast: true } : {}), ...(options.snapshotPruneBatchId ? { snapshotPruneBatchId: options.snapshotPruneBatchId } : {}) } + const showFailureToast = ( + error: string, + state: ReturnType + ): void => { + const hasKnownChanges = + (useAppStore.getState().gitStatusByWorktree[worktreeId]?.length ?? 0) > 0 + showDeleteWorktreeFailureToast({ + error, + canForceDelete: state?.canForceDelete ?? false, + canWaiveArchiveHook: state?.canWaiveArchiveHook === true, + forceDeleteReason: state?.forceDeleteReason ?? null, + lockReason: state?.lockReason ?? null, + hasKnownChanges, + onViewChanges: () => viewWorktreeDiff(worktreeId, target.executionHostId), + // Why (#19334): re-runs the archive hook and waives the failure this time, so the waiver + // is an informed choice made after reading the refusal -- not something `force` implied. + onDeleteAnyway: () => + retryFromToast({ force: options.force === true, allowFailedArchiveHook: true }), + // The explicit Force Delete retry may waive an unverified PTY-stop proof. + onForceDelete: () => + retryFromToast({ + force: true, + allowUnverifiedPtyStop: true, + failedTitle: translate( + 'auto.components.sidebar.delete.worktree.flow.4f3876c0f5', + 'Force delete failed' + ), + withViewAction: true + }), + worktreeId, + worktreeName + }) + } + + // Both toast buttons do the same thing: recapture focus (the user may have navigated while the + // toast was open), retry with one waiver added, and report a success through `onForceDeleted` so + // the caller's bookkeeping runs. Only the waiver and the failure copy differ. + const retryFromToast = (retry: { + force: boolean + allowUnverifiedPtyStop?: boolean + allowFailedArchiveHook?: boolean + failedTitle?: string + withViewAction?: boolean + }): void => { + const commitRetryFocus = prepareActiveWorktreeFocusAfterDelete(worktreeId) + const viewAction = retry.withViewAction + ? { + action: { + label: translate('auto.components.sidebar.delete.worktree.flow.7488ed8711', 'View'), + onClick: () => viewWorktreeDiff(worktreeId, target.executionHostId) + } + } + : {} + // Why re-show the full failure toast rather than a bare `toast.error` (#19334): a retry can + // fail for a DIFFERENT reason than the one the user just answered. Waiving a failed archive + // hook on a dirty checkout lands on the dirty preflight next, and a bare error offers no + // buttons — leaving the user stuck one step further in, which is the dead end this gate has + // now produced three times. Routing back through the same toast keeps every retry actionable. + const failed = (description: string): void => { + const retryState = getDeleteStateForWorktreeHost( + { id: worktreeId, hostId: target.executionHostId ?? undefined }, + useAppStore.getState().deleteStateByWorktreeId + ) + if (retryState?.canForceDelete === true || retryState?.canWaiveArchiveHook === true) { + showFailureToast(description, retryState) + return + } + toast.error( + retry.failedTitle ?? + translate( + 'auto.components.sidebar.delete.worktree.flow.ae57cbf6e4', + 'Failed to delete workspace' + ), + { description, ...viewAction } + ) + } + useAppStore + .getState() + .removeWorktree(target, retry.force, { + ...(retry.allowUnverifiedPtyStop ? { allowUnverifiedPtyStop: true } : {}), + ...(retry.allowFailedArchiveHook ? { allowFailedArchiveHook: true } : {}) + }) + .then((result) => { + if (!result.ok) { + failed(result.error) + return + } + commitRetryFocus() + // "A retry started from this toast completed the delete" — callers hang their bookkeeping + // off it, so without this a batch or Space-panel delete keeps listing what it removed. + options.onForceDeleted?.(target) + }) + .catch((err: unknown) => failed(err instanceof Error ? err.message : String(err))) + } + const removal = Object.keys(removeOptions).length > 0 ? removeWorktree(target, options.force === true, removeOptions) @@ -61,73 +156,13 @@ export function runWorktreeDeleteWithToast( } return true } - const state = getDeleteStateForWorktreeHost( - { id: worktreeId, hostId: target.executionHostId ?? undefined }, - useAppStore.getState().deleteStateByWorktreeId + showFailureToast( + result.error, + getDeleteStateForWorktreeHost( + { id: worktreeId, hostId: target.executionHostId ?? undefined }, + useAppStore.getState().deleteStateByWorktreeId + ) ) - const canForceDelete = state?.canForceDelete ?? false - const hasKnownChanges = - (useAppStore.getState().gitStatusByWorktree[worktreeId]?.length ?? 0) > 0 - showDeleteWorktreeFailureToast({ - error: result.error, - canForceDelete, - forceDeleteReason: state?.forceDeleteReason ?? null, - lockReason: state?.lockReason ?? null, - hasKnownChanges, - onViewChanges: () => viewWorktreeDiff(worktreeId, target.executionHostId), - onForceDelete: () => { - // Recapture focus because the user may have navigated while the toast was open. - const commitForceFocus = prepareActiveWorktreeFocusAfterDelete(worktreeId) - // The explicit Force Delete retry may waive an unverified PTY-stop proof. - const forceRemoval = useAppStore - .getState() - .removeWorktree(target, true, { allowUnverifiedPtyStop: true }) - forceRemoval - .then((forceResult) => { - if (!forceResult.ok) { - toast.error( - translate( - 'auto.components.sidebar.delete.worktree.flow.4f3876c0f5', - 'Force delete failed' - ), - { - description: forceResult.error, - action: { - label: translate( - 'auto.components.sidebar.delete.worktree.flow.7488ed8711', - 'View' - ), - onClick: () => viewWorktreeDiff(worktreeId, target.executionHostId) - } - } - ) - return - } - commitForceFocus() - options.onForceDeleted?.(target) - }) - .catch((err: unknown) => { - toast.error( - translate( - 'auto.components.sidebar.delete.worktree.flow.ae57cbf6e4', - 'Failed to delete workspace' - ), - { - description: err instanceof Error ? err.message : String(err), - action: { - label: translate( - 'auto.components.sidebar.delete.worktree.flow.7488ed8711', - 'View' - ), - onClick: () => viewWorktreeDiff(worktreeId, target.executionHostId) - } - } - ) - }) - }, - worktreeId, - worktreeName - }) return false }) .catch((err: unknown) => { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index e4345a6f5cc..2a0fe5755d1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -5802,6 +5802,11 @@ "unstoppedPtyLive": "This workspace still has running terminals, so Orca stopped before deleting any files. Force Delete will kill them and discard any uncommitted work they hold.", "runningAgentSession": "Orca could not confirm every agent session in this workspace has closed, so it stopped before deleting any files. Use Force Delete to remove it anyway.", "runningAgentSessionLive": "This workspace still has running agent sessions that Orca could not close, so it stopped before deleting any files. Force Delete will discard any work they hold." + }, + "failure": { + "archive": { + "waiver": "Delete Anyway" + } } } }, @@ -10953,7 +10958,8 @@ "orcaCloudSignOut": "Sign out", "orcaCloudConnect": "Connect profile", "orcaCloudRefresh": "Refresh status", - "orcaCloudNotConfigured": "Set ORCA_CLOUD_API_URL and ORCA_CLOUD_CLIENT_ID to preview Orca Cloud sign-in in this dev build." + "orcaCloudNotConfigured": "Set ORCA_CLOUD_API_URL and ORCA_CLOUD_CLIENT_ID to preview Orca Cloud sign-in in this dev build.", + "deleteAnywayClicked": "Delete Anyway clicked" }, "EphemeralVmRecipeRow": { "useInWorkspace": "Use in workspace" diff --git a/src/renderer/src/lib/ipc-error.test.ts b/src/renderer/src/lib/ipc-error.test.ts index cf7ba9d6817..572bf8ffa4f 100644 --- a/src/renderer/src/lib/ipc-error.test.ts +++ b/src/renderer/src/lib/ipc-error.test.ts @@ -131,3 +131,38 @@ describe('readIpcErrorDetail', () => { ).toBe('SSH connection failed: Relay package not found.') }) }) + +// Why (#19334): a typed main-process error keeps its class name after the wrapper comes off, and +// the worktree-removal refusal is rendered to a user who does not care what the class was called. +describe('typed main-process errors', () => { + const wrapped = new Error( + "Error invoking remote method 'worktrees:remove': WorktreeArchiveHookFailedError: " + + 'Archive hook failed for worktree: /w/feature — exited 23.\nbackup target unreachable' + ) + + it('drops the error class name along with the wrapper', () => { + expect(readIpcErrorDetail(wrapped)).toBe( + 'Archive hook failed for worktree: /w/feature — exited 23.\nbackup target unreachable' + ) + }) + + it('keeps the detail lines a refusal needs, unlike the clamped read', () => { + // The hook's own output is the actionable part of an archive refusal, so the unclamped read + // has to survive the newline that `readIpcErrorMessage` deliberately cuts at. + expect(readIpcErrorMessage(wrapped)).toBe( + 'Archive hook failed for worktree: /w/feature — exited 23.' + ) + }) + + it('leaves a renderer-local error its class name, having unwrapped nothing', () => { + expect(readIpcErrorDetail(new Error('TypeError: x is not a function'))).toBe( + 'TypeError: x is not a function' + ) + }) + + it('does not mistake an errno prefix for a class name', () => { + expect( + readIpcErrorDetail(new Error("Error occurred in handler for 'fs:read': EACCES: denied")) + ).toBe('EACCES: denied') + }) +}) diff --git a/src/renderer/src/lib/ipc-error.ts b/src/renderer/src/lib/ipc-error.ts index 020bc7e9629..739119f3f5d 100644 --- a/src/renderer/src/lib/ipc-error.ts +++ b/src/renderer/src/lib/ipc-error.ts @@ -1,10 +1,17 @@ // Unanchored so caller-owned context around an Electron wrapper survives. const IPC_INVOKE_PREFIX = /Error invoking remote method '[^']*':\s*(?:Error:\s*)?/ const IPC_HANDLER_PREFIX = /Error occurred in handler for '[^']*':\s*(?:Error:\s*)?/ +// Why (#19334): once the wrapper is gone, a typed main-process error still leads with its class +// name — "WorktreeArchiveHookFailedError: Archive hook failed for worktree: …". That is noise to +// someone reading a toast, and it pushes the sentence that matters off the first line. +const ERROR_CLASS_PREFIX = /^(?:[A-Za-z_$][\w$]*)?Error:\s*/ function unwrapIpcErrorMessage(message: string): string | undefined { + const wrapped = IPC_INVOKE_PREFIX.test(message) || IPC_HANDLER_PREFIX.test(message) const detail = message.replace(IPC_INVOKE_PREFIX, '').replace(IPC_HANDLER_PREFIX, '').trim() - return detail || undefined + // Only strip the class name off something we actually unwrapped, so a renderer-local + // `TypeError: …` keeps the prefix that tells you what it was. + return (wrapped ? detail.replace(ERROR_CLASS_PREFIX, '').trim() : detail) || undefined } export function compactIpcErrorMessage(message: string): string | undefined { diff --git a/src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts b/src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts index 797b58cc25d..27d4ecc5201 100644 --- a/src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts +++ b/src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts @@ -377,18 +377,34 @@ describe('removeWorktree cascade', () => { }) }) - it('offers force delete for Electron-wrapped local dirty preflight errors', async () => { + // Why a table (#19334): these three differ only in the wrapped message and the classification it + // earns. The shared body is what matters — the IPC wrapper is stripped for display while + // classification still reads the wrapped input. + it.each([ + [ + 'offers force delete for Electron-wrapped local dirty preflight errors', + "Error invoking remote method 'worktrees:remove': Error: Failed to delete worktree at /workspace/feature-wt. ?? scratch.txt", + 'Failed to delete worktree at /workspace/feature-wt. ?? scratch.txt', + { canForceDelete: true, forceDeleteReason: 'dirty' } + ], + [ + 'offers force delete when Git already removed an unregistered worktree', + "Error invoking remote method 'worktrees:remove': Error: Worktree is no longer registered with Git and its directory is already gone.", + 'Worktree is no longer registered with Git and its directory is already gone.', + { canForceDelete: true, forceDeleteReason: 'missing-registration' } + ], + [ + 'does not offer force delete when Electron wraps SSH filesystem provider failures', + "Error invoking remote method 'worktrees:remove': Error: SSH filesystem provider unavailable", + 'SSH filesystem provider unavailable', + { canForceDelete: false, forceDeleteReason: null } + ] + ])('%s', async (_title, wrapped, displayed, classification) => { const store = createTestStore() const worktreeId = 'repo1::/workspace/feature-wt' - const error = - "Error invoking remote method 'worktrees:remove': Error: Failed to delete worktree at /workspace/feature-wt. ?? scratch.txt" - - mockApi.worktrees.remove.mockRejectedValueOnce(new Error(error)) - + mockApi.worktrees.remove.mockRejectedValueOnce(new Error(wrapped)) seedStore(store, { - worktreesByRepo: { - repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1' })] - }, + worktreesByRepo: { repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1' })] }, tabsByWorktree: {}, ptyIdsByTabId: {}, terminalLayoutsByTabId: {} @@ -396,12 +412,11 @@ describe('removeWorktree cascade', () => { const result = await store.getState().removeWorktree({ id: worktreeId, executionHostId: null }) - expect(result).toEqual({ ok: false, error }) + expect(result).toEqual({ ok: false, error: displayed }) expect(store.getState().deleteStateByWorktreeId[worktreeId]).toEqual({ isDeleting: false, - error, - canForceDelete: true, - forceDeleteReason: 'dirty' + error: displayed, + ...classification }) }) @@ -462,34 +477,6 @@ describe('removeWorktree cascade', () => { }) }) - it('offers force delete when Git already removed an unregistered worktree', async () => { - const store = createTestStore() - const worktreeId = 'repo1::/workspace/deleted-wt' - const error = - "Error invoking remote method 'worktrees:remove': Error: Worktree is no longer registered with Git and its directory is already gone." - - mockApi.worktrees.remove.mockRejectedValueOnce(new Error(error)) - - seedStore(store, { - worktreesByRepo: { - repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1' })] - }, - tabsByWorktree: {}, - ptyIdsByTabId: {}, - terminalLayoutsByTabId: {} - }) - - const result = await store.getState().removeWorktree({ id: worktreeId, executionHostId: null }) - - expect(result).toEqual({ ok: false, error }) - expect(store.getState().deleteStateByWorktreeId[worktreeId]).toEqual({ - isDeleting: false, - error, - canForceDelete: true, - forceDeleteReason: 'missing-registration' - }) - }) - it('sets canForceDelete=false when force=true removal fails', async () => { const store = createTestStore() const worktreeId = 'repo1::/path/wt1' @@ -579,34 +566,6 @@ describe('removeWorktree cascade', () => { expect(store.getState().deleteStateByWorktreeId[worktreeId]?.canForceDelete).toBe(false) }) - it('does not offer force delete when Electron wraps SSH filesystem provider failures', async () => { - const store = createTestStore() - const worktreeId = 'repo1::/path/wt1' - const error = - "Error invoking remote method 'worktrees:remove': Error: SSH filesystem provider unavailable" - - mockApi.worktrees.remove.mockRejectedValueOnce(new Error(error)) - - seedStore(store, { - worktreesByRepo: { - repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1' })] - }, - tabsByWorktree: {}, - ptyIdsByTabId: {}, - terminalLayoutsByTabId: {} - }) - - const result = await store.getState().removeWorktree({ id: worktreeId, executionHostId: null }) - - expect(result).toEqual({ ok: false, error }) - expect(store.getState().deleteStateByWorktreeId[worktreeId]).toEqual({ - isDeleting: false, - error, - canForceDelete: false, - forceDeleteReason: null - }) - }) - it.each([ 'Could not connect to the remote Orca runtime.', 'Remote Orca runtime closed the connection.', @@ -617,6 +576,8 @@ describe('removeWorktree cascade', () => { const store = createTestStore() const worktreeId = 'repo1::/path/wt1' const error = `Error invoking remote method 'runtime-environments:call': Error: ${runtimeFailure}` + // The wrapper is stripped for display; the runtime failure text is what the user sees. + const displayed = runtimeFailure mockApi.runtimeEnvironments.call.mockImplementation((args: { method: string }) => { const compatibility = createCompatibleRuntimeStatusResponseIfNeeded(args) @@ -648,10 +609,10 @@ describe('removeWorktree cascade', () => { .getState() .removeWorktree({ id: worktreeId, executionHostId: null }) - expect(result).toEqual({ ok: false, error }) + expect(result).toEqual({ ok: false, error: displayed }) expect(store.getState().deleteStateByWorktreeId[worktreeId]).toEqual({ isDeleting: false, - error, + error: displayed, canForceDelete: false, forceDeleteReason: null }) diff --git a/src/renderer/src/store/slices/worktree-delete-state-types.ts b/src/renderer/src/store/slices/worktree-delete-state-types.ts index 85178bc15dc..ecbb4a50f3e 100644 --- a/src/renderer/src/store/slices/worktree-delete-state-types.ts +++ b/src/renderer/src/store/slices/worktree-delete-state-types.ts @@ -10,6 +10,8 @@ export type WorktreeDeleteState = { canForceDelete: boolean forceDeleteReason: WorktreeForceDeleteReason | null lockReason?: string | null + /** The removal was refused by a failed archive hook, so "Delete anyway" is offered (#19334). */ + canWaiveArchiveHook?: boolean } export type WorktreeDeleteStateTarget = Pick diff --git a/src/renderer/src/store/slices/worktree-removal-options.ts b/src/renderer/src/store/slices/worktree-removal-options.ts index 04096db9a50..c77568248de 100644 --- a/src/renderer/src/store/slices/worktree-removal-options.ts +++ b/src/renderer/src/store/slices/worktree-removal-options.ts @@ -9,6 +9,9 @@ export type RemoveWorktreeOptions = { // Why (#11960): only an explicit Force Delete waives the proof that every // PTY stopped; `force` alone is set by the ordinary delete confirmation. allowUnverifiedPtyStop?: boolean + // Why (#19334): waives a FAILED archive hook. Set only by the explicit "Delete anyway" retry + // after the user has seen the refusal -- never by the ordinary confirmation, never by `force`. + allowFailedArchiveHook?: boolean snapshotPruneBatchId?: string /** Fresh cleanup-scan evidence for a same-id owner not represented in the catalog. */ sameIdSurvivingHostId?: ExecutionHostId diff --git a/src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts b/src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts index 528c9a977d5..dc54b8c7763 100644 --- a/src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts +++ b/src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ARCHIVE_HOOK_TIMEOUT_MS } from '../../../../shared/worktree/archive-hook-removal-gate' import type { AppState } from '../types' import type { RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture' import { makeWorktree } from './worktrees-slice-test-fixtures' @@ -64,7 +65,8 @@ describe('worktree remote runtime mutations', () => { allowUnverifiedPtyStop: false, runHooks: true }, - timeoutMs: 60_000, + // Hooks run here, so the client must outlast the host's archive-hook budget (#19334). + timeoutMs: ARCHIVE_HOOK_TIMEOUT_MS + 60_000, expectedEnvironmentPairingRevision: undefined, expectedRuntimeId: undefined }) @@ -127,7 +129,8 @@ describe('worktree remote runtime mutations', () => { allowUnverifiedPtyStop: false, runHooks: true }, - timeoutMs: 60_000, + // Hooks run here, so the client must outlast the host's archive-hook budget (#19334). + timeoutMs: ARCHIVE_HOOK_TIMEOUT_MS + 60_000, expectedEnvironmentPairingRevision: undefined, expectedRuntimeId: undefined }) @@ -225,7 +228,8 @@ describe('worktree remote runtime mutations', () => { allowUnverifiedPtyStop: false, runHooks: true }, - timeoutMs: 60_000 + // Hooks run here, so the client must outlast the host's archive-hook budget (#19334). + timeoutMs: ARCHIVE_HOOK_TIMEOUT_MS + 60_000 }) expect(mockApi.worktrees.remove).not.toHaveBeenCalled() expect(store.getState().worktreesByRepo['repo-ssh']).toEqual([]) @@ -601,6 +605,8 @@ describe('worktree remote runtime mutations', () => { force: undefined, // Why (#11960): an ordinary remove never waives the PTY-stop proof. allowUnverifiedPtyStop: false, + // Why (#19334): nor a failed archive hook — only the explicit "Delete anyway" retry does. + allowFailedArchiveHook: false, skipArchive: false }) expect(runtimeEnvironmentCall).not.toHaveBeenCalled() diff --git a/src/renderer/src/store/slices/worktrees/teardown/dispatch-worktree-removal.ts b/src/renderer/src/store/slices/worktrees/teardown/dispatch-worktree-removal.ts index 2ca0212b08f..3b2a11da6e4 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/dispatch-worktree-removal.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/dispatch-worktree-removal.ts @@ -3,6 +3,7 @@ import type { RemoveWorktreeResult } from '../../../../../../shared/worktree/cre import { callRuntimeRpc, type getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client' import { toRuntimeWorktreeSelector } from '../../../../runtime/runtime-worktree-selector' import type { RemoveWorktreeOptions } from '../../worktree-removal-options' +import { ARCHIVE_HOOK_TIMEOUT_MS } from '../../../../../../shared/worktree/archive-hook-removal-gate' /** * Sends the destructive removal over whichever transport owns this workspace. @@ -36,6 +37,7 @@ export async function dispatchWorktreeRemoval(args: { hostId, force, allowUnverifiedPtyStop: options?.allowUnverifiedPtyStop === true, + allowFailedArchiveHook: options?.allowFailedArchiveHook === true, skipArchive, ...snapshotPruneBatch }) @@ -50,9 +52,19 @@ export async function dispatchWorktreeRemoval(args: { ...(effectiveHostId ? { hostId: effectiveHostId } : {}), force, allowUnverifiedPtyStop: options?.allowUnverifiedPtyStop === true, + // Why only when set, unlike the IPC branch: this crosses a version boundary, and a host + // that predates the gate drops unknown params silently. Send it when it means something. + ...(options?.allowFailedArchiveHook === true ? { allowFailedArchiveHook: true } : {}), runHooks: !skipArchive }, - { timeoutMs: 60_000 } + { + // Why not a flat 60s (#19334): the host may run an archive hook for up to + // ARCHIVE_HOOK_TIMEOUT_MS before it decides anything. A client that gives up first reports a + // failure for a removal that is still in progress — and if the hook then succeeds, the host + // deletes the checkout while the user has been told the delete failed. Outlast the hook when + // one can run; keep the short budget when none will. + timeoutMs: skipArchive ? 60_000 : ARCHIVE_HOOK_TIMEOUT_MS + 60_000 + } ) } diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts index a6a9c87d838..56bd12581f0 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts @@ -7,6 +7,8 @@ import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed' import { getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client' import { forgetHugeRepoWarningDismissalsForWorktrees } from '@/lib/source-control-huge-repo-warning-dismissals' import { forgetWorktreeSleepIntent } from '@/lib/worktree-sleep-intent' +import { readIpcErrorDetail } from '@/lib/ipc-error' +import { isArchiveHookRemovalError } from '../../../../../../shared/worktree/archive-hook-removal-gate' import { showPreservedBranchToast } from '@/components/sidebar/preserved-branch-toast' import { resolveWorktreeOperationRouteResult, @@ -297,13 +299,18 @@ export function createRemoveWorktree( } catch (err) { // Why: git refusing a non-force delete for dirty/untracked files is a handled user decision, not an app error. console.warn('Failed to remove worktree:', err) - const error = err instanceof Error ? err.message : String(err) + // The raw message arrives wrapped in Electron's IPC channel and class names; this string is + // read by a user in a toast, and the refusal sentence has to lead it. + const error = readIpcErrorDetail(err) ?? (err instanceof Error ? err.message : String(err)) const forceDeleteReason = classifyWorktreeForceDeleteReason( error, force, options?.allowUnverifiedPtyStop === true ) const locked = isLockedWorktreeRemovalError(error) + // Why (#19334): the refusal is the only failure a retry can clear by waiving rather than by + // fixing state, so the toast needs to know it may offer that choice. + const canWaiveArchiveHook = isArchiveHookRemovalError(error) set((s) => ({ deleteStateByWorktreeId: { ...s.deleteStateByWorktreeId, @@ -313,6 +320,7 @@ export function createRemoveWorktree( error, canForceDelete: forceDeleteReason !== null, forceDeleteReason, + ...(canWaiveArchiveHook ? { canWaiveArchiveHook: true } : {}), ...(locked ? { lockReason: getLockedWorktreeRemovalReason(error) } : {}) } } diff --git a/src/shared/cli-argument-boundary.ts b/src/shared/cli-argument-boundary.ts index 088f6ff0e76..ad89c9a8978 100644 --- a/src/shared/cli-argument-boundary.ts +++ b/src/shared/cli-argument-boundary.ts @@ -3,6 +3,7 @@ export const CLI_GLOBAL_FLAGS: readonly string[] = ['help', 'json', ...CLI_GLOBA export const CLI_BOOLEAN_FLAGS = new Set([ 'all', + 'allow-failed-archive-hook', 'attachments', 'children', 'comments', diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 6afdf0b3b62..bd2ca84c95f 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -114,6 +114,17 @@ export const TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY = 'terminal.quick-comman // status.worktreeCreateIdempotency carries the optional host retention policy. export const WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY = 'worktree.create-idempotency.v1' as const +// Scope of the claim: a hook that RUNS and fails cannot delete the checkout. It does not promise +// the hook was found — an SSH host whose orca.yaml cannot be read answers "no hook" and the removal +// proceeds, because a failed read is indistinguishable from an absent file across the relay +// (#20196 tracks the provider contract that would separate them). +// Why (#19334): "accepts --run-hooks" and "refuses to delete when the archive hook fails" were +// indistinguishable from the outside — both take the flag and behave identically on success, so +// the only way to tell an unfixed host apart was to fail a hook and see whether the checkout +// survived. Lifecycle integrations keep teardown evidence inside the checkout and cannot risk +// that. Advertised unconditionally: every build carrying this constant has the gate. +export const WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY = + 'worktree.archive-failure-blocking.v1' as const export const CODEX_RESET_CREDIT_RUNTIME_CAPABILITY = 'accounts.codex-reset-credit.v1' as const export const ACCOUNT_IMPORT_RUNTIME_CAPABILITY = 'accounts.import-host-credentials.v1' as const // Why: older hosts cannot reconcile terminal.create's mutation after losing the reply, so clients may only retry unknown outcomes when advertised. @@ -283,6 +294,7 @@ export const RUNTIME_CAPABILITIES = [ TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY, TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY, WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY, + WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY, TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY, SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY, SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY, diff --git a/src/shared/rpc-contract/worktree-params.ts b/src/shared/rpc-contract/worktree-params.ts index 00ed7627f9d..74b74dd799c 100644 --- a/src/shared/rpc-contract/worktree-params.ts +++ b/src/shared/rpc-contract/worktree-params.ts @@ -171,7 +171,10 @@ export const WorktreeRemove = WorktreeSelector.extend({ // desktop sets `force` for an ordinary confirmed delete too, so the PTY-stop // waiver travels on its own field. allowUnverifiedPtyStop: OptionalBoolean, - runHooks: OptionalBoolean + runHooks: OptionalBoolean, + // Why (#19334): a failed archive hook blocks removal. This waives that refusal and is recorded + // in the result; it is NOT `force`, and it does not decide whether the hook runs. + allowFailedArchiveHook: OptionalBoolean }) export const WorktreeForceDeleteBranch = WorktreeSelector.extend({ diff --git a/src/shared/worktree/archive-failure-blocking-capability.test.ts b/src/shared/worktree/archive-failure-blocking-capability.test.ts new file mode 100644 index 00000000000..be6e010fc5e --- /dev/null +++ b/src/shared/worktree/archive-failure-blocking-capability.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' +import { + RUNTIME_CAPABILITIES, + WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY +} from '../protocol-version' + +// Why (#19334): the reporter's integration (Harbour) keeps its teardown ownership evidence inside +// the checkout, so it must know *before* removing anything whether this host refuses to delete on a +// failed archive hook. It cannot probe for that — probing means risking the data loss. +describe('worktree.archive-failure-blocking.v1', () => { + it('uses the id the reporting integration already codes against', () => { + expect(WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY).toBe( + 'worktree.archive-failure-blocking.v1' + ) + }) + + it('is advertised by every build that carries the gate', () => { + expect(RUNTIME_CAPABILITIES).toContain(WORKTREE_ARCHIVE_FAILURE_BLOCKING_RUNTIME_CAPABILITY) + }) +}) diff --git a/src/shared/worktree/archive-hook-removal-gate.ts b/src/shared/worktree/archive-hook-removal-gate.ts new file mode 100644 index 00000000000..9dcf270d28e --- /dev/null +++ b/src/shared/worktree/archive-hook-removal-gate.ts @@ -0,0 +1,117 @@ +// Why (#19334): the archive hook is a user's last chance to save work off a checkout Orca is +// about to delete. A failed hook used to be logged and stepped over, so the delete went ahead +// with nothing archived. It is a precondition, evaluated before any stop/delete mutation. + +/** + * How long an archive hook gets before it is cut off. Shared because a client waiting on a removal + * has to outlast it: a client that gives up first reports a failure for a hook that is still + * running, and the host then completes the delete anyway — telling the user the opposite of what + * happened to their checkout (#19334). + */ +export const ARCHIVE_HOOK_TIMEOUT_MS = 120_000 + +/** RPC/CLI error code for a removal refused because the repo's archive hook did not succeed. */ +export const ARCHIVE_HOOK_FAILED_REMOVAL_CODE = 'worktree_archive_hook_failed' + +export const ARCHIVE_HOOK_FAILED_REMOVAL_PREFIX = 'Archive hook failed for worktree:' + +// One string, three surfaces: the CLI, RPC callers, and the desktop toast that now carries its own +// Delete Anyway button. Naming only the CLI flag sent desktop users to a terminal for a button that +// was six inches away, so both affordances are named and neither is presented as the only one. +export const ARCHIVE_HOOK_OVERRIDE_HINT = + 'Nothing was stopped, deleted or deregistered. Fix the hook and retry, or delete anyway with an explicit waiver — "Delete Anyway" in the app, or --allow-failed-archive-hook on the CLI.' + +/** + * `exited` means the host reported a non-zero exit for this hook run. `unverifiable` covers every + * case where the hook's outcome was never observed — spawn failure, timeout, lost contact with the + * execution host. Loss of contact is never evidence that the hook passed, so both block removal. + * Vocabulary is deliberately the `UnstoppedPtyVerdict` spelling; see docs/reference/ssh-execution-boundary.md. + */ +export type ArchiveHookOutcome = 'exited' | 'unverifiable' + +export type ArchiveHookFailure = { + worktreePath: string + outcome: ArchiveHookOutcome + /** Only ever set for `exited` — an absent code is not a zero code. */ + exitCode?: number + output: string +} + +/** What a caller sees when the failure was explicitly overridden instead of blocking. */ +export type ArchiveHookOverride = ArchiveHookFailure & { overridden: true } + +export class WorktreeArchiveHookFailedError extends Error { + readonly code = ARCHIVE_HOOK_FAILED_REMOVAL_CODE + readonly data: ArchiveHookFailure + + constructor(failure: ArchiveHookFailure) { + super(formatArchiveHookFailure(failure)) + this.name = 'WorktreeArchiveHookFailedError' + this.data = failure + } +} + +function describeArchiveHookVerdict(failure: ArchiveHookFailure): string { + return failure.outcome === 'exited' + ? `exited ${failure.exitCode}` + : 'outcome unverifiable (the hook never reported an exit)' +} + +export function formatArchiveHookFailure(failure: ArchiveHookFailure): string { + const output = failure.output.trim() + return [ + `${ARCHIVE_HOOK_FAILED_REMOVAL_PREFIX} ${failure.worktreePath} — ${describeArchiveHookVerdict(failure)}.`, + ARCHIVE_HOOK_OVERRIDE_HINT, + ...(output ? [output] : []) + ].join(' ') +} + +/** + * The waived case says the opposite of the refusal: the removal DID go ahead. Reusing + * `formatArchiveHookFailure` here printed "Nothing was stopped, deleted or deregistered" directly + * after deleting the checkout. + */ +export function formatArchiveHookOverride(override: ArchiveHookOverride): string { + const output = override.output.trim() + return [ + `Archive hook failed for worktree: ${override.worktreePath} — ${describeArchiveHookVerdict(override)}.`, + 'Deleted anyway because the failure was explicitly waived; nothing was archived.', + ...(output ? [output] : []) + ].join(' ') +} + +/** + * Narrow an unknown rejection to the typed refusal, or rethrow it. This is the branch a real + * caller writes, so tests asserting on a refusal should go through it rather than re-deriving it. + */ +export function asArchiveHookRefusal(error: unknown): WorktreeArchiveHookFailedError { + if (error instanceof WorktreeArchiveHookFailedError) { + return error + } + throw error +} + +/** Recognise the refusal on a surface that only has the message, e.g. a renderer toast. */ +export function isArchiveHookRemovalError(error: string): boolean { + return error.includes(ARCHIVE_HOOK_FAILED_REMOVAL_PREFIX) +} + +/** Shape both the local and the SSH archive runners answer with. */ +export type ArchiveHookRunResult = { + success: boolean + output: string + /** Omitted whenever no exit was observed, which classifies the failure as `unverifiable`. */ + exitCode?: number +} + +export function classifyArchiveHookFailure( + worktreePath: string, + result: ArchiveHookRunResult +): ArchiveHookFailure { + return { + worktreePath, + outcome: typeof result.exitCode === 'number' ? 'exited' : 'unverifiable', + ...(typeof result.exitCode === 'number' ? { exitCode: result.exitCode } : {}), + output: result.output + } +} diff --git a/src/shared/worktree/create-types.ts b/src/shared/worktree/create-types.ts index d8f3cfc81c5..7d41f1363be 100644 --- a/src/shared/worktree/create-types.ts +++ b/src/shared/worktree/create-types.ts @@ -1,4 +1,5 @@ import type { ExecutionHostId } from '../execution-host' +import type { ArchiveHookOverride } from './archive-hook-removal-gate' import type { WorkspaceSource } from '../workspace-source' import type { TaskSourceContext } from '../task-source-context' import type { WorkspaceKey } from '../folder-workspace-types' @@ -207,6 +208,8 @@ export type PreservedWorktreeBranch = { export type RemoveWorktreeResult = { preservedBranch?: PreservedWorktreeBranch + /** Present only when a FAILED archive hook was explicitly waived for this removal (#19334). */ + archiveHookOverride?: ArchiveHookOverride } export type ForceDeleteWorktreeBranchResult = { From f107499e4423ff9d9a0bc203dad3ca56c41ce7f8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:24:30 -0700 Subject: [PATCH 27/34] fix(lint): enable anti-slop/no-reflect-get (#20786) `anti-slop/no-reflect-get` rejects every call to `Reflect.get`. The reflective read bypasses ordinary property access and throws away the type evidence the compiler would otherwise give you: the result is `any`/`unknown` with no narrowing, so a typo in the key or a shape drift in the source object is invisible until runtime. The rule's remedy is to parse dynamic input into a named domain type (or narrow it with `in`) and then read the field normally. Baseline: 86 violations across 67 files. Now zero unsuppressed violations under `npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile`. Fix pattern ----------- 44 of the 86 were rewritten. The dominant shape was an `unknown` value read through `Reflect.get` right after a `typeof === 'object'` guard; those became `in`-narrowed property access, which TypeScript checks: - Reflect.get(value, 'agents') + 'agents' in value ? value.agents : null Two further shapes: - `Reflect.get(Object(x), 'k')` on a possibly-primitive envelope became a small named reader that boxes once and indexes a `Record` (`settingsField` in mobile/src/transport/settings-read-operations.ts). - Tests reaching into private state moved to TypeScript's checked bracket-index escape hatch (`runtime['layoutQueues']`), or to a documented read-only accessor on the owning class (`SearchSubprocessLineAccumulator.retainedCapacityBytes()`, `CodexSubagentExecutions.retentionSizes()`). No type assertion was added anywhere: the diff contains zero net-new `as` casts, `as any`, `as unknown as`, `@ts-ignore`, or `@ts-expect-error`, so nothing was laundered into the sibling assertion rules. Suppressions ------------ 42x `// oxlint-disable-next-line anti-slop/no-reflect-get` across 38 files. Every one is the default-forward branch of a `Proxy` `get` trap: get(target, property, receiver) { ... return Reflect.get(target, property, receiver) } `Reflect.get(target, property, receiver)` is the only construct that forwards with correct `receiver` semantics; `target[property]` invokes an accessor with the wrong `this` and silently breaks getters that read sibling state. There is no typed alternative, so these are suppressed rather than rewritten. 3x `// oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface` in tests/e2e/github-url-smart-input-transition.spec.ts, tests/e2e/linear-url-workspace-entry.spec.ts, and tests/e2e/worktree-active-delete-scroll-position.spec.ts. Replacing `Reflect.get(window, 'x')` with typed `window.x` requires a `declare global { interface Window }` block, and `interface` is mandatory for declaration merging. Matches the existing convention at tests/e2e/helpers/runtime-types.ts:63. 1x `// eslint-disable-next-line no-var -- main-process gate handle for this spec` in tests/e2e/project-group-creation-visibility.spec.ts, for the same reason a `var` global is needed to type the handle. Matches tests/e2e/agent-session-log-tail-stability.spec.ts:24. Also updates two source-text anchors in mobile's rpc-recording mutation harness (mobile/src/test-support/rpc-recording/operation-mutations.ts and recording-runner.test.ts), which pin the exact text of the rewritten line in settings-read-operations.ts and would otherwise fail with "Mutant anchor matched 0 sites, expected 1". --- config/oxlint-anti-slop.json | 2 +- .../mutants/operation-mutations.ts | 4 +- .../rpc-recording/recording-runner.test.ts | 2 +- .../src/transport/settings-read-operations.ts | 10 ++++- .../managed-hook-detection-commands.ts | 8 ++-- ...session-document-stream-boundaries.test.ts | 2 +- src/main/codex/codex-prompt-registry.ts | 7 +--- .../codex/codex-subagent-executions.test.ts | 8 ++-- src/main/codex/codex-subagent-executions.ts | 5 +++ src/main/daemon/daemon-client-rpc-request.ts | 7 +++- ...ructured-agent-session-close-retry.test.ts | 1 + ...issing-worktree-terminal-reconciliation.ts | 1 + .../mobile-subscribe-integration.test.ts | 40 ++++++++++++------- .../terminal-listing.spec.ts | 3 +- .../mailbox-pointer-stage.test.ts | 6 ++- .../runtime/remote-desktop-driver.test.ts | 20 +++++----- .../runtime/runtime-linear-command-surface.ts | 1 + ...erminal-orphan-topology-validation.test.ts | 1 + .../structured-agent-session-runtime.test.ts | 4 +- .../skill-bundle-install-service.test.ts | 1 + .../skill-cloud-grant-installation.test.ts | 1 + ...pload-session-admission-regression.test.ts | 9 ++--- src/main/updater-test-harness.ts | 3 +- src/main/workspace-space-repo-scan.test.ts | 1 + src/main/worktree-name-retirement.ts | 7 +++- src/relay/managed-hook-installer.ts | 4 +- ...handler-inventory-process-evidence.test.ts | 1 + src/relay/pty-source-credit-ledger.test.ts | 1 + ...ard-snapshot-orchestration-routing.test.ts | 1 + .../use-agent-row-conversation-name.test.ts | 1 + .../components/editor/tiptap-marked-facade.ts | 1 + ...ssue-attribute-filter-primary-team.test.ts | 1 + .../ProjectCombobox.dialog-handoff.test.tsx | 3 +- .../active-checks-status.test.ts | 1 + ...rent-pr-checks-projection-selector.test.ts | 5 ++- .../parent-pr-checks-projection-selector.ts | 3 +- ...worktree-agent-orchestration-batch.test.ts | 3 ++ ...worktree-agent-orchestration-index.test.ts | 1 + .../terminal-tab-activity-status.test.ts | 1 + ...-watcher-synchronization.react185.test.tsx | 1 + ...minal-provider-snapshot-capability.test.ts | 1 + .../useIpcEvents-rate-limit-hydration.test.ts | 4 +- .../hooks/useIpcEvents-updater-status.test.ts | 7 ++-- .../hooks/useIpcEvents-zoom-routing.test.ts | 8 ++-- .../src/lib/codex-pane-selection-lane.test.ts | 3 +- .../pane-manager/terminal-ligatures-addon.ts | 1 + ...ession-write-subscriber-allocation.test.ts | 1 + .../store/project-host-setup-selector.test.ts | 4 +- src/renderer/src/store/selectors.test.ts | 1 + .../slices/tab-group-reference-repair.test.ts | 1 + ...ted-workspace-reconciliation-batch.test.ts | 1 + .../slices/terminal-tab-owner-index.test.ts | 1 + .../slices/terminal-tab-title-batch.test.ts | 1 + ...ace-cleanup-enrichment-performance.test.ts | 1 + .../src/web/preload-api/web-fallback-api.ts | 1 + .../web/web-preload-api-composition.test.ts | 3 +- .../web/web-preload-api-runtime-calls.test.ts | 2 +- src/shared/automation-list-scope.test.ts | 1 + .../host-balanced-listing-scaling.test.ts | 1 + src/shared/pr-bot-author-overrides.test.ts | 1 + src/shared/search-subprocess-lines.test.ts | 4 +- src/shared/search-subprocess-lines.ts | 5 +++ .../host-terminal-runtime-stub.ts | 1 + .../versioned-agent-session-wire.ts | 4 +- .../github-url-smart-input-transition.spec.ts | 36 +++++++++++++---- tests/e2e/linear-url-workspace-entry.spec.ts | 14 +++++-- .../project-group-creation-visibility.spec.ts | 7 +++- ...tree-active-delete-scroll-position.spec.ts | 10 ++++- 68 files changed, 211 insertions(+), 96 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 9e5eda11ae0..bf41f269552 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -33,7 +33,7 @@ "anti-slop/no-object-parameters": "off", "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", - "anti-slop/no-reflect-get": "off", + "anti-slop/no-reflect-get": "error", "anti-slop/no-runtime-typeof": "off", "anti-slop/no-shape-in-symbol-names": "off", "anti-slop/no-unknown-parameters": "off", diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index 457bae3c2a8..6a4c488b296 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -36,8 +36,8 @@ export const OPERATION_MUTATIONS = { // Reads the overrides one level above the settings envelope. 'bot-overrides-envelope': { file: 'settings-read-operations.ts', - before: "settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')", - after: "raw == null ? undefined : Reflect.get(Object(raw), 'prBotAuthorOverrides')" + before: "settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')", + after: "raw == null ? undefined : settingsField(raw, 'prBotAuthorOverrides')" }, // Publishes the settings envelope instead of the accepted operation value. 'workspace-context-envelope': { 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 0a92941e602..7b523be49ff 100644 --- a/mobile/src/test-support/rpc-recording/recording-runner.test.ts +++ b/mobile/src/test-support/rpc-recording/recording-runner.test.ts @@ -445,7 +445,7 @@ describe('recording boundaries', () => { const root = mkdtempSync(join(tmpdir(), 'rpc-mutant-')) try { const anchor = - "const overrides = settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')" + "const overrides = settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')" mkdirSync(join(root, 'mod'), { recursive: true }) writeFileSync( join(root, 'mod/settings-read-operations.ts'), diff --git a/mobile/src/transport/settings-read-operations.ts b/mobile/src/transport/settings-read-operations.ts index 549b512c87c..d33e66458d2 100644 --- a/mobile/src/transport/settings-read-operations.ts +++ b/mobile/src/transport/settings-read-operations.ts @@ -7,6 +7,12 @@ function settingsMember(raw: unknown): unknown { return boxed!.settings } +// Box primitives so a non-object settings value reads as absent instead of throwing. +function settingsField(settings: unknown, key: string): unknown { + const boxed: Record = Object(settings) + return boxed[key] +} + // Settings remain opaque: callers historically retain fields without validating their shapes. const settingsReader: RpcCompatibleReader = (raw) => ({ compatible: true, @@ -27,7 +33,7 @@ const optionalSettingsReader: RpcCompatibleReader = (raw) => { const settings = raw == null ? undefined : settingsMember(raw) const overrides: unknown = - settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides') + settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides') return { compatible: true, variant: 'bot-logins', @@ -85,7 +91,7 @@ export const newTabSettingsRead = bindDeferredRpcOperation( const copyTrimsGutterReader: RpcCompatibleReader = (raw) => { const settings = raw == null ? undefined : settingsMember(raw) const trims: unknown = - settings == null ? undefined : Reflect.get(Object(settings), 'terminalCopyTrimsGutter') + settings == null ? undefined : settingsField(settings, 'terminalCopyTrimsGutter') return { compatible: true, variant: 'copy-trims-gutter', diff --git a/src/main/agent-hooks/managed-hook-detection-commands.ts b/src/main/agent-hooks/managed-hook-detection-commands.ts index b5183af7ec0..f9507160e27 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.ts @@ -53,10 +53,12 @@ export function readManagedHookDetectionResult(value: unknown): { if (value === null || typeof value !== 'object') { return { agents: [], claudeVersion: null } } - const agents = detectedManagedHookAgents(Reflect.get(value, 'agents')) - const versions = Reflect.get(value, 'versions') + const agents = detectedManagedHookAgents('agents' in value ? value.agents : null) + const versions = 'versions' in value ? value.versions : null const rawClaudeVersion = - versions !== null && typeof versions === 'object' ? Reflect.get(versions, 'claude') : null + versions !== null && typeof versions === 'object' && 'claude' in versions + ? versions.claude + : null return { agents, claudeVersion: parseClaudeCliVersion( diff --git a/src/main/ai-vault/session-document-stream-boundaries.test.ts b/src/main/ai-vault/session-document-stream-boundaries.test.ts index 67901707cbd..f6dfe349000 100644 --- a/src/main/ai-vault/session-document-stream-boundaries.test.ts +++ b/src/main/ai-vault/session-document-stream-boundaries.test.ts @@ -88,7 +88,7 @@ describe('independent JSON boundary review', () => { expect(await parseHermesSessionDocument(file, bytes(content, 1), 'linux', options)).toEqual( await parseHermesSessionContent(file, content, 'linux', options) ) - expect(Reflect.get({}, 'polluted')).toBeUndefined() + expect('polluted' in {}).toBe(false) }) for (const content of [ '{"messages":[],}', diff --git a/src/main/codex/codex-prompt-registry.ts b/src/main/codex/codex-prompt-registry.ts index c6d0d7f4bef..f3ba3fa3601 100644 --- a/src/main/codex/codex-prompt-registry.ts +++ b/src/main/codex/codex-prompt-registry.ts @@ -9,6 +9,7 @@ import { readQuestionIds, readQuestionOptionAnswers } from './codex-prompt-registry-bounds' +import { readRecord, readString as readRecordString } from './codex-item-field-readers' export const CODEX_COMMAND_APPROVAL_METHOD = 'item/commandExecution/requestApproval' export const CODEX_FILE_CHANGE_APPROVAL_METHOD = 'item/fileChange/requestApproval' @@ -36,11 +37,7 @@ export type CodexPromptClaim = { } 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 + return readRecordString(readRecord(params), key) } export function isCodexPromptMethod(method: string): boolean { diff --git a/src/main/codex/codex-subagent-executions.test.ts b/src/main/codex/codex-subagent-executions.test.ts index aceff7ab465..c7f7955bb23 100644 --- a/src/main/codex/codex-subagent-executions.test.ts +++ b/src/main/codex/codex-subagent-executions.test.ts @@ -13,8 +13,9 @@ describe('CodexSubagentExecutions retention and identity', () => { executions.observeTurn(id, id, 'completed') } expect(executions.workingChildren().map((child) => child.agentThreadId)).toEqual(['long-lived']) - expect(Reflect.get(executions, 'children').size).toBeLessThanOrEqual(128) - expect(Reflect.get(executions, 'settledTurns').size).toBeLessThanOrEqual(256) + const { children, settledTurns } = executions.retentionSizes() + expect(children).toBeLessThanOrEqual(128) + expect(settledTurns).toBeLessThanOrEqual(256) }) it('retains early live owner events at capacity and makes room only after settlement', () => { @@ -45,7 +46,6 @@ describe('CodexSubagentExecutions retention and identity', () => { executions.observeTurn('child', 'turn', 'failed') expect(executions.workingChildren()[0]?.execution?.turnId).toBe('new-turn') executions.clear() - expect(Reflect.get(executions, 'children').size).toBe(0) - expect(Reflect.get(executions, 'settledTurns').size).toBe(0) + expect(executions.retentionSizes()).toEqual({ children: 0, settledTurns: 0 }) }) }) diff --git a/src/main/codex/codex-subagent-executions.ts b/src/main/codex/codex-subagent-executions.ts index cd33b4eb1d5..d5ac62bfa74 100644 --- a/src/main/codex/codex-subagent-executions.ts +++ b/src/main/codex/codex-subagent-executions.ts @@ -106,6 +106,11 @@ export class CodexSubagentExecutions { this.settledTurns.clear() } + /** Retention bounds are not observable through the child/turn API, so expose the two counts. */ + retentionSizes(): { children: number; settledTurns: number } { + return { children: this.children.size, settledTurns: this.settledTurns.size } + } + private child(agentThreadId: string): CodexExecutionChild | undefined { const existing = this.children.get(agentThreadId) if (existing) { diff --git a/src/main/daemon/daemon-client-rpc-request.ts b/src/main/daemon/daemon-client-rpc-request.ts index fb72538eaa0..2bd4f6741de 100644 --- a/src/main/daemon/daemon-client-rpc-request.ts +++ b/src/main/daemon/daemon-client-rpc-request.ts @@ -59,8 +59,11 @@ export function requestDaemonRpc(opts: DaemonRpcRequestOptions): Promise { const createTimeoutError = (): DaemonRequestTimeoutError => new DaemonRequestTimeoutError(`Request ${type} timed out after ${opts.timeoutMs}ms`) const createSessionId = - type === 'createOrAttach' && payload !== null && typeof payload === 'object' - ? Reflect.get(payload, 'sessionId') + type === 'createOrAttach' && + payload !== null && + typeof payload === 'object' && + 'sessionId' in payload + ? payload.sessionId : null const requestPayload = type === 'createOrAttach' && payload !== null && typeof payload === 'object' diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts index b79091c7f9f..1141f821174 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts @@ -91,6 +91,7 @@ function flakyClose(journal: AgentSessionJournal, failures: number): AgentSessio return new Proxy(journal, { get(target, property, receiver) { if (property !== 'close') { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } return async () => { diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts index 11f888a5404..c8d5e06900c 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -24,6 +24,7 @@ function withSharedProcessSnapshot(provider: IPtyProvider): IPtyProvider { // receiver, a provider whose own method called `this.listProcesses()` // would silently read this sweep's cached snapshot instead of the live // host — the batching must not leak past the calls it was built for. + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const member: unknown = Reflect.get(target, property) return typeof member === 'function' ? member.bind(target) : member } diff --git a/src/main/runtime/mobile-subscribe-integration.test.ts b/src/main/runtime/mobile-subscribe-integration.test.ts index 61e064681c1..b3be748d5f0 100644 --- a/src/main/runtime/mobile-subscribe-integration.test.ts +++ b/src/main/runtime/mobile-subscribe-integration.test.ts @@ -87,8 +87,24 @@ const store = { } } +/** Reclaim clears protected retention maps that no public reader exposes. */ +class ObservableRuntime extends OrcaRuntimeService { + get restoreTimers(): typeof this.pendingRestoreTimers { + return this.pendingRestoreTimers + } + get softLeavers(): typeof this.pendingSoftLeavers { + return this.pendingSoftLeavers + } + get fitOverrides(): typeof this.terminalFitOverrides { + return this.terminalFitOverrides + } + get drivers(): typeof this.terminalDrivers { + return this.terminalDrivers + } +} + function createRuntime() { - const runtime = new OrcaRuntimeService(store) + const runtime = new ObservableRuntime(store) const ptySizes = new Map() ptySizes.set('pty-1', { cols: 150, rows: 40 }) ptySizes.set('pty-2', { cols: 120, rows: 35 }) @@ -865,9 +881,9 @@ describe('mobile subscribe integration', () => { runtime.handleMobileUnsubscribe('pty-1', 'client-a') await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 }) - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map + const pendingRestore = runtime.restoreTimers pendingRestore.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-b' }) - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingSoft = runtime.softLeavers expect(pendingSoft.has('pty-1')).toBe(true) await runtime.reclaimTerminalForDesktop('pty-1') expect(pendingRestore.has('pty-1')).toBe(false) @@ -878,10 +894,10 @@ describe('mobile subscribe integration', () => { const { runtime } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) runtime.handleMobileUnsubscribe('pty-1', 'client-a') - ;(Reflect.get(runtime, 'terminalFitOverrides') as Map).delete('pty-1') + runtime.fitOverrides.delete('pty-1') - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingRestore = runtime.restoreTimers + const pendingSoft = runtime.softLeavers await runtime.reclaimTerminalForDesktop('pty-1') expect(pendingRestore.has('pty-1')).toBe(false) expect(pendingSoft.has('pty-1')).toBe(false) @@ -891,15 +907,11 @@ describe('mobile subscribe integration', () => { const { runtime } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) runtime.handleMobileUnsubscribe('pty-1', 'client-a') - ;(Reflect.get(runtime, 'terminalFitOverrides') as Map).delete('pty-1') - ;( - Reflect.get(runtime, 'terminalDrivers') as { - set: (ptyId: string, driver: { kind: 'idle' }) => void - } - ).set('pty-1', { kind: 'idle' }) + runtime.fitOverrides.delete('pty-1') + runtime.drivers.set('pty-1', { kind: 'idle' }) - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingRestore = runtime.restoreTimers + const pendingSoft = runtime.softLeavers expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false) expect(pendingRestore.has('pty-1')).toBe(false) expect(pendingSoft.has('pty-1')).toBe(false) diff --git a/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts index 9d87b49f904..fae08b482fb 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts @@ -436,10 +436,11 @@ describe('OrcaRuntimeService', () => { throw new Error('onPtyData should use the PTY leaf index') } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, prop, target) return typeof value === 'function' ? value.bind(target) : value } - }) as Map + }) runtime.onPtyData(`pty-${targetIndex}`, 'hello indexed\n', 123) diff --git a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts index ed02d7e71ec..4f26d57555b 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts @@ -99,10 +99,11 @@ describe('mailbox pointer staging watermark', () => { throw new Error('SQLITE_BUSY') } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } - }) as OrchestrationDb + }) const state = new OrchestrationMailboxPointerState() const args = stageArgs(db, state) @@ -178,10 +179,11 @@ describe('mailbox pointer staging watermark', () => { stealNextClaim = false return () => false } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } - }) as OrchestrationDb + }) const writePty = vi.fn(() => WRITE_ACCEPTED) const delivery = new OrchestrationMailboxPointerDelivery({ diff --git a/src/main/runtime/remote-desktop-driver.test.ts b/src/main/runtime/remote-desktop-driver.test.ts index bf2370ee924..bb25c20a94a 100644 --- a/src/main/runtime/remote-desktop-driver.test.ts +++ b/src/main/runtime/remote-desktop-driver.test.ts @@ -340,17 +340,18 @@ describe('remote desktop viewer width driver', () => { const { runtime } = createRuntime() await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 100, 30) await runtime.updateRemoteDesktopViewer('pty-1', 'sub-B', 'viewer-B', 80, 24, false) - const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map< - string, - { running: Promise; pending: { target: { ownerSubscriptionKey?: string } }[] } - > - layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) + const layoutQueues = runtime['layoutQueues'] + layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) void runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 90, 28) void runtime.claimRemoteDesktopViewer('pty-1', 'sub-B') expect( - layoutQueues.get('pty-1')?.pending.map(({ target }) => target.ownerSubscriptionKey) + layoutQueues + .get('pty-1') + ?.pending.map(({ target }) => + 'ownerSubscriptionKey' in target ? target.ownerSubscriptionKey : undefined + ) ).toEqual(['sub-A', 'sub-B']) layoutQueues.delete('pty-1') }) @@ -358,11 +359,8 @@ describe('remote desktop viewer width driver', () => { it('makes a host claim join a pending disconnect reclaim', async () => { const { runtime } = createRuntime() await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 80, 24) - const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map< - string, - { running: Promise; pending: { waiters: unknown[] }[] } - > - layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) + const layoutQueues = runtime['layoutQueues'] + layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) void runtime.unregisterRemoteDesktopViewer('pty-1', 'sub-A') void runtime.claimRemoteDesktopHost('pty-1', 150, 40) diff --git a/src/main/runtime/runtime-linear-command-surface.ts b/src/main/runtime/runtime-linear-command-surface.ts index 0f234768203..cf9ad9e69df 100644 --- a/src/main/runtime/runtime-linear-command-surface.ts +++ b/src/main/runtime/runtime-linear-command-surface.ts @@ -47,6 +47,7 @@ function overrideAwareReceiver( return override.bind(facade) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. return Reflect.get(target, property, proxyReceiver) } }) diff --git a/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts b/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts index cb8f22e864b..9fff4da0edf 100644 --- a/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts +++ b/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts @@ -35,6 +35,7 @@ it('validates large restored MRU lists with linear tab-order reads', () => { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, key, receiver) } }) diff --git a/src/main/runtime/structured-agent-session-runtime.test.ts b/src/main/runtime/structured-agent-session-runtime.test.ts index 2ce51b1c29b..29ee4740414 100644 --- a/src/main/runtime/structured-agent-session-runtime.test.ts +++ b/src/main/runtime/structured-agent-session-runtime.test.ts @@ -5,7 +5,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { agentSessionJournalCloseRetries } from '../native-chat/agent-session-journal/journal-close-retry' import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-store-test-open' -import type { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store' import type { AgentSessionClaimStatus, AgentSessionExecutionLocation, @@ -346,6 +345,7 @@ describe('a teardown that fails is retried by the next stop', () => { const flaky = new Proxy(real, { get(target, property, receiver) { if (property !== 'close') { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } return async () => { @@ -356,7 +356,7 @@ describe('a teardown that fails is retried by the next stop', () => { await target.close() } } - }) as AgentSessionJournal + }) await agentSessionJournalCloseRetries.closeOrRetain(flaky) // The host's teardown runs the registry retry, so this stop surfaces it. diff --git a/src/main/skills/skill-bundle-install-service.test.ts b/src/main/skills/skill-bundle-install-service.test.ts index 78c8eae0652..f5de4faf7eb 100644 --- a/src/main/skills/skill-bundle-install-service.test.ts +++ b/src/main/skills/skill-bundle-install-service.test.ts @@ -104,6 +104,7 @@ describe('skill bundle installation', () => { } } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, target) as unknown return typeof value === 'function' ? value.bind(target) : value } diff --git a/src/main/skills/skill-cloud-grant-installation.test.ts b/src/main/skills/skill-cloud-grant-installation.test.ts index 5355e6f4869..8534ce7a176 100644 --- a/src/main/skills/skill-cloud-grant-installation.test.ts +++ b/src/main/skills/skill-cloud-grant-installation.test.ts @@ -195,6 +195,7 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])( if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/main/skills/skill-upload-session-admission-regression.test.ts b/src/main/skills/skill-upload-session-admission-regression.test.ts index f0c7cd54405..9bbdb71ef68 100644 --- a/src/main/skills/skill-upload-session-admission-regression.test.ts +++ b/src/main/skills/skill-upload-session-admission-regression.test.ts @@ -4,6 +4,7 @@ import type * as NodeFsPromises from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' +import type { SkillUploadRetainedPaths } from './skill-upload-retained-paths' import { SkillUploadSessionService } from './skill-upload-session-service' const roots: string[] = [] @@ -30,10 +31,6 @@ vi.mock('node:fs/promises', async (importOriginal) => { } }) -type RetainedPathCleanup = { - removeFailedCleanup(path: string): Promise -} - afterEach(async () => { vi.useRealTimers() openGate.release = null @@ -51,8 +48,8 @@ function identity(bytes: Buffer) { } } -function retainedPathCleanup(service: SkillUploadSessionService): RetainedPathCleanup { - return Reflect.get(service, 'retainedPaths') as RetainedPathCleanup +function retainedPathCleanup(service: SkillUploadSessionService): SkillUploadRetainedPaths { + return service['retainedPaths'] } async function stagedArchiveCount(uploads: string): Promise { diff --git a/src/main/updater-test-harness.ts b/src/main/updater-test-harness.ts index 36687d0a79e..83379b7ac9b 100644 --- a/src/main/updater-test-harness.ts +++ b/src/main/updater-test-harness.ts @@ -146,6 +146,7 @@ export function createUpdaterMocks(): UpdaterMocks { const loadedGeneration = currentGeneration return new Proxy(autoUpdaterMock, { get(target, property) { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. const value = Reflect.get(target, property) if (loadedGeneration === currentGeneration || typeof value !== 'function') { return value @@ -155,7 +156,7 @@ export function createUpdaterMocks(): UpdaterMocks { set(target, property, value) { return loadedGeneration === currentGeneration ? Reflect.set(target, property, value) : true } - }) as AutoUpdaterMock + }) } const reset = () => { diff --git a/src/main/workspace-space-repo-scan.test.ts b/src/main/workspace-space-repo-scan.test.ts index fbf570f12c1..5447ca93550 100644 --- a/src/main/workspace-space-repo-scan.test.ts +++ b/src/main/workspace-space-repo-scan.test.ts @@ -15,6 +15,7 @@ describe('summarizeWorkspaceSpaceRows', () => { ) { reads[property] += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/main/worktree-name-retirement.ts b/src/main/worktree-name-retirement.ts index 58ffc99bbfd..99a6fdd559c 100644 --- a/src/main/worktree-name-retirement.ts +++ b/src/main/worktree-name-retirement.ts @@ -75,7 +75,12 @@ export function normalizeRetirableGeneratedName(name: string): string | null { /** A sparse create error carries this marker only when its rollback also failed, leaving the path * occupied even though creation rejected. */ export function failedWorktreeCreationNeedsRetirement(error: unknown): boolean { - return typeof error === 'object' && error !== null && Reflect.get(error, 'cleanupFailed') === true + return ( + typeof error === 'object' && + error !== null && + 'cleanupFailed' in error && + error.cleanupFailed === true + ) } async function getRetirementProbePath( diff --git a/src/relay/managed-hook-installer.ts b/src/relay/managed-hook-installer.ts index bdd65a789f6..3fa57dd5ed8 100644 --- a/src/relay/managed-hook-installer.ts +++ b/src/relay/managed-hook-installer.ts @@ -45,7 +45,9 @@ function readAgents(params: unknown): AgentHookTarget[] { function readClaudeVersion(params: unknown): string | undefined { const raw = - params !== null && typeof params === 'object' ? Reflect.get(params, 'claudeVersion') : null + params !== null && typeof params === 'object' && 'claudeVersion' in params + ? params.claudeVersion + : null return parseClaudeCliVersion(typeof raw === 'string' ? raw : null) ?? undefined } diff --git a/src/relay/pty-handler-inventory-process-evidence.test.ts b/src/relay/pty-handler-inventory-process-evidence.test.ts index c12e6da62b4..f0d5b068304 100644 --- a/src/relay/pty-handler-inventory-process-evidence.test.ts +++ b/src/relay/pty-handler-inventory-process-evidence.test.ts @@ -100,6 +100,7 @@ function countingRows(rows: ProcessTableRow[]): { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/relay/pty-source-credit-ledger.test.ts b/src/relay/pty-source-credit-ledger.test.ts index f4ff366c946..57c00d1adec 100644 --- a/src/relay/pty-source-credit-ledger.test.ts +++ b/src/relay/pty-source-credit-ledger.test.ts @@ -104,6 +104,7 @@ describe('RelayPtySourceCreditLedger', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { indexedReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts index 424c936ab0f..97697f5f8a4 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts @@ -109,6 +109,7 @@ describe('buildDashboardSnapshot orchestration routing', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts b/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts index dbf5cae455b..9065cc64e34 100644 --- a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts +++ b/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts @@ -113,6 +113,7 @@ describe('useAgentRowConversationName', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { tabReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/components/editor/tiptap-marked-facade.ts b/src/renderer/src/components/editor/tiptap-marked-facade.ts index 823217be6ef..80cadb343f9 100644 --- a/src/renderer/src/components/editor/tiptap-marked-facade.ts +++ b/src/renderer/src/components/editor/tiptap-marked-facade.ts @@ -74,6 +74,7 @@ export function createTiptapMarkedFacade(): typeof marked { return facade } default: + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts index 5a4bfe471a2..9adb06f6586 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts @@ -55,6 +55,7 @@ it('selects a primary team without pairwise membership checks or sorting all tea if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, key, receiver) } } diff --git a/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx b/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx index 339fd60840f..f77c1a34928 100644 --- a/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx +++ b/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx @@ -68,7 +68,8 @@ beforeEach(() => { ? element.getAttribute('data-state') === 'closed' ? 'exit' : 'enter' - : Reflect.get(target, property) + : // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. + Reflect.get(target, property) }) } return style diff --git a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts index 5bf09276c72..84b65d83f5b 100644 --- a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts +++ b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts @@ -182,6 +182,7 @@ describe('getActiveChecksStatus caching', () => { { get(target, prop, receiver) { reads.add(prop) + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, prop, receiver) }, has(target, prop) { diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts index 0616bb510e9..7d7e1b1e4ad 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts @@ -61,9 +61,10 @@ describe('parent PR checks projection selector', () => { const observedCache = new Proxy( {}, { - get: (target, property, receiver) => { + get: (target, property) => { cacheRead(property) - return Reflect.get(target, property, receiver) + const entries: Record = target + return entries[property] } } ) diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts index 02677575e48..42ad55de0a8 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts @@ -25,6 +25,7 @@ function trackCacheReads( ): ReviewCacheState[K] { return new Proxy(state[cacheName], { get: (target, property, receiver) => { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, receiver) if (typeof property === 'string') { dependencies.push({ cacheName, key: property, value }) @@ -41,7 +42,7 @@ function dependenciesAreCurrent( ): boolean { return dependencies.every( ({ cacheName, key, value }) => - state[cacheName] === previousState[cacheName] || Reflect.get(state[cacheName], key) === value + state[cacheName] === previousState[cacheName] || state[cacheName][key] === value ) } diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts index 705a1e0c43e..3d5a5e14c8c 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts @@ -364,6 +364,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) @@ -456,6 +457,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) @@ -636,6 +638,7 @@ describe('selectRuntimeAgentOrchestrationBatch live-map churn', () => { if (typeof key === 'string') { reads.push(key) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(source, key, receiver) } }) diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts index 2d9ba917520..d97a7d41906 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts @@ -312,6 +312,7 @@ describe('selectWorktreeAgentOrchestration', () => { if (typeof key === 'string') { onRead() } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(source, key, receiver) } }) diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts index 8511fff1913..fc626b99132 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts @@ -356,6 +356,7 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { ownKeys, get: (target, property, receiver) => { valueReads += 1 + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx index 1e385354c21..bb2f44320cd 100644 --- a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx @@ -218,6 +218,7 @@ describe('parked terminal watcher synchronization', () => { if (typeof property === 'string') { harness.reconciliationPtyReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts b/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts index 8825f0489c5..e1985aa2555 100644 --- a/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts +++ b/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts @@ -79,6 +79,7 @@ describe('terminal provider snapshot capabilities', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { indexedReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts b/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts index a95d1095d5e..f1116ad3001 100644 --- a/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts @@ -118,8 +118,8 @@ describe('useIpcEvents rate-limit hydration', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } diff --git a/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts b/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts index 1ff5554f59c..002e071a19d 100644 --- a/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts @@ -281,10 +281,11 @@ describe('useIpcEvents updater integration', () => { })) vi.doMock('@/lib/zoom-events', () => ({ dispatchZoomLevelChanged: vi.fn() })) - const makeEvents = (target: Record = {}): Record => + const makeEvents = ( + target: Record = {} + ): Record => new Proxy(target, { - get: (namespace, prop) => - prop in namespace ? Reflect.get(namespace, prop) : () => () => {} + get: (namespace, prop) => (prop in namespace ? namespace[prop] : () => () => {}) }) vi.stubGlobal('window', { diff --git a/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts b/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts index dc954e451a6..5e20aa0994b 100644 --- a/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts @@ -180,8 +180,8 @@ describe('useIpcEvents zoom routing', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } @@ -326,8 +326,8 @@ describe('useIpcEvents zoom routing', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } diff --git a/src/renderer/src/lib/codex-pane-selection-lane.test.ts b/src/renderer/src/lib/codex-pane-selection-lane.test.ts index a4b1dcefc56..c6729d291ae 100644 --- a/src/renderer/src/lib/codex-pane-selection-lane.test.ts +++ b/src/renderer/src/lib/codex-pane-selection-lane.test.ts @@ -367,9 +367,10 @@ describe('resolveCodexPaneSelectionLane', () => { if (property === 'worktreesByRepo') { throw new Error('state read blew up') } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. return Reflect.get(target, property) } - }) as LaneState + }) // Why: this call sits outside the scan's per-pane failure guard, so a throw // would lose the notice for every pane in the batch, not just this one. expect( diff --git a/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts b/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts index 256f38befdc..774eb7e8fb7 100644 --- a/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts +++ b/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts @@ -111,6 +111,7 @@ export class TerminalLigaturesAddon extends LigaturesAddon { target.refresh(start, end) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, target) as unknown return typeof value === 'function' ? value.bind(target) : value } diff --git a/src/renderer/src/lib/session-write-subscriber-allocation.test.ts b/src/renderer/src/lib/session-write-subscriber-allocation.test.ts index 74c9c6db968..3f2c5ce284c 100644 --- a/src/renderer/src/lib/session-write-subscriber-allocation.test.ts +++ b/src/renderer/src/lib/session-write-subscriber-allocation.test.ts @@ -210,6 +210,7 @@ describe('session write subscriber allocation', () => { if (typeof property === 'string') { read.add(property) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/project-host-setup-selector.test.ts b/src/renderer/src/store/project-host-setup-selector.test.ts index 610e44c1feb..c6de8f65820 100644 --- a/src/renderer/src/store/project-host-setup-selector.test.ts +++ b/src/renderer/src/store/project-host-setup-selector.test.ts @@ -20,8 +20,7 @@ function countCollectionReads(items: readonly T[]): { get(array, property) { if (property === 'map' || property === 'flatMap') { counters[property] += 1 - const method = Reflect.get(array, property) as (...args: unknown[]) => unknown - return method.bind(array) + return array[property].bind(array) } if (property === Symbol.iterator) { counters.iterator += 1 @@ -30,6 +29,7 @@ function countCollectionReads(items: readonly T[]): { if (property === 'length') { counters.length += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(array, property) } }) diff --git a/src/renderer/src/store/selectors.test.ts b/src/renderer/src/store/selectors.test.ts index 45278caf40d..43491683ce3 100644 --- a/src/renderer/src/store/selectors.test.ts +++ b/src/renderer/src/store/selectors.test.ts @@ -695,6 +695,7 @@ describe('selectFloatingWorkspaceHasUnread', () => { if (typeof property === 'string') { terminalUnreadReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/store/slices/tab-group-reference-repair.test.ts b/src/renderer/src/store/slices/tab-group-reference-repair.test.ts index a7cb3dca125..e2f265ba6a6 100644 --- a/src/renderer/src/store/slices/tab-group-reference-repair.test.ts +++ b/src/renderer/src/store/slices/tab-group-reference-repair.test.ts @@ -35,6 +35,7 @@ describe('appendOwnedTabIdsToGroups', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { reads++ } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts b/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts index e6a7bfe7002..ebe87a28ab5 100644 --- a/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts +++ b/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts @@ -136,6 +136,7 @@ describe('whole-session workspace tab-model reconciliation', () => { if (property === 'filter') { scans += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts b/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts index 0600296d1d4..09ecdf4ccb9 100644 --- a/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts @@ -73,6 +73,7 @@ describe('terminal tab owner index', () => { if (typeof property === 'string' && property.startsWith('wt-')) { bucketVisits += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts b/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts index e269ffaae86..5fa1700357e 100644 --- a/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts @@ -140,6 +140,7 @@ describe('terminal tab title batches', () => { if (typeof property === 'string' && property.startsWith('wt-')) { bucketVisits += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts b/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts index ed710d1af7c..5478cdd973a 100644 --- a/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts +++ b/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts @@ -42,6 +42,7 @@ function countOpenFileScans( return target.filter(predicate) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/web/preload-api/web-fallback-api.ts b/src/renderer/src/web/preload-api/web-fallback-api.ts index 3cfef000ef1..2f59cd72615 100644 --- a/src/renderer/src/web/preload-api/web-fallback-api.ts +++ b/src/renderer/src/web/preload-api/web-fallback-api.ts @@ -4,6 +4,7 @@ export function withFallback(target: T, path: string[]): T { return new Proxy(target, { get(current, property, receiver) { if (property in current) { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(current, property, receiver) as unknown if (value && typeof value === 'object' && !Array.isArray(value)) { return withFallback(value as object, [...path, String(property)]) diff --git a/src/renderer/src/web/web-preload-api-composition.test.ts b/src/renderer/src/web/web-preload-api-composition.test.ts index f7091cac9bc..8bac20f5097 100644 --- a/src/renderer/src/web/web-preload-api-composition.test.ts +++ b/src/renderer/src/web/web-preload-api-composition.test.ts @@ -78,7 +78,8 @@ describe('web preload API composition', () => { 'telemetryAcknowledgeBanner' ]) expect(Object.keys(globals.window.api.projects)).toEqual([]) - expect(Reflect.get(globals.window.api.projects, 'then')).toBeUndefined() + const projects: Record = globals.window.api.projects + expect(projects.then).toBeUndefined() }) it('snapshots E2E config before runtime storage initialization', async () => { diff --git a/src/renderer/src/web/web-preload-api-runtime-calls.test.ts b/src/renderer/src/web/web-preload-api-runtime-calls.test.ts index fcf24437595..48d366d1a72 100644 --- a/src/renderer/src/web/web-preload-api-runtime-calls.test.ts +++ b/src/renderer/src/web/web-preload-api-runtime-calls.test.ts @@ -102,7 +102,7 @@ describe('web preload runtime calls', () => { if (!(rejection instanceof Error)) { throw new Error('Expected a domain Error rejection') } - expect(Reflect.get(rejection, 'code')).toBe('repo_unavailable') + expect('code' in rejection ? rejection.code : undefined).toBe('repo_unavailable') expect( JSON.parse(globals.storage.getItem('orca.web.runtimeEnvironment.v1') ?? '{}') ).toMatchObject({ runtimeId: 'runtime-domain-failure' }) diff --git a/src/shared/automation-list-scope.test.ts b/src/shared/automation-list-scope.test.ts index d9ff87eb99c..90915b4da06 100644 --- a/src/shared/automation-list-scope.test.ts +++ b/src/shared/automation-list-scope.test.ts @@ -210,6 +210,7 @@ describe('projectAutomationList', () => { if (property === 'map' || property === 'filter') { collectionMethodReads.push(String(property)) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/shared/host-balanced-listing-scaling.test.ts b/src/shared/host-balanced-listing-scaling.test.ts index aa7e40c2eb6..0a0d2a093bf 100644 --- a/src/shared/host-balanced-listing-scaling.test.ts +++ b/src/shared/host-balanced-listing-scaling.test.ts @@ -22,6 +22,7 @@ it('retires exhausted host buckets from subsequent listing rounds', () => { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/shared/pr-bot-author-overrides.test.ts b/src/shared/pr-bot-author-overrides.test.ts index 46476f0c5d1..e9e8d8a4e49 100644 --- a/src/shared/pr-bot-author-overrides.test.ts +++ b/src/shared/pr-bot-author-overrides.test.ts @@ -18,6 +18,7 @@ describe('PR bot author override normalization', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/shared/search-subprocess-lines.test.ts b/src/shared/search-subprocess-lines.test.ts index 34425801105..1a2c72de5fc 100644 --- a/src/shared/search-subprocess-lines.test.ts +++ b/src/shared/search-subprocess-lines.test.ts @@ -69,9 +69,9 @@ describe('SearchSubprocessLineAccumulator', () => { } expect(accepted).toBe(true) - expect(Reflect.get(parser, 'buffer')).toBeInstanceOf(Buffer) + expect(parser.retainedCapacityBytes()).toBeGreaterThanOrEqual(200_000) expect(parser.finish()).toBe('x'.repeat(200_000)) - expect(Reflect.get(parser, 'buffer')).toBeNull() + expect(parser.retainedCapacityBytes()).toBeNull() }) it('rejects invalid byte limits', () => { diff --git a/src/shared/search-subprocess-lines.ts b/src/shared/search-subprocess-lines.ts index 26f98b4d348..54e1defa5c0 100644 --- a/src/shared/search-subprocess-lines.ts +++ b/src/shared/search-subprocess-lines.ts @@ -65,6 +65,11 @@ export class SearchSubprocessLineAccumulator { this.bytes = 0 } + /** Capacity of the retained growable buffer, or null once it has been released. */ + retainedCapacityBytes(): number | null { + return this.buffer?.length ?? null + } + private append(segment: Buffer): void { const requiredBytes = this.bytes + segment.length if (!this.buffer || this.buffer.length < requiredBytes) { diff --git a/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts b/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts index f353ec3a097..db7bddcf2cf 100644 --- a/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts +++ b/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts @@ -198,6 +198,7 @@ export function createHostTerminalRuntimeStub( } return () => undefined } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts index 4d9e2de68ec..78efd420f83 100644 --- a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts +++ b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts @@ -69,10 +69,10 @@ type DispatcherModule = { function registeredMethodNames(methods: readonly unknown[]): string[] { return methods .flatMap((method) => { - if (!method || typeof method !== 'object') { + if (!method || typeof method !== 'object' || !('name' in method)) { return [] } - const name = Reflect.get(method, 'name') + const { name } = method return typeof name === 'string' ? [name] : [] }) .sort() diff --git a/tests/e2e/github-url-smart-input-transition.spec.ts b/tests/e2e/github-url-smart-input-transition.spec.ts index 3e2b0198812..f7e988a9c29 100644 --- a/tests/e2e/github-url-smart-input-transition.spec.ts +++ b/tests/e2e/github-url-smart-input-transition.spec.ts @@ -66,13 +66,24 @@ type TransitionFrame = { targetSelected: boolean } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Per-provider capture buffers written by startTransitionCapture below. + __githubUrlTransitionFrames?: TransitionFrame[] + __gitlabUrlTransitionFrames?: TransitionFrame[] + } +} + +type TransitionFrameKey = '__githubUrlTransitionFrames' | '__gitlabUrlTransitionFrames' + function pasteChord(): string { return process.platform === 'darwin' ? 'Meta+V' : 'Control+V' } async function startTransitionCapture( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, wrongTitle: string, targetTitle: string ): Promise { @@ -95,20 +106,29 @@ async function startTransitionCapture( requestAnimationFrame(capture) } } - Reflect.set(window, frameKey, frames) + window[frameKey] = frames capture() }, { frameKey, frameLimit: TRANSITION_FRAME_LIMIT, wrongTitle, targetTitle } ) } -async function readTransitionFrames(page: Page, frameKey: string): Promise { - return page.evaluate((key) => Reflect.get(window, key) as TransitionFrame[], frameKey) +async function readTransitionFrames( + page: Page, + frameKey: TransitionFrameKey +): Promise { + return page.evaluate((key) => { + const frames = window[key] + if (!frames) { + throw new Error(`Transition capture ${key} was never installed`) + } + return frames + }, frameKey) } async function expectLookupHeldWithoutStaleRow( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, targetUrl: string, wrongOption: Locator, targetOption: Locator @@ -125,7 +145,7 @@ async function expectLookupHeldWithoutStaleRow( async function expectExactTargetAfterLookup( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, targetUrl: string, targetOption: Locator ): Promise { @@ -268,7 +288,7 @@ test('a pasted GitHub URL never selects a stale cached issue', async ({ }) await expect(wrongOption).toBeVisible() - const frameKey = '__githubUrlTransitionFrames' + const frameKey: TransitionFrameKey = '__githubUrlTransitionFrames' await startTransitionCapture(orcaPage, frameKey, WRONG_TITLE, TARGET_TITLE) await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), TARGET_URL) @@ -317,7 +337,7 @@ test('a pasted GitLab URL never selects a stale cached merge request', async ({ }) await expect(wrongOption).toBeVisible() - const frameKey = '__gitlabUrlTransitionFrames' + const frameKey: TransitionFrameKey = '__gitlabUrlTransitionFrames' await startTransitionCapture(orcaPage, frameKey, GITLAB_WRONG_TITLE, GITLAB_TARGET_TITLE) await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), GITLAB_TARGET_URL) diff --git a/tests/e2e/linear-url-workspace-entry.spec.ts b/tests/e2e/linear-url-workspace-entry.spec.ts index d1cb2677257..16491a785db 100644 --- a/tests/e2e/linear-url-workspace-entry.spec.ts +++ b/tests/e2e/linear-url-workspace-entry.spec.ts @@ -25,6 +25,14 @@ const LINEAR_ISSUE: LinearIssue = { updatedAt: '2026-08-12T00:00:00.000Z' } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Set by the fixture below while a Linear lookup is deliberately held open. + __orcaTestReleaseLinearLookup?: () => void + } +} + function pasteChord(): string { return process.platform === 'darwin' ? 'Meta+V' : 'Control+V' } @@ -86,7 +94,7 @@ async function installLinearFixture( async function releaseHeldLinearLookup(page: Page): Promise { await page.evaluate(() => { - const release = Reflect.get(window, '__orcaTestReleaseLinearLookup') + const release = window.__orcaTestReleaseLinearLookup if (typeof release !== 'function') { throw new Error('Linear lookup is not held') } @@ -131,9 +139,7 @@ test.describe('Linear URL workspace entry', () => { await pasteLinearUrl(orcaPage, input) await expect .poll(() => - orcaPage.evaluate( - () => typeof Reflect.get(window, '__orcaTestReleaseLinearLookup') === 'function' - ) + orcaPage.evaluate(() => typeof window.__orcaTestReleaseLinearLookup === 'function') ) .toBe(true) await input.press('Enter') diff --git a/tests/e2e/project-group-creation-visibility.spec.ts b/tests/e2e/project-group-creation-visibility.spec.ts index d659734570a..90deffecb1f 100644 --- a/tests/e2e/project-group-creation-visibility.spec.ts +++ b/tests/e2e/project-group-creation-visibility.spec.ts @@ -7,6 +7,11 @@ import { runProcess } from '../../src/shared/child-process/run-process' test.use({ seedTestRepo: false }) +declare global { + // Resolved by the main-process gate this spec installs around the group-create response. + var __releaseGroupCreateResponse: (() => void) | undefined +} + for (const delayCreateResponse of [false, true]) { test(`created groups survive sidebar expansion (${delayCreateResponse ? 'refresh first' : 'ordinary timing'})`, async ({ orcaPage, @@ -97,7 +102,7 @@ for (const delayCreateResponse of [false, true]) { .toBe(true) } finally { await electronApp.evaluate(() => { - const release = Reflect.get(globalThis, '__releaseGroupCreateResponse') + const release = globalThis.__releaseGroupCreateResponse if (typeof release !== 'function') { throw new Error('Group create response gate unavailable') } diff --git a/tests/e2e/worktree-active-delete-scroll-position.spec.ts b/tests/e2e/worktree-active-delete-scroll-position.spec.ts index 5e2f15f5b75..15cf6c97914 100644 --- a/tests/e2e/worktree-active-delete-scroll-position.spec.ts +++ b/tests/e2e/worktree-active-delete-scroll-position.spec.ts @@ -17,6 +17,14 @@ type RowRemovalFrame = { targetExists: boolean } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Frame sampling started in the page and awaited once the removal animation settles. + __activeDeleteRowRemovalFrames?: Promise + } +} + async function pauseForVisualProof(page: Page): Promise { if (process.env.ORCA_E2E_RECORD_VIDEO === '1') { await page.waitForTimeout(VISUAL_PROOF_PAUSE_MS) @@ -215,7 +223,7 @@ async function startRowRemovalSampling( async function finishRowRemovalSampling(page: Page): Promise { return page.evaluate(async () => { - const pending = Reflect.get(window, '__activeDeleteRowRemovalFrames') + const pending = window.__activeDeleteRowRemovalFrames if (!(pending instanceof Promise)) { throw new Error('Row removal sampling was not started') } From c0fb04c8d21ab67ca06e6df283ee120166ed22e1 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:25:41 -0700 Subject: [PATCH 28/34] fix(relay): open the real null device when detaching Windows stdio (#20808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(relay): open the real null device when detaching Windows stdio `openSync('NUL')` does not reach the null device on Windows. node's fs runs the path through `toNamespacedPath`, which resolves it against cwd and prefixes `\\?\` — and that prefix turns off DOS device-name mapping, so CreateFileW creates a regular file named `NUL` in the relay's install dir and pins fds 0/1 to it instead of to a discard sink. Verified on a Windows 11 host: `fs.openSync('NUL', 'w')` + a 5-byte write produced a 5-byte file named `NUL` in cwd. `\\.\NUL` is passed through `toNamespacedPath` verbatim; the same write discards and a read answers EOF, with no file created. It also escaped into shipped artifacts. release-cut.yml runs the relay watcher fault harness with cwd = out/relay/win32-x64, so every Windows installer since v1.4.169 carries `resources/relay/win32-x64/NUL`, which NSIS extracts as `_NUL`. * test(relay): prove the `\\?\` rewrite on a drive-letter path `toNamespacedPath('NUL')` off Windows only resolves against a POSIX cwd and stops; with no drive letter it never reaches the branch that adds `\\?\`. So the assertion held for the wrong reason and did not demonstrate the rewrite the comment describes. Assert it on an absolute drive path, which takes the same branch on every host. --- src/relay/relay-primary-channel.test.ts | 29 +++++++++++++++++++++++++ src/relay/relay-primary-channel.ts | 16 +++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/relay/relay-primary-channel.test.ts diff --git a/src/relay/relay-primary-channel.test.ts b/src/relay/relay-primary-channel.test.ts new file mode 100644 index 00000000000..d1ac209e161 --- /dev/null +++ b/src/relay/relay-primary-channel.test.ts @@ -0,0 +1,29 @@ +import { win32 } from 'node:path' +import { describe, expect, it } from 'vitest' +import { nullDevicePath } from './relay-primary-channel' + +describe('nullDevicePath', () => { + it('names the POSIX null device off win32', () => { + expect(nullDevicePath('linux')).toBe('/dev/null') + expect(nullDevicePath('darwin')).toBe('/dev/null') + }) + + /** + * The defect this pins: `openSync('NUL')` on Windows does NOT open the null device. + * node runs the path through `toNamespacedPath`, which resolves it against cwd and + * prefixes `\\?\` — and `\\?\` turns off DOS device-name mapping, so CreateFileW makes + * a real file. v1.4.203's Windows installer shipped one at + * `resources/relay/win32-x64/NUL` because of it. + */ + it('uses a device path win32 cannot rewrite into a file in the relay cwd', () => { + const path = nullDevicePath('win32') + + expect(path).toBe('\\\\.\\NUL') + expect(win32.toNamespacedPath(path)).toBe(path) + // Bare `NUL` never survives as a device name: it is resolved against cwd, and a + // drive-letter cwd then also takes the `\\?\` prefix. Spelled absolute because off + // Windows `resolve` finds no drive letter and stops before that second rewrite. + expect(win32.toNamespacedPath('NUL')).not.toBe('NUL') + expect(win32.toNamespacedPath(String.raw`C:\relay\NUL`)).toBe(String.raw`\\?\C:\relay\NUL`) + }) +}) diff --git a/src/relay/relay-primary-channel.ts b/src/relay/relay-primary-channel.ts index 9b2e50eaaa1..e3dbffeae7f 100644 --- a/src/relay/relay-primary-channel.ts +++ b/src/relay/relay-primary-channel.ts @@ -2,6 +2,17 @@ import { closeSync, openSync } from 'node:fs' import { RelayDispatcher } from './dispatcher' import { RELAY_SENTINEL } from './protocol' +/** + * Why the `\\.\` device prefix and not bare `NUL`: node's fs resolves a relative path + * through `toNamespacedPath`, which hands CreateFileW a `\\?\C:\…\NUL` — and that prefix + * disables DOS device-name mapping, so the open creates a real FILE named `NUL` in the + * relay's cwd and pins fds 0/1 to it. One shipped in the 1.4.203 Windows installer as + * `resources/relay/win32-x64/NUL`. A `\\.\` path is passed through verbatim. + */ +export function nullDevicePath(platform: NodeJS.Platform = process.platform): string { + return platform === 'win32' ? String.raw`\\.\NUL` : '/dev/null' +} + export class RelayPrimaryChannel { readonly dispatcher: RelayDispatcher private stdoutAlive = true @@ -111,14 +122,13 @@ export class RelayPrimaryChannel { // Already closed by the peer. } } - const devNull = process.platform === 'win32' ? 'NUL' : '/dev/null' try { - openSync(devNull, 'r') + openSync(nullDevicePath(), 'r') } catch { // Best-effort pin of the lowest free descriptor. } try { - openSync(devNull, 'w') + openSync(nullDevicePath(), 'w') } catch { // Best-effort pin of the next free descriptor. } From 37394e9cb75e61fd3c4145c15a90fc38e670874a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:25:45 -0700 Subject: [PATCH 29/34] build(release): compile the Windows relay process-table addon (#20809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build(release): compile the Windows relay process-table addon #16598 added build-windows-process-tree-relay-addon.mjs and the ORCA_REQUIRE_RELAY_NATIVE_ADDONS gate, but wired both into dev-channel-win-build.yml only. release-cut.yml was never touched, and stageWindowsProcessTreeAddon merely logs when the addon is absent, so every stable release has shipped Windows relays without windows-process-tree.node. Confirmed by extracting the installers: v1.4.191 (the first stable carrying the feature), v1.4.198 and v1.4.203 all have no windows-process-tree.node in relay/win32-x64 or relay/win32-arm64. Those hosts have been taking the CIM fallback the whole time — 1247ms and a powershell.exe per scan against 57ms native, on #16598's own ~1490-process measurement host. Mirror the dev-channel steps. Same windows-2022 image, so the MSVC ARM64 cross toolset the arm64 leg needs is already proven there, and the addon build runs before the long packaging step so a missing component fails in seconds with MSB8020 naming it. * build(release): keep the Build app env rationale attached to its step The new addon step landed between the ORCA_POSTHOG_WRITE_KEY / BUILD_IDENTITY / DIAGNOSTICS_TOKEN_URL comment block and the Build app step it documents, orphaning it. Move the step above the block and record why it carries no run_attempt guard: Build app is ungated, so a guarded addon step would let a rerun reach the required-addon check with nothing staged. --- .github/workflows/release-cut.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index a644aef53c1..ac2e7f904e3 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1311,6 +1311,20 @@ jobs: echo "identity=$identity" >>"$GITHUB_OUTPUT" echo "Classified $TAG as $identity" + # Why here and not in build:relay: only a Windows runner can compile it, and + # arm64 cross-compiles from this same x64 agent. Mirrors dev-channel-win-build.yml, + # which had it while release-cut did not — so every stable installer through + # v1.4.203 shipped Windows relays with no windows-process-tree.node, silently + # falling back to the PowerShell scan on every Windows SSH host. + # Why no run_attempt guard, unlike the artifact steps below: Build app is ungated, + # so a rerun would reach the required-addon check with nothing staged and fail. + - name: Build Windows process-table addon for the relay + if: matrix.platform == 'win' + shell: bash + run: | + node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=x64 + node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=arm64 + # Why ORCA_POSTHOG_WRITE_KEY here: this is the only build that # produces a published binary, so this is the only place the secret # needs to be in scope. The key is a PostHog *project* API key, not @@ -1333,6 +1347,9 @@ jobs: ORCA_BUILD_IDENTITY: ${{ steps.tag-classify.outputs.identity }} ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }} + # Fail the release rather than ship a relay that silently falls back to + # the PowerShell scan on every Windows SSH host. + ORCA_REQUIRE_RELAY_NATIVE_ADDONS: ${{ matrix.platform == 'win' && 'x64,arm64' || '' }} - name: Gate runtime file-watcher process isolation if: runner.os == 'Linux' From e4a9d24e0c261f81a82a81d060dc909c90dede91 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:28:23 -0700 Subject: [PATCH 30/34] fix(automations): repair cron step expansion and day restriction (#20202) The semantic half of the cron repair. Both defects change what an already-saved schedule does, so they ship together and behind a decision. #15723: parseCronField set end = start for a bare numeric field even with a slash step, so 5/15 expanded to [5] and fired hourly instead of every fifteen minutes. N/step is the open-ended N-max/step sequence now. #15896: day restriction came from expanded set cardinality, so 1-31 read as unrestricted and */2 as restricted. Restriction is lexical now: a day field restricts iff no term of it ranges over a star, matching vixie cron and robfig/cron rather than crontab(5)'s prose. Verified differentially against robfig/cron v1.2.0 across 22 expressions, 424 days, zero divergences. The two cannot ship apart: 0 9 1/1 * 1 matches 124 days under the old parser, 104 under #15723 alone, and 730 under both, because the old cardinality flags react to the corrected expansion. describeAutomationScheduleDrift reads a saved expression under both semantics and reports the ones that moved, so neither direction is silent; the service names them once at startup. No expression Orca's own presets generate drifts. Fixes #15723 Fixes #15896 --- .../automations/schedule-drift-report.test.ts | 61 ++++++++ src/main/automations/schedule-drift-report.ts | 31 ++++ src/main/automations/service.ts | 2 + src/shared/automation-cron-dialect.test.ts | 132 ++++++++++++++++++ src/shared/automation-cron-field-parsing.ts | 22 ++- src/shared/automation-cron-occurrence.ts | 1 + src/shared/automation-schedule-drift.test.ts | 83 +++++++++++ src/shared/automation-schedule-drift.ts | 123 ++++++++++++++++ src/shared/automation-schedule-parsing.ts | 41 +++--- src/shared/automation-schedules.test.ts | 4 +- src/shared/automation-schedules.ts | 28 ++-- 11 files changed, 489 insertions(+), 39 deletions(-) create mode 100644 src/main/automations/schedule-drift-report.test.ts create mode 100644 src/main/automations/schedule-drift-report.ts create mode 100644 src/shared/automation-cron-dialect.test.ts create mode 100644 src/shared/automation-schedule-drift.test.ts create mode 100644 src/shared/automation-schedule-drift.ts diff --git a/src/main/automations/schedule-drift-report.test.ts b/src/main/automations/schedule-drift-report.test.ts new file mode 100644 index 00000000000..f719998576b --- /dev/null +++ b/src/main/automations/schedule-drift-report.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { Automation } from '../../shared/automations-types' +import { reportAutomationScheduleDrift } from './schedule-drift-report' + +const makeAutomation = (name: string, rrule: string): Automation => ({ + id: `id-${name}`, + name, + prompt: 'Check the repo', + precheck: null, + agentId: 'claude', + projectId: 'r1', + executionTargetType: 'local', + executionTargetId: 'local', + schedulerOwner: 'local_host_service', + workspaceMode: 'existing', + workspaceId: 'wt1', + baseBranch: null, + reuseSession: false, + timezone: 'UTC', + rrule, + dtstart: 0, + enabled: true, + nextRunAt: 0, + missedRunPolicy: 'run_once_within_grace', + missedRunGraceMinutes: 720, + createdAt: 0, + updatedAt: 0 +}) + +describe('automation schedule drift report', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('names each affected record and which way it moved', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const count = reportAutomationScheduleDrift([ + makeAutomation('Quarter-hourly sweep', '5/15 * * * *'), + makeAutomation('Odd days and Mondays', '0 9 */2 * 1'), + makeAutomation('Weekday standup', '30 9 * * 1-5') + ]) + + expect(count).toBe(2) + const lines = warn.mock.calls.map((call) => String(call[0])) + expect(lines[0]).toContain('2 saved schedule(s) changed meaning') + expect( + lines.some((l) => l.includes('Quarter-hourly sweep') && l.includes('now runs more')) + ).toBe(true) + expect( + lines.some((l) => l.includes('Odd days and Mondays') && l.includes('now runs fewer')) + ).toBe(true) + // The untouched preset must not be named, or the report trains the reader to skip it. + expect(lines.some((l) => l.includes('Weekday standup'))).toBe(false) + }) + + it('says nothing when no saved schedule drifted', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + expect(reportAutomationScheduleDrift([makeAutomation('Hourly', '0 * * * *')])).toBe(0) + expect(warn).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/automations/schedule-drift-report.ts b/src/main/automations/schedule-drift-report.ts new file mode 100644 index 00000000000..a2e38d65ac7 --- /dev/null +++ b/src/main/automations/schedule-drift-report.ts @@ -0,0 +1,31 @@ +/** + * Reports saved schedules whose meaning changed in the release that repaired the cron parser. + * + * Both repairs were correct, but a persisted cadence can now fire several times more — or + * several times less — than it did yesterday. The louder direction announces itself through + * spend; the quieter one does not, because nobody notices a job that stopped running. One + * line per affected record at startup is the smallest signal that makes either detectable. + */ +import type { Automation } from '../../shared/automations-types' +import { describeAutomationScheduleDrift } from '../../shared/automation-schedule-drift' + +export function reportAutomationScheduleDrift(automations: readonly Automation[]): number { + const drifted = automations.flatMap((automation) => { + const drift = describeAutomationScheduleDrift(automation.rrule) + return drift ? [{ automation, drift }] : [] + }) + if (drifted.length === 0) { + return 0 + } + console.warn( + `[automations] ${drifted.length} saved schedule(s) changed meaning when the cron parser was repaired; review them:` + ) + for (const { automation, drift } of drifted) { + const direction = drift.currentRunsPerYear > drift.previousRunsPerYear ? 'more' : 'fewer' + console.warn( + `[automations] "${automation.name}" (${automation.id}) "${drift.expression}" now runs ` + + `${direction}: about ${drift.currentRunsPerYear}/year, was about ${drift.previousRunsPerYear}/year` + ) + } + return drifted.length +} diff --git a/src/main/automations/service.ts b/src/main/automations/service.ts index b683fb6c5a2..4be15f095db 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -25,6 +25,7 @@ import { type AutomationRunTerminalObserver } from './run-completion-watcher' import { createAutomationRunWriter, type AutomationRunWriter } from './automation-run-writer' +import { reportAutomationScheduleDrift } from './schedule-drift-report' import { describeScheduledRefusal, recordRefusedAutomationRun, @@ -115,6 +116,7 @@ export class AutomationService { void this.evaluateDueRuns() }, this.tickMs) this.completionWatcher?.reconcileRetainedRuns(this.store.listAutomationRuns()) + reportAutomationScheduleDrift(this.store.listAutomations()) // Why: headless serve never gets a renderer-ready IPC, but due runs still // need the same startup catch-up pass desktop gets after renderer attach. if (this.rendererReady || this.headlessDispatcher) { diff --git a/src/shared/automation-cron-dialect.test.ts b/src/shared/automation-cron-dialect.test.ts new file mode 100644 index 00000000000..dd6500b1549 --- /dev/null +++ b/src/shared/automation-cron-dialect.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest' +import { cronMatches } from './automation-cron-occurrence' +import { parseCronExpression } from './automation-schedule-parsing' + +const ascending = (values: Set): number[] => [...values].sort((left, right) => left - right) + +const EVERY_DAY_OF_MAY = Array.from({ length: 31 }, (_, index) => index + 1) + +/** + * Independent calendar oracle. May 2026 opens on a Friday, so its Mondays are 4/11/18/25, + * its Sundays 3/10/17/24/31, and its weekend days 2/3, 9/10, 16/17, 23/24 and 30/31. Every + * expectation below is that hand calendar, never a second call into the parser. + */ +function matchingDaysOfMay2026(expression: string): number[] { + const rule = parseCronExpression(expression) + const days: number[] = [] + for (let day = 1; day <= 31; day += 1) { + if (cronMatches(rule, new Date(2026, 4, day, 9, 0, 0, 0).getTime())) { + days.push(day) + } + } + return days +} + +describe('cron bare stepped values (#15723)', () => { + it('expands `N/step` as the open-ended `N-max/step` sequence in every field', () => { + expect(ascending(parseCronExpression('5/15 * * * *').minutes)).toEqual([5, 20, 35, 50]) + expect(ascending(parseCronExpression('* 2/7 * * *').hours)).toEqual([2, 9, 16, 23]) + expect(ascending(parseCronExpression('0 9 5/10 * *').daysOfMonth)).toEqual([5, 15, 25]) + expect(ascending(parseCronExpression('0 9 * MAR/3 *').months)).toEqual([3, 6, 9, 12]) + // 1, 4 and 7, with Sunday normalized off 7. + expect(ascending(parseCronExpression('0 9 * * 1/3').daysOfWeek)).toEqual([0, 1, 4]) + }) + + it('matches the explicit `N-max/step` range it is defined to mean', () => { + const equivalents: [string, string][] = [ + ['5/15 * * * *', '5-59/15 * * * *'], + ['* 2/7 * * *', '* 2-23/7 * * *'], + ['0 9 5/10 * *', '0 9 5-31/10 * *'], + ['0 9 * MAR/3 *', '0 9 * MAR-DEC/3 *'], + ['0 9 * * 1/3', '0 9 * * 1-7/3'] + ] + for (const [bare, explicit] of equivalents) { + const left = parseCronExpression(bare) + const right = parseCronExpression(explicit) + expect([ + ascending(left.minutes), + ascending(left.hours), + ascending(left.daysOfMonth), + ascending(left.months), + ascending(left.daysOfWeek) + ]).toEqual([ + ascending(right.minutes), + ascending(right.hours), + ascending(right.daysOfMonth), + ascending(right.months), + ascending(right.daysOfWeek) + ]) + } + }) + + it('leaves a bare value with no step as itself', () => { + expect(ascending(parseCronExpression('5 * * * *').minutes)).toEqual([5]) + expect(ascending(parseCronExpression('0 9 5 * *').daysOfMonth)).toEqual([5]) + expect(ascending(parseCronExpression('0 9 * MAR *').months)).toEqual([3]) + expect(ascending(parseCronExpression('0 9 * * FRI').daysOfWeek)).toEqual([5]) + }) + + // Separateness probe: the #15723 repair only reaches the bare-value branch, so it moves + // neither an oversized step nor the day-restriction flags (#15896). + it('leaves oversized-step and full-range-day expansions exactly where they were', () => { + expect(ascending(parseCronExpression('*/90 * * * *').minutes)).toEqual([0]) + expect(ascending(parseCronExpression('5/90 * * * *').minutes)).toEqual([5]) + expect(ascending(parseCronExpression('0 9 1-31 * 1').daysOfMonth)).toEqual(EVERY_DAY_OF_MAY) + }) +}) + +describe('cron day restriction (#15896)', () => { + // Dialect: a day field is restricted iff no term of it ranges over a star; when both day + // fields are restricted the day matches on either, otherwise on both. Every expectation + // below was taken from robfig/cron v1.2.0, an independent implementation of the same rule. + it('ORs an explicit full day-of-month range against a restricted day-of-week', () => { + expect(matchingDaysOfMay2026('0 9 1-31 * 1')).toEqual(EVERY_DAY_OF_MAY) + }) + + it('ANDs a wildcard day-of-month against a restricted day-of-week', () => { + expect(matchingDaysOfMay2026('0 9 * * 1')).toEqual([4, 11, 18, 25]) + }) + + it('ORs two partially restricted day fields', () => { + expect(matchingDaysOfMay2026('0 9 1,15 * 1')).toEqual([1, 4, 11, 15, 18, 25]) + }) + + it('ANDs a restricted day-of-month against a wildcard day-of-week', () => { + expect(matchingDaysOfMay2026('0 9 1,15 * *')).toEqual([1, 15]) + }) + + // A star step is still a star, so it does not flip the day rule to OR. Reading `*/2` as + // restricted would fire this ~8x more: the 18 odd-or-Monday days, not the 2 that are both. + it('keeps AND when a day field steps over a star', () => { + expect(matchingDaysOfMay2026('0 9 */2 * 1')).toEqual([11, 25]) + expect(matchingDaysOfMay2026('0 9 */1 * 1')).toEqual([4, 11, 18, 25]) + expect(matchingDaysOfMay2026('0 9 * * */2')).toEqual([ + 2, 3, 5, 7, 9, 10, 12, 14, 16, 17, 19, 21, 23, 24, 26, 28, 30, 31 + ]) + }) + + // The star test is per comma term, so a list that reaches a star anywhere is unrestricted. + it('treats a day list containing a star term as a star', () => { + expect(matchingDaysOfMay2026('0 9 */3 * 1,5')).toEqual([1, 4, 22, 25]) + }) + + it('normalizes Sunday from 0, from 7 and from the name', () => { + expect(matchingDaysOfMay2026('0 9 * * 0')).toEqual([3, 10, 17, 24, 31]) + expect(matchingDaysOfMay2026('0 9 * * 7')).toEqual([3, 10, 17, 24, 31]) + expect(matchingDaysOfMay2026('0 9 * * SUN')).toEqual([3, 10, 17, 24, 31]) + }) + + it('reads month and day names on both sides of the restriction rule', () => { + expect(matchingDaysOfMay2026('0 9 * MAY MON')).toEqual([4, 11, 18, 25]) + expect(matchingDaysOfMay2026('0 9 * JUN MON')).toEqual([]) + expect(matchingDaysOfMay2026('0 9 1-31 MAY MON')).toEqual(EVERY_DAY_OF_MAY) + }) + + // Preset-built schedules always write a literal `*` day-of-month, so they keep AND. + it('leaves preset-shaped schedules on AND semantics', () => { + expect(matchingDaysOfMay2026('0 9 * * 1-5')).toEqual([ + 1, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 25, 26, 27, 28, 29 + ]) + expect(matchingDaysOfMay2026('0 9 * * *')).toEqual(EVERY_DAY_OF_MAY) + }) +}) diff --git a/src/shared/automation-cron-field-parsing.ts b/src/shared/automation-cron-field-parsing.ts index b43e4d4fe5a..306b44032a3 100644 --- a/src/shared/automation-cron-field-parsing.ts +++ b/src/shared/automation-cron-field-parsing.ts @@ -1,6 +1,10 @@ -// Cron field parsing for Orca's automation schedules. -// A field step is bounded by the count of distinct values the field holds: a step of 90 on -// minutes is one value at :00, never "every 90 minutes", so it is refused as input (#15895). +// Orca's cron dialect (vixie/POSIX): +// - `N/step` is the open-ended sequence `N-max/step`; a bare `N` is only itself (#15723). +// - A day field is restricted iff no term of it ranges over a star, so `1-31` restricts but +// `*/2` does not (#15896). Restriction is lexical: the expanded set cannot tell `1-31` from +// `*`. When both day fields are restricted the day matches on either; otherwise on both. +// - A field step is bounded by the count of distinct values the field holds; a step of 90 on +// minutes is one value at :00, never "every 90 minutes" (#15895). export type CronParseOptions = { /** Input-time gate: reject a step wider than the field's domain instead of silently * degenerating to a single value. Off for persisted rows, which must keep running the @@ -104,7 +108,8 @@ export function parseCronField(args: { end = parseCronNumber(endPart, args.names ?? null, args.field) } else { start = parseCronNumber(rangePart, args.names ?? null, args.field) - end = start + // `N/step` is the open-ended `N-max/step` sequence; a bare `N` is only itself. + end = stepPart === undefined ? start : args.max } const normalizedStart = args.normalize?.(start) ?? start @@ -131,3 +136,12 @@ export function parseCronField(args: { } return result } + +// A day field restricts iff none of its terms ranges over a star, matching what vixie cron +// and robfig/cron both do. crontab(5) says "restricted (ie, are not *)", which reads as a +// literal-`*` test, but vixie's own entry.c sets DOM_STAR/DOW_STAR off the field's leading +// character, so `*/2` is a star there too; we follow the implementations over the prose, +// because reading `*/2` as restricted flips its day rule to OR and fires it ~8x more. +export function isCronDayFieldRestricted(field: string): boolean { + return !field.split(',').some((term) => term.split('/')[0].trim() === '*') +} diff --git a/src/shared/automation-cron-occurrence.ts b/src/shared/automation-cron-occurrence.ts index 6ad88468e16..3cb37a83034 100644 --- a/src/shared/automation-cron-occurrence.ts +++ b/src/shared/automation-cron-occurrence.ts @@ -30,6 +30,7 @@ export function cronDateMatches(rule: ParsedCron, timestamp: number): boolean { } const dayOfMonthMatches = rule.daysOfMonth.has(date.getDate()) const dayOfWeekMatches = rule.daysOfWeek.has(date.getDay()) + // Dialect rule; the flags are lexical (`isCronDayFieldRestricted`), not set sizes. if (rule.dayOfMonthRestricted && rule.dayOfWeekRestricted) { return dayOfMonthMatches || dayOfWeekMatches } diff --git a/src/shared/automation-schedule-drift.test.ts b/src/shared/automation-schedule-drift.test.ts new file mode 100644 index 00000000000..751b411bd73 --- /dev/null +++ b/src/shared/automation-schedule-drift.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest' +import { describeAutomationScheduleDrift } from './automation-schedule-drift' + +// Both lists were recorded by running the same corpus through the real parent build +// (729491597f3) and this branch, not by re-deriving them from the detector under test. +const DRIFTED = [ + '5/15 * * * *', + '0/30 * * * *', + '5/15 9 * * *', + '0 9/4 * * *', + '0 9 1/7 * *', + '0 9 * 1/3 *', + '0 9 * * 1/2', + '0 9 1-31 * 1', + '0 9 */2 * 1', + '0 9 */3 * 1', + '0 9 * MAR/3 *' +] + +const STABLE = [ + '0 * * * *', + '30 9 * * *', + '30 9 * * 1-5', + '30 9 * * 3', + '*/15 * * * *', + '*/5 * * * *', + '5 * * * *', + '0 9 */1 * 1', + '0 9 1,15 * 1', + '0 9 * * 0-6', + '0 9 * * 0-7', + '0 9 * * 1-7', + '0 9 */2 * *', + '0 9 * * */2', + '0 9 1-31 * *', + '0 9 * * *', + '0 9 15 * *', + '0 9 1-15 * 1', + '*/90 * * * *', + '5/90 * * * *', + '0 9 * * */8', + '0 9 * MAY MON', + '0 9 * * FRI' +] + +const ANCHOR = new Date(2026, 0, 1).getTime() + +describe('automation schedule drift', () => { + it('flags every schedule the repair changed', () => { + for (const expression of DRIFTED) { + expect(describeAutomationScheduleDrift(expression, ANCHOR), expression).not.toBeNull() + } + }) + + // The restriction flags move on several of these while the days they fire do not; reporting + // those would train the reader to ignore the notice. + it('stays silent on schedules the repair left alone', () => { + for (const expression of STABLE) { + expect(describeAutomationScheduleDrift(expression, ANCHOR), expression).toBeNull() + } + }) + + it('reports the direction and size of the change', () => { + // 1x/hour -> 4x/hour: the cadence users will feel as spend. + expect(describeAutomationScheduleDrift('5/15 * * * *', ANCHOR)).toEqual({ + expression: '5/15 * * * *', + previousRunsPerYear: 8760, + currentRunsPerYear: 35040 + }) + // The quiet direction: an automation that now skips most of the days it used to run. + const fewer = describeAutomationScheduleDrift('0 9 */2 * 1', ANCHOR) + expect(fewer!.currentRunsPerYear).toBeLessThan(fewer!.previousRunsPerYear / 4) + }) + + it('ignores RRULE presets, which never used the repaired parser', () => { + expect(describeAutomationScheduleDrift('FREQ=DAILY;BYHOUR=9;BYMINUTE=0', ANCHOR)).toBeNull() + }) + + it('reports nothing for a schedule that cannot be read at all', () => { + expect(describeAutomationScheduleDrift('0 9 32 * *', ANCHOR)).toBeNull() + expect(describeAutomationScheduleDrift('not a cron', ANCHOR)).toBeNull() + }) +}) diff --git a/src/shared/automation-schedule-drift.ts b/src/shared/automation-schedule-drift.ts new file mode 100644 index 00000000000..fc4f18564f9 --- /dev/null +++ b/src/shared/automation-schedule-drift.ts @@ -0,0 +1,123 @@ +// Detects saved cron schedules whose meaning changed in the release that repaired the parser +// (#15723, #15896). Both repairs were correct, but a persisted cadence can now fire several +// times more — or several times less — than it did yesterday, with nothing to notice it by. +import { + getAutomationCronExpressionFields, + parseCronExpression, + type ParsedCron +} from './automation-schedule-parsing' +import { cronDateMatches } from './automation-cron-occurrence' + +// Two years covers every day-of-month against day-of-week pairing a schedule can land on, +// which is the only part of matching that depends on the calendar rather than the sets. +const DRIFT_SCAN_DAYS = 730 + +export type AutomationScheduleDrift = { + expression: string + /** Runs a year under the cadence as it was read before the repair, and as it reads now. */ + previousRunsPerYear: number + currentRunsPerYear: number +} + +/** + * The pre-repair reading of a field: a bare value carrying a step lost the step, so `5/15` + * meant `5`. A star or a range kept its step, and is left alone. + */ +function toPreRepairField(field: string): string { + return field + .split(',') + .map((term) => { + const [range, step] = term.split('/') + if (step === undefined || range.includes('*') || range.includes('-')) { + return term + } + return range + }) + .join(',') +} + +/** + * The pre-repair reading of a whole expression. Day restriction came from how many values a + * field expanded to rather than from what the user wrote, so `1-31` read as unrestricted. + */ +function parsePreRepairCron(expression: string): ParsedCron { + const fields = getAutomationCronExpressionFields(expression, 6) + const parsed = parseCronExpression(fields.map(toPreRepairField).join(' ')) + return { + ...parsed, + dayOfMonthRestricted: parsed.daysOfMonth.size !== 31, + dayOfWeekRestricted: parsed.daysOfWeek.size !== 7 + } +} + +/** Walks both readings over the same calendar so the comparison is which days, not how many. */ +function compareMatchingDays( + previous: ParsedCron, + current: ParsedCron, + anchor: number +): { previousDays: number; currentDays: number; sameDays: boolean } { + const cursor = new Date(anchor) + cursor.setHours(12, 0, 0, 0) + let previousDays = 0 + let currentDays = 0 + let sameDays = true + for (let i = 0; i < DRIFT_SCAN_DAYS; i += 1) { + const at = cursor.getTime() + const previousMatch = cronDateMatches(previous, at) + const currentMatch = cronDateMatches(current, at) + if (previousMatch) { + previousDays += 1 + } + if (currentMatch) { + currentDays += 1 + } + if (previousMatch !== currentMatch) { + sameDays = false + } + cursor.setDate(cursor.getDate() + 1) + } + return { previousDays, currentDays, sameDays } +} + +function runsPerYear(rule: ParsedCron, days: number): number { + return Math.round((days / 2) * rule.hours.size * rule.minutes.size) +} + +/** + * Null when the saved cadence still means what it did before the repair. Only cron schedules + * can drift; RRULE presets never went through the repaired field parser. + */ +export function describeAutomationScheduleDrift( + schedule: string, + anchor = Date.now() +): AutomationScheduleDrift | null { + const expression = schedule.trim() + if (expression.includes('=')) { + return null + } + let current: ParsedCron + let previous: ParsedCron + try { + current = parseCronExpression(expression) + previous = parsePreRepairCron(expression) + } catch { + // An unreadable schedule drifts nowhere; the tick reports it separately (#16303). + return null + } + const sameClock = + previous.minutes.size === current.minutes.size && + previous.hours.size === current.hours.size && + [...current.minutes].every((minute) => previous.minutes.has(minute)) && + [...current.hours].every((hour) => previous.hours.has(hour)) + const { previousDays, currentDays, sameDays } = compareMatchingDays(previous, current, anchor) + // Compare what the schedule fires, not how it parsed: the restriction flags move on + // expressions whose matched days do not, and those are not worth telling anyone about. + if (sameClock && sameDays) { + return null + } + return { + expression, + previousRunsPerYear: runsPerYear(previous, previousDays), + currentRunsPerYear: runsPerYear(current, currentDays) + } +} diff --git a/src/shared/automation-schedule-parsing.ts b/src/shared/automation-schedule-parsing.ts index af3ce324f1a..497809bcc0f 100644 --- a/src/shared/automation-schedule-parsing.ts +++ b/src/shared/automation-schedule-parsing.ts @@ -5,6 +5,7 @@ import { isClipboardTextByteLengthOverLimit } from './clipboard-text' import { DAY_NAMES, MONTH_NAMES, + isCronDayFieldRestricted, parseCronField, type CronParseOptions } from './automation-cron-field-parsing' @@ -76,23 +77,6 @@ export function parseCronExpression( } const [minute, hour, dayOfMonth, month, dayOfWeek] = parts const rejectOversizedStep = options.rejectOversizedStep ?? false - const daysOfMonth = parseCronField({ - value: dayOfMonth, - min: 1, - max: 31, - field: 'day of month', - rejectOversizedStep - }) - const daysOfWeek = parseCronField({ - value: dayOfWeek, - min: 0, - max: 7, - field: 'day of week', - names: DAY_NAMES, - normalize: (value) => (value === 7 ? 0 : value), - distinctValueCount: 7, - rejectOversizedStep - }) return { kind: 'cron', minutes: parseCronField({ @@ -103,7 +87,13 @@ export function parseCronExpression( rejectOversizedStep }), hours: parseCronField({ value: hour, min: 0, max: 23, field: 'hour', rejectOversizedStep }), - daysOfMonth, + daysOfMonth: parseCronField({ + value: dayOfMonth, + min: 1, + max: 31, + field: 'day of month', + rejectOversizedStep + }), months: parseCronField({ value: month, min: 1, @@ -112,9 +102,18 @@ export function parseCronExpression( names: MONTH_NAMES, rejectOversizedStep }), - daysOfWeek, - dayOfMonthRestricted: daysOfMonth.size !== 31, - dayOfWeekRestricted: daysOfWeek.size !== 7 + daysOfWeek: parseCronField({ + value: dayOfWeek, + min: 0, + max: 7, + field: 'day of week', + names: DAY_NAMES, + normalize: (value) => (value === 7 ? 0 : value), + distinctValueCount: 7, + rejectOversizedStep + }), + dayOfMonthRestricted: isCronDayFieldRestricted(dayOfMonth), + dayOfWeekRestricted: isCronDayFieldRestricted(dayOfWeek) } } diff --git a/src/shared/automation-schedules.test.ts b/src/shared/automation-schedules.test.ts index 7005b7d59e0..71b1a2bf70b 100644 --- a/src/shared/automation-schedules.test.ts +++ b/src/shared/automation-schedules.test.ts @@ -242,7 +242,9 @@ describe('automation schedules', () => { expect(formatAutomationSchedule('0 9,17 * * MON-FRI')).toBe('Custom schedule') }) - it('treats all-value cron day fields as unrestricted for DOM/DOW matching', () => { + // Restriction is lexical (#15896), but a star step is still a star: `*/1` does not + // restrict, so the day rule stays AND and this fires on Mondays only. + it('treats a stepped cron day-of-month field as unrestricted for DOM/DOW matching', () => { const next = nextAutomationOccurrenceAfter( '0 9 */1 * MON', new Date('2026-05-01T00:00:00').getTime(), diff --git a/src/shared/automation-schedules.ts b/src/shared/automation-schedules.ts index 6d5a0053595..993d8fbf831 100644 --- a/src/shared/automation-schedules.ts +++ b/src/shared/automation-schedules.ts @@ -94,27 +94,29 @@ function classifyParsedCronSchedule(rule: ParsedCron): AutomationCronScheduleCla } const minute = getSingleSetValue(rule.minutes) const hour = getSingleSetValue(rule.hours) - const unrestrictedDayOfMonth = !rule.dayOfMonthRestricted const unrestrictedMonth = setContainsRange(rule.months, 1, 12) - const unrestrictedDayOfWeek = !rule.dayOfWeekRestricted - const unrestrictedCalendar = unrestrictedDayOfMonth && unrestrictedMonth - if ( - minute !== null && - setContainsRange(rule.hours, 0, 23) && - unrestrictedCalendar && - unrestrictedDayOfWeek - ) { + const everyDayOfMonth = setContainsRange(rule.daysOfMonth, 1, 31) + const everyDayOfWeek = setContainsRange(rule.daysOfWeek, 0, 6) + // Labels describe the days the rule actually fires on, so they need coverage of the + // matched set, not the lexical restriction flags that pick OR over AND. + const matchesEitherDayField = rule.dayOfMonthRestricted && rule.dayOfWeekRestricted + const everyDay = matchesEitherDayField + ? everyDayOfMonth || everyDayOfWeek + : everyDayOfMonth && everyDayOfWeek + const unrestrictedCalendar = everyDayOfMonth && unrestrictedMonth + if (minute !== null && setContainsRange(rule.hours, 0, 23) && unrestrictedMonth && everyDay) { return { kind: 'hourly', minute, label: `Hourly at :${String(minute).padStart(2, '0')}` } } - if (minute !== null && hour !== null && unrestrictedCalendar) { + if (minute !== null && hour !== null && unrestrictedMonth && everyDay) { + return { kind: 'daily', hour, minute, label: `Daily at ${formatTime(hour, minute)}` } + } + // Weekday/weekly names only read true under AND; under OR the day-of-month half fires too. + if (minute !== null && hour !== null && unrestrictedCalendar && !matchesEitherDayField) { const time = formatTime(hour, minute) - if (unrestrictedDayOfWeek) { - return { kind: 'daily', hour, minute, label: `Daily at ${time}` } - } if (setContainsExactly(rule.daysOfWeek, [1, 2, 3, 4, 5])) { return { kind: 'weekdays', hour, minute, label: `Weekdays at ${time}` } } From bfdec26352c0f0a36b35c7418f4bfa7f1d33bd25 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:59:58 -0700 Subject: [PATCH 31/34] fix(lint): enable anti-slop/no-object-parameters (#20781) The rule rejects the broad `object` type on any function input (declarations, expressions, arrows, methods, call/construct signatures, function types), plus local aliases and unions that resolve to `object`. `object` accepts every non-primitive while exposing no properties, so it documents nothing and pushes callers into assertions at the boundary. Fixes all 185 violations across src, config, tests and mobile, and flips the rule from "off" to "error" in config/oxlint-anti-slop.json. Approach: replace each `object` input with the type its owner already has. Most sites took an existing domain type or a type-only import (36 added); 40 new aliases name shapes that had none. Where a value is genuinely only compared by reference, it gets a named identity token instead of a shape -- `Record`, the built-in `WeakKey`, or a `unique symbol` brand, matching the branding already used in src/shared. Same treatment for WeakMap and Map key parameters. Two `as unknown as` casts became unnecessary once the parameter carried a real type and were removed; no new casts were added. Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no max-lines disable or per-file bump. Three files sat exactly at their max-lines cap, so the added type imports were made line-neutral rather than suppressed: - src/main/ipc/browser.ts exports the existing guest-registration args type (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line. - pane-scroll.ts takes TerminalScrollIntentTarget through the existing pane-manager-types import via a type-only re-export. - direct-rpc-client.ts drops the identity parameter entirely: the session check moved into the sendProbe callback that owns the token. Verified: anti-slop config reports zero violations over src config tests mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no runnable test/typecheck target in this worktree (expo is not installed), so its 6 files were typechecked against a standalone config and diffed against the base branch -- error sets are byte-identical, including test files. --- config/oxlint-anti-slop.json | 2 +- .../agent-status-hot-path-benchmark.test.ts | 9 +++- .../happy-dom-mutation-observer-retention.ts | 8 ++-- .../connection-diagnostics-screen-data.ts | 7 +++- .../mobile-terminal-viewport-resubscribe.ts | 11 +++-- mobile/src/transport/direct-rpc-client.ts | 6 +-- .../host-client-acquisition-registry.ts | 3 +- mobile/src/transport/relay-dial-stage.ts | 23 +++++++--- .../rpc-session-liveness-watchdog.ts | 5 ++- .../ai-vault-search/session-search-clock.ts | 6 +-- .../artifacts/artifact-cloud-recovery.test.ts | 12 ++++-- .../agent-browser-bridge-test-harness.ts | 19 +++++---- .../browser/browser-cookie-import-clear.ts | 17 ++++++-- .../browser-cookie-import-concurrency.test.ts | 5 ++- ...ser-cookie-import-google-exclusion.test.ts | 5 ++- ...r-cookie-import-partition-fidelity.test.ts | 5 ++- .../browser-cookie-import-replacement.test.ts | 5 ++- ...kie-import-route-partition-staging.test.ts | 5 ++- .../browser-cookie-import-scope.test.ts | 5 ++- ...rowser-cookie-import-undecryptable.test.ts | 5 ++- .../browser/browser-cookie-import.test.ts | 5 ++- .../browser/cdp-keyboard-us-layout.test.ts | 8 ++-- .../doc-preview-download-block-notice.test.ts | 5 ++- .../managed-codex-auth-readiness.test.ts | 2 +- .../codex-session-migration-scheduler.ts | 18 ++++++-- ...aemon-pty-adapter-history-recovery.test.ts | 6 ++- src/main/git/git-capability-state.test.ts | 8 +++- src/main/git/git-capability-state.ts | 5 ++- ...browser-preview-tool-authorization.test.ts | 10 ++++- src/main/ipc/browser.test.ts | 5 ++- src/main/ipc/browser.ts | 8 ++-- src/main/ipc/filesystem-test-harness.ts | 8 +++- .../ipc/runtime-watcher-process-pool.test.ts | 3 +- src/main/ipc/settings.test.ts | 6 ++- ...thoritative-local-metadata-pruning.test.ts | 2 +- .../ipc/worktrees-lineage-hydration.test.ts | 3 +- src/main/ipc/worktrees-test-ipc-surface.ts | 3 +- .../wsl-transcript-fs-process-dispatch.ts | 2 +- .../network/electron-proxy-credentials.ts | 13 +++--- .../hook-plugin-fail-open-ownership.test.ts | 6 ++- .../hook-plugin-lifecycle-delivery.test.ts | 8 +++- .../loading-store/automation-persistence.ts | 2 +- .../metadata-lineage-operations.ts | 2 +- .../mobile-tab-selection-persistence.ts | 2 +- .../loading-store/primary-state-writes.ts | 2 +- .../loading-store/profile-preferences.ts | 5 ++- .../project-collection-operations.ts | 2 +- .../loading-store/pty-binding-persistence.ts | 2 +- .../repo-lifecycle-operations.ts | 2 +- .../retired-worktree-name-persistence.ts | 2 +- .../loading-store/session-host-partitions.ts | 2 +- .../session-snapshot-operations.ts | 2 +- .../sparse-preset-persistence.ts | 2 +- .../ssh-lease-recovery-operations.ts | 2 +- .../loading-store/ssh-profile-operations.ts | 2 +- .../loading-store/store-domain-composition.ts | 3 +- .../loading-store/write-flush-barriers.ts | 2 +- .../loading-store/write-scheduling.ts | 2 +- ...ng-local-worktree-metadata-pruning.test.ts | 3 +- .../browser-client-download-transfer-store.ts | 8 +++- ...st-lease-download-transfer-cleanup.test.ts | 4 +- .../relay/relay-control-client.test.ts | 5 ++- .../runtime/relay/relay-control-client.ts | 2 +- .../runtime/relay/relay-control-requests.ts | 42 +++++++++++-------- .../runtime/runtime-browser-page-registry.ts | 6 ++- .../runtime/runtime-linear-command-surface.ts | 15 ++++--- ...ructured-session-worktree-teardown.test.ts | 5 ++- .../structured-worker-terminal-read.test.ts | 9 +++- .../hosted-review-branch-cache.ts | 11 +++-- .../worktree-retirement-backfill-scan.test.ts | 2 +- src/main/worktree-retirement-backfill-scan.ts | 6 ++- ...dispatcher-frame-guard-regressions.test.ts | 5 ++- src/relay/dispatcher.test.ts | 11 ++--- .../relay-filesystem-watch-registry.test.ts | 3 +- .../agent/AgentSettingsDialog.test.tsx | 9 +++- .../useAgentBucketCounts.gate.test.ts | 3 +- ...fCommentDecorator.model-lifecycle.test.tsx | 11 ++++- .../editor/markdown-preview-search.ts | 19 ++++++--- .../editor/rich-markdown-auto-focus.test.ts | 5 ++- .../editor/rich-markdown-key-handler.test.ts | 6 +-- .../rich-markdown-list-continuation.test.ts | 4 +- .../editor/rich-markdown-paragraph.test.ts | 8 ++-- .../rich-markdown-tab-key-handler.test.ts | 20 ++++----- .../use-markdown-preview-source-foundation.ts | 3 +- .../src/components/github-checks-tab-state.ts | 7 +++- .../checks-tab-actions.ts | 9 ++-- .../inspect-pull-request/checks-tab.tsx | 5 ++- .../pull-request-page/checks/rerun.ts | 15 +++++-- .../pull-request-page/checks/tab.tsx | 9 ++-- .../GeneralWorkspaceSettingsSection.test.tsx | 3 +- ...RepositoryWorktreeDefaultsSection.test.tsx | 2 +- ...worktree-agent-orchestration-index.test.ts | 5 ++- .../task-page-github-work-item-quiet-state.ts | 9 ++-- .../hidden-output-restore-scheduler.ts | 12 ++++-- .../pty-renderer-delivery-claims.ts | 13 ++++-- .../terminal-captured-input-dispatch.ts | 4 +- .../terminal-ime-xterm-adversarial.test.ts | 11 +++-- .../terminal-pane-lifecycle-primitives.ts | 9 ++-- .../use-task-page-github-quiet-refresh.ts | 3 +- ...vigationMetadata.capability-owner.test.tsx | 12 +++++- ...WindowsTerminalCapabilityOwnerKey.test.tsx | 2 +- .../technical-literal-catalog-values.test.ts | 2 +- .../src/lib/ime-composition-keyboard-event.ts | 16 +++---- .../pane-cursor-blink-suspension.test.ts | 5 ++- .../lib/pane-manager/pane-lifecycle.test.ts | 4 +- .../lib/pane-manager/pane-manager-types.ts | 2 + .../pane-manager/pane-rendering-control.ts | 10 +++-- .../src/lib/pane-manager/pane-scroll.ts | 10 ++--- .../pane-terminal-output-ack-credit.ts | 5 ++- .../terminal-parsed-dirty-rows.ts | 19 ++++++--- .../terminal-scroll-intent-rebuild.ts | 24 +++++++---- .../terminal-webgl-hidden-retention.test.ts | 5 ++- .../terminal-webgl-hidden-retention.ts | 11 +++-- .../terminal-write-pipeline-health.ts | 33 ++++++++------- ...commit-cascade-store-write-samples.test.ts | 2 +- ...eact-commit-cascade-store-write-samples.ts | 26 ++++++------ .../src/lib/simulator-launch-coordination.ts | 5 ++- .../src/lib/state-collection-byte-estimate.ts | 12 ++++-- .../react-commit-cascade-write-probe.test.ts | 3 +- ...lient-host-reconciliation-protocol.test.ts | 5 ++- .../repro-7732-gitlab-job-id-dropped.test.ts | 2 +- 121 files changed, 593 insertions(+), 298 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index bf41f269552..3a115684ba9 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -30,7 +30,7 @@ "anti-slop/no-conditional-empty-object-spread": "off", "anti-slop/no-known-value-widening": "off", "anti-slop/no-module-mocking": "error", - "anti-slop/no-object-parameters": "off", + "anti-slop/no-object-parameters": "error", "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", "anti-slop/no-reflect-get": "error", diff --git a/config/scripts/agent-status-hot-path-benchmark.test.ts b/config/scripts/agent-status-hot-path-benchmark.test.ts index 6c30b82ebf7..ece89b89d50 100644 --- a/config/scripts/agent-status-hot-path-benchmark.test.ts +++ b/config/scripts/agent-status-hot-path-benchmark.test.ts @@ -244,7 +244,11 @@ describe('agent-status hot path benchmark', () => { let objectAssignCalls = 0 let objectAssignPropertyCopies = 0 let freshnessEntryVisits = 0 - Object.assign = ((target: object, ...sources: object[]) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `Object.assign` is an overload set no single arrow can satisfy; this wrapper only counts calls and forwards every argument to the captured native implementation. + Object.assign = (( + target: Record, + ...sources: readonly Record[] + ) => { objectAssignCalls += 1 for (const source of sources) { if (source && typeof source === 'object') { @@ -253,7 +257,8 @@ describe('agent-status hot path benchmark', () => { } return nativeObjectAssign(target, ...sources) }) as typeof Object.assign - Object.values = ((value: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: same overload-set limit as the `Object.assign` wrapper above; this one counts visited entries and returns the native result unchanged. + Object.values = ((value: Record) => { const result = nativeObjectValues(value) freshnessEntryVisits += result.length return result diff --git a/config/scripts/happy-dom-mutation-observer-retention.ts b/config/scripts/happy-dom-mutation-observer-retention.ts index a315b3d520c..c40a070bd58 100644 --- a/config/scripts/happy-dom-mutation-observer-retention.ts +++ b/config/scripts/happy-dom-mutation-observer-retention.ts @@ -48,12 +48,12 @@ export function installHappyDomMutationObserverRetention(): boolean { const disconnect = prototype.disconnect prototype.observe = function patchedObserve( - this: object, + this: PatchableMutationObserver, target: Node, options?: MutationObserverInit ): void { const existing = new Set(readMutationListeners(target)) - observe.call(this as unknown as PatchableMutationObserver, target, options) + observe.call(this, target, options) const pinned = retainedCallbacks.get(this) ?? new Set() for (const listener of readMutationListeners(target)) { if (existing.has(listener)) { @@ -69,8 +69,8 @@ export function installHappyDomMutationObserverRetention(): boolean { } } - prototype.disconnect = function patchedDisconnect(this: object): void { - disconnect.call(this as unknown as PatchableMutationObserver) + prototype.disconnect = function patchedDisconnect(this: PatchableMutationObserver): void { + disconnect.call(this) retainedCallbacks.delete(this) } diff --git a/mobile/src/diagnostics/connection-diagnostics-screen-data.ts b/mobile/src/diagnostics/connection-diagnostics-screen-data.ts index c8a2ed589c8..3ab974b46df 100644 --- a/mobile/src/diagnostics/connection-diagnostics-screen-data.ts +++ b/mobile/src/diagnostics/connection-diagnostics-screen-data.ts @@ -2,10 +2,13 @@ import type { ConnectionLogStore } from '../transport/connection-log-buffer' import type { ConnectionLogEntry, HostProfile } from '../transport/types' import type { RpcClientContextValue } from '../transport/rpc-client-context-contract' +/** Route identity token: compared by reference to detect navigating away and back, never read. */ +export type DiagnosticsRouteKey = Record + export type DiagnosticsHostSelection = { hostId: string requestedHostId: string | undefined - routeKey?: object + routeKey?: DiagnosticsRouteKey } export type DiagnosticsSubmissionState = 'sending' | 'sent' | 'failed' @@ -29,7 +32,7 @@ export function resolveDiagnosticsHostId( hosts: readonly HostProfile[], requestedHostId: string | undefined, manualSelection: DiagnosticsHostSelection | null, - routeKey?: object + routeKey?: DiagnosticsRouteKey ): string | null { const selected = manualSelection if (selected && selected.requestedHostId === requestedHostId && selected.routeKey === routeKey) { diff --git a/mobile/src/session/mobile-terminal-viewport-resubscribe.ts b/mobile/src/session/mobile-terminal-viewport-resubscribe.ts index 8ad3c99c13e..175eb9d92b8 100644 --- a/mobile/src/session/mobile-terminal-viewport-resubscribe.ts +++ b/mobile/src/session/mobile-terminal-viewport-resubscribe.ts @@ -79,6 +79,9 @@ export function shouldResubscribeAfterViewportMeasure(args: { return args.hostCols !== args.measured.cols || args.hostRows !== args.measured.rows } +/** Reference-identity token for a resubscribe attempt; carries no data, only compared by `===`. */ +type RetryGenerationToken = Readonly> + /** Per-handle resubscribe budget, mirroring the chat-side rearm bound: attempts * refill only when the handle actually left terminal.list and came back. A * still-listed non-converging handle re-funded on every list refresh would undo @@ -87,7 +90,7 @@ export class TerminalViewportResubscribeBudget { private readonly attemptsByHandle = new Map() private readonly absentSinceExhaustion = new Set() private readonly announcedExhaustion = new Set() - private readonly retryGenerationByHandle = new Map() + private readonly retryGenerationByHandle = new Map() attempts(handle: string): number { return this.attemptsByHandle.get(handle) ?? 0 @@ -97,17 +100,17 @@ export class TerminalViewportResubscribeBudget { this.attemptsByHandle.set(handle, this.attempts(handle) + 1) } - retryGeneration(handle: string): object { + retryGeneration(handle: string): RetryGenerationToken { const existing = this.retryGenerationByHandle.get(handle) if (existing) { return existing } - const generation = {} + const generation: RetryGenerationToken = {} this.retryGenerationByHandle.set(handle, generation) return generation } - isRetryGenerationCurrent(handle: string, generation: object): boolean { + isRetryGenerationCurrent(handle: string, generation: RetryGenerationToken): boolean { return this.retryGenerationByHandle.get(handle) === generation } diff --git a/mobile/src/transport/direct-rpc-client.ts b/mobile/src/transport/direct-rpc-client.ts index 0031de90c6c..b72c49bd1a7 100644 --- a/mobile/src/transport/direct-rpc-client.ts +++ b/mobile/src/transport/direct-rpc-client.ts @@ -72,7 +72,7 @@ export class DirectRpcClient implements RpcClient { }) this.liveness = new RpcSessionLivenessWatchdog({ transport: 'direct', - sendProbe: (identity) => this.sendLivenessProbe(identity), + sendProbe: (identity) => identity === this.livenessSession && this.sendLivenessProbe(), terminate: (identity) => { if (identity === this.livenessSession && this.socketSession === this.livenessSession) { this.socketClose.forceClose(this.livenessSession) @@ -297,8 +297,8 @@ export class DirectRpcClient implements RpcClient { return false } - private sendLivenessProbe(identity: object): boolean { - if (identity !== this.livenessSession || this.getState() !== 'connected') { + private sendLivenessProbe(): boolean { + if (this.getState() !== 'connected') { return false } return this.sendEncrypted({ diff --git a/mobile/src/transport/host-client-acquisition-registry.ts b/mobile/src/transport/host-client-acquisition-registry.ts index 4e09ebdd9dc..618a4be3c0a 100644 --- a/mobile/src/transport/host-client-acquisition-registry.ts +++ b/mobile/src/transport/host-client-acquisition-registry.ts @@ -1,4 +1,5 @@ -export type HostClientAcquisition = object +/** Holder identity token: the registry only compares references, never reads fields. */ +export type HostClientAcquisition = Record export class HostClientAcquisitionRegistry { private readonly acquisitions = new Map>() diff --git a/mobile/src/transport/relay-dial-stage.ts b/mobile/src/transport/relay-dial-stage.ts index c4a743f84f4..06c6e23477c 100644 --- a/mobile/src/transport/relay-dial-stage.ts +++ b/mobile/src/transport/relay-dial-stage.ts @@ -1,3 +1,5 @@ +import type { RpcClient } from './rpc-client' + // Where a relay dial is waiting, so a bound can tell "the cell never answered the // upgrade" from "the cell took the dial and is slow" — the two look identical from // ConnectionState, which stays 'connecting' until relay-hello arrives. @@ -17,12 +19,21 @@ export type RelayDialStageSource = { onDialStageChange(listener: (stage: RelayDialStage) => void): () => void } -export function relayDialStageSource(session: object): RelayDialStageSource | null { - const candidate = session as Partial - return typeof candidate.getDialStage === 'function' && - typeof candidate.onDialStageChange === 'function' - ? (candidate as RelayDialStageSource) - : null +/** An RPC client that may also report relay dial stages; only relay sessions do. */ +export type MaybeRelayDialStageSource = RpcClient & Partial + +function reportsDialStages( + session: MaybeRelayDialStageSource +): session is MaybeRelayDialStageSource & RelayDialStageSource { + return ( + typeof session.getDialStage === 'function' && typeof session.onDialStageChange === 'function' + ) +} + +export function relayDialStageSource( + session: MaybeRelayDialStageSource +): RelayDialStageSource | null { + return reportsDialStages(session) ? session : null } export class RelayDialStageTracker implements RelayDialStageSource { diff --git a/mobile/src/transport/rpc-session-liveness-watchdog.ts b/mobile/src/transport/rpc-session-liveness-watchdog.ts index 36525f60fb0..cbe891f810c 100644 --- a/mobile/src/transport/rpc-session-liveness-watchdog.ts +++ b/mobile/src/transport/rpc-session-liveness-watchdog.ts @@ -2,7 +2,10 @@ export const LIVENESS_IDLE_MS = 20_000 export const LIVENESS_PROBE_TIMEOUT_MS = 8_000 export const MISSED_PROBE_LIMIT = 3 -export type RpcSessionIdentity = object +declare const rpcSessionIdentityBrand: unique symbol + +/** Opaque per-session token; only ever compared by reference. */ +export type RpcSessionIdentity = object & { readonly [rpcSessionIdentityBrand]?: never } type WatchdogOptions = { transport: 'direct' | 'relay' diff --git a/src/main/ai-vault-search/session-search-clock.ts b/src/main/ai-vault-search/session-search-clock.ts index c9eaf609a34..3c973b273e6 100644 --- a/src/main/ai-vault-search/session-search-clock.ts +++ b/src/main/ai-vault-search/session-search-clock.ts @@ -2,8 +2,8 @@ // makes is "within one reconcile interval", and a guarantee stated in wall time // is only a claim until a test can advance the clock and watch it hold. -/** Opaque to the indexer; a fake clock hands back whatever it likes. */ -export type SessionSearchTimerHandle = object | number +/** Opaque to the indexer: the real clock hands back a timer, a fake clock an id. */ +export type SessionSearchTimerHandle = NodeJS.Timeout | number export type SessionSearchClock = { now(): number @@ -20,5 +20,5 @@ export const systemSessionSearchClock: SessionSearchClock = { timer.unref?.() return timer }, - clearTimeout: (handle) => clearTimeout(handle as NodeJS.Timeout) + clearTimeout: (handle) => clearTimeout(handle) } diff --git a/src/main/artifacts/artifact-cloud-recovery.test.ts b/src/main/artifacts/artifact-cloud-recovery.test.ts index fea3b73bda0..04eebbf25a8 100644 --- a/src/main/artifacts/artifact-cloud-recovery.test.ts +++ b/src/main/artifacts/artifact-cloud-recovery.test.ts @@ -207,7 +207,10 @@ class ArtifactFaultServer { rejectNextDeleteCode: string | null = null rejectNextUpdateStatus: number | null = null private readonly artifacts = new Map() - private readonly createsByKey = new Map() + private readonly createsByKey = new Map< + string, + { body: string; response: ArtifactResponseBody } + >() artifactSlugs(): string[] { return [...this.artifacts.keys()].sort() @@ -325,14 +328,17 @@ async function publishedLink(userDataPath: string): Promise { return result.status === 'ok' ? (result.value?.shareUrl ?? null) : null } -function jsonResponse(body: object, status: number): Response { +/** JSON payload the fake artifact API serialises for a response. */ +type ArtifactResponseBody = Record + +function jsonResponse(body: ArtifactResponseBody, status: number): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }) } -function createResponseBody(slug: string): object { +function createResponseBody(slug: string): ArtifactResponseBody { return { artifact: { version: 1, diff --git a/src/main/browser/agent-browser-bridge-test-harness.ts b/src/main/browser/agent-browser-bridge-test-harness.ts index 0e614600174..7aa38364a3c 100644 --- a/src/main/browser/agent-browser-bridge-test-harness.ts +++ b/src/main/browser/agent-browser-bridge-test-harness.ts @@ -1,4 +1,5 @@ import { vi, type Mock } from 'vitest' +import type { AgentBrowserBridge } from './agent-browser-bridge' import type { BrowserManager } from './browser-manager' export type ExecFileCallback = (error: unknown, stdout?: string, stderr?: string) => void @@ -95,15 +96,19 @@ export function mockWebContents( // Why: the bridge resolves webContents via dynamic require('electron').webContents.fromId // inside a try/catch. Override the private method to inject our mock. export function overrideBridgeWebContentsLookup( - bridgePrototype: object, + bridgePrototype: AgentBrowserBridge, webContentsFromIdMock: Mock ): void { - ;(bridgePrototype as { getWebContents: (id: number) => unknown }).getWebContents = function ( - id: number - ) { - const target = webContentsFromIdMock(id) as { isDestroyed: () => boolean } | null - return target && !target.isDestroyed() ? target : null - } + // Why defineProperty: getWebContents is protected, so a typed assignment is not expressible. + Object.defineProperty(bridgePrototype, 'getWebContents', { + configurable: true, + enumerable: true, + writable: true, + value: function (id: number) { + const target = webContentsFromIdMock(id) as { isDestroyed: () => boolean } | null + return target && !target.isDestroyed() ? target : null + } + }) } export function createSucceedWith(execFileMock: Mock, stdinWrites: string[]) { diff --git a/src/main/browser/browser-cookie-import-clear.ts b/src/main/browser/browser-cookie-import-clear.ts index af79c4249ed..b1b04a98ff9 100644 --- a/src/main/browser/browser-cookie-import-clear.ts +++ b/src/main/browser/browser-cookie-import-clear.ts @@ -54,7 +54,13 @@ export type CookieClearSession = { restoreClearIdentities: CookieClearStore['restoreClearIdentities'] } -const mutationLocks = new WeakMap>() +/** + * Reference identity of one live cookie jar — the partition's Electron Session on both import + * paths. Held weakly and compared by reference; the lock never reads a field off it. + */ +export type CookieMutationLockOwner = WeakKey + +const mutationLocks = new WeakMap>() function cookieClearKey(url: string, name: string): string { return JSON.stringify([url, name]) @@ -85,7 +91,9 @@ export function identitiesFromClearCookies( * remove cookies the newer import already reported as written. Callers that need the lock across a * try/finally take it directly; callers with a single callback use the wrapper below. */ -export async function acquireCookieMutationLock(owner: object): Promise<() => void> { +export async function acquireCookieMutationLock( + owner: CookieMutationLockOwner +): Promise<() => void> { const previous = mutationLocks.get(owner) ?? Promise.resolve() let release!: () => void const current = new Promise((resolve) => { @@ -99,7 +107,10 @@ export async function acquireCookieMutationLock(owner: object): Promise<() => vo return release } -export async function withCookieMutationLock(owner: object, run: () => Promise): Promise { +export async function withCookieMutationLock( + owner: CookieMutationLockOwner, + run: () => Promise +): Promise { const release = await acquireCookieMutationLock(owner) try { return await run() diff --git a/src/main/browser/browser-cookie-import-concurrency.test.ts b/src/main/browser/browser-cookie-import-concurrency.test.ts index 776ded2f7e7..2826cc1207c 100644 --- a/src/main/browser/browser-cookie-import-concurrency.test.ts +++ b/src/main/browser/browser-cookie-import-concurrency.test.ts @@ -2,6 +2,7 @@ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -33,11 +34,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: snapshotClearIdentitiesMock, restoreClearIdentities: async () => undefined, diff --git a/src/main/browser/browser-cookie-import-google-exclusion.test.ts b/src/main/browser/browser-cookie-import-google-exclusion.test.ts index 1cc13450cff..ee73dba1ed2 100644 --- a/src/main/browser/browser-cookie-import-google-exclusion.test.ts +++ b/src/main/browser/browser-cookie-import-google-exclusion.test.ts @@ -3,6 +3,7 @@ * path. Removing 'google.com' from NON_TRANSPLANTABLE_DOMAINS flips every test here red. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -33,12 +34,12 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise set?: (details: Record) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), // Why (STA-4300): the import writes go through CDP identities; route them to the same spy so // a missing method cannot silently reroute every write down the rejected-cookie path. diff --git a/src/main/browser/browser-cookie-import-partition-fidelity.test.ts b/src/main/browser/browser-cookie-import-partition-fidelity.test.ts index d7c6401eb42..f6fe9cedf6d 100644 --- a/src/main/browser/browser-cookie-import-partition-fidelity.test.ts +++ b/src/main/browser/browser-cookie-import-partition-fidelity.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -45,11 +46,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-replacement.test.ts b/src/main/browser/browser-cookie-import-replacement.test.ts index d645bf944ce..cf7c0795010 100644 --- a/src/main/browser/browser-cookie-import-replacement.test.ts +++ b/src/main/browser/browser-cookie-import-replacement.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -36,12 +37,12 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise set?: (details: Record) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), // Why (STA-4300): the import writes go through CDP identities; route them to the same spy so // a missing method cannot silently reroute every write down the rejected-cookie path. diff --git a/src/main/browser/browser-cookie-import-route-partition-staging.test.ts b/src/main/browser/browser-cookie-import-route-partition-staging.test.ts index 106b6742e80..b56b47ca7cc 100644 --- a/src/main/browser/browser-cookie-import-route-partition-staging.test.ts +++ b/src/main/browser/browser-cookie-import-route-partition-staging.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' import type * as NodeFs from 'node:fs' const { @@ -43,11 +44,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-scope.test.ts b/src/main/browser/browser-cookie-import-scope.test.ts index 33381f915d6..e64b33d94f1 100644 --- a/src/main/browser/browser-cookie-import-scope.test.ts +++ b/src/main/browser/browser-cookie-import-scope.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -34,11 +35,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-undecryptable.test.ts b/src/main/browser/browser-cookie-import-undecryptable.test.ts index 95714b36d9e..c17615df22f 100644 --- a/src/main/browser/browser-cookie-import-undecryptable.test.ts +++ b/src/main/browser/browser-cookie-import-undecryptable.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeCrypto from 'node:crypto' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -44,11 +45,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import.test.ts b/src/main/browser/browser-cookie-import.test.ts index 52662b2366d..32bd17d4018 100644 --- a/src/main/browser/browser-cookie-import.test.ts +++ b/src/main/browser/browser-cookie-import.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' import type * as NodeFs from 'node:fs' const { @@ -53,11 +54,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/cdp-keyboard-us-layout.test.ts b/src/main/browser/cdp-keyboard-us-layout.test.ts index b9bcffa50ec..30cfd6264f8 100644 --- a/src/main/browser/cdp-keyboard-us-layout.test.ts +++ b/src/main/browser/cdp-keyboard-us-layout.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { imeFallbackKeyEvent, parseCdpKeyEvent } from './cdp-keyboard-us-layout' +import { imeFallbackKeyEvent, parseCdpKeyEvent, type CdpKeyEvent } from './cdp-keyboard-us-layout' describe('parseCdpKeyEvent', () => { it('maps every printable ASCII character to a key event that types that character', () => { @@ -39,7 +39,7 @@ describe('parseCdpKeyEvent', () => { ['Ctrl+Shift+K', { keyCode: 75, key: 'K', modifiers: 10, text: null }], ['Meta+r', { keyCode: 82, key: 'r', modifiers: 4, text: null }], ['Control+Shift+r', { keyCode: 82, key: 'R', modifiers: 10, text: null }] - ])('parses the shortcut %s', (raw: string, expected: object) => { + ])('parses the shortcut %s', (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject(expected) }) @@ -66,7 +66,7 @@ describe('parseCdpKeyEvent', () => { ['ContextMenu', { keyCode: 93, text: null }], ['F5', { keyCode: 116, key: 'F5', code: 'F5', text: null }], ['F12', { keyCode: 123, text: null }] - ])('parses the named key %s', (raw: string, expected: object) => { + ])('parses the named key %s', (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject(expected) }) @@ -77,7 +77,7 @@ describe('parseCdpKeyEvent', () => { ['Meta', { keyCode: 91, key: 'Meta', code: 'MetaLeft', modifiers: 4, selfModifier: 4 }] ])( 'reports the own modifier bit and left-side location for a bare %s press', - (raw: string, expected: object) => { + (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject({ ...expected, location: 1, text: null }) } ) diff --git a/src/main/browser/doc-preview-download-block-notice.test.ts b/src/main/browser/doc-preview-download-block-notice.test.ts index c57998ea4e5..8cd45d436cc 100644 --- a/src/main/browser/doc-preview-download-block-notice.test.ts +++ b/src/main/browser/doc-preview-download-block-notice.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ publishDocPreviewFailure: vi.fn(), - boundGrantIdByGuest: new Map(), + boundGrantIdByGuest: new Map(), revocationListener: null as null | ((grant: { id: string }) => void) })) @@ -10,7 +10,8 @@ vi.mock('./doc-preview-failure-notice', () => ({ publishDocPreviewFailure: mocks.publishDocPreviewFailure })) vi.mock('./doc-preview-guest-policy', () => ({ - readDocPreviewGuestBoundGrantId: (guest: object) => mocks.boundGrantIdByGuest.get(guest) ?? null + readDocPreviewGuestBoundGrantId: (guest: Electron.WebContents) => + mocks.boundGrantIdByGuest.get(guest) ?? null })) vi.mock('./doc-preview-grant-registry', () => ({ onDocPreviewGrantRevoked: (listener: (grant: { id: string }) => void) => { diff --git a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts index d65a9344acc..e6354826366 100644 --- a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts +++ b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts @@ -263,6 +263,6 @@ function createFixture(): { } } -function writeAuth(home: string, auth: object): void { +function writeAuth(home: string, auth: Record): void { writeFileSync(join(home, 'auth.json'), JSON.stringify(auth), { mode: 0o600 }) } diff --git a/src/main/codex/codex-session-migration-scheduler.ts b/src/main/codex/codex-session-migration-scheduler.ts index d0f696031ae..879a63817db 100644 --- a/src/main/codex/codex-session-migration-scheduler.ts +++ b/src/main/codex/codex-session-migration-scheduler.ts @@ -253,12 +253,21 @@ export function createCodexSessionMigrationScheduler(args: { } } +type MigrationFailureCountKey = 'failedDirectories' | 'failedFiles' | 'failedHealAuditRecords' + +/** The run-result fields the scheduler consults; each runner returns its own summary shape. */ +type MigrationResultFields = Partial> + +function isMigrationResultFields(result: unknown): result is MigrationResultFields { + return typeof result === 'object' && result !== null +} + function isStoppedMigrationResult(result: unknown): boolean { return Boolean(result && typeof result === 'object' && 'stopped' in result && result.stopped) } function isIncompleteBackfillResult(result: unknown): boolean { - if (!result || typeof result !== 'object') { + if (!isMigrationResultFields(result)) { return true } return ( @@ -269,7 +278,10 @@ function isIncompleteBackfillResult(result: unknown): boolean { ) } -function readPositiveResultCount(result: object, key: string): boolean { - const value = key in result ? (result as Record)[key] : undefined +function readPositiveResultCount( + result: MigrationResultFields, + key: MigrationFailureCountKey +): boolean { + const value = result[key] return typeof value === 'number' && value > 0 } diff --git a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts index 820f318d6a8..4703f11300b 100644 --- a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts +++ b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts @@ -235,12 +235,16 @@ describe('DaemonPtyAdapter history recovery', () => { ).id ) ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `checkpointSessions` and `runExclusiveCheckpoint` are `protected` on the checkpoint scheduler, so they are absent from the adapter's public type; the shape below mirrors their declarations and this suite only spies on them. const internals = historyAdapter as unknown as { checkpointSessions( sessionIds: Iterable, opts?: { final?: boolean; teardown?: boolean } ): Promise> - runExclusiveCheckpoint(operation: () => Promise, options?: object): Promise + runExclusiveCheckpoint( + operation: () => Promise, + options?: { rescheduleDirty?: boolean; callerDeadlineMs?: number } + ): Promise } const originalCheckpointSessions = internals.checkpointSessions.bind(historyAdapter) // Call-through spy: entering the exclusive gate is the observable "queued behind the in-flight checkpoint" moment. diff --git a/src/main/git/git-capability-state.test.ts b/src/main/git/git-capability-state.test.ts index b6e655efe62..845c2f47bf8 100644 --- a/src/main/git/git-capability-state.test.ts +++ b/src/main/git/git-capability-state.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshGitProvider } from '../providers/ssh-git-provider' import { clearGitCapabilityStateForTests, getLocalGitCapabilityCache, @@ -10,6 +11,9 @@ import { seedWslLinkedWorktreeGitRoutingForTests } from './wsl-linked-worktree-git-routing' +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cache keys providers by reference only and never calls a method on them. +const createProviderIdentity = (): SshGitProvider => ({}) as SshGitProvider + describe('Git capability execution-host state', () => { beforeEach(() => { clearGitCapabilityStateForTests() @@ -32,8 +36,8 @@ describe('Git capability execution-host state', () => { }) it('shares one SSH provider lifetime without leaking into a replacement provider', () => { - const provider = {} - const replacementProvider = {} + const provider = createProviderIdentity() + const replacementProvider = createProviderIdentity() expect(getSshGitCapabilityCache(provider)).toBe(getSshGitCapabilityCache(provider)) expect(getSshGitCapabilityCache(provider)).not.toBe( diff --git a/src/main/git/git-capability-state.ts b/src/main/git/git-capability-state.ts index 7721df722c8..d998c21ee75 100644 --- a/src/main/git/git-capability-state.ts +++ b/src/main/git/git-capability-state.ts @@ -1,4 +1,5 @@ import { GitCapabilityCache } from '../../shared/git-capability-cache' +import type { SshGitProvider } from '../providers/ssh-git-provider' import { parseWslUncPath } from '../../shared/wsl-paths' import { isWslLinkedWorktreeGitRoutingCandidate, @@ -14,7 +15,7 @@ type LocalGitCapabilityTarget = { const localCapabilitiesByExecutionHost = new Map() // Why: reconnecting creates a new provider, while concurrent IPC/runtime users // of one SSH connection must share the same remote Git capability results. -let sshCapabilitiesByProvider = new WeakMap() +let sshCapabilitiesByProvider = new WeakMap() function getLocalGitExecutionHostKey(target: LocalGitCapabilityTarget): string { const wslDistro = @@ -56,7 +57,7 @@ export function withLocalGitCapabilityCacheForExecution( ) } -export function getSshGitCapabilityCache(provider: object): GitCapabilityCache { +export function getSshGitCapabilityCache(provider: SshGitProvider): GitCapabilityCache { let cache = sshCapabilitiesByProvider.get(provider) if (!cache) { cache = new GitCapabilityCache() diff --git a/src/main/ipc/browser-preview-tool-authorization.test.ts b/src/main/ipc/browser-preview-tool-authorization.test.ts index 0b09f2e0ead..58d72ded647 100644 --- a/src/main/ipc/browser-preview-tool-authorization.test.ts +++ b/src/main/ipc/browser-preview-tool-authorization.test.ts @@ -179,6 +179,12 @@ function grantForNewDocPage(): { id: string; browserPageId: string } { return { id: grant.id, browserPageId } } +/** The fake WebContents a preview's policy installs onto; tools are matched against its identity. */ +type PreviewGuestContents = { + isDestroyed: () => boolean + getURL: () => string +} + /** A preview guest already showing its document, which is the only state a tool can act in. */ function renderPreviewForGrant( grant: { id: string; browserPageId: string }, @@ -186,7 +192,7 @@ function renderPreviewForGrant( ): { grantId: string browserPageId: string - contents: object + contents: PreviewGuestContents markContentsDestroyed: () => void } { const browserPageId = grant.browserPageId @@ -250,7 +256,7 @@ function toolArgs(channel: string, browserPageId: string): Record ({ } })) -import { registerBrowserHandlers, setAgentBrowserBridgeRef } from './browser' +import { registerBrowserHandlers, setAgentBrowserBridgeRef, type BrowserGuestArgs } from './browser' import { waitForAnyTabRegistration, waitForTabRegistration, @@ -136,9 +136,10 @@ describe('registerBrowserHandlers', () => { registerGuestMock.mockReturnValue(false) const settled = Promise.allSettled([waitForTabRegistration('page-1', 1000)]) registerBrowserHandlers() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ipcMain.handle's mock records handlers as a loose tuple; this is the signature registerBrowserHandlers registered for this channel. const registerHandler = handleMock.mock.calls.find( ([channel]) => channel === 'browser:registerGuest' - )?.[1] as (event: { sender: Electron.WebContents }, args: object) => boolean + )?.[1] as (event: { sender: Electron.WebContents }, args: BrowserGuestArgs) => boolean const result = registerHandler( { diff --git a/src/main/ipc/browser.ts b/src/main/ipc/browser.ts index d9aa0409c08..d9841cf3bd1 100644 --- a/src/main/ipc/browser.ts +++ b/src/main/ipc/browser.ts @@ -24,7 +24,7 @@ import type { BrowserWebAuthnAccountResponse } from '../../shared/browser-webaut let agentBrowserBridgeRef: AgentBrowserBridge | null = null -type BrowserGuestRegistrationArgs = { +export type BrowserGuestArgs = { browserPageId: string workspaceId: string worktreeId: string @@ -48,7 +48,7 @@ export function registerBrowserHandlers(): void { const registerGuest = ( event: Electron.IpcMainInvokeEvent, - args: BrowserGuestRegistrationArgs, + args: BrowserGuestArgs, repairPolicies: boolean ): boolean => { if (!isTrustedBrowserRenderer(event.sender)) { @@ -96,7 +96,7 @@ export function registerBrowserHandlers(): void { return true } - ipcMain.handle('browser:registerGuest', (event, args: BrowserGuestRegistrationArgs) => + ipcMain.handle('browser:registerGuest', (event, args: BrowserGuestArgs) => registerGuest(event, args, false) ) @@ -136,7 +136,7 @@ export function registerBrowserHandlers(): void { } ) - ipcMain.handle('browser:repairGuestRegistration', (event, args: BrowserGuestRegistrationArgs) => + ipcMain.handle('browser:repairGuestRegistration', (event, args: BrowserGuestArgs) => registerGuest(event, args, true) ) diff --git a/src/main/ipc/filesystem-test-harness.ts b/src/main/ipc/filesystem-test-harness.ts index 47efa88d6cd..a3a06b95e85 100644 --- a/src/main/ipc/filesystem-test-harness.ts +++ b/src/main/ipc/filesystem-test-harness.ts @@ -207,12 +207,16 @@ export async function withPlatform( } } -function collectMocks(moduleMock: object): IpcMock[] { +function isMockContainer(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function collectMocks(moduleMock: Record): IpcMock[] { return Object.values(moduleMock).flatMap((value) => { if (vi.isMockFunction(value)) { return [value as IpcMock] } - return value && typeof value === 'object' ? collectMocks(value) : [] + return isMockContainer(value) ? collectMocks(value) : [] }) } diff --git a/src/main/ipc/runtime-watcher-process-pool.test.ts b/src/main/ipc/runtime-watcher-process-pool.test.ts index c722b5ccdd3..5b326191d03 100644 --- a/src/main/ipc/runtime-watcher-process-pool.test.ts +++ b/src/main/ipc/runtime-watcher-process-pool.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { WatcherProcessFailure } from './parcel-watcher-process-failure' +import type { WatcherProcessSubscribeOptions } from './parcel-watcher-process-protocol' import type { WatcherProcessCallback, WatcherProcessHooks, @@ -29,7 +30,7 @@ class FakeSupervisor { async subscribe( dir: string, _callback: WatcherProcessCallback, - _opts: object, + _opts: WatcherProcessSubscribeOptions, hooks: WatcherProcessHooks ): Promise { if (this.subscribeError) { diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index 31d587bd056..e2a27ad6aab 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' +import type { GlobalSettings } from '../../shared/global-settings-types' const { applyAppIconMock, @@ -840,7 +841,10 @@ describe('registerSettingsHandlers', () => { 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 })) + store.updateSettings.mockImplementation((args: Partial) => ({ + ...before, + ...args + })) registerSettingsHandlers(store as never) const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( event: typeof settingsInvokeEvent, diff --git a/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts index aa3fff56f77..1bd306c5c2d 100644 --- a/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts +++ b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts @@ -107,7 +107,7 @@ vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).pty const REPO_ID = 'repo-1' const REPO_PATH = '/workspace/repo' -const LOCAL_HOST_ID = 'local' +const LOCAL_HOST_ID = 'local' as const function worktree(path: string, overrides: Partial = {}): GitWorktreeInfo { return { diff --git a/src/main/ipc/worktrees-lineage-hydration.test.ts b/src/main/ipc/worktrees-lineage-hydration.test.ts index e109a13848f..79ece2cc369 100644 --- a/src/main/ipc/worktrees-lineage-hydration.test.ts +++ b/src/main/ipc/worktrees-lineage-hydration.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type { Worktree } from '../../shared/worktree/types' import { toSshExecutionHostId } from '../../shared/execution-host' import { LINEAGE_HYDRATION_TIMEOUT_MS } from './worktrees/metadata/host-lineage-listing' @@ -390,7 +391,7 @@ describe('registerWorktreeHandlers', () => { [childId]: { instanceId: 'child-instance' } } store.getWorktreeMeta.mockImplementation((id: string) => metaById[id]) - store.setWorktreeMeta.mockImplementation((id: string, updates: object) => ({ + store.setWorktreeMeta.mockImplementation((id: string, updates: Partial) => ({ ...metaById[id], ...updates })) diff --git a/src/main/ipc/worktrees-test-ipc-surface.ts b/src/main/ipc/worktrees-test-ipc-surface.ts index a858aacf6b8..7df4f6cda0a 100644 --- a/src/main/ipc/worktrees-test-ipc-surface.ts +++ b/src/main/ipc/worktrees-test-ipc-surface.ts @@ -1,4 +1,5 @@ import { type Mock, vi } from 'vitest' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' export type HandlerMap = Record unknown> @@ -7,7 +8,7 @@ type StoreMock = Mock<(...args: unknown[]) => unknown> /** Store lookups tests re-implement per id, so the first arg stays narrowed. */ type KeyedStoreMock = Mock<(id: string, ...rest: unknown[]) => unknown> /** Store writers tests re-implement by merging the patch they receive. */ -type KeyedStoreWriteMock = Mock<(id: string, patch: object) => unknown> +type KeyedStoreWriteMock = Mock<(id: string, patch: Partial) => unknown> export type TestMainWindow = { isDestroyed: () => boolean diff --git a/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts b/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts index fda412aa0fc..f601ebe0f11 100644 --- a/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts +++ b/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts @@ -86,7 +86,7 @@ export function closeWslTranscriptFsProcess(handle: WslTranscriptFsProcessHandle } export function isWslTranscriptFsProcessHandle( - value: object + value: FileHandle | WslTranscriptFsProcessHandle ): value is WslTranscriptFsProcessHandle { return 'wslTranscriptFsProcessHandle' in value } diff --git a/src/main/network/electron-proxy-credentials.ts b/src/main/network/electron-proxy-credentials.ts index 43a93659474..d9ff26eb940 100644 --- a/src/main/network/electron-proxy-credentials.ts +++ b/src/main/network/electron-proxy-credentials.ts @@ -1,4 +1,5 @@ import { normalizeProxyUrl } from '../../shared/network-proxy' +import type { ProxySession } from './electron-default-proxy-session' export type ElectronProxyCredentials = { host: string @@ -20,7 +21,7 @@ const DEFAULT_PROXY_PORTS: Record = { 'socks5:': 1080 } -let proxyCredentialsBySession = new WeakMap() +let proxyCredentialsBySession = new WeakMap() function decodeProxyCredential(value: string): string { try { @@ -64,7 +65,7 @@ export function haveSameElectronProxyCredentials( } export function setElectronProxyCredentialsForSession( - proxySession: object, + proxySession: ProxySession, credentials: ElectronProxyCredentials | null ): void { if (credentials) { @@ -74,11 +75,11 @@ export function setElectronProxyCredentialsForSession( } } -export function clearElectronProxyCredentialsForSession(proxySession: object): void { +export function clearElectronProxyCredentialsForSession(proxySession: ProxySession): void { proxyCredentialsBySession.delete(proxySession) } -export function resetElectronProxyCredentialsForTests(proxySession?: object): void { +export function resetElectronProxyCredentialsForTests(proxySession?: ProxySession): void { if (proxySession) { clearElectronProxyCredentialsForSession(proxySession) } else { @@ -88,11 +89,11 @@ export function resetElectronProxyCredentialsForTests(proxySession?: object): vo export function handleElectronProxyLogin( event: { preventDefault(): void }, - webContents: { session: object } | null, + webContents: { session: ProxySession } | null, _authenticationResponseDetails: unknown, authInfo: { isProxy: boolean; host: string; port: number; scheme?: string; realm?: string }, callback: (username?: string, password?: string) => void, - defaultProxySession?: object + defaultProxySession?: ProxySession ): void { if (!authInfo.isProxy) { return diff --git a/src/main/opencode/hook-plugin-fail-open-ownership.test.ts b/src/main/opencode/hook-plugin-fail-open-ownership.test.ts index 9f48059604f..6262768da4d 100644 --- a/src/main/opencode/hook-plugin-fail-open-ownership.test.ts +++ b/src/main/opencode/hook-plugin-fail-open-ownership.test.ts @@ -19,6 +19,10 @@ vi.mock('electron', () => ({ import { _internals } from './hook-service' type SessionFixture = { id: string; parentID?: string } +/** The session half of the SDK client, as the plugin's ancestry lookup uses it. */ +type SessionClientFixture = { + list: (options?: { signal?: AbortSignal }) => Promise<{ data: SessionFixture[] }> +} type PluginEvent = { type: string; properties?: Record } type PluginEventHandler = (input: { event: PluginEvent }) => Promise type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise } @@ -83,7 +87,7 @@ describe('OpenCode plugin fail-open ownership', () => { return loadHooksWithSession({ list }) } - async function loadHooksWithSession(session: object): Promise { + async function loadHooksWithSession(session: SessionClientFixture): Promise { return loadHooksWithContext({ client: { session } }) } diff --git a/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts b/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts index 9fc69c8c78d..83a71533f1c 100644 --- a/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts +++ b/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts @@ -19,6 +19,12 @@ vi.mock('electron', () => ({ import { _internals } from './hook-service' type SessionFixture = { id: string; parentID?: string } + +/** The plugin probes both SDK call conventions — current `(parameters, options)` and legacy + * single-options — so fixtures for one session-client method differ in arity. */ +type SessionClientCall = (...args: never[]) => Promise<{ data: SessionFixture[] }> + +type SessionClientFixture = { list: SessionClientCall; get?: SessionClientCall } type PluginEvent = { type: string; properties?: Record } type PluginEventHandler = (input: { event: PluginEvent }) => Promise type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise } @@ -83,7 +89,7 @@ describe('OpenCode plugin lifecycle delivery', () => { return loadHooksWithSession({ list }) } - async function loadHooksWithSession(session: object): Promise { + async function loadHooksWithSession(session: SessionClientFixture): Promise { const pluginPath = join(tempDir, 'orca-opencode-status.mjs') writeFileSync(pluginPath, _internals.getOpenCodePluginSource()) const module = (await import(pathToFileURL(pluginPath).href)) as { diff --git a/src/main/persistence/loading-store/automation-persistence.ts b/src/main/persistence/loading-store/automation-persistence.ts index 9f33f508fd9..2bed89ff09b 100644 --- a/src/main/persistence/loading-store/automation-persistence.ts +++ b/src/main/persistence/loading-store/automation-persistence.ts @@ -243,7 +243,7 @@ export function getAutomationRunWorkspaceDisplayName( } export function installAutomationPersistenceContext( - target: object, + target: AutomationPersistence, source: AutomationPersistence ): void { Object.defineProperty(target, automationPersistenceContext, { diff --git a/src/main/persistence/loading-store/metadata-lineage-operations.ts b/src/main/persistence/loading-store/metadata-lineage-operations.ts index 4b0a301fe26..2532ff3b2d3 100644 --- a/src/main/persistence/loading-store/metadata-lineage-operations.ts +++ b/src/main/persistence/loading-store/metadata-lineage-operations.ts @@ -316,7 +316,7 @@ export function removeWorkspaceLineageForFolderParent( } export function installMetadataLineageOperationsContext( - target: object, + target: MetadataLineageOperations, source: MetadataLineageOperations ): void { Object.defineProperty(target, metadataLineageOperationsContext, { diff --git a/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts b/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts index 321baad1aaf..8f0428c46bb 100644 --- a/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts +++ b/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts @@ -33,7 +33,7 @@ export class MobileTabSelectionPersistence { } export function installMobileTabSelectionPersistenceContext( - target: object, + target: MobileTabSelectionPersistence, source: MobileTabSelectionPersistence ): void { Object.defineProperty(target, mobileTabSelectionPersistenceContext, { diff --git a/src/main/persistence/loading-store/primary-state-writes.ts b/src/main/persistence/loading-store/primary-state-writes.ts index f61f6c691bd..3820723fffb 100644 --- a/src/main/persistence/loading-store/primary-state-writes.ts +++ b/src/main/persistence/loading-store/primary-state-writes.ts @@ -280,7 +280,7 @@ export function writeToDiskSync( } export function installPrimaryStateWriteOperationsContext( - target: object, + target: PrimaryStateWriteOperations, source: PrimaryStateWriteOperations ): void { Object.defineProperty(target, primaryStateWriteOperationsContext, { diff --git a/src/main/persistence/loading-store/profile-preferences.ts b/src/main/persistence/loading-store/profile-preferences.ts index 0ec4e383c34..8e910ed235d 100644 --- a/src/main/persistence/loading-store/profile-preferences.ts +++ b/src/main/persistence/loading-store/profile-preferences.ts @@ -189,7 +189,10 @@ export function getFeatureInteractionOperations( } } -export function installProfilePreferencesContext(target: object, source: ProfilePreferences): void { +export function installProfilePreferencesContext( + target: ProfilePreferences, + source: ProfilePreferences +): void { Object.defineProperty(target, profilePreferencesContext, { value: source[profilePreferencesContext] }) diff --git a/src/main/persistence/loading-store/project-collection-operations.ts b/src/main/persistence/loading-store/project-collection-operations.ts index ecb1130072f..e8d22147769 100644 --- a/src/main/persistence/loading-store/project-collection-operations.ts +++ b/src/main/persistence/loading-store/project-collection-operations.ts @@ -226,7 +226,7 @@ export function getFolderWorkspaceOperations( } export function installProjectCollectionOperationsContext( - target: object, + target: ProjectCollectionOperations, source: ProjectCollectionOperations ): void { Object.defineProperty(target, projectCollectionOperationsContext, { diff --git a/src/main/persistence/loading-store/pty-binding-persistence.ts b/src/main/persistence/loading-store/pty-binding-persistence.ts index 27d62c8602b..9cdb51fffe7 100644 --- a/src/main/persistence/loading-store/pty-binding-persistence.ts +++ b/src/main/persistence/loading-store/pty-binding-persistence.ts @@ -266,7 +266,7 @@ function applyPtyBinding( } export function installPtyBindingPersistenceOperationsContext( - target: object, + target: PtyBindingPersistenceOperations, source: PtyBindingPersistenceOperations ): void { Object.defineProperty(target, ptyBindingPersistenceOperationsContext, { diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index ec1df02d714..2573c63305f 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -323,7 +323,7 @@ export function hydrateRepo(owner: RepoLifecycleOperations, repo: Repo): Repo { } export function installRepoLifecycleOperationsContext( - target: object, + target: RepoLifecycleOperations, source: RepoLifecycleOperations ): void { Object.defineProperty(target, repoLifecycleOperationsContext, { diff --git a/src/main/persistence/loading-store/retired-worktree-name-persistence.ts b/src/main/persistence/loading-store/retired-worktree-name-persistence.ts index c5260850522..bf0e7383667 100644 --- a/src/main/persistence/loading-store/retired-worktree-name-persistence.ts +++ b/src/main/persistence/loading-store/retired-worktree-name-persistence.ts @@ -110,7 +110,7 @@ export function applyRetiredWorktreeNames( } export function installRetiredWorktreeNamePersistenceContext( - target: object, + target: RetiredWorktreeNamePersistence, source: RetiredWorktreeNamePersistence ): void { Object.defineProperty(target, retiredWorktreeNamePersistenceContext, { diff --git a/src/main/persistence/loading-store/session-host-partitions.ts b/src/main/persistence/loading-store/session-host-partitions.ts index 353c89da4e5..d358f4c8f62 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -210,7 +210,7 @@ export function setHostWorkspaceSession( } export function installSessionHostPartitionOperationsContext( - target: object, + target: SessionHostPartitionOperations, source: SessionHostPartitionOperations ): void { Object.defineProperty(target, sessionHostPartitionOperationsContext, { diff --git a/src/main/persistence/loading-store/session-snapshot-operations.ts b/src/main/persistence/loading-store/session-snapshot-operations.ts index 2f5f1c64b86..37ffd9d366b 100644 --- a/src/main/persistence/loading-store/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/session-snapshot-operations.ts @@ -93,7 +93,7 @@ export function getSessionSnapshotOperationsContext(owner: SessionSnapshotOperat } export function installSessionSnapshotOperationsContext( - target: object, + target: SessionSnapshotOperations, source: SessionSnapshotOperations ): void { Object.defineProperty(target, sessionSnapshotOperationsContext, { diff --git a/src/main/persistence/loading-store/sparse-preset-persistence.ts b/src/main/persistence/loading-store/sparse-preset-persistence.ts index 2de83313ecd..20b78fcd9cc 100644 --- a/src/main/persistence/loading-store/sparse-preset-persistence.ts +++ b/src/main/persistence/loading-store/sparse-preset-persistence.ts @@ -46,7 +46,7 @@ export class SparsePresetPersistence { } export function installSparsePresetPersistenceContext( - target: object, + target: SparsePresetPersistence, source: SparsePresetPersistence ): void { Object.defineProperty(target, sparsePresetPersistenceContext, { diff --git a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts index 75aa19ad24b..7da5144898e 100644 --- a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts +++ b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts @@ -245,7 +245,7 @@ export function getSshPtyLeaseOperations(owner: SshLeaseRecoveryOperations): Ssh } export function installSshLeaseRecoveryOperationsContext( - target: object, + target: SshLeaseRecoveryOperations, source: SshLeaseRecoveryOperations ): void { Object.defineProperty(target, sshLeaseRecoveryOperationsContext, { diff --git a/src/main/persistence/loading-store/ssh-profile-operations.ts b/src/main/persistence/loading-store/ssh-profile-operations.ts index dce7a92890f..5fed1a3d021 100644 --- a/src/main/persistence/loading-store/ssh-profile-operations.ts +++ b/src/main/persistence/loading-store/ssh-profile-operations.ts @@ -146,7 +146,7 @@ export function getSshTargetStateOperations(owner: SshProfileOperations): SshTar } export function installSshProfileOperationsContext( - target: object, + target: SshProfileOperations, source: SshProfileOperations ): void { Object.defineProperty(target, sshProfileOperationsContext, { diff --git a/src/main/persistence/loading-store/store-domain-composition.ts b/src/main/persistence/loading-store/store-domain-composition.ts index 2bbe91a1a2e..c3f2059efe1 100644 --- a/src/main/persistence/loading-store/store-domain-composition.ts +++ b/src/main/persistence/loading-store/store-domain-composition.ts @@ -1,4 +1,5 @@ import type { StoreRuntimeState } from './store-runtime-state' +import type { Store } from './store' import { LoadedStateAdaptationOperations } from './loaded-state-adaptation' import { BackupRecoveryRotationOperations } from './backup-recovery-rotation' import { LoadedCohortMigrationOperations } from './loaded-cohort-migrations' @@ -108,7 +109,7 @@ export const STORE_DOMAIN_OPERATION_CLASSES = [ WriteFlushBarrierOperations ] as const -export function installStoreDomainContexts(target: object, domains: StoreDomains): void { +export function installStoreDomainContexts(target: Store, domains: StoreDomains): void { installWriteSchedulingOperationsContext(target, domains.scheduling) installPrimaryStateWriteOperationsContext(target, domains.writes) installProjectCollectionOperationsContext(target, domains.projects) diff --git a/src/main/persistence/loading-store/write-flush-barriers.ts b/src/main/persistence/loading-store/write-flush-barriers.ts index 1a80c77976f..c08d4367989 100644 --- a/src/main/persistence/loading-store/write-flush-barriers.ts +++ b/src/main/persistence/loading-store/write-flush-barriers.ts @@ -269,7 +269,7 @@ export function writeGithubCacheSnapshotSync(owner: WriteFlushBarrierOperations) } export function installWriteFlushBarrierOperationsContext( - target: object, + target: WriteFlushBarrierOperations, source: WriteFlushBarrierOperations ): void { Object.defineProperty(target, writeFlushBarrierOperationsContext, { diff --git a/src/main/persistence/loading-store/write-scheduling.ts b/src/main/persistence/loading-store/write-scheduling.ts index c78a5d0dfd4..0301da34a7e 100644 --- a/src/main/persistence/loading-store/write-scheduling.ts +++ b/src/main/persistence/loading-store/write-scheduling.ts @@ -64,7 +64,7 @@ export function scheduleSave(owner: WriteSchedulingOperations): void { } export function installWriteSchedulingOperationsContext( - target: object, + target: WriteSchedulingOperations, source: WriteSchedulingOperations ): void { Object.defineProperty(target, writeSchedulingOperationsContext, { diff --git a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts index 0588a2f6dae..2b744f6cdf0 100644 --- a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts +++ b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts @@ -3,6 +3,7 @@ import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../s import type { PersistedState } from '../../../shared/persisted-state-types' import type { Project } from '../../../shared/project-types' import type { Repo } from '../../../shared/repo-types' +import type { SshRemotePtyLease } from '../../../shared/ssh-types' import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' import type { WorktreeMeta } from '../../../shared/worktree/meta-types' import { @@ -299,7 +300,7 @@ describe('pruneSessionlessMissingLocalWorktreeMetadataForRepo', () => { for (const worktreeId of allIds) { state.worktreeMeta[worktreeId] = makeMeta(worktreeId) } - const lease = (worktreeId: string, index: number, extra: object) => ({ + const lease = (worktreeId: string, index: number, extra: Partial) => ({ targetId: 'builder', ptyId: `pty-${index}`, worktreeId, diff --git a/src/main/runtime/browser-client-download-transfer-store.ts b/src/main/runtime/browser-client-download-transfer-store.ts index 7da5042d7ea..c6c63b322fe 100644 --- a/src/main/runtime/browser-client-download-transfer-store.ts +++ b/src/main/runtime/browser-client-download-transfer-store.ts @@ -21,7 +21,11 @@ type RuntimeFileChannelHost = { statRuntimeFile(worktree: string, relativePath: string): Promise } -const stores = new WeakMap() +// Release runs from the lease registry, which only knows the runtime by id; the store itself is +// only ever created for a file-channel host. +type DownloadTransferRuntime = RuntimeFileChannelHost | { getRuntimeId(): string } + +const stores = new WeakMap() /** * Drops every staged download a page still owns. @@ -31,7 +35,7 @@ const stores = new WeakMap() * opened a file channel. */ export function releaseBrowserClientDownloadTransfersForPage( - runtime: object, + runtime: DownloadTransferRuntime, browserPageId: string ): Promise { return stores.get(runtime)?.releasePage(browserPageId) ?? Promise.resolve() diff --git a/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts b/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts index aaa78e6d45b..77b716218aa 100644 --- a/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts +++ b/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts @@ -18,7 +18,9 @@ function createRuntime() { return { runtime, removed } } -async function stageTransfer(runtime: object, browserPageId: string): Promise { +type FakeRuntime = ReturnType['runtime'] + +async function stageTransfer(runtime: FakeRuntime, browserPageId: string): Promise { await getBrowserClientDownloadTransferStore(runtime as never).accept({ transferId: `transfer-${browserPageId}`, browserPageId, diff --git a/src/main/runtime/relay/relay-control-client.test.ts b/src/main/runtime/relay/relay-control-client.test.ts index 745a79ac84e..431d26574cf 100644 --- a/src/main/runtime/relay/relay-control-client.test.ts +++ b/src/main/runtime/relay/relay-control-client.test.ts @@ -8,6 +8,9 @@ import { MOBILE_RELAY_CLOSE_CODE } from '../../../shared/mobile-relay-close-code import { RelayControlClient } from './relay-control-client' const encoder = new TextEncoder() + +/** A JSON control frame, including the forward-compat frames the client must ignore. */ +type ControlFrame = { type: string } & Record const HOST_PROOF_DOMAIN = 'orca-relay-host-proof/v1' const CHALLENGE_DOMAIN = 'orca-relay-host-challenge/v1' @@ -410,7 +413,7 @@ class FakeControlSocket extends EventEmitter { this.close(1006) } - deliver(message: object): void { + deliver(message: ControlFrame): void { this.emit('message', JSON.stringify(message), false) } } diff --git a/src/main/runtime/relay/relay-control-client.ts b/src/main/runtime/relay/relay-control-client.ts index 0c863cce3ad..8321bf8caf2 100644 --- a/src/main/runtime/relay/relay-control-client.ts +++ b/src/main/runtime/relay/relay-control-client.ts @@ -269,7 +269,7 @@ export class RelayControlClient { this.clearConnectPromise() } - private sendActive(payload: object): void { + private sendActive(payload: Record): void { if (!this.socket || (this.state !== 'active' && this.state !== 'draining')) { throw new Error('relay_control_not_active') } diff --git a/src/main/runtime/relay/relay-control-requests.ts b/src/main/runtime/relay/relay-control-requests.ts index bbceb067a59..6151d634f0d 100644 --- a/src/main/runtime/relay/relay-control-requests.ts +++ b/src/main/runtime/relay/relay-control-requests.ts @@ -22,6 +22,23 @@ export type DeviceCredentialInstallAuthorization = | { mode: 'relay-basis'; basisConnId: string } | { mode: 'authenticated-direct'; directAuthId: string } +export type DeviceCredentialInstallInput = { + relayDeviceId: string + newResumeTokenHash: string + expectedCurrentHash?: string + authorization: DeviceCredentialInstallAuthorization +} + +/** Every control-plane request this class hands to `send`. */ +type RelayControlRequestPayload = + | { type: 'invite-create'; reqId: string; relayDeviceId: string } + | { type: 'device-revoke'; reqId: string; relayDeviceId: string } + | ({ type: 'device-credential-install'; v: 1; reqId: string } & DeviceCredentialInstallInput) + | { type: 'device-credential-install-status'; v: 1; reqId: string; relayDeviceId: string } + | { type: 'device-resume-confirm'; v: 1; reqId: string; basisConnId: string } + +type SendRelayControlRequest = (payload: RelayControlRequestPayload) => void + export class RelayControlRequests { private readonly pending = new Map() @@ -34,7 +51,7 @@ export class RelayControlRequests { createInvite( reqId: string, relayDeviceId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -44,11 +61,7 @@ export class RelayControlRequests { ) as Promise } - revokeDevice( - reqId: string, - relayDeviceId: string, - send: (payload: object) => void - ): Promise { + revokeDevice(reqId: string, relayDeviceId: string, send: SendRelayControlRequest): Promise { return this.request( reqId, 'revoke', @@ -59,13 +72,8 @@ export class RelayControlRequests { installCredential( reqId: string, - input: { - relayDeviceId: string - newResumeTokenHash: string - expectedCurrentHash?: string - authorization: DeviceCredentialInstallAuthorization - }, - send: (payload: object) => void + input: DeviceCredentialInstallInput, + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -78,7 +86,7 @@ export class RelayControlRequests { credentialInstallStatus( reqId: string, relayDeviceId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -91,7 +99,7 @@ export class RelayControlRequests { confirmResume( reqId: string, basisConnId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -156,8 +164,8 @@ export class RelayControlRequests { private request( reqId: string, kind: PendingRequest['kind'], - payload: object, - send: (payload: object) => void + payload: RelayControlRequestPayload, + send: SendRelayControlRequest ): Promise { if (this.pending.has(reqId)) { return Promise.reject(new Error('duplicate_relay_request_id')) diff --git a/src/main/runtime/runtime-browser-page-registry.ts b/src/main/runtime/runtime-browser-page-registry.ts index 9209e24e4af..f32f83dd105 100644 --- a/src/main/runtime/runtime-browser-page-registry.ts +++ b/src/main/runtime/runtime-browser-page-registry.ts @@ -226,9 +226,11 @@ export class RuntimeBrowserPageRegistry { } } -const registries = new WeakMap() +/** Keyed by runtime identity alone; this module never reads from the runtime, and the callers' + * declared host types share no member. */ +const registries = new WeakMap() -export function getRuntimeBrowserPageRegistry(runtime: object): RuntimeBrowserPageRegistry { +export function getRuntimeBrowserPageRegistry(runtime: WeakKey): RuntimeBrowserPageRegistry { let registry = registries.get(runtime) if (!registry) { registry = new RuntimeBrowserPageRegistry() diff --git a/src/main/runtime/runtime-linear-command-surface.ts b/src/main/runtime/runtime-linear-command-surface.ts index cf9ad9e69df..97c91e3af6d 100644 --- a/src/main/runtime/runtime-linear-command-surface.ts +++ b/src/main/runtime/runtime-linear-command-surface.ts @@ -13,9 +13,12 @@ type LinearFacadeInstance = { type LinearMethodBag = Record unknown> const delegators = new WeakSet() -const receiverByCommands = new WeakMap() +const receiverByCommands = new WeakMap() -function collectMethodNames(instancePrototype: object, stopAt: object | null): Set { +function collectMethodNames( + instancePrototype: RuntimeLinearBrowseCommands, + stopAt: RuntimeLinearBrowseCommands | null +): Set { const names = new Set() let prototype: object | null = instancePrototype while (prototype && prototype !== Object.prototype && prototype !== stopAt) { @@ -31,10 +34,10 @@ function collectMethodNames(instancePrototype: object, stopAt: object | null): S // Why: the chain used to live on the facade, so a facade override (test spy) has to win for re-entrant `this` calls too. function overrideAwareReceiver( - facade: object, - commands: object, + facade: LinearFacadeInstance, + commands: LinearMethodBag, surfaceNames: ReadonlySet -): object { +): LinearMethodBag { const cached = receiverByCommands.get(commands) if (cached) { return cached @@ -55,7 +58,7 @@ function overrideAwareReceiver( return receiver } -export function installRuntimeLinearCommandSurface(target: object): void { +export function installRuntimeLinearCommandSurface(target: LinearFacadeInstance): void { const names = collectMethodNames( RuntimeLinearCommands.prototype, RuntimeLinearCommandBase.prototype diff --git a/src/main/runtime/structured-session-worktree-teardown.test.ts b/src/main/runtime/structured-session-worktree-teardown.test.ts index 86258ad9dab..915fbd52986 100644 --- a/src/main/runtime/structured-session-worktree-teardown.test.ts +++ b/src/main/runtime/structured-session-worktree-teardown.test.ts @@ -144,7 +144,10 @@ function destructiveDeps(extra: { allowUnverifiedStop?: boolean; timeoutMs?: num } } -function runtimeDouble(hooks: object): TeardownRuntime { +/** Keys are pinned to the real runtime; each stub narrows its own args to what the case drives. */ +type TeardownRuntimeStubs = Partial> + +function runtimeDouble(hooks: TeardownRuntimeStubs): TeardownRuntime { return Object.assign(Object.create(null), hooks) } diff --git a/src/main/runtime/structured-worker-terminal-read.test.ts b/src/main/runtime/structured-worker-terminal-read.test.ts index 97859a988e0..8d7f4ef6b70 100644 --- a/src/main/runtime/structured-worker-terminal-read.test.ts +++ b/src/main/runtime/structured-worker-terminal-read.test.ts @@ -161,12 +161,17 @@ describe('reading a structured worker through the terminal-read path', () => { // could be perfect and a peer would still get `terminal_handle_stale` if nothing called it. const handle = registerWorker() installHost({ items: [message('i1', 'hello')] }) - const runtime = Object.assign(Object.create(OrcaRuntimeWithResolveTerminalPane.prototype), { + const runtime: { + readTerminal: ( + handle: string, + opts?: { cursor?: number; limit?: number; screen?: boolean } + ) => Promise<{ tail: string[] }> + } = Object.assign(Object.create(OrcaRuntimeWithResolveTerminalPane.prototype), { getOrchestrationDbIfAvailable: () => null, getLivePtyForHandle: () => { throw new Error('the PTY lookup must never be reached for a structured worker') } - }) as { readTerminal: (handle: string, opts?: object) => Promise<{ tail: string[] }> } + }) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ tail: ['[assistant] hello'], source: 'stream' diff --git a/src/main/source-control/hosted-review-branch-cache.ts b/src/main/source-control/hosted-review-branch-cache.ts index 7fafc855ba2..0e6f207ab9c 100644 --- a/src/main/source-control/hosted-review-branch-cache.ts +++ b/src/main/source-control/hosted-review-branch-cache.ts @@ -59,9 +59,14 @@ type CacheEntry = { startedAt: number } +declare const inflightTokenBrand: unique symbol + +/** Identity token for one lookup; only ever compared by reference. */ +type InflightToken = { readonly [inflightTokenBrand]?: never } + type InflightRecord = { /** Identity, so a detached lookup can only ever clear its own entry. */ - token: object + token: InflightToken startedAt: number promise: Promise /** Releases the callers and unpins the branch; idempotent. */ @@ -154,7 +159,7 @@ function storeEntry(key: string, entry: CacheEntry): void { } /** Clears the key's in-flight record only if it is still this lookup's. */ -function releaseInflight(key: string, token: object): boolean { +function releaseInflight(key: string, token: InflightToken): boolean { if (inflight.get(key)?.token !== token) { return false } @@ -271,7 +276,7 @@ function startLookup( ): Promise { const startedAt = Date.now() const generation = scopeGeneration(scope) - const token = {} + const token: InflightToken = {} /** The deadline released the callers; the lookup itself runs on, detached. */ let timedOut = false let completed = false diff --git a/src/main/worktree-retirement-backfill-scan.test.ts b/src/main/worktree-retirement-backfill-scan.test.ts index f90d4ccf405..0f02f11222f 100644 --- a/src/main/worktree-retirement-backfill-scan.test.ts +++ b/src/main/worktree-retirement-backfill-scan.test.ts @@ -32,7 +32,7 @@ function stallingScan(): { } /** Drive one namespace to the state where its listing is abandoned but still stuck in the kernel. */ -async function stallPastDeadline(store: object, scanKey: string) { +async function stallPastDeadline(store: WeakKey, scanKey: string) { const scan = stallingScan() const pending = runRetirementBackfillScan(store, scanKey, scan.run) const settled = expect(pending).rejects.toThrow(/exceeded/) diff --git a/src/main/worktree-retirement-backfill-scan.ts b/src/main/worktree-retirement-backfill-scan.ts index 8ca5b26ccd5..c0c111729a3 100644 --- a/src/main/worktree-retirement-backfill-scan.ts +++ b/src/main/worktree-retirement-backfill-scan.ts @@ -20,7 +20,9 @@ type BackfillScan = { outstanding: boolean } -const scansByStore = new WeakMap>() +/** Only the store's identity is the memo key — this module never reads from it, and cannot name the + * store's own type without importing its caller. */ +const scansByStore = new WeakMap>() /** Monotonic, like the WSL gate's own stuck timer: wall time misjudges a backoff across laptop * sleep or an NTP step, either pinning a namespace in its failure memo or ending it early. */ @@ -59,7 +61,7 @@ function withScanDeadline(scan: Promise): Promise { * the rule per namespace rather than process-wide is deliberate: a global budget lets one bad mount * spend it on its own retries and starve every healthy repo. */ export function runRetirementBackfillScan( - store: object, + store: WeakKey, scanKey: string, scan: () => Promise ): Promise> { diff --git a/src/relay/dispatcher-frame-guard-regressions.test.ts b/src/relay/dispatcher-frame-guard-regressions.test.ts index eb39e90bbdb..6574579d4ee 100644 --- a/src/relay/dispatcher-frame-guard-regressions.test.ts +++ b/src/relay/dispatcher-frame-guard-regressions.test.ts @@ -1,11 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import { RelayDispatcher } from './dispatcher' +import type { RelayClient } from './dispatcher-contract' import type { JsonRpcNotification } from './protocol' type DispatcherInternals = { - primaryClient: object + primaryClient: RelayClient estimateFrameBytes: (msg: JsonRpcNotification) => number - enqueueFrame: (client: object, msg: JsonRpcNotification, lane: string) => boolean + enqueueFrame: (client: RelayClient, msg: JsonRpcNotification, lane: string) => boolean } describe('RelayDispatcher frame guards', () => { diff --git a/src/relay/dispatcher.test.ts b/src/relay/dispatcher.test.ts index 280f9351200..f8ccc11c153 100644 --- a/src/relay/dispatcher.test.ts +++ b/src/relay/dispatcher.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { RelayDispatcher, type SinkWriteSettlement } from './dispatcher' +import type { PreparedRelayFrame, RelayClient } from './dispatcher-contract' import { relayWriterControlReserve } from './dispatcher-writer-admission' import { encodeJsonRpcFrame, @@ -723,18 +724,18 @@ describe('RelayDispatcher', () => { describe('legacy PTY chunk sizing', () => { type DispatcherInternals = { - primaryClient: object + primaryClient: RelayClient estimateFrameBytes: (msg: JsonRpcNotification) => number - prepareFrame: (msg: JsonRpcNotification) => object + prepareFrame: (msg: JsonRpcNotification) => PreparedRelayFrame enqueueFrame: ( - client: object, + client: RelayClient, msg: JsonRpcNotification, lane: string, onSettled?: (result: SinkWriteSettlement) => void ) => boolean enqueuePreparedFrame: ( - client: object, - frame: object, + client: RelayClient, + frame: PreparedRelayFrame, lane: string, onSettled?: (result: SinkWriteSettlement) => void ) => boolean diff --git a/src/relay/relay-filesystem-watch-registry.test.ts b/src/relay/relay-filesystem-watch-registry.test.ts index de924230f0f..dcea47e7d44 100644 --- a/src/relay/relay-filesystem-watch-registry.test.ts +++ b/src/relay/relay-filesystem-watch-registry.test.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { WatcherProcessFailure } from '../main/ipc/parcel-watcher-process-failure' import { WatcherProcessSupervisor } from '../main/ipc/parcel-watcher-process-supervisor' +import type { WatcherProcessSubscribeOptions } from '../main/ipc/parcel-watcher-process-protocol' import type { WatcherProcessCallback, WatcherProcessHooks, @@ -50,7 +51,7 @@ class FakeWatcherPool { async subscribe( rootPath: string, callback: WatcherProcessCallback, - _options: object, + _options: WatcherProcessSubscribeOptions, hooks: WatcherProcessHooks ): Promise { const unsubscribe = vi.fn(async () => undefined) diff --git a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx index ee45c71c30e..ab3ea61b1e2 100644 --- a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx +++ b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx @@ -16,8 +16,15 @@ const testState = vi.hoisted(() => ({ runtimeEnvironments: [] as { id: string; createdAt: number; pairingRevision?: number }[] })) +type MockedAppStoreState = { + settings: GlobalSettings | null + updateSettings: (settings: Partial) => void + runtimeEnvironments: { id: string; createdAt: number; pairingRevision?: number }[] + runtimeStatusByEnvironmentId: Map +} + vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => + useAppStore: (selector: (state: MockedAppStoreState) => unknown) => selector({ settings: testState.settings, updateSettings: testState.updateSettings, diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts b/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts index 618d04a9169..5f89b90f715 100644 --- a/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts +++ b/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts @@ -65,7 +65,8 @@ function countAllocations(run: () => void): { entries: number; maps: number } { const RealMap = globalThis.Map let entries = 0 let maps = 0 - Object.entries = ((target: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `Object.entries` is an overload set no single arrow can satisfy; this wrapper only counts calls and returns the native result unchanged. + Object.entries = ((target: Record) => { entries += 1 return realEntries(target) }) as typeof Object.entries diff --git a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx index fb92076c081..b8ef6823236 100644 --- a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx +++ b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx @@ -19,6 +19,13 @@ afterEach(() => { vi.clearAllMocks() }) +/** No zones exist in this suite, so the hook never reaches these. */ +const viewZoneAccessor: MonacoEditor.IViewZoneChangeAccessor = { + addZone: () => '', + removeZone: () => undefined, + layoutZone: () => undefined +} + describe('useDiffCommentDecorator model lifecycle', () => { it('rebuilds model-scoped resources when a retained editor swaps models', () => { const editorDomNode = document.createElement('div') @@ -26,13 +33,15 @@ describe('useDiffCommentDecorator model lifecycle', () => { const disposeMouseMove = vi.fn() const disposeMouseLeave = vi.fn() const disposeScroll = vi.fn() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial stand-in for Monaco's ICodeEditor; useDiffCommentDecorator calls only the members defined here, and a real editor needs a laid-out DOM this suite does not build. const editor = { getDomNode: () => editorDomNode, getOption: () => 19, onMouseMove: () => ({ dispose: disposeMouseMove }), onMouseLeave: () => ({ dispose: disposeMouseLeave }), onDidScrollChange: () => ({ dispose: disposeScroll }), - changeViewZones: (callback: (accessor: object) => void) => callback({}) + changeViewZones: (callback: (accessor: MonacoEditor.IViewZoneChangeAccessor) => void) => + callback(viewZoneAccessor) } as unknown as MonacoEditor.ICodeEditor const hook = renderHook( ({ monacoModelIdentity }) => diff --git a/src/renderer/src/components/editor/markdown-preview-search.ts b/src/renderer/src/components/editor/markdown-preview-search.ts index 1b92f958fa8..3dc168ddd61 100644 --- a/src/renderer/src/components/editor/markdown-preview-search.ts +++ b/src/renderer/src/components/editor/markdown-preview-search.ts @@ -219,8 +219,15 @@ function getHighlightApi(): { // window). Track each instance's ranges by its own token and paint the UNION, // so a second preview's Find does not clobber the first's highlights. Ranges // live in each instance's own subtree, so the union paints every pane correctly. -const searchRangesByInstance = new Map() -const activeRangeByInstance = new Map() +declare const markdownPreviewSearchInstanceBrand: unique symbol + +/** Per-preview identity for the highlight maps; only compared by reference. */ +export type MarkdownPreviewSearchInstance = { + readonly [markdownPreviewSearchInstanceBrand]?: never +} + +const searchRangesByInstance = new Map() +const activeRangeByInstance = new Map() // Avoid array spread when collecting union ranges — a large doc can produce // 100k+ ranges and create()/registry writes must not build variadic arg lists. @@ -250,7 +257,9 @@ function paintActiveHighlight(api: NonNullable + function setupScheduledFocus( - activeElement: object | null, + activeElement: StubbedActiveElement | null, force = false ): { focus: ReturnType diff --git a/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts b/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts index 26847626853..e6893049633 100644 --- a/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import { createIsolatedMarkdownExtensionForTests } from './isolated-markdown-extension-for-tests' import { createRichMarkdownKeyHandler, type KeyHandlerContext } from './rich-markdown-key-handler' @@ -14,7 +14,7 @@ vi.mock('@/lib/shortcut-platform', () => ({ const extensions = [StarterKit, createIsolatedMarkdownExtensionForTests()] -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { return new Editor({ element: null, extensions, @@ -133,7 +133,7 @@ function createContext(editor: Editor, typedMarker: boolean): KeyHandlerContext } } -function emptyTopLevelOrderedList(): object { +function emptyTopLevelOrderedList(): JSONContent { return { type: 'doc', content: [ diff --git a/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts b/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts index 31a3e06aa01..a184882a2e0 100644 --- a/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import { createIsolatedMarkdownExtensionForTests } from './isolated-markdown-extension-for-tests' import { @@ -10,7 +10,7 @@ import { isSingleEmptyTopLevelOrderedList } from './rich-markdown-list-continuation' -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { // Why: each Editor needs its own marked registry; sharing one module-scoped // extension accumulates tokenizer state across tests. return new Editor({ diff --git a/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts b/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts index d59cb0c4c04..1ec3fcc04df 100644 --- a/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts @@ -2,9 +2,11 @@ import { describe, expect, it, vi } from 'vitest' import { RichMarkdownParagraph } from './rich-markdown-paragraph' vi.mock('@tiptap/extension-paragraph', async () => { - const actual = (await vi.importActual('@tiptap/extension-paragraph')) as { - Paragraph: { extend: (config: object) => { config: Record } } - } + const actual = await vi.importActual<{ + Paragraph: { + extend: (config: Record) => { config: Record } + } + }>('@tiptap/extension-paragraph') // Simulates a Tiptap upgrade that drops `parseMarkdown` from the upstream paragraph. const Paragraph = actual.Paragraph.extend({}) Paragraph.config.parseMarkdown = undefined diff --git a/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts b/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts index 1d597023b3e..a25a06141ec 100644 --- a/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import TaskList from '@tiptap/extension-task-list' import TaskItem from '@tiptap/extension-task-item' @@ -8,7 +8,7 @@ import { createRichMarkdownExtensions } from './rich-markdown-extensions' import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport' import { createRichMarkdownKeyHandler, type KeyHandlerContext } from './rich-markdown-key-handler' -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { // Why: each Editor needs its own marked registry; sharing one module-scoped // extension accumulates tokenizer state across tests. return new Editor({ @@ -43,7 +43,7 @@ function createMarkdownEditor(markdown: string): Editor { * editor has no plain-text markdown paste transform. The DOM-less test env * cannot parse HTML, so assert against the node shapes that paste produces. */ -function createNodeEditor(content: object): Editor { +function createNodeEditor(content: JSONContent): Editor { return new Editor({ element: null, extensions: createRichMarkdownExtensions({ @@ -53,18 +53,18 @@ function createNodeEditor(content: object): Editor { }) } -function para(text: string): object { +function para(text: string): JSONContent { return { type: 'paragraph', content: [{ type: 'text', text }] } } -function bullets(...items: object[][]): object { +function bullets(...items: JSONContent[][]): JSONContent { return { type: 'bulletList', content: items.map((content) => ({ type: 'listItem', content })) } } -function tasks(...items: object[][]): object { +function tasks(...items: JSONContent[][]): JSONContent { return { type: 'taskList', content: items.map((content) => ({ @@ -75,7 +75,7 @@ function tasks(...items: object[][]): object { } } -function doc(...content: object[]): object { +function doc(...content: JSONContent[]): JSONContent { return { type: 'doc', content } } @@ -175,7 +175,7 @@ function createContext(editor: Editor): KeyHandlerContext { } } -function bulletListDocument(): object { +function bulletListDocument(): JSONContent { return { type: 'doc', content: [ @@ -196,7 +196,7 @@ function bulletListDocument(): object { } } -function parentAndFixesDocument(): object { +function parentAndFixesDocument(): JSONContent { return { type: 'doc', content: [ @@ -226,7 +226,7 @@ function parentAndFixesDocument(): object { } } -function taskListDocument(): object { +function taskListDocument(): JSONContent { return { type: 'doc', content: [ diff --git a/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts b/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts index 1f32ed4f668..ca6f4c023f2 100644 --- a/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts +++ b/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts @@ -6,6 +6,7 @@ import { isMarkdownComment } from '@/lib/diff-comment-compat' import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client' import { useAppStore } from '@/store' import { prewarmMarkdownPreviewLocalImages } from './markdown-preview-local-images' +import type { MarkdownPreviewSearchInstance } from './markdown-preview-search' import { deriveMarkdownPreviewSourceRoot, findMarkdownPreviewSourceOpenFile, @@ -40,7 +41,7 @@ export function useMarkdownPreviewSourceFoundation({ input.select() }, []) const matchesRef = useRef([]) - const searchInstanceRef = useRef({}) + const searchInstanceRef = useRef({}) const lastAppliedInitialAnchorRef = useRef(null) const pendingEditorRevealFrameIdsRef = useRef([]) const [isSearchOpen, setIsSearchOpen] = useState(false) diff --git a/src/renderer/src/components/github-checks-tab-state.ts b/src/renderer/src/components/github-checks-tab-state.ts index 18c12cf089b..6d60adaf71a 100644 --- a/src/renderer/src/components/github-checks-tab-state.ts +++ b/src/renderer/src/components/github-checks-tab-state.ts @@ -7,9 +7,14 @@ export type CheckDetailsLoadState = { error: string | null } +declare const checksContextOwnerBrand: unique symbol + +/** Identity minted per checks context; only its reference is ever compared. */ +export type GitHubChecksContextOwner = object & { readonly [checksContextOwnerBrand]?: never } + export type GitHubChecksTabState = { contextKey: string - contextOwner: object + contextOwner: GitHubChecksContextOwner sourceChecks: GitHubChecksSource localChecks: PRCheckDetail[] | null expandedCheckKey: string | null diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts index b4a46e24c33..21b10c2cd88 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts @@ -4,6 +4,7 @@ import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import { resetGitHubChecksTabForSource, updateGitHubChecksTabLocalChecks, + type GitHubChecksContextOwner, type GitHubChecksTabState } from '@/components/github-checks-tab-state' import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' @@ -28,21 +29,21 @@ export type ChecksTabActionContext = { headSha: string | undefined prRepo: GitHubOwnerRepo | null mountedRef: { current: boolean } - committedChecksContextOwnerRef: { current: object } + committedChecksContextOwnerRef: { current: GitHubChecksContextOwner } nextChecksRefreshRequestIdRef: { current: number } activeChecksRefreshRequestIdRef: { current: number | null } nextCheckDetailsRequestIdRef: { current: number } setChecksState: React.Dispatch> setRefreshingOwner: React.Dispatch< - React.SetStateAction<{ contextOwner: object; requestId: number } | null> + React.SetStateAction<{ contextOwner: GitHubChecksContextOwner; requestId: number } | null> > - setRerunningOwner: React.Dispatch> + setRerunningOwner: React.Dispatch> onChecksUpdated: (checks: PRCheckDetail[]) => void } export async function refreshGitHubChecksTab( ctx: ChecksTabActionContext, - expectedContextOwner?: object + expectedContextOwner?: GitHubChecksContextOwner ): Promise { if (!ctx.canUseChecksRepoContext) { toast.error( diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx index 3e9811d997c..9692d59fc18 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx @@ -40,6 +40,9 @@ import { import { requestGitHubCheckDetails } from './checks-tab-request-details' import { ChecksTabActions, ChecksTabCompactHeader } from './checks-tab-header' +/** Identity token for one checks context; compared by reference so a stale refresh is dropped. */ +type ChecksContextOwner = Record + export function ChecksTab({ item, repoPath, @@ -111,7 +114,7 @@ export function ChecksTab({ const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0) const handleRefresh = useCallback( - async (expectedContextOwner?: object): Promise => + async (expectedContextOwner?: ChecksContextOwner): Promise => refreshGitHubChecksTab( { canUseChecksRepoContext, diff --git a/src/renderer/src/components/pull-request-page/checks/rerun.ts b/src/renderer/src/components/pull-request-page/checks/rerun.ts index 40637c10993..6f8eaec76a7 100644 --- a/src/renderer/src/components/pull-request-page/checks/rerun.ts +++ b/src/renderer/src/components/pull-request-page/checks/rerun.ts @@ -6,12 +6,21 @@ import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request- import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { PRCheckDetail } from '../../../../../shared/github/check-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' +import type { GitHubChecksTabState } from '../../github-checks-tab-state' + +/** The checks tab mints one of these per context; only its reference identity is ever read. */ +type ChecksContextOwner = GitHubChecksTabState['contextOwner'] export async function rerunPullRequestChecks(args: { canUseChecksRepoContext: boolean rerunning: boolean - committedChecksContextOwnerRef: { current: object } - setRerunningOwner: (value: object | null | ((current: object | null) => object | null)) => void + committedChecksContextOwnerRef: { current: ChecksContextOwner } + setRerunningOwner: ( + value: + | ChecksContextOwner + | null + | ((current: ChecksContextOwner | null) => ChecksContextOwner | null) + ) => void runtimeHost: GitHubRuntimeHost | null sourceContext?: TaskSourceContext | null repoId: string | null @@ -21,7 +30,7 @@ export async function rerunPullRequestChecks(args: { prRepo: GitHubOwnerRepo | null failedOnly: boolean mountedRef: { current: boolean } - handleRefresh: (expectedContextOwner?: object) => Promise + handleRefresh: (expectedContextOwner?: ChecksContextOwner) => Promise }): Promise { if (!args.canUseChecksRepoContext || args.rerunning) { return diff --git a/src/renderer/src/components/pull-request-page/checks/tab.tsx b/src/renderer/src/components/pull-request-page/checks/tab.tsx index 9ccbc4d546e..0c12d91ae7e 100644 --- a/src/renderer/src/components/pull-request-page/checks/tab.tsx +++ b/src/renderer/src/components/pull-request-page/checks/tab.tsx @@ -6,7 +6,8 @@ import { CHECK_COLOR, CHECK_ICON } from '@/components/right-sidebar/checks-panel import { createGitHubChecksTabState, resolveGitHubChecksTabState, - toggleGitHubChecksTabExpandedKey + toggleGitHubChecksTabExpandedKey, + type GitHubChecksContextOwner } from '@/components/github-checks-tab-state' import { getCheckDetailsKey } from '@/components/github/pr-check-presentation' import { getCheckCounts, getChecksSummaryLabel } from '@/components/pr-check-counts' @@ -94,11 +95,11 @@ export function ChecksTab({ const nextChecksRefreshRequestIdRef = useRef(0) const activeChecksRefreshRequestIdRef = useRef(null) const [refreshingOwner, setRefreshingOwner] = useState<{ - contextOwner: object + contextOwner: GitHubChecksContextOwner requestId: number } | null>(null) const refreshing = refreshingOwner?.contextOwner === resolvedChecksState.contextOwner - const [rerunningOwner, setRerunningOwner] = useState(null) + const [rerunningOwner, setRerunningOwner] = useState(null) const rerunning = rerunningOwner === resolvedChecksState.contextOwner useLayoutEffect(() => { committedChecksContextOwnerRef.current = resolvedChecksState.contextOwner @@ -173,7 +174,7 @@ export function ChecksTab({ const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0) const handleRefresh = useCallback( - async (expectedContextOwner?: object) => + async (expectedContextOwner?: GitHubChecksContextOwner) => refreshPullRequestChecks({ canUseChecksRepoContext, expectedContextOwner, diff --git a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx index a601584f60e..12aed316a9c 100644 --- a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx +++ b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import { GeneralWorkspaceSettingsSection } from './GeneralWorkspaceSettingsSection' import type { ReactNode } from 'react' +import type { GlobalSettings } from '../../../../shared/global-settings-types' vi.mock('./WorkspaceDirectorySetting', () => ({ WorkspaceDirectorySetting: () => null })) vi.mock('./OpenInMenuSetting', () => ({ OpenInMenuSetting: () => null })) @@ -30,7 +31,7 @@ afterEach(() => { }) function renderSection( - updateSettings: (updates: object) => void | Promise, + updateSettings: (updates: Partial) => void | Promise, options: { defaultsSupported?: boolean sourceDefaultsSupported?: boolean diff --git a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx index a01d7d99ffe..f764f6264b3 100644 --- a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx +++ b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx @@ -64,7 +64,7 @@ afterEach(() => { function render( repo: Repo, - updateRepo: (repoId: string, updates: object) => void | Promise, + updateRepo: React.ComponentProps['updateRepo'], options: { settings?: Pick | null refreshRepo?: (repoId: string) => void | Promise diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts index d97a7d41906..452cec1f813 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts @@ -306,7 +306,10 @@ describe('selectWorktreeAgentOrchestration', () => { } let liveReads = 0 let retainedReads = 0 - const countReads = (target: object, onRead: () => void): object => + const countReads = ( + target: Record, + onRead: () => void + ): Record => new Proxy(target, { get(source, key, receiver) { if (typeof key === 'string') { diff --git a/src/renderer/src/components/task-page-github-work-item-quiet-state.ts b/src/renderer/src/components/task-page-github-work-item-quiet-state.ts index e1862e51b56..2d9891020b5 100644 --- a/src/renderer/src/components/task-page-github-work-item-quiet-state.ts +++ b/src/renderer/src/components/task-page-github-work-item-quiet-state.ts @@ -1,5 +1,8 @@ import { taskPageGitHubFamilyDirtyKey } from './task-page-github-work-item-mutation-keys' +/** Identity token for the caller driving one quiet run; compared by reference, never read. */ +export type QuietRevalidateRunOwner = Record + export type QuietRevalidateState = { inFlight: boolean trailingQueued: boolean @@ -10,7 +13,7 @@ export type QuietRevalidateState = { networkFailureAttempts: number lastConfirmAt: number runGeneration: number - runOwner: object | null + runOwner: QuietRevalidateRunOwner | null } const quietByQueryKey = new Map() @@ -37,7 +40,7 @@ export function getOrCreateQuietRevalidateState(queryKey: string): QuietRevalida export function beginTaskPageQuietRevalidateRun( state: QuietRevalidateState, - owner: object + owner: QuietRevalidateRunOwner ): number | null { if (state.inFlight && state.runOwner === owner) { state.trailingQueued = true @@ -52,7 +55,7 @@ export function beginTaskPageQuietRevalidateRun( export function finishTaskPageQuietRevalidateRun( state: QuietRevalidateState, - owner: object, + owner: QuietRevalidateRunOwner, generation: number ): boolean { if (state.runOwner !== owner || state.runGeneration !== generation) { diff --git a/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts b/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts index df22453e59d..8d4ca1fb33e 100644 --- a/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts +++ b/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts @@ -1,3 +1,9 @@ +import type { Terminal } from '@xterm/xterm' + +/** The pane's terminal, used only as the queue's identity key — no member is ever read, so a + * bare stand-in is a valid target. */ +type HiddenOutputRestoreTarget = Partial + type HiddenOutputRestorePriority = 'active' | 'inactive' /** Returns whether the pane actually started a replay; a guard-only return is free. */ @@ -11,7 +17,7 @@ type HiddenOutputRestoreEntry = { // on the active pane while still catching watched split panes up quickly. const INACTIVE_RESTORE_INTERVAL_MS = 16 -const inactiveRestoreQueue = new Map() +const inactiveRestoreQueue = new Map() let inactiveRestoreTimer: ReturnType | null = null function clearInactiveRestoreTimer(): void { @@ -51,7 +57,7 @@ function drainInactiveRestoreQueue(): void { } export function scheduleHiddenOutputRestore( - target: object, + target: HiddenOutputRestoreTarget, requestRestore: HiddenOutputRestoreRequest, priority: HiddenOutputRestorePriority ): void { @@ -64,7 +70,7 @@ export function scheduleHiddenOutputRestore( scheduleInactiveRestoreDrain() } -export function cancelScheduledHiddenOutputRestore(target: object): void { +export function cancelScheduledHiddenOutputRestore(target: HiddenOutputRestoreTarget): void { inactiveRestoreQueue.delete(target) if (inactiveRestoreQueue.size === 0) { clearInactiveRestoreTimer() diff --git a/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts b/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts index 57644da8a83..9296809816b 100644 --- a/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts +++ b/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts @@ -5,7 +5,14 @@ const hiddenClaimCounts = new Map() type VisibilityClaim = { ptyId: string; visible: boolean } -const visibilityClaimsByOwner = new Map() +declare const visibilityClaimOwnerBrand: unique symbol + +/** The mounted transport holding a claim; only its reference is ever compared. */ +export type RendererPtyVisibilityClaimOwner = object & { + readonly [visibilityClaimOwnerBrand]?: never +} + +const visibilityClaimsByOwner = new Map() const visibleClaimCounts = new Map() function sendHiddenState(ptyId: string, hidden: boolean): void { @@ -72,7 +79,7 @@ function removeVisibleClaim(claim: VisibilityClaim): boolean { * a retiring pane from hiding a PTY after its replacement has already bound. */ export function setRendererPtyVisibilityClaim( - owner: object, + owner: RendererPtyVisibilityClaimOwner, ptyId: string, visible: boolean ): void { @@ -103,7 +110,7 @@ export function setRendererPtyVisibilityClaim( } } -export function releaseRendererPtyVisibilityClaim(owner: object): void { +export function releaseRendererPtyVisibilityClaim(owner: RendererPtyVisibilityClaimOwner): void { const previous = visibilityClaimsByOwner.get(owner) if (!previous) { return diff --git a/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts b/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts index 8f96ba80efb..7a2780a5d32 100644 --- a/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts +++ b/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts @@ -1,3 +1,4 @@ +import type { IDisposable } from '@xterm/xterm' import type { PtyTransport } from './pty-transport' type CapturedTerminalInputDispatch = { @@ -38,8 +39,9 @@ export function sendCapturedTerminalInput({ return sent } +/** currentBinding arrives as the pane's raw xterm binding; only its identity is read. */ export function requestCapturedTerminalReconfirmation( - currentBinding: object | undefined, + currentBinding: IDisposable | TerminalCapturedInputBinding | undefined, capturedBinding: TerminalCapturedInputBinding | undefined ): void { if (currentBinding === capturedBinding) { diff --git a/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts b/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts index 94dbb221e43..394951d1a4e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts @@ -197,12 +197,16 @@ describe.each([ terminal.dispose() }) + /** The handle this suite's setTimeout stub hands back; only its identity is compared. */ + type FakeTimerToken = Record + it('keeps newer timer slots when canceled callbacks are forced', () => { const { terminal, textarea } = openTerminal(TerminalType) const callbacks: (() => void)[] = [] - const cleared = new Set() + const cleared = new Set() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub hands back an identity token instead of a real timer handle, which `typeof setTimeout` cannot express; only the clearTimeout stub below ever receives it. vi.spyOn(globalThis, 'setTimeout').mockImplementation(((callback: () => void) => { - const token = {} + const token: FakeTimerToken = {} callbacks.push(() => { if (!cleared.has(token)) { callback() @@ -210,7 +214,8 @@ describe.each([ }) return token }) as typeof setTimeout) - vi.spyOn(globalThis, 'clearTimeout').mockImplementation(((token: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the matching stub for the setTimeout token above; `typeof clearTimeout` declares a real timer handle this suite never creates. + vi.spyOn(globalThis, 'clearTimeout').mockImplementation(((token: FakeTimerToken) => { cleared.add(token) }) as typeof clearTimeout) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts index d3d14e99204..d569e265098 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts @@ -1,5 +1,6 @@ import type { Terminal } from '@xterm/xterm' import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { PtyPaneStartup } from './pty-connection-types' import type { PtyTransport } from './pty-transport' import type { PaneCwdMap } from './resolve-split-cwd' import { writeTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' @@ -180,15 +181,15 @@ export function resolveTerminalHomePathFromEnv( } export function paneOwnsQueuedStartup( - paneStartup: object | null | undefined, - queuedStartup: object | null | undefined + paneStartup: PtyPaneStartup | null | undefined, + queuedStartup: PtyPaneStartup | null | undefined ): boolean { return queuedStartup != null && paneStartup === queuedStartup } export function createQueuedStartupConsumer( - paneStartup: object | null | undefined, - queuedStartup: object | null | undefined, + paneStartup: PtyPaneStartup | null | undefined, + queuedStartup: PtyPaneStartup | null | undefined, consume: () => void, isStillQueued: () => boolean ): (() => void) | undefined { diff --git a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts index 50267617a13..3359dd346f6 100644 --- a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts +++ b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts @@ -1,6 +1,7 @@ import type { TaskPageGitHubLandingRefreshModel } from './use-task-page-github-landing-refresh' import { useMountedRef } from '@/hooks/useMountedRef' import { useRef } from 'react' +import type { QuietRevalidateRunOwner } from '@/components/task-page-github-work-item-quiet-state' import { advanceTaskPageQuietRevalidateScope } from '@/components/task-page-github-work-item-mutations' import { useTaskPageGitHubQuietRefreshEffect } from './use-task-page-github-quiet-refresh-effect' export type TaskPageGitHubQuietRefreshPreludeModel = ReturnType< @@ -12,7 +13,7 @@ export function useTaskPageGitHubQuietRefreshPrelude(model: TaskPageGitHubLandin // shared quietState (inFlight/trailingQueued), so a nonce-triggered re-render // must NOT cancel the in-flight run's trailing bookkeeping. const quietRevalidateMountedRef = useMountedRef() - const quietRevalidateOwnerRef = useRef({}) + const quietRevalidateOwnerRef = useRef({}) const quietRevalidateScopeRef = useRef({ queryKey: githubWorkItemMutationQueryKey, generation: 0 diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx index fc0e4e04464..733716d2482 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx @@ -6,6 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { SettingsNavSection } from '@/lib/settings-navigation-types' +import type { RuntimeEnvironmentStatus } from '@/store/slices/runtime-status-types' +import type { Repo } from '../../../shared/repo-types' import { resetWindowsTerminalCapabilitiesForTests } from '@/lib/windows-terminal-capabilities' const testState = vi.hoisted(() => ({ @@ -14,8 +16,16 @@ const testState = vi.hoisted(() => ({ runtimeEnvironments: [] as { id: string; createdAt: number; pairingRevision?: number }[] })) +/** Only the store fields this screen's selectors read; the mock supplies nothing else. */ +type MockedSettingsNavState = { + settings: GlobalSettings | null + repos: Repo[] + runtimeEnvironments: typeof testState.runtimeEnvironments + runtimeStatusByEnvironmentId: Map +} + vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => + useAppStore: (selector: (state: MockedSettingsNavState) => unknown) => selector({ settings: testState.settings, repos: [], diff --git a/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx b/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx index 0b8a177016f..d3adc9bfe83 100644 --- a/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx +++ b/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx @@ -15,7 +15,7 @@ const testState = vi.hoisted(() => ({ })) vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => selector(testState) + useAppStore: (selector: (state: typeof testState) => unknown) => selector(testState) })) vi.mock('@/lib/web-client-location', () => ({ diff --git a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts b/src/renderer/src/i18n/technical-literal-catalog-values.test.ts index 5cf9963175b..c8e24528c5f 100644 --- a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts +++ b/src/renderer/src/i18n/technical-literal-catalog-values.test.ts @@ -53,7 +53,7 @@ const repairedEntries = [ const catalogs = { es, ko, zh } as const -function readValue(catalog: object, key: string): unknown { +function readValue(catalog: Record, key: string): unknown { return key.split('.').reduce((value, part) => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { return undefined diff --git a/src/renderer/src/lib/ime-composition-keyboard-event.ts b/src/renderer/src/lib/ime-composition-keyboard-event.ts index 280a26476b2..554a0dcd565 100644 --- a/src/renderer/src/lib/ime-composition-keyboard-event.ts +++ b/src/renderer/src/lib/ime-composition-keyboard-event.ts @@ -13,14 +13,16 @@ type ImeModifierGestureEvent = ImeKeyboardEvent & { shiftKey?: boolean } -/** True when the IME, rather than Orca, owns a keyboard event. */ -export function isImeOwnedKeyboardEvent(event: object): boolean { - const candidate = event as ImeKeyboardEvent +/** True when the IME, rather than Orca, owns a keyboard event. Generic so synthetic, native, and + * gesture events each pass their own richer shape. */ +export function isImeOwnedKeyboardEvent( + event: KeyEvent +): boolean { return ( - candidate.isComposing === true || - candidate.keyCode === 229 || - candidate.nativeEvent?.isComposing === true || - candidate.nativeEvent?.keyCode === 229 + event.isComposing === true || + event.keyCode === 229 || + event.nativeEvent?.isComposing === true || + event.nativeEvent?.keyCode === 229 ) } diff --git a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts index 66dc3fc872b..4d0696a2012 100644 --- a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts @@ -87,7 +87,10 @@ function renderedText(pane: TestPane): string { } /** Reveal = the manager's resume pass, then the terminal regains real DOM focus. */ -async function reveal(panes: TestPane[], owner?: object): Promise { +async function reveal( + panes: TestPane[], + owner?: Parameters[1] +): Promise { resumePaneRendering(panes, owner) for (const pane of panes) { pane.terminal.focus() diff --git a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts index e84638c33ea..d613cac4a3c 100644 --- a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { ITerminalAddon } from '@xterm/xterm' import { WebglAddon } from '@xterm/addon-webgl' import type { ManagedPaneInternal } from './pane-manager-types' import { @@ -508,6 +509,7 @@ describe('openTerminal — addon and provider wiring', () => { }) ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a hand-built stand-in for xterm's Terminal; openTerminal touches only the members defined here, and a real Terminal needs a rendering canvas this suite has no DOM for. const terminal = { element: fakeTerminalElement, textarea: null, @@ -516,7 +518,7 @@ describe('openTerminal — addon and provider wiring', () => { open: vi.fn(() => { events.push('open') }), - loadAddon: vi.fn((addon: object) => { + loadAddon: vi.fn((addon: ITerminalAddon) => { if (addon === fitAddon) { events.push('loadAddon:fit') } else if (addon === searchAddon) { diff --git a/src/renderer/src/lib/pane-manager/pane-manager-types.ts b/src/renderer/src/lib/pane-manager/pane-manager-types.ts index a26be1e3a9f..341e40a891b 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager-types.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager-types.ts @@ -10,6 +10,8 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TerminalLeafId } from '../../../../shared/stable-pane-id' import type { TerminalWebglAutoDecision } from './terminal-webgl-auto-policy' +export type { TerminalScrollIntentTarget } from './terminal-scroll-intent' + // --------------------------------------------------------------------------- // Public interfaces // --------------------------------------------------------------------------- diff --git a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts index 219f17f4be6..114a0b850ee 100644 --- a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts +++ b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts @@ -21,7 +21,8 @@ import { } from './pane-webgl-reattach' import { releaseHiddenWebglRetention, - tryRetainHiddenPanesWebgl + tryRetainHiddenPanesWebgl, + type HiddenWebglRetentionOwner } from './terminal-webgl-hidden-retention' export function setPaneGpuRenderingState( @@ -59,7 +60,10 @@ export function markPaneComplexScriptOutput( export function suspendPaneRendering( panes: Iterable, - retention?: { owner: object; livePanes: () => Iterable } + retention?: { + owner: HiddenWebglRetentionOwner + livePanes: () => Iterable + } ): void { const suspended = Array.from(panes) // Why: both branches must leave a suspended pane in the same state; only the retention @@ -89,7 +93,7 @@ export function suspendPaneRendering( export function resumePaneRendering( panes: Iterable, - retentionOwner?: object + retentionOwner?: HiddenWebglRetentionOwner ): void { if (retentionOwner) { releaseHiddenWebglRetention(retentionOwner) diff --git a/src/renderer/src/lib/pane-manager/pane-scroll.ts b/src/renderer/src/lib/pane-manager/pane-scroll.ts index bb79faa4556..5945e7257c2 100644 --- a/src/renderer/src/lib/pane-manager/pane-scroll.ts +++ b/src/renderer/src/lib/pane-manager/pane-scroll.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import type { ScrollState } from './pane-manager-types' +import type { ScrollState, TerminalScrollIntentTarget } from './pane-manager-types' import { captureLogicalLineAnchor, resolveLogicalCellOffsetLine @@ -8,7 +8,7 @@ import { forceTerminalViewportScrollbarSync } from './terminal-viewport-scrollba const terminalOutputEpochs = new WeakMap() const deferredScrollRestores = new WeakMap< - object, + TerminalScrollIntentTarget, { cancelled: boolean rafIds: number[] @@ -17,7 +17,7 @@ const deferredScrollRestores = new WeakMap< } >() const pendingFitScrollRestores = new WeakMap< - object, + TerminalScrollIntentTarget, { cancelled: boolean rafId: number | null @@ -38,7 +38,7 @@ export function getTerminalOutputEpoch(terminal: Terminal): number { return terminalOutputEpochs.get(terminal) ?? 0 } -export function cancelDeferredScrollRestore(terminal: object): void { +export function cancelDeferredScrollRestore(terminal: TerminalScrollIntentTarget): void { cancelPendingFitScrollRestore(terminal) const pending = deferredScrollRestores.get(terminal) if (!pending) { @@ -323,7 +323,7 @@ export function releaseScrollStateMarker(state: ScrollState): void { state.firstVisibleLineMarker = state.firstVisibleLogicalLineMarker = undefined } -function cancelPendingFitScrollRestore(terminal: object): void { +function cancelPendingFitScrollRestore(terminal: TerminalScrollIntentTarget): void { const pending = pendingFitScrollRestores.get(terminal) if (!pending) { return diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts b/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts index a245f2d82b9..7ba4c418f1f 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts @@ -1,4 +1,7 @@ -type TerminalOutputAckTarget = object +import type { ForegroundTerminalOutputTarget } from './pane-terminal-foreground-render-settle' + +/** The xterm instance the credits belong to; only its reference is used as a key. */ +type TerminalOutputAckTarget = ForegroundTerminalOutputTarget const inFlightAckCompletions = new WeakMap void>>() diff --git a/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts b/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts index 0b0185035c3..829075d11dd 100644 --- a/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts +++ b/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts @@ -15,6 +15,13 @@ export type ParsedDirtyRowSpan = { start: number; end: number } type RequestRefreshRowsEvent = { start: number; end: number } | undefined +/** + * A terminal instance, used as the span cache's identity. Not `ParsedDirtyRowSource`: callers hold + * their own partial structural views of the same xterm terminal, so the internals below stay a + * defensive probe rather than a requirement on the caller's type. + */ +export type ParsedDirtyRowTerminal = WeakKey + type ParsedDirtyRowSource = { _core?: { _inputHandler?: { @@ -35,9 +42,9 @@ type ParsedDirtyRowTracker = { // `null` marks a terminal whose parse spans cannot be observed, so callers keep // the full-grid behavior instead of narrowing on an absent signal. -const trackersByTerminal = new WeakMap() +const trackersByTerminal = new WeakMap() -function attachTracker(terminal: object): ParsedDirtyRowTracker | null { +function attachTracker(terminal: ParsedDirtyRowTerminal): ParsedDirtyRowTracker | null { const existing = trackersByTerminal.get(terminal) if (existing !== undefined) { return existing @@ -82,7 +89,7 @@ function attachTracker(terminal: object): ParsedDirtyRowTracker | null { } /** Start (or reset) parse-span observation for the write that is about to run. */ -export function resetParsedDirtyRows(terminal: object): void { +export function resetParsedDirtyRows(terminal: ParsedDirtyRowTerminal): void { const tracker = attachTracker(terminal) if (!tracker) { return @@ -98,7 +105,9 @@ export function resetParsedDirtyRows(terminal: object): void { * unknown (unobservable terminal, no parse seen, or an xterm full-refresh * request) and the caller must repaint the whole viewport. */ -export function readParsedDirtyRowSpan(terminal: object): ParsedDirtyRowSpan | null { +export function readParsedDirtyRowSpan( + terminal: ParsedDirtyRowTerminal +): ParsedDirtyRowSpan | null { const tracker = trackersByTerminal.get(terminal) if (!tracker || !tracker.observed || tracker.wholeViewport) { return null @@ -106,7 +115,7 @@ export function readParsedDirtyRowSpan(terminal: object): ParsedDirtyRowSpan | n return { start: tracker.start, end: tracker.end } } -export function disposeParsedDirtyRows(terminal: object): void { +export function disposeParsedDirtyRows(terminal: ParsedDirtyRowTerminal): void { const tracker = trackersByTerminal.get(terminal) if (tracker) { try { diff --git a/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts b/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts index 3de8e79a9f1..8ecc03617f5 100644 --- a/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts +++ b/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts @@ -1,15 +1,17 @@ +import type { TerminalScrollIntentTarget } from './terminal-scroll-intent' + // Why: buffer rebuilds (snapshot replay clear + rewrite) parse asynchronously. // Until the rebuild's bytes have parsed, viewportY/baseY describe a transient // half-cleared buffer; any intent capture/enforce latched from it pins the // terminal at line 0. Callers bracket the rebuild and re-apply intent once // after parse (see terminal-scroll-intent.ts). -const terminalScrollIntentRebuilds = new WeakMap() +const terminalScrollIntentRebuilds = new WeakMap() const terminalScrollIntentRebuildCompletions = new WeakMap< - object, + TerminalScrollIntentTarget, Set<(completed: boolean) => void> >() const deferredTerminalGeometryMutations = new WeakMap< - object, + TerminalScrollIntentTarget, { mutations: Map void> } @@ -30,11 +32,11 @@ function notifyRebuildCompletions( } } -export function beginTerminalScrollIntentBufferRebuild(terminal: object): void { +export function beginTerminalScrollIntentBufferRebuild(terminal: TerminalScrollIntentTarget): void { terminalScrollIntentRebuilds.set(terminal, (terminalScrollIntentRebuilds.get(terminal) ?? 0) + 1) } -export function endTerminalScrollIntentBufferRebuild(terminal: object): void { +export function endTerminalScrollIntentBufferRebuild(terminal: TerminalScrollIntentTarget): void { const count = terminalScrollIntentRebuilds.get(terminal) ?? 0 if (count <= 1) { terminalScrollIntentRebuilds.delete(terminal) @@ -46,12 +48,14 @@ export function endTerminalScrollIntentBufferRebuild(terminal: object): void { terminalScrollIntentRebuilds.set(terminal, count - 1) } -export function isTerminalScrollIntentRebuildInFlight(terminal: object): boolean { +export function isTerminalScrollIntentRebuildInFlight( + terminal: TerminalScrollIntentTarget +): boolean { return (terminalScrollIntentRebuilds.get(terminal) ?? 0) > 0 } export function onTerminalScrollIntentBufferRebuildComplete( - terminal: object, + terminal: TerminalScrollIntentTarget, completion: (completed: boolean) => void ): () => void { if (!isTerminalScrollIntentRebuildInFlight(terminal)) { @@ -75,7 +79,7 @@ export function onTerminalScrollIntentBufferRebuildComplete( // Why: source-dimension replay must finish and restore its viewport before // unrelated fit/resize work is allowed to reflow the rebuilt buffer. export function deferTerminalGeometryMutationDuringRebuild( - terminal: object, + terminal: TerminalScrollIntentTarget, operationKey: string, mutation: () => void ): boolean { @@ -117,7 +121,9 @@ export function deferTerminalGeometryMutationDuringRebuild( return true } -export function cancelTerminalScrollIntentBufferRebuildCompletions(terminal: object): void { +export function cancelTerminalScrollIntentBufferRebuildCompletions( + terminal: TerminalScrollIntentTarget +): void { const completions = terminalScrollIntentRebuildCompletions.get(terminal) terminalScrollIntentRebuildCompletions.delete(terminal) notifyRebuildCompletions(completions, false) diff --git a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts index 708beed44f5..0c7cad8f89d 100644 --- a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts +++ b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts @@ -6,7 +6,8 @@ import { releaseHiddenWebglRetention, resetHiddenWebglRetentionForTest, retainedHiddenWebglOwnerCountForTest, - tryRetainHiddenPanesWebgl + tryRetainHiddenPanesWebgl, + type HiddenWebglRetentionOwner } from './terminal-webgl-hidden-retention' function createPane(withAddon = true): ManagedPaneInternal { @@ -24,7 +25,7 @@ function createPane(withAddon = true): ManagedPaneInternal { } as unknown as ManagedPaneInternal } -function retentionFor(owner: object, panes: ManagedPaneInternal[]) { +function retentionFor(owner: HiddenWebglRetentionOwner, panes: ManagedPaneInternal[]) { return { owner, livePanes: () => panes } } diff --git a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts index e4ca8fa63b0..bd467b6e71a 100644 --- a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts +++ b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts @@ -6,8 +6,11 @@ import { disposeWebgl } from './pane-webgl-renderer' // letting hidden worktrees grow that cost with the mounted-pane population. const MAX_RETAINED_HIDDEN_WEBGL_CONTEXTS = 6 +/** Identity of the surface whose hidden panes are retained; compared by reference only. */ +export type HiddenWebglRetentionOwner = WeakKey + type RetainedHiddenEntry = { - owner: object + owner: HiddenWebglRetentionOwner livePanes: () => Iterable } @@ -30,7 +33,7 @@ function disposeEntryContexts(entry: RetainedHiddenEntry): void { } } -function removeEntry(owner: object): void { +function removeEntry(owner: HiddenWebglRetentionOwner): void { const index = retainedEntries.findIndex((entry) => entry.owner === owner) if (index !== -1) { retainedEntries.splice(index, 1) @@ -43,7 +46,7 @@ function removeEntry(owner: object): void { * least-recently-hidden owners to stay under the context cap. */ export function tryRetainHiddenPanesWebgl( - owner: object, + owner: HiddenWebglRetentionOwner, livePanes: () => Iterable ): boolean { removeEntry(owner) @@ -68,7 +71,7 @@ export function tryRetainHiddenPanesWebgl( } /** Drop retention bookkeeping on reveal/destroy; never disposes live addons. */ -export function releaseHiddenWebglRetention(owner: object): void { +export function releaseHiddenWebglRetention(owner: HiddenWebglRetentionOwner): void { removeEntry(owner) } diff --git a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts index 5c67e7138ea..cd0c0adb2be 100644 --- a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts +++ b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts @@ -20,26 +20,26 @@ export type UndeliverableWriteReason = 'write-stalled' | 'replay-wedged' type UndeliverableWriteHandler = (reason: UndeliverableWriteReason) => void -const handlersByTerminal = new WeakMap() -const certifiedDeadTerminals = new WeakSet() +const handlersByTerminal = new WeakMap() +const certifiedDeadTerminals = new WeakSet() // Why: wedge verdicts must distinguish "dead" from "alive but behind". A // generation avoids same-millisecond misses and wall-clock adjustments while // keeping the completion hot path constant-time and terminal-scoped. -const parseProgressGenerationByTerminal = new WeakMap() +const parseProgressGenerationByTerminal = new WeakMap() /** Report one parsed write completion for this terminal. */ -export function recordTerminalParseProgress(terminal: object): void { +export function recordTerminalParseProgress(terminal: WriteTarget): void { const nextGeneration = (parseProgressGenerationByTerminal.get(terminal) ?? 0) + 1 parseProgressGenerationByTerminal.set(terminal, nextGeneration) } /** Capture the current parse-progress generation for a later quiet-window check. */ -export function captureTerminalParseProgressGeneration(terminal: object): number { +export function captureTerminalParseProgressGeneration(terminal: WriteTarget): number { return parseProgressGenerationByTerminal.get(terminal) ?? 0 } /** Whether a write completion parsed after `generation` was captured. */ -export function hasTerminalParseProgressSince(terminal: object, generation: number): boolean { +export function hasTerminalParseProgressSince(terminal: WriteTarget, generation: number): boolean { return captureTerminalParseProgressGeneration(terminal) !== generation } @@ -51,11 +51,11 @@ type StallWatch = { mode: StallWatchMode } -const stallWatchByTerminal = new WeakMap() +const stallWatchByTerminal = new WeakMap() export const WRITE_PIPELINE_STALL_CHECK_MS = 10_000 -function certifyTerminalWritePipelineDead(terminal: object, expectedWatch?: StallWatch): void { +function certifyTerminalWritePipelineDead(terminal: WriteTarget, expectedWatch?: StallWatch): void { const watch = stallWatchByTerminal.get(terminal) // Why: a real parse can settle and remove the watch before a stale probe // deadline runs. Only the watch that armed that deadline may certify. @@ -75,7 +75,7 @@ function certifyTerminalWritePipelineDead(terminal: object, expectedWatch?: Stal } export function registerUndeliverableWriteHandler( - terminal: object, + terminal: WriteTarget, handler: UndeliverableWriteHandler ): () => void { handlersByTerminal.set(terminal, handler) @@ -88,7 +88,10 @@ export function registerUndeliverableWriteHandler( /** One notification per terminal instance: recovery replaces the xterm, so a * second notification for the same object is always a duplicate. */ -export function notifyUndeliverableWrite(terminal: object, reason: UndeliverableWriteReason): void { +export function notifyUndeliverableWrite( + terminal: WriteTarget, + reason: UndeliverableWriteReason +): void { if (certifiedDeadTerminals.has(terminal)) { return } @@ -102,7 +105,7 @@ export function notifyUndeliverableWrite(terminal: object, reason: Undeliverable } } -export function isTerminalWritePipelineCertifiedDead(terminal: object): boolean { +export function isTerminalWritePipelineCertifiedDead(terminal: WriteTarget): boolean { return certifiedDeadTerminals.has(terminal) } @@ -196,7 +199,7 @@ export function requestTerminalWritePipelineProbe( } /** Cancel a pending watch without claiming that any bytes parsed. */ -export function cancelTerminalWriteStallWatch(terminal: object): void { +export function cancelTerminalWriteStallWatch(terminal: WriteTarget): void { const watch = stallWatchByTerminal.get(terminal) if (!watch) { return @@ -206,7 +209,7 @@ export function cancelTerminalWriteStallWatch(terminal: object): void { } /** Write completed normally — the pipeline is healthy; drop any pending watch. */ -export function settleTerminalWriteStallWatch(terminal: object): void { +export function settleTerminalWriteStallWatch(terminal: WriteTarget): void { recordTerminalParseProgress(terminal) if (stallWatchByTerminal.get(terminal)?.mode === 'fifo-probe') { return @@ -216,11 +219,11 @@ export function settleTerminalWriteStallWatch(terminal: object): void { /** A synchronous terminal.write failure proves the pipeline cannot accept the * issued bytes. Recover immediately without reporting fake parse progress. */ -export function failTerminalWriteStallWatch(terminal: object): void { +export function failTerminalWriteStallWatch(terminal: WriteTarget): void { certifyTerminalWritePipelineDead(terminal) } -export function _resetWritePipelineHealthForTests(terminal?: object): void { +export function _resetWritePipelineHealthForTests(terminal?: WriteTarget): void { if (terminal) { const watch = stallWatchByTerminal.get(terminal) if (watch) { diff --git a/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts b/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts index 17489399883..11a545ae2cf 100644 --- a/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts +++ b/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts @@ -13,7 +13,7 @@ import { // Not an intersection with ErrorConstructor: both fields have to stay optional // so the absent-captureStackTrace platform can be simulated. type ErrorWithCapture = { - captureStackTrace?: (target: object, constructorOpt?: unknown) => void + captureStackTrace?: (target: { stack?: string }, constructorOpt?: unknown) => void stackTraceLimit?: number } const errorWithCapture = Error as unknown as ErrorWithCapture diff --git a/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts b/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts index ef8565255a8..6801a5c49b3 100644 --- a/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts +++ b/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts @@ -33,6 +33,9 @@ export const MAX_REPORTED_CHANGED_KEYS = 12 type SampledWrite = { stack?: string } +/** The wrapping `set` function: V8 elides it and every frame above it, so only identity matters. */ +export type ReactCommitCascadeWriteBoundary = (...args: never[]) => unknown + let storeWrites = 0 let samples: SampledWrite[] = [] let changedKeys: Set | null = null @@ -58,10 +61,12 @@ export function resetReactCommitCascadeWriteSamples(): void { /** * Call only while armed. `boundary` is the wrapping `set` function, so V8 elides - * our own frames and the first captured frame is the real caller. Typed as - * `object` because zustand's `set` is an overload set, not a plain signature. + * our own frames and the first captured frame is the real caller. */ -export function noteReactCommitCascadeStoreWrite(boundary: object, partial: unknown): void { +export function noteReactCommitCascadeStoreWrite( + boundary: ReactCommitCascadeWriteBoundary, + partial: unknown +): void { storeWrites += 1 // Why the write count and not samples.length: samples only grows where // Error.captureStackTrace exists, so that cap would never engage without it and @@ -76,23 +81,20 @@ export function noteReactCommitCascadeStoreWrite(boundary: object, partial: unkn changedKeys.add(key) } } - const capture = Error as ErrorConstructor & { - captureStackTrace?: (target: object, constructorOpt?: unknown) => void - stackTraceLimit?: number - } - if (typeof capture.captureStackTrace !== 'function') { + // Only V8 has it; a non-V8 host gets no samples rather than a synthesized stack. + if (typeof Error.captureStackTrace !== 'function') { return } - const previousLimit = capture.stackTraceLimit + const previousLimit = Error.stackTraceLimit const sample: SampledWrite = {} try { - capture.stackTraceLimit = CAPTURE_STACK_FRAME_LIMIT - capture.captureStackTrace(sample, boundary) + Error.stackTraceLimit = CAPTURE_STACK_FRAME_LIMIT + Error.captureStackTrace(sample, boundary) samples.push(sample) } catch { // Best-effort crash evidence only. } finally { - capture.stackTraceLimit = previousLimit + Error.stackTraceLimit = previousLimit } } diff --git a/src/renderer/src/lib/simulator-launch-coordination.ts b/src/renderer/src/lib/simulator-launch-coordination.ts index db363683385..fee47276361 100644 --- a/src/renderer/src/lib/simulator-launch-coordination.ts +++ b/src/renderer/src/lib/simulator-launch-coordination.ts @@ -87,7 +87,10 @@ export function dispatchManualSimulatorLaunchFailed(worktreeId: string, message: }) } -function dispatchManualSimulatorLaunchEvent(type: string, detail: object): void { +function dispatchManualSimulatorLaunchEvent( + type: string, + detail: { worktreeId: string; message?: string } +): void { if (typeof window === 'undefined') { return } diff --git a/src/renderer/src/lib/state-collection-byte-estimate.ts b/src/renderer/src/lib/state-collection-byte-estimate.ts index 2491826420b..8f1d28dc16f 100644 --- a/src/renderer/src/lib/state-collection-byte-estimate.ts +++ b/src/renderer/src/lib/state-collection-byte-estimate.ts @@ -149,7 +149,9 @@ function estimateValueBytes(value: unknown, depth: number, ctx: EstimateContext) if (ArrayBuffer.isView(value)) { return BYTES_OBJECT_BASE + value.byteLength } - return BYTES_OBJECT_BASE + estimatePlainObjectEntries(value, depth, ctx) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the typeof switch and null check above leave only a non-null, non-collection object here. + const plainObject = value as Record + return BYTES_OBJECT_BASE + estimatePlainObjectEntries(plainObject, depth, ctx) } function estimateArrayElements(value: unknown[], depth: number, ctx: EstimateContext): number { @@ -208,7 +210,11 @@ function estimateIterableEntries( : Math.round((sampledBytes / sampledCount + BYTES_ENTRY_OVERHEAD) * size) } -function estimatePlainObjectEntries(value: object, depth: number, ctx: EstimateContext): number { +function estimatePlainObjectEntries( + value: Record, + depth: number, + ctx: EstimateContext +): number { let ownCount = 0 const sampledKeys: string[] = [] const entryFloor = depth === 0 ? ENTRY_DESCENT_RESERVE : 0 @@ -232,7 +238,7 @@ function estimatePlainObjectEntries(value: object, depth: number, ctx: EstimateC let sampledBytes = 0 for (const key of sampledKeys) { sampledBytes += BYTES_STRING_BASE + key.length * BYTES_PER_CHAR - sampledBytes += estimateValueBytes((value as Record)[key], depth + 1, ctx) + sampledBytes += estimateValueBytes(value[key], depth + 1, ctx) } return sampledKeys.length === 0 ? 0 diff --git a/src/renderer/src/store/react-commit-cascade-write-probe.test.ts b/src/renderer/src/store/react-commit-cascade-write-probe.test.ts index 0c18706e976..34d9db8fb6e 100644 --- a/src/renderer/src/store/react-commit-cascade-write-probe.test.ts +++ b/src/renderer/src/store/react-commit-cascade-write-probe.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create, type StoreApi, type UseBoundStore } from 'zustand' import { withReactCommitCascadeWriteProbe } from './react-commit-cascade-write-probe' +import type { ReactCommitCascadeWriteBoundary } from '@/lib/react-commit-cascade-store-write-samples' const { probe, noteWrite } = vi.hoisted(() => ({ probe: { armed: false }, @@ -8,7 +9,7 @@ const { probe, noteWrite } = vi.hoisted(() => ({ })) vi.mock('@/lib/react-commit-cascade-store-write-samples', () => ({ reactCommitCascadeWriteProbe: probe, - noteReactCommitCascadeStoreWrite: (boundary: object, partial: unknown) => + noteReactCommitCascadeStoreWrite: (boundary: ReactCommitCascadeWriteBoundary, partial: unknown) => noteWrite(boundary, partial) })) diff --git a/src/shared/browser-client-host-reconciliation-protocol.test.ts b/src/shared/browser-client-host-reconciliation-protocol.test.ts index 3d3fc526cc6..9ce314ee9b2 100644 --- a/src/shared/browser-client-host-reconciliation-protocol.test.ts +++ b/src/shared/browser-client-host-reconciliation-protocol.test.ts @@ -19,7 +19,10 @@ const inventoryPage = { state: 'active' as const } -const command = (reconciliationCommand: object) => ({ +/** Raw command input for the parser under test, including deliberately malformed shapes. */ +type RawReconciliationCommand = { type: string } & Record + +const command = (reconciliationCommand: RawReconciliationCommand) => ({ type: 'command' as const, authorityRuntimeId: 'runtime-a', authorityEpoch: 'epoch-new', diff --git a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts index 2f108571dcf..72a460a7146 100644 --- a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts +++ b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts @@ -4,7 +4,7 @@ import type { GitLabPipelineJob } from './gitlab-types' // Repro for #7732: the Checks side panel can only ask for a GitLab job trace if the mapped // check row still carries the numeric GitLab job id (gitlab:jobTrace takes { jobId }). -function numericHandles(value: object): number[] { +function numericHandles(value: Record): number[] { return Object.values(value).filter((v): v is number => typeof v === 'number') } From 231e805b1e1e49c765e96b9409f88e09d117346a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:00:27 -0700 Subject: [PATCH 32/34] fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785) Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear every violation under src, config, tests and mobile. What the rule bans ------------------ The case-insensitive substring "shape" in any JS/TS identifier: variables, functions, parameters, types, type parameters, class members, private names, object-literal keys and JSX identifiers. The one exemption is a statically accessed member read owned by another value (`zodObject.shape` is fine), so third-party APIs stay readable without a suppression. "Shape" names a value's structure rather than its domain role. `UserShape`, `validateArgShape` and `errorShape` all tell you the symbol is "an object with some fields" -- which is already what a type says -- while saying nothing about what the value is for or who owns it. The rule forces the name to carry the domain instead. Violations fixed ---------------- 689 violations across 109 files at baseline (verified by re-running the audit against the pre-change tree with the rule set to "error"). Fix pattern ----------- Rename for the domain role, not the structure: -type FieldShape = 'list' | 'map' | 'whole' -const FIELD_SHAPES = { ... } satisfies Record +type FieldEncoding = 'list' | 'map' | 'whole' +const FIELD_ENCODINGS = { ... } satisfies Record -function assertGitPushTargetShape(target: unknown): void +function assertValidGitPushTarget(target: unknown): void -function describeReadDirPathShape(p: string): ReadDirPathKind +function classifyReadDirPath(p: string): ReadDirPathKind Predicates became statements about the value (`isDeltaShapedProviderFrameKind` -> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` -> `discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` -> `isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the remaining name was already unambiguous (`GhGraphqlErrorShape` -> `GhGraphqlError`). No wire-visible name was renamed: no IPC or RPC channel, stream opcode, request/response param, persisted field, or i18n key. The `--shape=symlink|copy` CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged -- only the local variable holding it was renamed. Exemptions ---------- They are file-scoped entries in config/oxlint-anti-slop.json, not inline `oxlint-disable` comments. An inline directive naming an anti-slop rule reads back as an UNUSED directive under the root lint scan, which does not load this plugin -- the changed-code quality gate counts that warning, so the comment form cannot be used for a rule that lives only in this config. * src/renderer/src/components/browser-pane/annotate/**: in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow, rect, ellipse, highlight. That is a genuine domain noun, and it pervades every symbol in the module. * repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx: lucide exports the icon component as `Shapes`. The name is theirs, and the matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the desktop picker -- renaming it would orphan saved repo icons. * src/shared/onboarding-state-types.ts, src/shared/constants.ts: `shapedSidebar` is a persisted onboarding-checklist field and a telemetry enum member; renaming it would orphan saved state. * src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape` property is what selects the ZodObject branch of the conditional type. No exemption was added merely to avoid a rename. Eight symbols initially suppressed as "a cross-module refactor outside this change" were proven to have zero non-TypeScript references repo-wide and renamed instead. Zod's `ZodRawShape` needed no exemption at all: `Readonly>` is its definition, so repo-update-params.ts and ui-update-value-tolerance-params.ts spell it out instead. Likewise telemetry-event-classification.ts now reads `.shape` through an `in` narrowing, which also retires two pre-existing type assertions; three more assertions the rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite row read) became annotations and an explicit row mapping. Verified -------- * Audit reports zero violations; confirmed the rule genuinely fires by planting a probe violation. * node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0. * Vitest over src/shared, src/main/github/project-view, the annotate module, the repo-icon components and the Chromium SameSite electron spec: all green. * All 66 removed "shape" identifiers grepped repo-wide across every file type; none survive. * node config/scripts/generate-rpc-params-catalog.mjs --check exits 0. * node --check on every changed .mjs; oxfmt clean on all changed files. * `pnpm run check:code-quality:changed` reports 0 findings. Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve `expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated specs. All are rename- or comment-only hunks, read in full. --- config/oxlint-anti-slop.json | 52 ++++++++++++++++++- .../agent-lineage-cycle-cleanup-benchmark.mjs | 8 +-- .../agent-lineage-reachability-benchmark.mjs | 12 +++-- .../mobile-markdown-placeholder-benchmark.mjs | 4 +- .../redactor-environment-lines-benchmark.mjs | 4 +- .../repo-icon-source-href-benchmark.mjs | 8 +-- .../rich-markdown-line-scan-benchmark.mjs | 6 +-- .../terminal-output-frame-chunk-benchmark.mjs | 6 +-- .../tool-preview-whitespace-benchmark.mjs | 4 +- .../scripts/verify-skill-update-roundtrip.mjs | 16 +++--- config/scripts/wsl-git-shell-benchmark.mjs | 2 +- mobile/src/components/MobileRepoIcon.tsx | 1 + ...le-tasks-provider-view-projection.test.tsx | 48 ++++++++++------- src/cli/handlers/skills.ts | 4 +- .../session-search-lifecycle-matrix.test.ts | 14 ++--- .../session-search-query-planner.ts | 4 +- src/main/ai-vault/session-delete-target.ts | 4 +- .../browser-cookie-samesite.electron.test.ts | 27 ++++++---- ...ime-auth-service-readback-identity.test.ts | 4 +- .../codex-structured-item-translation.test.ts | 4 +- src/main/cursor/hook-service.ts | 6 +-- src/main/git/push-target-validation.ts | 4 +- .../github/client-stack-merge-guard.test.ts | 6 +-- .../github/default-branch-stale-pr.test.ts | 4 +- src/main/github/project-view/internals.ts | 4 +- .../project-error-classification.ts | 8 +-- .../project-view/project-view-item-page.ts | 6 +-- .../git-remote/branch-mutation-handlers.ts | 8 +-- .../filesystem/git-remote/sync-handlers.ts | 6 +-- .../ipc/readdir-error-diagnostics.test.ts | 33 ++++++------ src/main/ipc/readdir-error-diagnostics.ts | 4 +- src/main/ipc/worktree-remote.ts | 4 +- .../journal-lifecycle-batch-partition.ts | 6 +-- ...ssion-history-page-grouping-parity.test.ts | 10 ++-- .../provider-frame-disposition.test.ts | 4 +- .../provider-frame-disposition.ts | 4 +- src/main/observability/redactor.test.ts | 4 +- .../persisted-state-redundancy.test.ts | 4 +- .../titlebar-extension-overlay-path.test.ts | 8 +-- ...al-output-frame-chunks-equivalence.test.ts | 14 ++--- src/main/runtime/terminal-wait-tail-state.ts | 6 +-- src/main/skills/skill-bundle-artifacts.ts | 6 +-- src/main/ssh/ssh-host-key-store.test.ts | 2 +- ...h-orphan-sweep-pane-state-verdicts.test.ts | 30 ++++++----- src/main/ssh/ssh-relay-deploy-helpers.test.ts | 4 +- src/main/ssh/ssh-remote-platform-detection.ts | 6 +-- ...xt-generation-failure-sanitization.test.ts | 2 +- src/main/wsl-unc-delete-symlink-repro.test.ts | 2 +- src/main/wsl-unc-delete.wsl.test.ts | 2 +- src/main/wsl.test.ts | 8 +-- src/relay/git-exec-validator.ts | 4 +- ...it-handler-branch-diff-equivalence.test.ts | 6 +-- .../git-handler-comparison-operations.ts | 4 +- src/relay/git-handler-fetch-operations.ts | 4 +- src/relay/git-handler-push-target.ts | 4 +- src/relay/git-handler-sync-operations.ts | 4 +- .../rich-markdown-html-superscript-link.ts | 4 +- .../src/components/repo/repo-icon.tsx | 1 + .../commit/discard-confirmation.ts | 6 +-- ...source-control-entry-failure-toast.test.ts | 2 +- .../source-control-entry-failure-toast.ts | 10 ++-- .../commit/use-discard-confirmation.ts | 4 +- .../components/shared/useDaemonActions.tsx | 4 +- ...comment-markdown-native-chat-file-links.ts | 4 +- .../rows/repo-header-project-actions.tsx | 1 + ...ge-toast-flood-and-stuck-reconnect.test.ts | 10 ++-- .../terminal-search-decoration-leak.test.ts | 4 +- .../src/hooks/useEditorExternalWatch.ts | 4 +- .../tab-agent-identity-decision-table.test.ts | 4 +- .../lib/typing-latency/diagnostic-summary.ts | 4 +- ...ktree-runtime-owner-index.detected.test.ts | 6 +-- ...time-graph-agent-status-projection.test.ts | 22 ++++---- ...-runtime-graph-projection-hot-path.test.ts | 24 ++++----- .../src/store/slices/usage-provider-slices.ts | 44 +++++++++------- src/shared/agent-feature-install-commands.ts | 4 +- .../agent-resume-launch-command.test.ts | 4 +- src/shared/agent-session-journal-schemas.ts | 2 +- src/shared/agent-session-record.ts | 4 +- src/shared/git-push-target-validation.test.ts | 10 ++-- src/shared/git-push-target-validation.ts | 2 +- src/shared/native-chat-ask.ts | 6 +-- src/shared/onboarding-state-types.ts | 2 + .../pane-agent-identity-resolver.test.ts | 12 ++--- .../plugin-language-pack-artifact.test.ts | 4 +- .../plugins/plugin-language-pack-artifact.ts | 4 +- src/shared/remote-pairing-verification.ts | 4 +- src/shared/rpc-contract/repo-update-params.ts | 12 +++-- src/shared/rpc-contract/rpc-send-params.ts | 18 +++---- .../ui-update-value-tolerance-params.ts | 16 +++--- src/shared/skills-cli-agent-keys.test.ts | 6 +-- src/shared/skills-cli-agent-keys.ts | 2 +- src/shared/telemetry-event-classification.ts | 18 +++---- src/shared/zod-salvage-absence.test.ts | 10 ++-- .../host-created-terminal-retention-oracle.ts | 4 +- .../terminal-cjk-ime-committed-text.spec.ts | 10 ++-- .../tools/win-crash-survival-e2e/cli-args.mjs | 4 +- 96 files changed, 441 insertions(+), 348 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 3a115684ba9..bba8588d949 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -35,7 +35,7 @@ "anti-slop/no-reflect-apply": "error", "anti-slop/no-reflect-get": "error", "anti-slop/no-runtime-typeof": "off", - "anti-slop/no-shape-in-symbol-names": "off", + "anti-slop/no-shape-in-symbol-names": "error", "anti-slop/no-unknown-parameters": "off", "anti-slop/no-unknown-returns": "off", "anti-slop/no-unknown-type-aliases": "error", @@ -55,6 +55,56 @@ "rules": { "anti-slop/no-module-mocking": "off" } + }, + // The exemptions below are file-scoped rather than inline `oxlint-disable` comments + // because the root lint scan does not load this plugin, so an inline directive naming + // an anti-slop rule always reads back as an unused directive there. + // + // In the screenshot annotator a "shape" is the drawn geometry -- pen, arrow, rect, + // ellipse, highlight. A domain noun, and it pervades every symbol in the module. + // mobile/src/test-support/rpc-recording is the golden recorder engine. recorder-digest.ts + // hashes these files' RAW BYTES into every golden's `recorderSha256` header, so any edit + // here -- a rename or even an added comment -- invalidates all 208 recordings. The exemption + // is config-scoped for that reason: an inline directive would change the bytes it protects. + { + "files": ["**/test-support/rpc-recording/**"], + "rules": { + "anti-slop/no-shape-in-symbol-names": "off" + } + }, + { + "files": ["**/browser-pane/annotate/**"], + "rules": { + "anti-slop/no-shape-in-symbol-names": "off" + } + }, + // lucide exports the icon component as `Shapes`, and the matching REPO_LUCIDE_ICONS key + // is the persisted icon name shared by the desktop picker and mobile. + { + "files": [ + "**/components/repo/repo-icon.tsx", + "**/worktree-list/rows/repo-header-project-actions.tsx", + "**/components/MobileRepoIcon.tsx" + ], + "rules": { + "anti-slop/no-shape-in-symbol-names": "off" + } + }, + // `shapedSidebar` is a persisted onboarding-checklist field and a telemetry enum member; + // renaming it would orphan saved state. + { + "files": ["**/src/shared/constants.ts", "**/src/shared/onboarding-state-types.ts"], + "rules": { + "anti-slop/no-shape-in-symbol-names": "off" + } + }, + // Matching zod's own literal `shape` property is what selects the ZodObject branch of + // RpcSendInput's conditional type. + { + "files": ["**/rpc-contract/rpc-send-params.ts"], + "rules": { + "anti-slop/no-shape-in-symbol-names": "off" + } } ] } diff --git a/config/scripts/agent-lineage-cycle-cleanup-benchmark.mjs b/config/scripts/agent-lineage-cycle-cleanup-benchmark.mjs index 47407764e0f..59f4e387beb 100644 --- a/config/scripts/agent-lineage-cycle-cleanup-benchmark.mjs +++ b/config/scripts/agent-lineage-cycle-cleanup-benchmark.mjs @@ -50,7 +50,7 @@ for (let sample = 0; sample < 500; sample++) { } const results = [] -for (const [shape, count] of [ +for (const [topology, count] of [ ['flat', 1000], ['all-cycles', 1000], ['mixed-cycles', 100], @@ -58,9 +58,9 @@ for (const [shape, count] of [ ['mixed-cycles', 1000] ]) { const rows = Array.from({ length: count }, (_, index) => - row(index, shape === 'flat' ? undefined : index ^ 1) + row(index, topology === 'flat' ? undefined : index ^ 1) ) - if (shape === 'mixed-cycles') { + if (topology === 'mixed-cycles') { rows.unshift(row('root', undefined)) } assert.deepEqual(after(rows), before(rows)) @@ -83,7 +83,7 @@ for (const [shape, count] of [ samples[arm].push({ wallMs, cpuMs: (used.user + used.system) / 30_000 }) } } - results.push({ shape, count, samples }) + results.push({ topology, count, samples }) } console.log( JSON.stringify({ baseline, node: process.version, parityGraphs: 500, results }, null, 2) diff --git a/config/scripts/agent-lineage-reachability-benchmark.mjs b/config/scripts/agent-lineage-reachability-benchmark.mjs index 4122ac18dab..6d542367c18 100644 --- a/config/scripts/agent-lineage-reachability-benchmark.mjs +++ b/config/scripts/agent-lineage-reachability-benchmark.mjs @@ -58,15 +58,19 @@ for (let trial = 0; trial < 5000; trial += 1) { const results = [] for (const count of [8, 32, 128, 512, 1024]) { - for (const shape of ['flat', 'fanout', 'balanced', 'chain']) { + for (const topology of ['flat', 'fanout', 'balanced', 'chain']) { const rows = Array.from({ length: count }, (_, index) => { const parent = - shape === 'fanout' ? 0 : shape === 'balanced' ? Math.floor((index - 1) / 4) : index - 1 + topology === 'fanout' + ? 0 + : topology === 'balanced' + ? Math.floor((index - 1) / 4) + : index - 1 return { paneKey: `pane-${index}`, entry: { orchestration: - index > 0 && shape !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined + index > 0 && topology !== 'flat' ? { parentPaneKey: `pane-${parent}` } : undefined } } }) @@ -92,7 +96,7 @@ for (const count of [8, 32, 128, 512, 1024]) { } results.push({ count, - shape, + topology, iterations, meanMicrosecondsPerTree: Object.fromEntries( Object.entries(samples).map(([arm, values]) => [ diff --git a/config/scripts/mobile-markdown-placeholder-benchmark.mjs b/config/scripts/mobile-markdown-placeholder-benchmark.mjs index 20280e5a8d2..dd5cf22d9dc 100644 --- a/config/scripts/mobile-markdown-placeholder-benchmark.mjs +++ b/config/scripts/mobile-markdown-placeholder-benchmark.mjs @@ -40,7 +40,7 @@ function measure(fn, input, repeats) { return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)] } const results = [] -for (const [shape, input] of [ +for (const [inputCase, input] of [ ['ordinary Markdown', '# Hello\n\n

Use `Array` and bold.

'], ...[2048, 8192, 16384].map((length) => [ `${length} underscore collision`, @@ -49,7 +49,7 @@ for (const [shape, input] of [ ]) { assert.equal(after(input), before(input)) results.push({ - shape, + inputCase, bytes: Buffer.byteLength(input), beforeMs: measure(before, input, 5), afterMs: measure(after, input, 15) diff --git a/config/scripts/redactor-environment-lines-benchmark.mjs b/config/scripts/redactor-environment-lines-benchmark.mjs index 71aebf9fe88..b30e56fee05 100644 --- a/config/scripts/redactor-environment-lines-benchmark.mjs +++ b/config/scripts/redactor-environment-lines-benchmark.mjs @@ -25,7 +25,7 @@ function median(fn, input, repeats) { return samples.sort((a, b) => a - b)[Math.floor(samples.length / 2)] } const rows = [] -for (const [shape, input] of [ +for (const [label, input] of [ ['8KiB blank lines', '\n'.repeat(8192)], ['16KiB blank lines', '\n'.repeat(16384)], ['32KiB blank lines', '\n'.repeat(32768)], @@ -37,7 +37,7 @@ for (const [shape, input] of [ const beforeMs = median(before, input, 3) const afterMs = median(redactString, input, 15) rows.push({ - shape, + label, bytes: Buffer.byteLength(input), beforeMs, afterMs, diff --git a/config/scripts/repo-icon-source-href-benchmark.mjs b/config/scripts/repo-icon-source-href-benchmark.mjs index 76c42d261b4..da560724a23 100644 --- a/config/scripts/repo-icon-source-href-benchmark.mjs +++ b/config/scripts/repo-icon-source-href-benchmark.mjs @@ -36,15 +36,15 @@ function measurePair(source) { const results = [] for (const size of [8192, 16384, 32768]) { - for (const shape of ['no icon', 'rel without href', 'unterminated link starts']) { + for (const variant of ['no icon', 'rel without href', 'unterminated link starts']) { const source = - shape === 'unterminated link starts' + variant === 'unterminated link starts' ? ' interactive login shell -> git', fast: 'env -> git' }, diff --git a/mobile/src/components/MobileRepoIcon.tsx b/mobile/src/components/MobileRepoIcon.tsx index e4f7f9664cf..2ea1f8d573b 100644 --- a/mobile/src/components/MobileRepoIcon.tsx +++ b/mobile/src/components/MobileRepoIcon.tsx @@ -16,6 +16,7 @@ import { Palette, Rocket, Server, + // `Shapes` is lucide's own export name; exempted in config/oxlint-anti-slop.json. Shapes, Sparkles, SquareTerminal, diff --git a/mobile/src/tasks/use-mobile-tasks-provider-view-projection.test.tsx b/mobile/src/tasks/use-mobile-tasks-provider-view-projection.test.tsx index 0eb8d3d32f7..3ab8ae149d4 100644 --- a/mobile/src/tasks/use-mobile-tasks-provider-view-projection.test.tsx +++ b/mobile/src/tasks/use-mobile-tasks-provider-view-projection.test.tsx @@ -243,7 +243,7 @@ function countLinearWork(run: () => void): WorkCounts { } } -function shape(sections: LinearIssueSection[]) { +function summarizeSections(sections: LinearIssueSection[]) { return sections.map((section) => ({ key: section.key, label: section.label, @@ -268,8 +268,8 @@ describe('useMobileTasksProviderViewProjection linear sections', () => { (linearGroupBy) => { const projection = mount({ linearGroupBy }) expect(projection.linearBoardSections).toBe(projection.linearIssueSections) - expect(shape(projection.linearBoardSections)).toEqual( - shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections) + expect(summarizeSections(projection.linearBoardSections)).toEqual( + summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy }).boardSections) ) } ) @@ -278,8 +278,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => { const projection = mount({ linearGroupBy: 'none' }) const legacy = legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }) expect(projection.linearBoardSections).not.toBe(projection.linearIssueSections) - expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections)) - expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections)) + expect(summarizeSections(projection.linearIssueSections)).toEqual( + summarizeSections(legacy.listSections) + ) + expect(summarizeSections(projection.linearBoardSections)).toEqual( + summarizeSections(legacy.boardSections) + ) expect(projection.linearIssueSections.map((section) => section.key)).toEqual(['all']) expect(projection.linearBoardSections.length).toBeGreaterThan(1) expect(projection.linearListEntries.every((entry) => entry.type === 'issue')).toBe(true) @@ -293,8 +297,12 @@ describe('useMobileTasksProviderViewProjection linear sections', () => { linearGroupBy, linearOrderBy: order }) - expect(shape(projection.linearIssueSections)).toEqual(shape(legacy.listSections)) - expect(shape(projection.linearBoardSections)).toEqual(shape(legacy.boardSections)) + expect(summarizeSections(projection.linearIssueSections)).toEqual( + summarizeSections(legacy.listSections) + ) + expect(summarizeSections(projection.linearBoardSections)).toEqual( + summarizeSections(legacy.boardSections) + ) expect(projection.linearIssuesForView.map((issue) => issue.id)).toEqual( legacy.issuesForView.map((issue) => issue.id) ) @@ -310,20 +318,24 @@ describe('useMobileTasksProviderViewProjection transitions', () => { const grouped = rerender({ linearGroupBy: 'status' }) expect(grouped.linearBoardSections).toBe(grouped.linearIssueSections) - expect(shape(grouped.linearBoardSections)).toEqual( - shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections) + expect(summarizeSections(grouped.linearBoardSections)).toEqual( + summarizeSections( + legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'status' }).boardSections + ) ) const assignee = rerender({ linearGroupBy: 'assignee' }) expect(assignee.linearBoardSections).toBe(assignee.linearIssueSections) - expect(shape(assignee.linearBoardSections)).toEqual( - shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections) + expect(summarizeSections(assignee.linearBoardSections)).toEqual( + summarizeSections( + legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'assignee' }).boardSections + ) ) const none = rerender({ linearGroupBy: 'none' }) expect(none.linearBoardSections).not.toBe(none.linearIssueSections) - expect(shape(none.linearBoardSections)).toEqual( - shape(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections) + expect(summarizeSections(none.linearBoardSections)).toEqual( + summarizeSections(legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'none' }).boardSections) ) }) @@ -333,8 +345,8 @@ describe('useMobileTasksProviderViewProjection transitions', () => { const next = rerender({ linearGroupBy: 'priority', linearOrderBy: 'identifier' }) expect(next.linearBoardSections).not.toBe(firstSections) expect(next.linearBoardSections).toBe(next.linearIssueSections) - expect(shape(next.linearBoardSections)).toEqual( - shape( + expect(summarizeSections(next.linearBoardSections)).toEqual( + summarizeSections( legacyProjection({ ...DEFAULT_INPUT, linearGroupBy: 'priority', @@ -369,17 +381,17 @@ describe('useMobileTasksProviderViewProjection transitions', () => { const refreshed = rerender({ linearGroupBy: 'status', items: makeItems(50) }) expect(refreshed.linearBoardSections).not.toBe(sections) expect(refreshed.linearBoardSections).toBe(refreshed.linearIssueSections) - expect(shape(refreshed.linearBoardSections)).toEqual(shape(sections)) + expect(summarizeSections(refreshed.linearBoardSections)).toEqual(summarizeSections(sections)) }) it('does not mutate the shared sections when the list entries are built', () => { const projection = mount({ linearGroupBy: 'status' }) - const before = shape(projection.linearIssueSections) + const before = summarizeSections(projection.linearIssueSections) const entryIssueIds = projection.linearListEntries .filter((entry) => entry.type === 'issue') .map((entry) => (entry.type === 'issue' ? entry.issue.id : '')) expect(entryIssueIds).toHaveLength(50) - expect(shape(projection.linearBoardSections)).toEqual(before) + expect(summarizeSections(projection.linearBoardSections)).toEqual(before) }) }) diff --git a/src/cli/handlers/skills.ts b/src/cli/handlers/skills.ts index 1b068fc80b0..325262a42f5 100644 --- a/src/cli/handlers/skills.ts +++ b/src/cli/handlers/skills.ts @@ -17,7 +17,7 @@ import { UnsafeWindowsBatchArgumentsError, WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL } from '../../shared/windows-batch-spawn' -import { isSkillsCliAgentKeyShaped, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys' +import { isUsableSkillsCliAgentKey, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys' import { buildAgentFeatureSkillInstallArgs, buildAgentFeatureSkillUpdateArgs @@ -150,7 +150,7 @@ function resolveInstallAgentKeys(flags: Map): string[] if (keys.length === 0) { throw new RuntimeClientError('invalid_argument', 'Missing required --agent') } - const unusable = keys.find((key) => !isSkillsCliAgentKeyShaped(key)) + const unusable = keys.find((key) => !isUsableSkillsCliAgentKey(key)) if (unusable !== undefined) { // Why: the skills CLI drops a value starting with `-`, which leaves it with // no target and installs into every agent it knows. diff --git a/src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts b/src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts index 79cbe70eb7f..220e7d8390d 100644 --- a/src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts +++ b/src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts @@ -47,7 +47,7 @@ const CAN_DENY_READ = process.platform !== 'win32' && process.getuid?.() !== 0 const INTERVAL_MS = 20_000 const SESSIONS = ['aaaaaaaa', 'bbbbbbbb', 'cccccccc'] -type RootShape = { +type RootLayout = { name: string /** Where the unreachable root's transcripts live, and where its files go. */ detachedRoot: (harness: SessionSearchIndexerHarness) => string @@ -59,7 +59,7 @@ type RootShape = { const OPENCLAW_SESSION_DIR = join('agents', 'main', 'sessions') -const ROOT_SHAPES: RootShape[] = [ +const ROOT_LAYOUTS: RootLayout[] = [ { name: 'roots discovery reports one per directory', detachedRoot: (harness) => harness.roots.claudeProjectsDir ?? '', @@ -81,7 +81,7 @@ const ROOT_SHAPES: RootShape[] = [ } ] -type UnreachableShape = { +type UnreachableMode = { name: string needsDeniedRead: boolean /** @@ -97,7 +97,7 @@ type UnreachableShape = { attach: (root: string, transcriptDir: string, parked: string) => Promise } -const UNREACHABLE_SHAPES: UnreachableShape[] = [ +const UNREACHABLE_MODES: UnreachableMode[] = [ { name: 'the root itself is not there', needsDeniedRead: false, @@ -276,8 +276,8 @@ function indexedSessions(): string[] { .sort() } -for (const roots of ROOT_SHAPES) { - for (const unreachable of UNREACHABLE_SHAPES) { +for (const roots of ROOT_LAYOUTS) { + for (const unreachable of UNREACHABLE_MODES) { describe.skipIf(unreachable.needsDeniedRead && !CAN_DENY_READ)( `${roots.name}, ${unreachable.name}`, () => { @@ -298,7 +298,7 @@ for (const roots of ROOT_SHAPES) { // pass, so the setup drives passes until the index has caught up. await driveUntilIndexed(SESSIONS.length * 2) const detachedIds = detachedPaths.map((_path, index) => - roots === ROOT_SHAPES[0] + roots === ROOT_LAYOUTS[0] ? fullSessionId(SESSIONS[index] ?? '') : (SESSIONS[index] ?? '') ) diff --git a/src/main/ai-vault-search/session-search-query-planner.ts b/src/main/ai-vault-search/session-search-query-planner.ts index 6c2c2f3b91c..91e8711fbe3 100644 --- a/src/main/ai-vault-search/session-search-query-planner.ts +++ b/src/main/ai-vault-search/session-search-query-planner.ts @@ -17,7 +17,7 @@ const MAX_TERMS = 64 // A query that quotes something from a transcript: camelCase, SCREAMING_SNAKE, // a dotted or snake_case name, a path, a filename, a PR number, a ticket, code // punctuation, or an error word. -const LITERAL_SHAPE = +const LITERAL_PATTERN = /[A-Za-z0-9_]*[a-z][A-Z][A-Za-z0-9_]*|\b[A-Z][A-Z0-9]{2,}(_[A-Z0-9]+)+\b|\b\w{2,}[._]\w{2,}\b|\b[\w.-]+\/[\w/.-]+\b|\b\w+\.(ts|tsx|js|jsx|py|rs|go|json|md|sh|yml|yaml|toml|c|cc|h|java|sql)\b|#\d{3,}|\b[A-Z]{2,6}-\d{2,}\b|[(){};=]|::|->|--\w|\b(Error|Exception|Traceback|error:|warning:)\b/ const QUOTED = /"[^"]{3,}"|'[^']{3,}'/ @@ -36,7 +36,7 @@ export type SessionSearchQueryPlan = { } export function isLiteralQuery(query: string): boolean { - return QUOTED.test(query) || LITERAL_SHAPE.test(query) + return QUOTED.test(query) || LITERAL_PATTERN.test(query) } /** diff --git a/src/main/ai-vault/session-delete-target.ts b/src/main/ai-vault/session-delete-target.ts index 13320ce39e9..a893e639fad 100644 --- a/src/main/ai-vault/session-delete-target.ts +++ b/src/main/ai-vault/session-delete-target.ts @@ -21,7 +21,7 @@ import type { AiVaultScanOptions } from './session-scanner-types' // Agents whose session IS the directory holding the scanned file: everything // beside it belongs to the same session (rovo's session_context.json, grok's // chat_history.jsonl), so the directory is the only complete delete unit. -const AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS = new Set([ +const AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS = new Set([ 'rovo', 'grok', 'cline' @@ -109,7 +109,7 @@ function sessionDeleteRemovals(args: { }): readonly AiVaultSessionDeleteRemoval[] | null { const { agent, resolvedPath, matchedRoot, roots } = args - if (AI_VAULT_DIRECTORY_SHAPED_DELETE_AGENTS.has(agent)) { + if (AI_VAULT_WHOLE_DIRECTORY_DELETE_AGENTS.has(agent)) { const sessionDir = dirname(resolvedPath) if (sessionDir === matchedRoot || !isPathInsideOrEqual(matchedRoot, sessionDir)) { return null diff --git a/src/main/browser/browser-cookie-samesite.electron.test.ts b/src/main/browser/browser-cookie-samesite.electron.test.ts index 4f04f93c72e..561bff2a407 100644 --- a/src/main/browser/browser-cookie-samesite.electron.test.ts +++ b/src/main/browser/browser-cookie-samesite.electron.test.ts @@ -33,7 +33,7 @@ type FixtureResult = { afterCookies: JarCookie[] } -type SourceShape = { +type SourceCookieRow = { name: string samesite: number | null is_secure: number @@ -147,18 +147,26 @@ run().catch((error) => { ` } -function readSourceShape(sourceDbPath: string): SourceShape[] { +function readSourceCookieRows(sourceDbPath: string): SourceCookieRow[] { const db = new DatabaseSync(sourceDbPath, { readOnly: true }) try { return db .prepare('SELECT name, samesite, is_secure FROM cookies ORDER BY rowid') - .all() as SourceShape[] + .all() + .map((row) => ({ + name: String(row.name), + samesite: row.samesite === null ? null : Number(row.samesite), + is_secure: Number(row.is_secure) + })) } finally { db.close() } } -async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: SourceShape[] }> { +async function runFixture(): Promise<{ + fixture: FixtureResult + sourceCookieRows: SourceCookieRow[] +}> { const root = mkdtempSync(join(tmpdir(), 'orca-samesite-enum-')) fixtureRoots.push(root) const bundlePath = join(root, 'cookie-import-samesite.cjs') @@ -176,7 +184,7 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour }) ) createChromiumCookieTestDatabase(sourceDbPath, rows).close() - const sourceShape = readSourceShape(sourceDbPath) + const sourceCookieRows = readSourceCookieRows(sourceDbPath) writeFileSync( bundleEntryPath, `export { importCookiesFromBrowser } from ${JSON.stringify(join(process.cwd(), 'src/main/browser/browser-cookie-import.ts'))}` @@ -212,22 +220,23 @@ async function runFixture(): Promise<{ fixture: FixtureResult; sourceShape: Sour const fixtureResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result' expect(run.error).toBeUndefined() expect(run.status, `${fixtureResult}\n${run.stdout}\n${run.stderr}`).toBe(0) - return { fixture: JSON.parse(fixtureResult) as FixtureResult, sourceShape } + const fixture: FixtureResult = JSON.parse(fixtureResult) + return { fixture, sourceCookieRows } } describe('Chromium SameSite storage enum import', () => { let fixture: FixtureResult - let sourceShape: SourceShape[] + let sourceCookieRows: SourceCookieRow[] beforeAll(async () => { - ;({ fixture, sourceShape } = await runFixture()) + ;({ fixture, sourceCookieRows } = await runFixture()) }, 120_000) it('runs the real Chromium import against the complete synthetic matrix', () => { expect(fixture.step).toBe('import finished') expect(fixture.beforeCookieCount).toBe(0) expect(fixture.importResult.ok).toBe(true) - expect(sourceShape).toEqual( + expect(sourceCookieRows).toEqual( [REJECTION_CONTROL, ...VALID_COMBINATIONS, NULL_CASE].map( ({ name, rawSameSite, secure }) => ({ name, diff --git a/src/main/claude-accounts/runtime-auth-service-readback-identity.test.ts b/src/main/claude-accounts/runtime-auth-service-readback-identity.test.ts index 1cd86014c29..2806d44e82a 100644 --- a/src/main/claude-accounts/runtime-auth-service-readback-identity.test.ts +++ b/src/main/claude-accounts/runtime-auth-service-readback-identity.test.ts @@ -70,7 +70,7 @@ describe('ClaudeRuntimeAuthService', () => { it('rejects wrong-shaped refreshed credentials during read-back', async () => { const runtimeCredentialsPath = join(testState.fakeHomeDir, '.claude', '.credentials.json') const originalCredentials = createClaudeCredentialsJson('user@example.com', 'original') - const wrongShapedRefresh = `${JSON.stringify({ + const malformedRefresh = `${JSON.stringify({ claudeAiOauth: { email: 'user@example.com', expiresAt: Date.now() + 120_000 @@ -91,7 +91,7 @@ describe('ClaudeRuntimeAuthService', () => { settings.activeClaudeManagedAccountId = 'account-1' await service.syncForCurrentSelection() - writeFileSync(runtimeCredentialsPath, wrongShapedRefresh, 'utf-8') + writeFileSync(runtimeCredentialsPath, malformedRefresh, 'utf-8') await service.syncForCurrentSelection() expect(readManagedCredentialsForTest('account-1', managedAuthPath)).toBe(originalCredentials) diff --git a/src/main/codex/codex-structured-item-translation.test.ts b/src/main/codex/codex-structured-item-translation.test.ts index 53cf94e265c..ad63d624f58 100644 --- a/src/main/codex/codex-structured-item-translation.test.ts +++ b/src/main/codex/codex-structured-item-translation.test.ts @@ -806,7 +806,7 @@ describe('codex item bodies', () => { // Both the row label and the run header read top-level input keys only, so a // shape whose detail sits inside `action` renders as the input's raw JSON. const url = 'https://example.com/docs/page' - const shapes: [string, unknown, string, string][] = [ + const cases: [string, unknown, string, string][] = [ ['started', null, '', ''], [ 'search', @@ -823,7 +823,7 @@ describe('codex item bodies', () => { ], ['other', { type: 'other' }, 'other', ''] ] - for (const [name, action, label, brief] of shapes) { + for (const [name, action, label, brief] of cases) { // Codex leaves the item's own `query` empty on most completed searches. const query = name === 'search' || name === 'findInPage' ? 'a sample query' : '' const input = toolCallInput({ type: 'webSearch', id: 'w', query, action }) diff --git a/src/main/cursor/hook-service.ts b/src/main/cursor/hook-service.ts index b07639f108a..ed431509346 100644 --- a/src/main/cursor/hook-service.ts +++ b/src/main/cursor/hook-service.ts @@ -168,13 +168,13 @@ export class CursorHookService { } const cleaned = removeManagedCommands(definitions, isManagedCommand) // Also strip entries with the command at the top level (Cursor schema). - const strippedCursorShape = cleaned.filter( + const strippedTopLevelCommands = cleaned.filter( (definition) => !isManagedCommand(definition.command) ) - if (strippedCursorShape.length === 0) { + if (strippedTopLevelCommands.length === 0) { delete nextHooks[eventName] } else { - nextHooks[eventName] = strippedCursorShape + nextHooks[eventName] = strippedTopLevelCommands } } diff --git a/src/main/git/push-target-validation.ts b/src/main/git/push-target-validation.ts index 055eab8537c..133b1ee8b2b 100644 --- a/src/main/git/push-target-validation.ts +++ b/src/main/git/push-target-validation.ts @@ -1,5 +1,5 @@ import type { GitPushTarget } from '../../shared/worktree/types' -import { assertGitPushTargetShape } from '../../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../../shared/git-push-target-validation' import { gitExecFileAsync } from './runner' import type { GitExecOptions as GitCommandExecOptions } from './command-runner/git-exec-options' @@ -10,7 +10,7 @@ export async function validateGitPushTarget( target: unknown, options: GitExecOptions = {} ): Promise { - assertGitPushTargetShape(target) + assertValidGitPushTarget(target) await gitExecFileAsync(['check-ref-format', '--branch', target.branchName], { cwd: repoPath, ...options diff --git a/src/main/github/client-stack-merge-guard.test.ts b/src/main/github/client-stack-merge-guard.test.ts index 57cd5df8bfd..c47f786d2ed 100644 --- a/src/main/github/client-stack-merge-guard.test.ts +++ b/src/main/github/client-stack-merge-guard.test.ts @@ -592,9 +592,9 @@ describe('GitHub GraphQL rate-limit guard', () => { }) it.each([ - { stackShape: 'omits stack', stackField: {} }, - { stackShape: 'sets stack to null', stackField: { stack: null } } - ])('keeps legacy merge when an ordinary GitHub response $stackShape', async (scenario) => { + { stackVariant: 'omits stack', stackField: {} }, + { stackVariant: 'sets stack to null', stackField: { stack: null } } + ])('keeps legacy merge when an ordinary GitHub response $stackVariant', async (scenario) => { ghExecFileAsyncMock .mockResolvedValueOnce({ stdout: JSON.stringify({ diff --git a/src/main/github/default-branch-stale-pr.test.ts b/src/main/github/default-branch-stale-pr.test.ts index 4361c9759e7..276d63e38ac 100644 --- a/src/main/github/default-branch-stale-pr.test.ts +++ b/src/main/github/default-branch-stale-pr.test.ts @@ -164,7 +164,7 @@ function primeGitExecForDefaultBranch({ }) } -type RestPRShape = { +type RestPROverrides = { number?: number state?: string merged_at?: string | null @@ -178,7 +178,7 @@ function restPR({ merged_at = null, head_ref = 'master', head_sha = 'stale-master-oid' -}: RestPRShape = {}): Record { +}: RestPROverrides = {}): Record { return { number, title: 'Historical PR', diff --git a/src/main/github/project-view/internals.ts b/src/main/github/project-view/internals.ts index 1c828909669..3bea5b5584d 100644 --- a/src/main/github/project-view/internals.ts +++ b/src/main/github/project-view/internals.ts @@ -17,7 +17,7 @@ import { classifyProjectError, driftError, rateLimitedError, - type GhGraphqlErrorShape + type GhGraphqlError } from './project-error-classification' export { @@ -172,7 +172,7 @@ export async function runGraphql( ...(exec?.host ? { host: exec.host } : {}) }) try { - const parsed = JSON.parse(stdout) as { data?: T; errors?: GhGraphqlErrorShape[] } + const parsed: { data?: T; errors?: GhGraphqlError[] } = JSON.parse(stdout) if (parsed.errors && parsed.errors.length > 0) { return { ok: false, diff --git a/src/main/github/project-view/project-error-classification.ts b/src/main/github/project-view/project-error-classification.ts index 6b3d9a3fe09..a9d12fa44f1 100644 --- a/src/main/github/project-view/project-error-classification.ts +++ b/src/main/github/project-view/project-error-classification.ts @@ -4,14 +4,14 @@ import type { GitHubProjectViewError } from '../../../shared/github/project-result-types' import { githubProjectHost } from '../../../shared/github/project-identity' -export type GhGraphqlErrorShape = { +export type GhGraphqlError = { type?: string message?: string path?: (string | number)[] extensions?: { code?: string } } -export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlErrorShape[] { +export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlError[] { // `gh api graphql` prints the response JSON to stdout even on GraphQL // errors, and the stderr carries a summary. Try stdout first; if parsing // fails, fall back to stderr. @@ -21,7 +21,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE continue } try { - const parsed = JSON.parse(src) as { errors?: GhGraphqlErrorShape[] } + const parsed: { errors?: GhGraphqlError[] } = JSON.parse(src) if (parsed.errors && parsed.errors.length > 0) { return parsed.errors } @@ -32,7 +32,7 @@ export function extractGraphqlErrors(stderr: string, stdout: string): GhGraphqlE return [] } -export function errorsIndicateParentField(errors: GhGraphqlErrorShape[], stderr: string): boolean { +export function errorsIndicateParentField(errors: GhGraphqlError[], stderr: string): boolean { const lower = stderr.toLowerCase() // Preview-header shape: gh returns a 4xx with "preview" in the message. if (lower.includes('preview') && lower.includes('parent')) { diff --git a/src/main/github/project-view/project-view-item-page.ts b/src/main/github/project-view/project-view-item-page.ts index ca135fded51..e0ea53d059b 100644 --- a/src/main/github/project-view/project-view-item-page.ts +++ b/src/main/github/project-view/project-view-item-page.ts @@ -14,7 +14,7 @@ import { classifyProjectError, driftError, rateLimitedError, - type GhGraphqlErrorShape + type GhGraphqlError } from './project-error-classification' import { ownerQueryRoot } from './project-view-config' import type { RawItem } from './project-view-item-normalization' @@ -47,7 +47,7 @@ export async function fetchItemsPageWithRaw(args: { | { ok: false error: GitHubProjectViewError - rawErrors: GhGraphqlErrorShape[] + rawErrors: GhGraphqlError[] stderr: string } > { @@ -117,7 +117,7 @@ export async function fetchItemsPageWithRaw(args: { stdout = extracted.stdout execFailed = true } - let parsed: { data?: Record; errors?: GhGraphqlErrorShape[] } = {} + let parsed: { data?: Record; errors?: GhGraphqlError[] } = {} try { parsed = JSON.parse(stdout) } catch { diff --git a/src/main/ipc/filesystem/git-remote/branch-mutation-handlers.ts b/src/main/ipc/filesystem/git-remote/branch-mutation-handlers.ts index 2c3273b8c00..1ad9b926e21 100644 --- a/src/main/ipc/filesystem/git-remote/branch-mutation-handlers.ts +++ b/src/main/ipc/filesystem/git-remote/branch-mutation-handlers.ts @@ -8,7 +8,7 @@ import { } from '../../../providers/ssh-git-dispatch' import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache' import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options' -import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation' import { materializeWorktreePushTargetRemote, materializeWorktreePushTargetRemoteSsh @@ -35,7 +35,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl const publish = args.publish === true if (args.connectionId) { if (args.pushTarget) { - assertGitPushTargetShape(args.pushTarget) + assertValidGitPushTarget(args.pushTarget) } const provider = getSshGitProvider(args.connectionId) if (!provider) { @@ -99,7 +99,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl ): Promise => { if (args.connectionId) { if (args.pushTarget) { - assertGitPushTargetShape(args.pushTarget) + assertValidGitPushTarget(args.pushTarget) } const provider = getSshGitProvider(args.connectionId) if (!provider) { @@ -159,7 +159,7 @@ export function registerGitRemoteBranchMutationHandlers(context: FilesystemHandl ): Promise => { if (args.connectionId) { if (args.pushTarget) { - assertGitPushTargetShape(args.pushTarget) + assertValidGitPushTarget(args.pushTarget) } const provider = getSshGitProvider(args.connectionId) if (!provider) { diff --git a/src/main/ipc/filesystem/git-remote/sync-handlers.ts b/src/main/ipc/filesystem/git-remote/sync-handlers.ts index a924c393a04..b487d54f39e 100644 --- a/src/main/ipc/filesystem/git-remote/sync-handlers.ts +++ b/src/main/ipc/filesystem/git-remote/sync-handlers.ts @@ -15,7 +15,7 @@ import { } from '../../../providers/ssh-git-dispatch' import { resolveRegisteredWorktreePath } from '../../registered-worktree-roots-cache' import { getLocalGitOptionsForRegisteredWorktree } from '../../local-worktree-runtime-options' -import { assertGitPushTargetShape } from '../../../../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../../../../shared/git-push-target-validation' import { validateGitForkSyncExpectedUpstream } from '../../../../shared/git-fork-sync' import { materializeWorktreePushTargetRemote, @@ -34,7 +34,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext) ): Promise => { if (args.connectionId) { if (args.pushTarget) { - assertGitPushTargetShape(args.pushTarget) + assertValidGitPushTarget(args.pushTarget) } const provider = getSshGitProvider(args.connectionId) if (!provider) { @@ -65,7 +65,7 @@ export function registerGitRemoteSyncHandlers(context: FilesystemHandlerContext) ): Promise => { if (args.connectionId) { if (args.pushTarget) { - assertGitPushTargetShape(args.pushTarget) + assertValidGitPushTarget(args.pushTarget) } const provider = getSshGitProvider(args.connectionId) if (!provider) { diff --git a/src/main/ipc/readdir-error-diagnostics.test.ts b/src/main/ipc/readdir-error-diagnostics.test.ts index cf599afbdf1..c1c7041a662 100644 --- a/src/main/ipc/readdir-error-diagnostics.test.ts +++ b/src/main/ipc/readdir-error-diagnostics.test.ts @@ -1,24 +1,27 @@ import { describe, expect, it } from 'vitest' -import { buildReadDirErrorBreadcrumb, describeReadDirPathShape } from './readdir-error-diagnostics' +import { buildReadDirErrorBreadcrumb, classifyReadDirPath } from './readdir-error-diagnostics' -describe('describeReadDirPathShape', () => { +describe('classifyReadDirPath', () => { it('classifies a WSL UNC path without leaking it', () => { - const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', undefined) - expect(shape).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true }) + const classification = classifyReadDirPath( + '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', + undefined + ) + expect(classification).toEqual({ hasConnectionId: false, isUNC: true, isWsl: true }) }) it('classifies the legacy \\\\wsl$ root as WSL', () => { - expect(describeReadDirPathShape('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true) + expect(classifyReadDirPath('\\\\wsl$\\Ubuntu\\home', undefined).isWsl).toBe(true) }) it('classifies a plain network UNC share as UNC but not WSL', () => { - const shape = describeReadDirPathShape('\\\\fileserver\\share\\dir', undefined) - expect(shape).toMatchObject({ isUNC: true, isWsl: false }) - expect(shape.driveLetter).toBeUndefined() + const classification = classifyReadDirPath('\\\\fileserver\\share\\dir', undefined) + expect(classification).toMatchObject({ isUNC: true, isWsl: false }) + expect(classification.driveLetter).toBeUndefined() }) it('extracts an uppercased drive letter for mapped drives', () => { - expect(describeReadDirPathShape('z:\\projects\\repo', undefined)).toEqual({ + expect(classifyReadDirPath('z:\\projects\\repo', undefined)).toEqual({ hasConnectionId: false, isUNC: false, isWsl: false, @@ -27,18 +30,18 @@ describe('describeReadDirPathShape', () => { }) it('flags the SSH connection without recording it', () => { - const shape = describeReadDirPathShape('/remote/repo', 'ssh-1') - expect(shape).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false }) + const classification = classifyReadDirPath('/remote/repo', 'ssh-1') + expect(classification).toEqual({ hasConnectionId: true, isUNC: false, isWsl: false }) }) - it('never includes the raw path in the shape', () => { - const shape = describeReadDirPathShape('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9') - expect(JSON.stringify(shape)).not.toContain('secret') + it('never includes the raw path in the classification', () => { + const classification = classifyReadDirPath('\\\\wsl.localhost\\Ubuntu\\secret\\path', 'ssh-9') + expect(JSON.stringify(classification)).not.toContain('secret') }) }) describe('buildReadDirErrorBreadcrumb', () => { - it('captures throw site, error code/name, and path shape', () => { + it('captures throw site, error code/name, and path classification', () => { const breadcrumb = buildReadDirErrorBreadcrumb({ dirPath: '\\\\wsl.localhost\\Ubuntu\\home\\u\\repo', connectionId: undefined, diff --git a/src/main/ipc/readdir-error-diagnostics.ts b/src/main/ipc/readdir-error-diagnostics.ts index dd77dda8837..fc7c54c433f 100644 --- a/src/main/ipc/readdir-error-diagnostics.ts +++ b/src/main/ipc/readdir-error-diagnostics.ts @@ -11,7 +11,7 @@ export type ReadDirThrowSite = 'ssh-provider' | 'authorize' | 'readdir' * even though breadcrumbs are path-redacted downstream, never collecting the * raw path is the safer default. */ -export function describeReadDirPathShape( +export function classifyReadDirPath( dirPath: string, connectionId: string | undefined ): CrashReportBreadcrumbData { @@ -52,6 +52,6 @@ export function buildReadDirErrorBreadcrumb(args: { throwSite: args.throwSite, errorName: args.error instanceof Error ? args.error.name : typeof args.error, ...(errorCode(args.error) ? { errorCode: errorCode(args.error)! } : {}), - ...describeReadDirPathShape(args.dirPath, args.connectionId) + ...classifyReadDirPath(args.dirPath, args.connectionId) } } diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index ef65f2fcb53..c64710b804c 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -48,7 +48,7 @@ import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref' import { getHostedReviewForBranch } from '../source-control/hosted-review' import type { ForgeProviderId } from '../source-control/forge-provider' import { validateGitPushTarget } from '../git/push-target-validation' -import { assertGitPushTargetShape } from '../../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../../shared/git-push-target-validation' import { gitExecFileAsync } from '../git/runner' import type { OrcaRuntimeService, @@ -1277,7 +1277,7 @@ export async function prepareWorktreePushTargetSsh( store?: WorktreePushTargetStore, repoId?: string ): Promise { - assertGitPushTargetShape(target) + assertValidGitPushTarget(target) const execGit: GitRemoteExec = (args, cwd) => provider.exec(args, cwd) const { remoteCreated: _ignoredRemoteCreated, ...sanitizedTarget } = target await provider.exec(['check-ref-format', '--branch', target.branchName], repoPath) diff --git a/src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts b/src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts index 305fa462f60..f109451edb9 100644 --- a/src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts +++ b/src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.ts @@ -63,14 +63,12 @@ function serializedLifecycleBatchFits( fence: Number.MAX_SAFE_INTEGER, ts: Number.MAX_SAFE_INTEGER, settlementId, - mutations: mutations.map(lifecycleMutationRowShape) + mutations: mutations.map(toLifecycleMutationRow) } return Buffer.byteLength(JSON.stringify(row), 'utf8') + 1 <= MAX_JOURNAL_LIFECYCLE_BATCH_BYTES } -function lifecycleMutationRowShape( - mutation: JournalLifecycleMutationInput -): JournalLifecycleMutation { +function toLifecycleMutationRow(mutation: JournalLifecycleMutationInput): JournalLifecycleMutation { const itemId = agentJournalItemKey(mutation.identity) return mutation.kind === 'item' ? { diff --git a/src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts b/src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts index e1e8f68f7bd..677e75edc7d 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts @@ -75,14 +75,14 @@ function item(index: number, sequence: number): AgentJournalRenderItem { } } -/** Every sequence-run shape of `length` items, as run-length compositions. */ -function* runShapes(length: number): Generator { +/** Every run-length composition of `length` items. */ +function* runLengthCompositions(length: number): Generator { if (length === 0) { yield [] return } for (let first = 1; first <= length; first += 1) { - for (const rest of runShapes(length - first)) { + for (const rest of runLengthCompositions(length - first)) { yield [first, ...rest] } } @@ -105,7 +105,7 @@ function buildItems(runs: number[], repeatSequence: boolean): AgentJournalRender it('matches eager grouping at every newest-window limit for every run shape', () => { let cases = 0 for (let length = 0; length <= 7; length += 1) { - for (const runs of runShapes(length)) { + for (const runs of runLengthCompositions(length)) { for (const repeatSequence of [false, true]) { const items = buildItems(runs, repeatSequence) // Every boundary, including 0, each exact group edge, and past the end. @@ -127,7 +127,7 @@ it('matches eager byte bounding at every budget boundary in both directions', () let truncatedCases = 0 let partialCases = 0 for (let length = 1; length <= 6; length += 1) { - for (const runs of runShapes(length)) { + for (const runs of runLengthCompositions(length)) { for (const repeatSequence of [false, true]) { const items = buildItems(runs, repeatSequence) const perItem = historyEntryBytes(items[0]!, submissionBytes) diff --git a/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts b/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts index a57aa5d9ed1..b1acb14eee3 100644 --- a/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts +++ b/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts @@ -3,7 +3,7 @@ import { CODEX_APP_SERVER_NOTIFICATION_METHODS } from '../../codex/codex-app-ser import { CLAUDE_STREAM_JSON_FRAME_KINDS } from './claude-stream-json-frame-schema' import { classifyProviderFrame, - isDeltaShapedProviderFrameKind, + isDeltaProviderFrameKind, PROVIDER_FRAME_CLASSIFICATIONS } from './provider-frame-disposition' import { unhandledProviderFrameJournalItem } from './unhandled-provider-frame' @@ -25,7 +25,7 @@ describe('provider frame classification catalog', () => { const deltaKinds = [ ...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.codex), ...Object.keys(PROVIDER_FRAME_CLASSIFICATIONS.claude) - ].filter(isDeltaShapedProviderFrameKind) + ].filter(isDeltaProviderFrameKind) expect(deltaKinds.length).toBeGreaterThan(0) for (const kind of deltaKinds) { diff --git a/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts b/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts index 548a719ceb1..dea830b1315 100644 --- a/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts +++ b/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts @@ -226,7 +226,7 @@ function itemKind(kind: string): string | null { return kind.startsWith('item:') ? kind.slice('item:'.length) : null } -export function isDeltaShapedProviderFrameKind(kind: string): boolean { +export function isDeltaProviderFrameKind(kind: string): boolean { return notificationKind(kind).toLowerCase().endsWith('delta') } @@ -260,7 +260,7 @@ export function classifyProviderFrame( if (hasProviderError(payload)) { return 'error-surface' } - if (isDeltaShapedProviderFrameKind(kind)) { + if (isDeltaProviderFrameKind(kind)) { return 'stream-into-item' } if (provider === 'claude' && kind === 'message:result') { diff --git a/src/main/observability/redactor.test.ts b/src/main/observability/redactor.test.ts index 0b8ee0f9e61..19324cc08b2 100644 --- a/src/main/observability/redactor.test.ts +++ b/src/main/observability/redactor.test.ts @@ -25,7 +25,7 @@ const SECRETS = { pem: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ\n-----END PRIVATE KEY-----' } -const SHAPES: { label: string; raw: string; tag: string }[] = [ +const PROVIDER_KEY_CASES: { label: string; raw: string; tag: string }[] = [ { label: 'anthropic', raw: SECRETS.anthropic, tag: 'anthropic-key' }, { label: 'openai', raw: SECRETS.openai, tag: 'openai-key' }, { label: 'github', raw: SECRETS.github, tag: 'github-token' }, @@ -37,7 +37,7 @@ const SHAPES: { label: string; raw: string; tag: string }[] = [ ] describe('redactor — provider-key fingerprints', () => { - for (const { label, raw, tag } of SHAPES) { + for (const { label, raw, tag } of PROVIDER_KEY_CASES) { describe(`${label}`, () => { it('redacts when the secret appears as an attribute value', () => { // Bare "" without a labeled-kv keyword nearby — exercises the diff --git a/src/main/persistence/loading-store/persisted-state-redundancy.test.ts b/src/main/persistence/loading-store/persisted-state-redundancy.test.ts index bccef63b39c..9a30b26da38 100644 --- a/src/main/persistence/loading-store/persisted-state-redundancy.test.ts +++ b/src/main/persistence/loading-store/persisted-state-redundancy.test.ts @@ -138,7 +138,7 @@ function writeLegacyFile(dataFile: string): void { /** Inverse of everything this change does, applied to a compact file: what the old serializer * would have written for the same state. */ -function reexpandToLegacyShape(state: PersistedState): PersistedState { +function reexpandToLegacySerialization(state: PersistedState): PersistedState { const expanded = structuredClone(state) for (const map of [expanded.worktreeMeta, expanded.worktreeMetaByIdentity]) { for (const [key, meta] of Object.entries(map ?? {})) { @@ -207,7 +207,7 @@ describe('persisted-state redundancy', () => { // Apples to apples: re-expand the file we just wrote back into the old shape and compare, so // the number is the redundancy alone and not the settings defaults a synthetic fixture lacks. expect(Buffer.byteLength(rewritten)).toBeLessThan( - Buffer.byteLength(JSON.stringify(reexpandToLegacyShape(onDisk))) * 0.6 + Buffer.byteLength(JSON.stringify(reexpandToLegacySerialization(onDisk))) * 0.6 ) // load(save(state)) deep-equals the pre-save state for every field touched. diff --git a/src/main/pi/titlebar-extension-overlay-path.test.ts b/src/main/pi/titlebar-extension-overlay-path.test.ts index db5bfeedbd9..1a4ace7d352 100644 --- a/src/main/pi/titlebar-extension-overlay-path.test.ts +++ b/src/main/pi/titlebar-extension-overlay-path.test.ts @@ -8,7 +8,7 @@ const userDataDir = mkdtempSync(join(tmpdir(), 'orca-pi-overlay-path-userdata-') import { PiTitlebarExtensionService } from './titlebar-extension-service' -const PATH_SHAPED_PTY_ID = [ +const PATH_LIKE_PTY_ID = [ '50c010a2-bc8e-4eb1-8847-5812133ad6df', 'Users', 'dev', @@ -45,7 +45,7 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => { const svc = new PiTitlebarExtensionService() try { - const env = svc.buildPtyEnv(PATH_SHAPED_PTY_ID, piHome, 'pi') + const env = svc.buildPtyEnv(PATH_LIKE_PTY_ID, piHome, 'pi') expect(env.PI_CODING_AGENT_DIR).toBeUndefined() expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome) @@ -61,12 +61,12 @@ describe('PiTitlebarExtensionService legacy overlay paths', () => { }) it('clears legacy raw path-shaped daemon overlays during teardown', () => { - const legacyOverlayDir = legacyOverlayPath('pi', PATH_SHAPED_PTY_ID) + const legacyOverlayDir = legacyOverlayPath('pi', PATH_LIKE_PTY_ID) mkdirSync(legacyOverlayDir, { recursive: true }) writeFileSync(join(legacyOverlayDir, 'stale.txt'), 'stale overlay') const svc = new PiTitlebarExtensionService() - svc.clearPty(PATH_SHAPED_PTY_ID) + svc.clearPty(PATH_LIKE_PTY_ID) expect(existsSync(legacyOverlayDir)).toBe(false) }) diff --git a/src/main/runtime/rpc/terminal-output-frame-chunks-equivalence.test.ts b/src/main/runtime/rpc/terminal-output-frame-chunks-equivalence.test.ts index 233aa3e4bc2..85e01116c3c 100644 --- a/src/main/runtime/rpc/terminal-output-frame-chunks-equivalence.test.ts +++ b/src/main/runtime/rpc/terminal-output-frame-chunks-equivalence.test.ts @@ -132,10 +132,10 @@ function* legacyIterateTerminalOutputFrameChunks( } } -type FrameShape = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' } +type FrameSummary = { base64: string; seq: number | 'undefined'; opcode: number | 'undefined' } -function describeFrames(frames: Iterable): FrameShape[] { - const out: FrameShape[] = [] +function describeFrames(frames: Iterable): FrameSummary[] { + const out: FrameSummary[] = [] for (const frame of frames) { out.push({ base64: Buffer.from(frame.bytes).toString('base64'), @@ -170,10 +170,10 @@ const SURROGATE_EDGES = [ '\udfff\udc00' ] -// Meta shapes exercised against every fixture: no meta, seq-preserved (rawLength === +// Meta variants exercised against every fixture: no meta, seq-preserved (rawLength === // data.length), the delayed-final-seq path (rawLength !== data.length -> OutputSpan), // transformed, and cwd-only. -function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] { +function metaVariantsFor(data: string): { label: string; meta: TerminalOutputMeta | undefined }[] { return [ { label: 'no-meta', meta: undefined }, { label: 'seq-only', meta: { seq: 5_000_000 } }, @@ -187,8 +187,8 @@ function metaShapesFor(data: string): { label: string; meta: TerminalOutputMeta } function sweepAll(data: string, label: string): void { - for (const shape of metaShapesFor(data)) { - expectEquivalent(data, shape.meta, `${label} [${shape.label}]`) + for (const variant of metaVariantsFor(data)) { + expectEquivalent(data, variant.meta, `${label} [${variant.label}]`) } } diff --git a/src/main/runtime/terminal-wait-tail-state.ts b/src/main/runtime/terminal-wait-tail-state.ts index 712b301a961..8d0ce1f2e88 100644 --- a/src/main/runtime/terminal-wait-tail-state.ts +++ b/src/main/runtime/terminal-wait-tail-state.ts @@ -32,15 +32,15 @@ export function computeTerminalTailWaitState( partialLine: string, preview: string ): TerminalTailWaitState { - const tailShape = inspectTerminalWaitTail(lines, partialLine) - if (!tailShape.fromTail) { + const tailInspection = inspectTerminalWaitTail(lines, partialLine) + if (!tailInspection.fromTail) { return { waitText: preview, signal: findActionableTerminalWaitBlockedSignal(preview.toLowerCase()), fromTail: false } } - if (!tailShape.mayContainBlockedSignal) { + if (!tailInspection.mayContainBlockedSignal) { // Why: reads waitText only when a signal exists; avoid retaining a rebuilt 256 KiB string in the common case. return { waitText: '', signal: null, fromTail: true } } diff --git a/src/main/skills/skill-bundle-artifacts.ts b/src/main/skills/skill-bundle-artifacts.ts index 08e33a0eeba..f8c19b58185 100644 --- a/src/main/skills/skill-bundle-artifacts.ts +++ b/src/main/skills/skill-bundle-artifacts.ts @@ -18,7 +18,7 @@ export type SkillBundleArtifacts = { } const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/) -const snapshotShape = { +const snapshotFields = { releaseRevision: z.number().int().positive(), packageDigest: sha256Schema, gitTreeSha: z.string().regex(/^[a-f0-9]{40}$/), @@ -38,7 +38,7 @@ const snapshotShape = { ) .min(1) } -const knownSnapshotSchema = z.object(snapshotShape).strict() +const knownSnapshotSchema = z.object(snapshotFields).strict() const manifestSchema = z .object({ schemaVersion: z.literal(2), @@ -47,7 +47,7 @@ const manifestSchema = z .object({ name: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/), sourcePath: z.string().min(1), - ...snapshotShape + ...snapshotFields }) .strict() ) diff --git a/src/main/ssh/ssh-host-key-store.test.ts b/src/main/ssh/ssh-host-key-store.test.ts index 4e18df099e4..835601aeac2 100644 --- a/src/main/ssh/ssh-host-key-store.test.ts +++ b/src/main/ssh/ssh-host-key-store.test.ts @@ -303,7 +303,7 @@ describe('a host key store written by a newer version', () => { const storeFile = join(dir, 'ssh-host-keys.json') const future = JSON.stringify({ version: 99, - hostKeys: [{ shape: 'we do not understand' }] + hostKeys: [{ unrecognized: 'we do not understand' }] }) await writeFile(storeFile, future, 'utf-8') diff --git a/src/main/ssh/ssh-orphan-sweep-pane-state-verdicts.test.ts b/src/main/ssh/ssh-orphan-sweep-pane-state-verdicts.test.ts index 560d18b8b62..9bb42a2cb27 100644 --- a/src/main/ssh/ssh-orphan-sweep-pane-state-verdicts.test.ts +++ b/src/main/ssh/ssh-orphan-sweep-pane-state-verdicts.test.ts @@ -167,9 +167,9 @@ describe('what the host publishes about a pane, read by the sweep', () => { it('records that a backgrounded and a suspended shell are indistinguishable at tpgid/pgid', () => { // The premise of the whole file. If this ever fails, the fixtures drifted and every verdict // below is testing something other than the defect. Pids differ between captures, so the - // comparison is of the shell row's shape: who its parent is, whether it leads its own process - // group, whether that group owns the terminal, and its state flags. - const shellShape = (capture: { rootPid: number; table: readonly string[] }): string => { + // comparison is of the shell row's signature: who its parent is, whether it leads its own + // process group, whether that group owns the terminal, and its state flags. + const shellRowSignature = (capture: { rootPid: number; table: readonly string[] }): string => { const row = parseStrictProcessTableRows(capture.table.join('\n')).find( (candidate) => candidate.pid === capture.rootPid )! @@ -181,19 +181,21 @@ describe('what the host publishes about a pane, read by the sweep', () => { ].join(' ') } - expect(shellShape(CAPTURES.idle)).toBe('ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+') - expect(shellShape(CAPTURES.background)).toBe(shellShape(CAPTURES.idle)) - expect(shellShape(CAPTURES.ctrlz)).toBe(shellShape(CAPTURES.idle)) - expect(shellShape(CAPTURES.foreground)).not.toBe(shellShape(CAPTURES.idle)) + expect(shellRowSignature(CAPTURES.idle)).toBe( + 'ppid=1 leadsOwnGroup=true ownsTerminal=true stat=Ss+' + ) + expect(shellRowSignature(CAPTURES.background)).toBe(shellRowSignature(CAPTURES.idle)) + expect(shellRowSignature(CAPTURES.ctrlz)).toBe(shellRowSignature(CAPTURES.idle)) + expect(shellRowSignature(CAPTURES.foreground)).not.toBe(shellRowSignature(CAPTURES.idle)) // Same premise for the `set +m` captures, minus `ppid`: their harness keeps its parent alive - // rather than reparenting the shell to init, and the ppid is the one field of the shape the - // predicate never reads. - const paneShape = (capture: { rootPid: number; table: readonly string[] }): string => - shellShape(capture).split(' ').slice(1).join(' ') - expect(paneShape(CAPTURES.setMinusMBackground)).toBe(paneShape(CAPTURES.idle)) - expect(paneShape(CAPTURES.nottyGroupMember)).toBe(paneShape(CAPTURES.idle)) - expect(paneShape(CAPTURES.doubleForkedGroupMember)).toBe(paneShape(CAPTURES.idle)) + // rather than reparenting the shell to init, and the ppid is the one field of the signature + // the predicate never reads. + const paneRowSignature = (capture: { rootPid: number; table: readonly string[] }): string => + shellRowSignature(capture).split(' ').slice(1).join(' ') + expect(paneRowSignature(CAPTURES.setMinusMBackground)).toBe(paneRowSignature(CAPTURES.idle)) + expect(paneRowSignature(CAPTURES.nottyGroupMember)).toBe(paneRowSignature(CAPTURES.idle)) + expect(paneRowSignature(CAPTURES.doubleForkedGroupMember)).toBe(paneRowSignature(CAPTURES.idle)) }) it('sweeps an idle shell', async () => { diff --git a/src/main/ssh/ssh-relay-deploy-helpers.test.ts b/src/main/ssh/ssh-relay-deploy-helpers.test.ts index 1d73e188d19..adb14925636 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.test.ts +++ b/src/main/ssh/ssh-relay-deploy-helpers.test.ts @@ -232,9 +232,9 @@ describe('waitForSentinel', () => { it.each(['ssh2 channel', 'system-SSH child stdio'])( 'forwards write(false), callback settlement, and drain for a %s', - async (shape) => { + async (channelKind) => { const channel = createMockChannel() - if (shape.startsWith('system')) { + if (channelKind.startsWith('system')) { Object.assign(channel, { _process: new EventEmitter() }) } const callback = vi.fn() diff --git a/src/main/ssh/ssh-remote-platform-detection.ts b/src/main/ssh/ssh-remote-platform-detection.ts index 6fd0f87c767..499e088e49c 100644 --- a/src/main/ssh/ssh-remote-platform-detection.ts +++ b/src/main/ssh/ssh-remote-platform-detection.ts @@ -38,7 +38,7 @@ export async function detectRemoteHostPlatform( } // Why: only the PowerShell probe can settle a uname the parser cannot map // (Cygwin, say), so a refused or timed-out channel leaves it unsettled. - const windowsProbeNeverRan = windows.kind === 'failed' && isTransportShapedError(windows.error) + const windowsProbeNeverRan = windows.kind === 'failed' && isTransportFailure(windows.error) if ((uname.kind === 'unsupported' && !windowsProbeNeverRan) || windows.kind === 'unsupported') { const reported = uname.kind === 'unsupported' ? uname.uname : probeUname(windows) console.warn(`[ssh-relay] Remote reported an unsupported platform: ${reported}`) @@ -66,7 +66,7 @@ function undetectedPlatformError( windows: PlatformProbeOutcome ): Error { for (const outcome of [uname, windows]) { - if (outcome.kind === 'failed' && isTransportShapedError(outcome.error)) { + if (outcome.kind === 'failed' && isTransportFailure(outcome.error)) { return wrapProbeError(outcome.error) } } @@ -84,7 +84,7 @@ function undetectedPlatformError( // Why: a refused or timed-out channel explains the failure better than the // other probe's mundane non-zero exit (e.g. "sh: not found" on Windows). -function isTransportShapedError(error: unknown): boolean { +function isTransportFailure(error: unknown): boolean { return ( isSshSessionLimitError(error) || isUnconfirmedSshCommandTermination(error) || diff --git a/src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts b/src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts index 6d20998b625..ac6577b2d96 100644 --- a/src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts +++ b/src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts @@ -327,7 +327,7 @@ describe('generateCommitMessageFromContext', () => { '401: {"message":"slot 1:/Users/name/alt failed"}', 'Pi CLI command failed with code 1: 401: {"message":"slot 1:[path] failed"}' ] - ])('redacts a %s in provider bodies', async (_shape, stderr, expected) => { + ])('redacts a %s in provider bodies', async (_variant, stderr, expected) => { const result = await generateCommitMessageFromContext( { branch: 'main', diff --git a/src/main/wsl-unc-delete-symlink-repro.test.ts b/src/main/wsl-unc-delete-symlink-repro.test.ts index ace95afe933..a594b444e75 100644 --- a/src/main/wsl-unc-delete-symlink-repro.test.ts +++ b/src/main/wsl-unc-delete-symlink-repro.test.ts @@ -55,7 +55,7 @@ describe('WSL vault intermediate-symlink reproduction', () => { it.each([ ['file-shaped', `${FIXTURE_ROOT}/linked-project/session.json`, false], ['directory-shaped', `${FIXTURE_ROOT}/linked-project/session`, true] - ])('rejects a %s target before removal', async (_shape, target, recursive) => { + ])('rejects a %s target before removal', async (_targetKind, target, recursive) => { const options = { recursive, approvedRoots: [unc(FIXTURE_ROOT)] } let rejection: unknown diff --git a/src/main/wsl-unc-delete.wsl.test.ts b/src/main/wsl-unc-delete.wsl.test.ts index 36175b62626..25036380b00 100644 --- a/src/main/wsl-unc-delete.wsl.test.ts +++ b/src/main/wsl-unc-delete.wsl.test.ts @@ -46,7 +46,7 @@ describe.skipIf(!runRealWsl)('WSL contained delete integration', () => { it.each([ ['file-shaped', 'file-link/session.json', false], ['directory-shaped', 'dir-link/session', true] - ])('rejects a %s escape and preserves all outside entries', async (_shape, path, recursive) => { + ])('rejects a %s escape and preserves all outside entries', async (_label, path, recursive) => { const vaultRoot = `${fixtureRoot}/vault` await expect( diff --git a/src/main/wsl.test.ts b/src/main/wsl.test.ts index 6ef8adbbb61..55327c465e5 100644 --- a/src/main/wsl.test.ts +++ b/src/main/wsl.test.ts @@ -547,10 +547,10 @@ describe('WSL availability cache', () => { it.each([ ['wsl.exe reports WSL unusable', { status: 1 }], ['wsl.exe is not installed', { code: 'ENOENT' }] - ])('holds a definitive failure far longer than a timeout when %s', (_label, errorShape) => { + ])('holds a definitive failure far longer than a timeout when %s', (_label, errorFields) => { vi.useFakeTimers() execFileSyncMock.mockImplementationOnce(() => { - throw Object.assign(new Error('definitive failure'), errorShape) + throw Object.assign(new Error('definitive failure'), errorFields) }) execFileSyncMock.mockReturnValueOnce('') @@ -621,10 +621,10 @@ describe('WSL availability cache', () => { it.each([ ['a definitive failure', { status: 1 }], ['a timeout', { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' }] - ])('re-probes availability once a distro list succeeds after %s', (_label, errorShape) => { + ])('re-probes availability once a distro list succeeds after %s', (_label, errorFields) => { vi.useFakeTimers() execFileSyncMock.mockImplementationOnce(() => { - throw Object.assign(new Error('probe failed'), errorShape) + throw Object.assign(new Error('probe failed'), errorFields) }) try { diff --git a/src/relay/git-exec-validator.ts b/src/relay/git-exec-validator.ts index 82cc72d1d2e..9f9b5866ebc 100644 --- a/src/relay/git-exec-validator.ts +++ b/src/relay/git-exec-validator.ts @@ -96,7 +96,7 @@ const DIFF_ALLOWED_FLAGS = new Set([ // only those two exact shapes, held to the same remote-name and URL rules the // relay already enforces on every pushTarget-carrying RPC. Everything else -- // set-url, rename, prune, flags before the action -- stays blocked. -function isAllowedRemoteWriteShape(args: string[]): boolean { +function isAllowedRemoteWriteInvocation(args: string[]): boolean { if (args[1] === 'add') { return args.length === 4 && isSafeGitRemoteName(args[2]) && isSafePushTargetRemoteUrl(args[3]) } @@ -197,7 +197,7 @@ export function validateGitExecArgs(args: string[]): void { if ( remoteSubcmd && REMOTE_WRITE_SUBCOMMANDS.has(remoteSubcmd) && - !isAllowedRemoteWriteShape(args) + !isAllowedRemoteWriteInvocation(args) ) { throw new Error('Destructive git remote operations are not allowed via exec') } diff --git a/src/relay/git-handler-branch-diff-equivalence.test.ts b/src/relay/git-handler-branch-diff-equivalence.test.ts index d33886b4f5a..ff399d92a7c 100644 --- a/src/relay/git-handler-branch-diff-equivalence.test.ts +++ b/src/relay/git-handler-branch-diff-equivalence.test.ts @@ -128,10 +128,10 @@ describe('pinned and legacy branch diff equivalence against real Git', () => { for (const entry of compare.entries) { // Exactly what the renderer sends: paths from the compare entry list, // OIDs from the compare summary that produced that same list. - const callerShape = { filePath: entry.path, oldPath: entry.oldPath } - const legacy = await branchDiff(callerShape) + const callerParams = { filePath: entry.path, oldPath: entry.oldPath } + const legacy = await branchDiff(callerParams) const pinned = await branchDiff({ - ...callerShape, + ...callerParams, baseRef: compare.summary.mergeBase, headOid: compare.summary.headOid }) diff --git a/src/relay/git-handler-comparison-operations.ts b/src/relay/git-handler-comparison-operations.ts index e5ebe65365b..f4c1e55fa5e 100644 --- a/src/relay/git-handler-comparison-operations.ts +++ b/src/relay/git-handler-comparison-operations.ts @@ -5,7 +5,7 @@ import { parseBranchDiff } from './git-handler-utils' import { parseNumstat } from '../shared/git-uncommitted-line-stats' import { isNoUpstreamError, normalizeGitErrorMessage } from '../shared/git-remote-error' import { upstreamOnlyCommitsArePatchEquivalent } from '../shared/git-upstream-status' -import { assertGitPushTargetShape } from '../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../shared/git-push-target-validation' import { getPublishTargetStatus, type GitCommandRunner } from '../shared/git-publish-target-status' import type { GitPushTarget } from '../shared/worktree/types' import { getEffectiveGitUpstreamStatus } from '../shared/git-effective-upstream' @@ -46,7 +46,7 @@ export class GitHandlerComparisonOperations extends GitHandlerOperationContext { try { if (params.pushTarget !== undefined) { - assertGitPushTargetShape(params.pushTarget) + assertValidGitPushTarget(params.pushTarget) const pushTarget = params.pushTarget as GitPushTarget await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath) return await getPublishTargetStatus( diff --git a/src/relay/git-handler-fetch-operations.ts b/src/relay/git-handler-fetch-operations.ts index cd3a86fcb63..fd13cdc07c2 100644 --- a/src/relay/git-handler-fetch-operations.ts +++ b/src/relay/git-handler-fetch-operations.ts @@ -1,6 +1,6 @@ import type { RequestContext } from './dispatcher' import { GitHandlerOperationContext } from './git-handler-operation-context' -import { assertGitPushTargetShape } from '../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../shared/git-push-target-validation' import type { GitPushTarget } from '../shared/worktree/types' import { normalizeGitErrorMessage, isExecKilledError } from '../shared/git-remote-error' import { syncForkDefaultBranch, validateGitForkSyncExpectedUpstream } from '../shared/git-fork-sync' @@ -21,7 +21,7 @@ export class GitHandlerFetchOperations extends GitHandlerOperationContext { try { try { if (params.pushTarget !== undefined) { - assertGitPushTargetShape(params.pushTarget) + assertValidGitPushTarget(params.pushTarget) const pushTarget = params.pushTarget as GitPushTarget await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath) await this.git(['fetch', '--prune', pushTarget.remoteName], worktreePath) diff --git a/src/relay/git-handler-push-target.ts b/src/relay/git-handler-push-target.ts index 6663b5b3ad3..57a39e632d3 100644 --- a/src/relay/git-handler-push-target.ts +++ b/src/relay/git-handler-push-target.ts @@ -1,4 +1,4 @@ -import { assertGitPushTargetShape } from '../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../shared/git-push-target-validation' import { resolveConfiguredGitPushTarget, type ResolvedGitPushTarget @@ -15,7 +15,7 @@ export async function resolveRelayPushTarget( if (pushTarget === undefined) { return resolveConfiguredGitPushTarget((args) => git(args, worktreePath)) } - assertGitPushTargetShape(pushTarget) + assertValidGitPushTarget(pushTarget) const explicitTarget: GitPushTarget = pushTarget // Why here and not in the shared resolver: an explicit target arrives over the wire, // so the host re-validates its shape and asks Git to vet the branch name itself. diff --git a/src/relay/git-handler-sync-operations.ts b/src/relay/git-handler-sync-operations.ts index 262517b33cc..9c0922c34df 100644 --- a/src/relay/git-handler-sync-operations.ts +++ b/src/relay/git-handler-sync-operations.ts @@ -3,7 +3,7 @@ import type { RequestContext } from './dispatcher' import { GitHandlerOperationContext } from './git-handler-operation-context' import { resolveRelayPushTarget } from './git-handler-push-target' import { normalizeGitErrorMessage, runPullWithDivergenceFallback } from '../shared/git-remote-error' -import { assertGitPushTargetShape } from '../shared/git-push-target-validation' +import { assertValidGitPushTarget } from '../shared/git-push-target-validation' import type { GitCommandRunner } from '../shared/git-publish-target-status' import type { GitPushTarget } from '../shared/worktree/types' import { resolveEffectiveGitUpstream } from '../shared/git-effective-upstream' @@ -63,7 +63,7 @@ export class GitHandlerSyncOperations extends GitHandlerOperationContext { const worktreePath = params.worktreePath as string const runPull = async (effectiveArgs: string[]): Promise => { if (params.pushTarget !== undefined) { - assertGitPushTargetShape(params.pushTarget) + assertValidGitPushTarget(params.pushTarget) const pushTarget = params.pushTarget as GitPushTarget await this.git(['check-ref-format', '--branch', pushTarget.branchName], worktreePath) await this.git( diff --git a/src/renderer/src/components/editor/rich-markdown-html-superscript-link.ts b/src/renderer/src/components/editor/rich-markdown-html-superscript-link.ts index a60a6e9c777..6d8c2b44f15 100644 --- a/src/renderer/src/components/editor/rich-markdown-html-superscript-link.ts +++ b/src/renderer/src/components/editor/rich-markdown-html-superscript-link.ts @@ -149,7 +149,7 @@ function parseStructuredPayload(value: string): HtmlSuperscriptLinkSource | null } catch { return null } - if (!isCitationShape(candidate)) { + if (!isCitationSource(candidate)) { return null } const parsed = parseHtmlSuperscriptLinkSource(candidate.source) @@ -197,7 +197,7 @@ function hasOnlyAttributes(element: Element, allowed: string[]): boolean { return Array.from(element.attributes).every((attribute) => allowedSet.has(attribute.name)) } -function isCitationShape(value: unknown): value is HtmlSuperscriptLinkSource { +function isCitationSource(value: unknown): value is HtmlSuperscriptLinkSource { if (!value || typeof value !== 'object') { return false } diff --git a/src/renderer/src/components/repo/repo-icon.tsx b/src/renderer/src/components/repo/repo-icon.tsx index 9e9ad0c24ac..b778ead4426 100644 --- a/src/renderer/src/components/repo/repo-icon.tsx +++ b/src/renderer/src/components/repo/repo-icon.tsx @@ -16,6 +16,7 @@ import { Palette, Rocket, Server, + // `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json. Shapes, Sparkles, SquareTerminal, diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/discard-confirmation.ts b/src/renderer/src/components/right-sidebar/source-control/commit/discard-confirmation.ts index e9f5a65788e..1a6121b535b 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/discard-confirmation.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/discard-confirmation.ts @@ -13,9 +13,7 @@ export type DiscardConfirmationCopy = { * Untracked and newly-added paths have no HEAD version to restore, so Orca's discard removes the * working-tree file. Every surface that names the operation must say "delete" for these. */ -export function isDeleteShapedDiscardEntry( - entry: Pick -): boolean { +export function discardDeletesEntryFile(entry: Pick): boolean { return entry.area === 'untracked' || entry.status === 'untracked' || entry.status === 'added' } @@ -24,7 +22,7 @@ export function getDiscardEntryConfirmationCopy( ): DiscardConfirmationCopy { const name = basename(entry.path) - if (isDeleteShapedDiscardEntry(entry)) { + if (discardDeletesEntryFile(entry)) { return { title: translate( 'auto.components.right.sidebar.source.control.discard.confirmation.96c772bee9', diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts index 92f69010c9f..340b6e3a7e5 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts @@ -61,7 +61,7 @@ describe('showSourceControlEntryFailureToast', () => { it('says "delete" for an entry whose discard removes the file rather than restoring it', () => { // Why: untracked and added paths have no HEAD version, so the row button and the confirmation // dialog both say "delete" — the failure must not contradict the verb the user pressed. - show({ operation: 'discard', deleteShaped: true }) + show({ operation: 'discard', deletesFile: true }) expect(lastToast().title).toBe('Failed to delete “src/app.ts”') }) diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.ts b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.ts index 13ab65c6fb8..9668ee1e16a 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.ts @@ -26,7 +26,7 @@ export function dismissSourceControlEntryFailureToast(worktreeId: string | null) function entryFailureTitle( operation: SourceControlEntryOperation, filePath: string, - deleteShaped: boolean + deletesFile: boolean ): string { switch (operation) { case 'stage': @@ -42,7 +42,7 @@ function entryFailureTitle( { value0: filePath } ) case 'discard': - return deleteShaped + return deletesFile ? translate( 'auto.components.right.sidebar.SourceControl.entryDeleteFailed', 'Failed to delete “{{value0}}”', @@ -67,7 +67,7 @@ function entryFailureTitle( export function showSourceControlEntryFailureToast({ operation, filePath, - deleteShaped = false, + deletesFile = false, error, worktreeId, worktreeName, @@ -76,7 +76,7 @@ export function showSourceControlEntryFailureToast({ operation: SourceControlEntryOperation filePath: string /** True when this discard deletes the file rather than restoring it — see `discard-confirmation`. */ - deleteShaped?: boolean + deletesFile?: boolean error: unknown /** The worktree the failed attempt ran against. */ worktreeId: string | null @@ -85,7 +85,7 @@ export function showSourceControlEntryFailureToast({ onRetry?: () => void }): void { const isActiveWorktree = useAppStore.getState().activeWorktreeId === worktreeId - const title = entryFailureTitle(operation, filePath, deleteShaped) + const title = entryFailureTitle(operation, filePath, deletesFile) const offerRetry = Boolean(onRetry) && isActiveWorktree entryFailureSlotOwner = { worktreeId } toast.error( diff --git a/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts b/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts index f158c971a23..ae1c47398c5 100644 --- a/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts +++ b/src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts @@ -10,7 +10,7 @@ import { runDiscardAllForArea, type DiscardAllArea } from './discard-all-sequence' -import { isDeleteShapedDiscardEntry } from './discard-confirmation' +import { discardDeletesEntryFile } from './discard-confirmation' import { readIpcErrorMessage } from '@/lib/ipc-error' import { dismissSourceControlEntryFailureToast, @@ -62,7 +62,7 @@ export function useSourceControlDiscardConfirmation({ showSourceControlEntryFailureToast({ operation: 'discard', filePath: entry.path, - deleteShaped: isDeleteShapedDiscardEntry(entry), + deletesFile: discardDeletesEntryFile(entry), error, worktreeId: activeWorktreeId, worktreeName: worktreePath ? basename(worktreePath) : null diff --git a/src/renderer/src/components/shared/useDaemonActions.tsx b/src/renderer/src/components/shared/useDaemonActions.tsx index 8c386b5ef13..93c4477cf66 100644 --- a/src/renderer/src/components/shared/useDaemonActions.tsx +++ b/src/renderer/src/components/shared/useDaemonActions.tsx @@ -223,14 +223,14 @@ export function useDaemonActions(callbacks?: DaemonActionCallbacks): DaemonActio } } -type CopyShape = { +type DaemonActionCopy = { title: string description: React.ReactNode confirmLabel: string busyLabel: string } -function getCopy(kind: DaemonActionKind): CopyShape { +function getCopy(kind: DaemonActionKind): DaemonActionCopy { if (kind === 'restart') { return { title: translate( diff --git a/src/renderer/src/components/sidebar/comment-markdown-native-chat-file-links.ts b/src/renderer/src/components/sidebar/comment-markdown-native-chat-file-links.ts index ee81b859dcc..47ee65b2a72 100644 --- a/src/renderer/src/components/sidebar/comment-markdown-native-chat-file-links.ts +++ b/src/renderer/src/components/sidebar/comment-markdown-native-chat-file-links.ts @@ -164,11 +164,11 @@ function exactFileLink(value: string, allowSpacedRelative: boolean): ParsedTermi if (!parsed) { return null } - const hasPathShape = + const looksLikePath = ROOTED_PATH_PREFIX_PATTERN.test(parsed.pathText) || /[\\/]/.test(parsed.pathText) || /\.[\p{L}][\p{L}\p{N}\p{M}_+-]*$/u.test(parsed.pathText) - if (!hasPathShape) { + if (!looksLikePath) { return null } const explicitLink = { diff --git a/src/renderer/src/components/sidebar/worktree-list/rows/repo-header-project-actions.tsx b/src/renderer/src/components/sidebar/worktree-list/rows/repo-header-project-actions.tsx index ba4751b597c..cfa97732a53 100644 --- a/src/renderer/src/components/sidebar/worktree-list/rows/repo-header-project-actions.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/rows/repo-header-project-actions.tsx @@ -6,6 +6,7 @@ import { FolderInput, FolderTree, Plus, + // `Shapes` is lucide-react's own export name; exempted in config/oxlint-anti-slop.json. Shapes, SlidersHorizontal, Trash2 diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts index 9ac752b2b31..65c7a90e72f 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts @@ -31,7 +31,7 @@ import { REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS } from './remote-runtime-pty-re const ELECTRON_IPC_PREFIX = "Error invoking remote method 'runtimeEnvironments:call': " /** A rejection exactly as the renderer sees it after Electron IPC strips custom props. */ -function electronIpcShapedRejection(errorName: string, message: string): Error { +function electronIpcRejection(errorName: string, message: string): Error { return new Error(`${ELECTRON_IPC_PREFIX}${errorName}: ${message}`) } @@ -192,7 +192,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () = const { isRecoverableRemoteRuntimeConnectionError, toRemoteRuntimeClientErrorLike } = await import('../../../../shared/remote-runtime-client-error-classification') const rendererSide = toRemoteRuntimeClientErrorLike( - electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT) + electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT) ) // Electron IPC stripped the code; the fragment list still catches this one. expect(rendererSide.code).toBeUndefined() @@ -201,7 +201,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () = // per-selector RPC queue saturated by 15s-timeout calls) is classified // fatal even though its own code says "retry later". const overload = toRemoteRuntimeClientErrorLike( - electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW) + electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW) ) expect(overload.code).toBeUndefined() // DESIRED: transient capacity pressure during an outage is recoverable, @@ -220,7 +220,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () = runtimeCall.mockImplementation(async (request: { method: string; params?: unknown }) => { if (request.method === 'terminal.send') { sendRejections += 1 - throw electronIpcShapedRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW) + throw electronIpcRejection('RuntimeRpcCallQueueOverloadError', QUEUE_OVERLOAD_RAW) } return healthyImpl(request) }) @@ -294,7 +294,7 @@ describe('remote runtime outage: toast flood and stuck reconnect (issue3)', () = if (request.method === 'terminal.resolvePane') { throw Object.assign(new Error(fatalMessage), { code: 'unauthorized' }) } - throw electronIpcShapedRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT) + throw electronIpcRejection('RemoteRuntimeClientError', TIMEOUT_WITH_TAILSCALE_HINT) }) subscriptionCallbacks?.onClose?.() await vi.waitFor(() => expect(onError).toHaveBeenCalled()) diff --git a/src/renderer/src/components/terminal-search-decoration-leak.test.ts b/src/renderer/src/components/terminal-search-decoration-leak.test.ts index 07c4ba998e5..b508d6d6c00 100644 --- a/src/renderer/src/components/terminal-search-decoration-leak.test.ts +++ b/src/renderer/src/components/terminal-search-decoration-leak.test.ts @@ -107,7 +107,7 @@ function openTerminalWithSearch(): SearchHarness { * showed up for some of them, so the regression has to sweep rather than pin * one lucky case. */ -const CONTENT_SHAPES: readonly (readonly [string, string])[] = [ +const CONTENT_LAYOUTS: readonly (readonly [string, string])[] = [ ['matches on two lines', 'needle one\r\nneedle two\r\n'], ['matches on three lines', 'needle one\r\nneedle two\r\nneedle three\r\n'], ['matches on four lines', 'needle a\r\nneedle b\r\nneedle c\r\nneedle d\r\n'], @@ -128,7 +128,7 @@ describe('terminal search decoration cleanup (STA-2707)', () => { document.body.replaceChildren() }) - it.each(CONTENT_SHAPES)( + it.each(CONTENT_LAYOUTS)( 'leaves no highlighted cells after closing search (%s)', async (_name, content) => { // Sweeping the match-navigation count matters: which decoration is the diff --git a/src/renderer/src/hooks/useEditorExternalWatch.ts b/src/renderer/src/hooks/useEditorExternalWatch.ts index 20f02e719d5..c9b7db652f8 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.ts @@ -7,7 +7,7 @@ import { getEditorExternalWatchTargetKey, selectEditorExternalWatchTargets, type EditorExternalWatchTarget, - type EditorExternalWatchTargetState as EditorExternalWatchTargetStateShape + type EditorExternalWatchTargetState } from './editor-external-watch-targets' import { buildEditorExternalWatchEventHandler, @@ -15,7 +15,7 @@ import { } from './editor-external-watch-event-reconciliation' import { verifyLatchedEditorMoveDestinations } from './editor-external-watch-disk-verification' -export type EditorExternalWatchTargetState = EditorExternalWatchTargetStateShape +export type { EditorExternalWatchTargetState } function warnExternalWatchFailure(target: EditorExternalWatchTarget, err: unknown): void { console.warn('[filesystem-watch] failed to watch worktree', { diff --git a/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts b/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts index 6c543db3594..dc4fa218fa9 100644 --- a/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts +++ b/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts @@ -11,7 +11,7 @@ import type { TuiAgent } from '../../../shared/tui-agent' const AGENTS: readonly TuiAgent[] = ['claude', 'codex'] const SLOT_COUNT = 7 -const SHAPE_COUNT = 3 ** SLOT_COUNT * 4 * 2 +const COMBINATION_COUNT = 3 ** SLOT_COUNT * 4 * 2 const TITLES: readonly string[] = ['', 'zsh', 'Task - claude', 'Task - codex'] type Breakdown = Record< @@ -124,7 +124,7 @@ describe('renderer ladder decision table', () => { const proofFree = runDecisionTable(false) const freshProof = runDecisionTable(true) const result = { - shapes: SHAPE_COUNT, + combinations: COMBINATION_COUNT, proofOmitted: proofFree, freshProof, flippedByAddingProof: proofFree.flipped diff --git a/src/renderer/src/lib/typing-latency/diagnostic-summary.ts b/src/renderer/src/lib/typing-latency/diagnostic-summary.ts index 5cb96d684d9..a5a21fcef46 100644 --- a/src/renderer/src/lib/typing-latency/diagnostic-summary.ts +++ b/src/renderer/src/lib/typing-latency/diagnostic-summary.ts @@ -131,7 +131,7 @@ export type FocusedPaneCensus = { type CountableRecord = Record | null | undefined -export type TypingCensusStoreShape = { +export type TypingCensusStoreView = { worktreesByRepo?: Record | null tabsByWorktree?: Record | null unifiedTabsByWorktree?: Record | null @@ -231,7 +231,7 @@ function collectWorktrees( } export function summarizeTypingScaleCensus(input: { - state: TypingCensusStoreShape | null + state: TypingCensusStoreView | null appVersion: string | null livePaneCount: number | null instrumentedPaneCount: number diff --git a/src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts b/src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts index 2191a083e2a..eb868674b64 100644 --- a/src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts +++ b/src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts @@ -67,11 +67,11 @@ function buildCase(random: () => number): { worktreesByRepo: Record probeIds: string[] } { - const shape = random() - if (shape < 0.05) { + const roll = random() + if (roll < 0.05) { return { detectedWorktreesByRepo: undefined, worktreesByRepo: {}, probeIds: ['repo-0::absent'] } } - if (shape < 0.1) { + if (roll < 0.1) { return { detectedWorktreesByRepo: {}, worktreesByRepo: {}, probeIds: ['repo-0::absent'] } } const repoCount = 1 + Math.floor(random() * 6) diff --git a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts b/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts index 3ae6769bb15..2ac4a8bbfd7 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts @@ -65,19 +65,19 @@ function makeEntry(index: number, overrides: Record = {}): neve } describe('mobile agent-status projection equivalence', () => { - it('matches the whole-array serialization across shapes and cache reuse', () => { + it('matches the whole-array serialization across status maps and cache reuse', () => { resetRuntimeMobileAgentStatusProjectionCacheForTests() - const shapes: AppState['agentStatusByPaneKey'][] = [] - shapes.push({}) - shapes.push({ 'tab-0:leaf-0': makeEntry(0) }) - shapes.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) }) + const statusMaps: AppState['agentStatusByPaneKey'][] = [] + statusMaps.push({}) + statusMaps.push({ 'tab-0:leaf-0': makeEntry(0) }) + statusMaps.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) }) const many: AppState['agentStatusByPaneKey'] = {} for (let index = 0; index < 12; index += 1) { many[`tab-${index}:leaf-0`] = makeEntry(index) } - shapes.push(many) + statusMaps.push(many) // Optional fields absent entirely, which the ?? null fallbacks must cover. - shapes.push({ + statusMaps.push({ 'tab-9:leaf-1': makeEntry(9, { agentType: undefined, terminalTitle: undefined, @@ -89,17 +89,17 @@ describe('mobile agent-status projection equivalence', () => { }) }) // Keys deliberately out of insertion order to pin the sort. - shapes.push({ + statusMaps.push({ 'tab-z:leaf-0': makeEntry(2), 'tab-a:leaf-0': makeEntry(1), 'tab-m:leaf-0': makeEntry(3) }) - for (const [index, shape] of shapes.entries()) { + for (const [index, statusMap] of statusMaps.entries()) { expect({ index, - projection: buildRuntimeMobileAgentStatusProjectionForTests(shape) - }).toEqual({ index, projection: referenceProjection(shape) }) + projection: buildRuntimeMobileAgentStatusProjectionForTests(statusMap) + }).toEqual({ index, projection: referenceProjection(statusMap) }) } // Now exercise the cache: replace one entry the way setAgentStatus does and diff --git a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts index ed1991e60d2..1a163ada0a8 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts @@ -256,7 +256,7 @@ describe('editor draft projection on the typing path', () => { }) it('matches the uncached projection byte for byte across draft shapes', () => { - const shapes: Record[] = [ + const draftCases: Record[] = [ {}, { 'file-a': '' }, { 'file-a': 'hello' }, @@ -267,10 +267,10 @@ describe('editor draft projection on the typing path', () => { { 'file-a': 'hello', 'file-b': 'world', 'file-c': 'third' }, { 'file-a': 'HELLO', 'file-c': 'third' } ] - for (const [index, shape] of shapes.entries()) { - expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(shape) }).toEqual({ + for (const [index, draft] of draftCases.entries()) { + expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(draft) }).toEqual({ index, - projection: referenceEditorDraftsProjection(shape) + projection: referenceEditorDraftsProjection(draft) }) } }) @@ -402,7 +402,7 @@ describe('open-files and browser projections', () => { }) it('matches the uncached projections byte for byte across shapes', () => { - const openFileShapes: AppState['openFiles'][] = [ + const openFileCases: AppState['openFiles'][] = [ [] as unknown as AppState['openFiles'], [makeOpenFile(0)] as unknown as AppState['openFiles'], [makeOpenFile(0, { isDirty: true })] as unknown as AppState['openFiles'], @@ -412,14 +412,14 @@ describe('open-files and browser projections', () => { makeOpenFile(2, { isUntitled: true, deleteUntouchedOnClose: true, language: undefined }) ] as unknown as AppState['openFiles'] ] - for (const [index, shape] of openFileShapes.entries()) { - expect({ index, projection: buildRuntimeMobileOpenFilesProjection(shape) }).toEqual({ + for (const [index, openFiles] of openFileCases.entries()) { + expect({ index, projection: buildRuntimeMobileOpenFilesProjection(openFiles) }).toEqual({ index, - projection: referenceOpenFilesProjection(shape) + projection: referenceOpenFilesProjection(openFiles) }) } - const browserShapes: AppState[] = [ + const browserCases: AppState[] = [ makeState({}), makeState({ browserTabsByWorktree: { 'wt-1': [makeBrowserWorkspace(0)] } as never }), makeState({ @@ -437,10 +437,10 @@ describe('open-files and browser projections', () => { browserPagesByWorkspace: { 'ws-9': [makeBrowserPage(9, { url: 'a"b\\c' })] } as never }) ] - for (const [index, shape] of browserShapes.entries()) { - expect({ index, projection: buildRuntimeMobileBrowserProjection(shape) }).toEqual({ + for (const [index, state] of browserCases.entries()) { + expect({ index, projection: buildRuntimeMobileBrowserProjection(state) }).toEqual({ index, - projection: referenceBrowserProjection(shape) + projection: referenceBrowserProjection(state) }) } }) diff --git a/src/renderer/src/store/slices/usage-provider-slices.ts b/src/renderer/src/store/slices/usage-provider-slices.ts index 16855b08a48..5c275ef7e86 100644 --- a/src/renderer/src/store/slices/usage-provider-slices.ts +++ b/src/renderer/src/store/slices/usage-provider-slices.ts @@ -30,13 +30,17 @@ type UsageSnapshot = { recentSessions: object[] } -type UsageShape = { +type UsageProviderTypes< + Scope extends string, + Range extends string, + Snapshot extends UsageSnapshot +> = { scope: Scope range: Range snapshot: Snapshot } -type UsageData> = { +type UsageData> = { scope: T['scope'] range: T['range'] scanState: T['snapshot']['scanState'] | null @@ -47,7 +51,7 @@ type UsageData> = { recentSessions: T['snapshot']['recentSessions'] } -type UsageApi> = { +type UsageApi> = { getScanState: () => Promise setEnabled: (args: { enabled: boolean }) => Promise refresh: (args?: { force?: boolean }) => Promise @@ -61,7 +65,7 @@ type UsageApi> = { type ProviderUsageSlice< Prefix extends string, Name extends string, - T extends UsageShape + T extends UsageProviderTypes > = { [K in keyof UsageData as `${Prefix}Usage${Capitalize}`]: UsageData[K] } & Record<`set${Name}UsageEnabled`, (enabled: boolean) => Promise> & @@ -74,7 +78,7 @@ type ProviderUsageSlice< type UsageProviderConfig< Prefix extends string, Name extends string, - T extends UsageShape + T extends UsageProviderTypes > = { prefix: Prefix name: Name @@ -93,13 +97,13 @@ const usageDataFields = [ 'modelBreakdown', 'projectBreakdown', 'recentSessions' -] as const satisfies readonly (keyof UsageData>)[] +] as const satisfies readonly (keyof UsageData>)[] function usageDataKey(prefix: string, field: string): string { return `${prefix}Usage${field[0].toUpperCase()}${field.slice(1)}` } -function readUsageData>( +function readUsageData>( state: AppState, prefix: string ): UsageData { @@ -109,7 +113,7 @@ function readUsageData>( ) as UsageData } -function createUsagePatch>( +function createUsagePatch>( prefix: string, patch: Partial> ): Partial { @@ -123,7 +127,7 @@ function createUsagePatch>( function createUsageProviderSlice< Prefix extends string, Name extends string, - T extends UsageShape + T extends UsageProviderTypes >( config: UsageProviderConfig ): StateCreator> { @@ -255,18 +259,22 @@ function createUsageProviderSlice< } } -type ClaudeUsageShape = UsageShape -type CodexUsageShape = UsageShape -type OpenCodeUsageShape = UsageShape +type ClaudeUsageTypes = UsageProviderTypes +type CodexUsageTypes = UsageProviderTypes +type OpenCodeUsageTypes = UsageProviderTypes< + OpenCodeUsageScope, + OpenCodeUsageRange, + OpenCodeUsageSnapshot +> -export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageShape> -export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageShape> -export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageShape> +export type ClaudeUsageSlice = ProviderUsageSlice<'claude', 'Claude', ClaudeUsageTypes> +export type CodexUsageSlice = ProviderUsageSlice<'codex', 'Codex', CodexUsageTypes> +export type OpenCodeUsageSlice = ProviderUsageSlice<'openCode', 'OpenCode', OpenCodeUsageTypes> export const createClaudeUsageSlice = createUsageProviderSlice< 'claude', 'Claude', - ClaudeUsageShape + ClaudeUsageTypes >({ prefix: 'claude', name: 'Claude', @@ -276,7 +284,7 @@ export const createClaudeUsageSlice = createUsageProviderSlice< hasCachedData: (state) => state.hasAnyClaudeData }) -export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageShape>({ +export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', CodexUsageTypes>({ prefix: 'codex', name: 'Codex', initialScope: 'orca', @@ -288,7 +296,7 @@ export const createCodexUsageSlice = createUsageProviderSlice<'codex', 'Codex', export const createOpenCodeUsageSlice = createUsageProviderSlice< 'openCode', 'OpenCode', - OpenCodeUsageShape + OpenCodeUsageTypes >({ prefix: 'openCode', name: 'OpenCode', diff --git a/src/shared/agent-feature-install-commands.ts b/src/shared/agent-feature-install-commands.ts index 29d1f60b349..4813c17f807 100644 --- a/src/shared/agent-feature-install-commands.ts +++ b/src/shared/agent-feature-install-commands.ts @@ -1,4 +1,4 @@ -import { isSkillsCliAgentKeyShaped } from './skills-cli-agent-keys' +import { isUsableSkillsCliAgentKey } from './skills-cli-agent-keys' export const ORCA_SKILLS_REPOSITORY_URL = 'https://github.com/stablyai/orca' @@ -35,7 +35,7 @@ export function buildAgentFeatureSkillInstallArgs( } // Why: a value the skills CLI would drop leaves it with no target at all, which // is the same all-agents install as passing no --agent. - const unusable = agents.find((agent) => !isSkillsCliAgentKeyShaped(agent)) + const unusable = agents.find((agent) => !isUsableSkillsCliAgentKey(agent)) if (unusable !== undefined) { throw new Error(`"${unusable}" is not a usable install target.`) } diff --git a/src/shared/agent-resume-launch-command.test.ts b/src/shared/agent-resume-launch-command.test.ts index 5bf4797472b..544cd92cd20 100644 --- a/src/shared/agent-resume-launch-command.test.ts +++ b/src/shared/agent-resume-launch-command.test.ts @@ -17,7 +17,7 @@ const SHELLS: { platform: NodeJS.Platform; shell: AgentStartupShell }[] = [ /** Independent selector oracle — deliberately NOT the implementation's own * predicate, so a regression that shrinks the stripped set cannot also blind * this assertion. */ -function isSelectorShapedToken(token: string): boolean { +function isResumeSelectorToken(token: string): boolean { return ( ['--resume', '--continue', '-r', '-c'].includes(token) || ['--resume=', '--continue=', '-r=', '-c='].some((prefix) => token.startsWith(prefix)) @@ -31,7 +31,7 @@ function expectSingleAuthoritativeResume(command: string, shell: AgentStartupShe if (!tokenized.ok) { return } - const selectors = tokenized.tokens.filter(isSelectorShapedToken) + const selectors = tokenized.tokens.filter(isResumeSelectorToken) expect(selectors).toEqual(['--resume']) const index = tokenized.tokens.indexOf('--resume') expect(tokenized.tokens[index + 1]).toBe(SESSION_ID) diff --git a/src/shared/agent-session-journal-schemas.ts b/src/shared/agent-session-journal-schemas.ts index eaee6bb9a63..1c3ad47d3c6 100644 --- a/src/shared/agent-session-journal-schemas.ts +++ b/src/shared/agent-session-journal-schemas.ts @@ -242,7 +242,7 @@ export function isAdmissibleAgentJournalSubmission( * never reject a row a writer in this build produced. The schemas are * deliberately wider on open string fields, so only this direction holds. */ type Admits = T -export type CanonicalJournalShapesAreAdmissible = [ +export type CanonicalJournalTypesAreAdmissible = [ Admits ? true : false>, Admits ? true : false>, Admits< diff --git a/src/shared/agent-session-record.ts b/src/shared/agent-session-record.ts index a8add5c1128..2248647c3c2 100644 --- a/src/shared/agent-session-record.ts +++ b/src/shared/agent-session-record.ts @@ -337,7 +337,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor return false } const record = value as Partial - const shapeValid = + const fieldsValid = record.schemaVersion === AGENT_SESSION_RECORD_SCHEMA_VERSION && isAgentSessionId(record.sessionId) && isAgentSessionExecutionLocation(record.location) && @@ -356,7 +356,7 @@ export function isAgentSessionRecord(value: unknown): value is AgentSessionRecor record.lease.sessionId === record.sessionId && Number.isSafeInteger(record.createdAt) && Number.isSafeInteger(record.updatedAt) - if (!shapeValid) { + if (!fieldsValid) { return false } const validated = record as AgentSessionRecord diff --git a/src/shared/git-push-target-validation.test.ts b/src/shared/git-push-target-validation.test.ts index 7b5dd096eb8..0215611ce52 100644 --- a/src/shared/git-push-target-validation.test.ts +++ b/src/shared/git-push-target-validation.test.ts @@ -1,19 +1,19 @@ import { describe, expect, it } from 'vitest' -import { assertGitPushTargetShape } from './git-push-target-validation' +import { assertValidGitPushTarget } from './git-push-target-validation' -describe('assertGitPushTargetShape', () => { +describe('assertValidGitPushTarget', () => { it('accepts slash-separated git remote names', () => { expect(() => - assertGitPushTargetShape({ remoteName: 'foo/bar', branchName: 'feature/fix' }) + assertValidGitPushTarget({ remoteName: 'foo/bar', branchName: 'feature/fix' }) ).not.toThrow() }) it('rejects remote names with empty or parent segments', () => { expect(() => - assertGitPushTargetShape({ remoteName: 'foo//bar', branchName: 'feature/fix' }) + assertValidGitPushTarget({ remoteName: 'foo//bar', branchName: 'feature/fix' }) ).toThrow('Invalid git remote name') expect(() => - assertGitPushTargetShape({ remoteName: 'foo/../bar', branchName: 'feature/fix' }) + assertValidGitPushTarget({ remoteName: 'foo/../bar', branchName: 'feature/fix' }) ).toThrow('Invalid git remote name') }) }) diff --git a/src/shared/git-push-target-validation.ts b/src/shared/git-push-target-validation.ts index 4f907d568e7..7fa666136c1 100644 --- a/src/shared/git-push-target-validation.ts +++ b/src/shared/git-push-target-validation.ts @@ -32,7 +32,7 @@ export function isSafePushTargetRemoteUrl(remoteUrl: string): boolean { return GITHUB_CLONE_URL.test(remoteUrl) || GITHUB_SSH_URL.test(remoteUrl) } -export function assertGitPushTargetShape(target: unknown): asserts target is GitPushTarget { +export function assertValidGitPushTarget(target: unknown): asserts target is GitPushTarget { if (typeof target !== 'object' || target === null) { throw new Error('Invalid PR push target.') } diff --git a/src/shared/native-chat-ask.ts b/src/shared/native-chat-ask.ts index 7096db88353..f8655a11d2f 100644 --- a/src/shared/native-chat-ask.ts +++ b/src/shared/native-chat-ask.ts @@ -19,7 +19,7 @@ export function registerQuestionTool(toolName: string, parser: InteractiveQuesti QUESTION_TOOL_PARSERS.set(toolName, parser) } -function parseQuestionsShape(input: unknown): AskPrompt | null { +function parseCanonicalQuestionsInput(input: unknown): AskPrompt | null { if (!input || typeof input !== 'object') { return null } @@ -73,12 +73,12 @@ function parseOptions(raw: unknown): AskOption[] { } for (const name of ['AskUserQuestion', 'ask_user_question', 'askUserQuestion']) { - QUESTION_TOOL_PARSERS.set(name, parseQuestionsShape) + QUESTION_TOOL_PARSERS.set(name, parseCanonicalQuestionsInput) } function parseToolInput(toolName: string | undefined, input: unknown): AskPrompt | null { const parser = toolName ? QUESTION_TOOL_PARSERS.get(toolName) : undefined - return (parser ? parser(input) : null) ?? parseQuestionsShape(input) + return (parser ? parser(input) : null) ?? parseCanonicalQuestionsInput(input) } export function parseAskFromStatus( diff --git a/src/shared/onboarding-state-types.ts b/src/shared/onboarding-state-types.ts index 669c70b94f3..20aefb45e26 100644 --- a/src/shared/onboarding-state-types.ts +++ b/src/shared/onboarding-state-types.ts @@ -9,6 +9,8 @@ export type OnboardingChecklistState = { ranFirstAgent: boolean ranSecondAgentOnSameTask: boolean triedCmdJ: boolean + // Persisted field, also a telemetry enum member in ./telemetry-onboarding-foundation-schemas; + // renaming it would orphan saved state. Rule exemption: config/oxlint-anti-slop.json. shapedSidebar: boolean reviewedDiff: boolean openedPr: boolean diff --git a/src/shared/pane-agent-identity-resolver.test.ts b/src/shared/pane-agent-identity-resolver.test.ts index 2f14a54976a..d11416a1d8f 100644 --- a/src/shared/pane-agent-identity-resolver.test.ts +++ b/src/shared/pane-agent-identity-resolver.test.ts @@ -52,8 +52,8 @@ describe('resolvePaneAgentIdentity', () => { }) describe('run generation separates the bug from the legitimate reclaim', () => { - // Both shapes are `completed hook = A, title = B`. Ordering alone cannot tell them apart. - const shape = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [ + // Both cases are `completed hook = A, title = B`. Ordering alone cannot tell them apart. + const evidenceFor = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [ { source: 'completed-hook', agent: 'claude', run: { authorityId: H, incarnation: hookRun } }, { source: 'title', agent: 'codex', run: { authorityId: H, incarnation: titleRun } } ] @@ -61,7 +61,7 @@ describe('resolvePaneAgentIdentity', () => { it('keeps the completed hook when both belong to the current run', () => { // The reported bug: nothing new started, so the hook is still the truth. const result = resolvePaneAgentIdentity({ - evidence: shape(7, 7), + evidence: evidenceFor(7, 7), currentRun: { authorityId: H, incarnation: 7 } }) expect(result).toMatchObject({ agent: 'claude', source: 'completed-hook' }) @@ -72,7 +72,7 @@ describe('resolvePaneAgentIdentity', () => { // The legitimate reclaim: the pane was reused, so run 7's hook describes an agent that is // no longer there. It is ineligible, not merely outranked. const result = resolvePaneAgentIdentity({ - evidence: shape(7, 8), + evidence: evidenceFor(7, 8), currentRun: { authorityId: H, incarnation: 8 } }) expect(result).toMatchObject({ agent: 'codex', source: 'title' }) @@ -82,11 +82,11 @@ describe('resolvePaneAgentIdentity', () => { it('produces opposite answers from identical evidence, given only the run ids', () => { // The whole point, stated as one assertion. const bug = resolvePaneAgentIdentity({ - evidence: shape(7, 7), + evidence: evidenceFor(7, 7), currentRun: { authorityId: H, incarnation: 7 } }) const reclaim = resolvePaneAgentIdentity({ - evidence: shape(7, 8), + evidence: evidenceFor(7, 8), currentRun: { authorityId: H, incarnation: 8 } }) expect(bug.agent).not.toBe(reclaim.agent) diff --git a/src/shared/plugins/plugin-language-pack-artifact.test.ts b/src/shared/plugins/plugin-language-pack-artifact.test.ts index a313cc33839..1c58189845c 100644 --- a/src/shared/plugins/plugin-language-pack-artifact.test.ts +++ b/src/shared/plugins/plugin-language-pack-artifact.test.ts @@ -5,7 +5,7 @@ import { PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH, PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES, validatePluginLanguagePackCatalog, - validatePluginLanguagePackCatalogShape, + checkPluginLanguagePackCatalog, pluginLanguageResourceId } from './plugin-language-pack-artifact' @@ -205,7 +205,7 @@ describe('plugin language-pack artifacts', () => { } }) - expect(validatePluginLanguagePackCatalogShape(catalog)).toEqual({ + expect(checkPluginLanguagePackCatalog(catalog)).toEqual({ ok: true, entries: PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES }) diff --git a/src/shared/plugins/plugin-language-pack-artifact.ts b/src/shared/plugins/plugin-language-pack-artifact.ts index 7bcfacf47d4..b2daf8bc1c7 100644 --- a/src/shared/plugins/plugin-language-pack-artifact.ts +++ b/src/shared/plugins/plugin-language-pack-artifact.ts @@ -37,7 +37,7 @@ export function isPluginLanguagePackRegistration( pack.resourceLanguage === pluginLanguageResourceId(pack.id as `plugin:${string}`) && typeof pack.pluginKey === 'string' && typeof pack.locale === 'string' && - validatePluginLanguagePackCatalogShape(pack.catalog).ok + checkPluginLanguagePackCatalog(pack.catalog).ok ) } @@ -119,7 +119,7 @@ export function validatePluginLanguagePackCatalog(source: unknown): PluginLangua return { ok: true, catalog: result.catalog!, entries: result.entries } } -export function validatePluginLanguagePackCatalogShape( +export function checkPluginLanguagePackCatalog( source: unknown ): PluginLanguagePackValidationResult { const result = walkPluginLanguagePackCatalog(source, false) diff --git a/src/shared/remote-pairing-verification.ts b/src/shared/remote-pairing-verification.ts index 02d58b97bed..47475144b0a 100644 --- a/src/shared/remote-pairing-verification.ts +++ b/src/shared/remote-pairing-verification.ts @@ -31,7 +31,7 @@ function isNonNegativeSafeInteger(value: unknown): value is number { return Number.isSafeInteger(value) && Number(value) >= 0 } -function hasValidRuntimeStatusShape(status: Record): boolean { +function hasValidRuntimeStatus(status: Record): boolean { return ( typeof status.runtimeId === 'string' && status.runtimeId.length > 0 && @@ -103,7 +103,7 @@ export function verifyRemotePairingRuntimeStatus( : 'Update Orca on the remote host before adding it.' } } - if (!hasValidRuntimeStatusShape(status)) { + if (!hasValidRuntimeStatus(status)) { return { ok: false, kind: 'connection-interrupted', diff --git a/src/shared/rpc-contract/repo-update-params.ts b/src/shared/rpc-contract/repo-update-params.ts index 179bb1994dc..3eb1477adb2 100644 --- a/src/shared/rpc-contract/repo-update-params.ts +++ b/src/shared/rpc-contract/repo-update-params.ts @@ -34,12 +34,14 @@ export const RepoUpstream = z .nullable() .optional() -// The return type is inferred on purpose: an explicit z.ZodObject<...z.ZodRawShape> -// annotation widened `updates` to an open record, which erased all 24 named fields -// from RpcParams<'repo.update'> for every typed caller. -export function createRepoUpdateSchema(selectorShape: T) { +// The return type is inferred on purpose: an explicit z.ZodObject<...> annotation +// widened `updates` to an open record, which erased all 24 named fields from +// RpcParams<'repo.update'> for every typed caller. +export function createRepoUpdateSchema>>( + selectorFields: T +) { return z.object({ - ...selectorShape, + ...selectorFields, updates: z.object({ displayName: OptionalString, badgeColor: RepoBadgeColor, diff --git a/src/shared/rpc-contract/rpc-send-params.ts b/src/shared/rpc-contract/rpc-send-params.ts index fcb6d66a359..148f5aa6d6c 100644 --- a/src/shared/rpc-contract/rpc-send-params.ts +++ b/src/shared/rpc-contract/rpc-send-params.ts @@ -21,15 +21,15 @@ type Prettify = { [K in keyof T]: T[K] } & {} /** zod's own input-side key-optionality rule, copied from $InferObjectInput. */ type SendOptionalSchema = { _zod: { optin: 'optional' | 'defaulted' } } -type SendShape = Prettify< +type SendFields = Prettify< { - -readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? never : K]: RpcSendInput< - Shape[K] + -readonly [K in keyof Fields as Fields[K] extends SendOptionalSchema ? never : K]: RpcSendInput< + Fields[K] > } & { - -readonly [K in keyof Shape as Shape[K] extends SendOptionalSchema ? K : never]?: RpcSendInput< - Shape[K] - > + -readonly [ + K in keyof Fields as Fields[K] extends SendOptionalSchema ? K : never + ]?: RpcSendInput } > @@ -50,11 +50,11 @@ export type RpcSendInput = ? RpcSendInput[] : // ZodObject is the only schema carrying a `shape`, and matching on it keeps // .strict()/.extend()/.superRefine() results in this branch. - Schema extends { shape: infer Shape } - ? keyof Shape extends never + Schema extends { shape: infer Fields } + ? keyof Fields extends never ? // Mirrors $InferObjectOutput: a no-field object admits no properties. Record - : SendShape + : SendFields : // ZodDiscriminatedUnion extends ZodUnion, so both land here. Schema extends z.ZodUnion ? RpcSendInput diff --git a/src/shared/rpc-contract/ui-update-value-tolerance-params.ts b/src/shared/rpc-contract/ui-update-value-tolerance-params.ts index 1b8ca0c2cd4..308c1183d02 100644 --- a/src/shared/rpc-contract/ui-update-value-tolerance-params.ts +++ b/src/shared/rpc-contract/ui-update-value-tolerance-params.ts @@ -7,13 +7,15 @@ import type { z } from 'zod' * dropped from the payload and the rest of the batch still lands. Unknown KEYS * stay a hard rejection — the parity assertions exist to catch those. */ -export function tolerateUnknownValues(shape: TShape): TShape { - return Object.fromEntries( - Object.entries(shape).map(([key, schema]) => [ - key, - (schema as z.ZodType).catch(() => undefined) - ]) - ) as unknown as TShape +export function tolerateUnknownValues>>( + fields: TFields +): TFields { + const tolerant: Record = {} + for (const [key, schema] of Object.entries(fields)) { + tolerant[key] = schema.catch(() => undefined) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the loop copies every key of `fields` and only wraps its schema in `.catch()`, so the result carries exactly `TFields`' keys; Object.entries erases that key identity. + return tolerant as TFields } /** Drops the `undefined` entries `tolerateUnknownValues` leaves behind, so a diff --git a/src/shared/skills-cli-agent-keys.test.ts b/src/shared/skills-cli-agent-keys.test.ts index 4359d879c48..02bdd8b52df 100644 --- a/src/shared/skills-cli-agent-keys.test.ts +++ b/src/shared/skills-cli-agent-keys.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { TUI_AGENT_CONFIG } from './tui-agent-config' import { SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT, - isSkillsCliAgentKeyShaped, + isUsableSkillsCliAgentKey, SKILLS_CLI_UNIVERSAL_AGENT_KEY, toSkillsCliAgentKeys } from './skills-cli-agent-keys' @@ -117,10 +117,10 @@ describe('skills CLI agent keys', () => { it('rejects values the skills CLI would drop, and allows the explicit wildcard', () => { for (const bad of ['-y', '--copy', '', ' ', 'a b', 'a,b']) { - expect(isSkillsCliAgentKeyShaped(bad), bad).toBe(false) + expect(isUsableSkillsCliAgentKey(bad), bad).toBe(false) } for (const good of ['claude-code', 'universal', 'trae-cn', 'inference-sh', '*']) { - expect(isSkillsCliAgentKeyShaped(good), good).toBe(true) + expect(isUsableSkillsCliAgentKey(good), good).toBe(true) } }) diff --git a/src/shared/skills-cli-agent-keys.ts b/src/shared/skills-cli-agent-keys.ts index 3126c0a81e5..8f77675e8c0 100644 --- a/src/shared/skills-cli-agent-keys.ts +++ b/src/shared/skills-cli-agent-keys.ts @@ -68,7 +68,7 @@ export const SKILLS_CLI_UNIVERSAL_AGENT_KEY = 'universal' * emptiness. An unknown-but-plausible key is left to the CLI, which rejects it * loudly with its own valid list before writing anything. */ -export function isSkillsCliAgentKeyShaped(value: string): boolean { +export function isUsableSkillsCliAgentKey(value: string): boolean { return /^(?:\*|[a-z0-9][a-z0-9.-]*)$/i.test(value) } diff --git a/src/shared/telemetry-event-classification.ts b/src/shared/telemetry-event-classification.ts index 23b471fd4f5..cc932fe9411 100644 --- a/src/shared/telemetry-event-classification.ts +++ b/src/shared/telemetry-event-classification.ts @@ -8,32 +8,32 @@ export type EventName = keyof EventMap export type EventProps = EventMap[N] // Why: non-`ZodObject` schemas have no `.shape`; return null so `key in undefined` can't throw at module load. -function eventSchemaShape(schema: z.ZodTypeAny): z.ZodRawShape | null { +// Why `object` and not zod's own field-record type: callers only ask `key in fields`. +function eventSchemaFields(schema: z.ZodTypeAny): object | null { if (schema instanceof z.ZodObject) { return schema.shape } - const shapeBearingSchema = schema as { shape?: unknown } // Why: refined object schemas may expose `.shape` even when refinement breaks `instanceof ZodObject`. - if (shapeBearingSchema.shape && typeof shapeBearingSchema.shape === 'object') { - return shapeBearingSchema.shape as z.ZodRawShape + if ('shape' in schema && typeof schema.shape === 'object' && schema.shape !== null) { + return schema.shape } return null } -function eventsWithShapeKey(key: string): ReadonlySet { +function eventsDeclaringKey(key: string): ReadonlySet { return new Set( (Object.entries(eventSchemas) as [EventName, z.ZodTypeAny][]) .filter(([, schema]) => { - const shape = eventSchemaShape(schema) - return shape !== null && key in shape + const fields = eventSchemaFields(schema) + return fields !== null && key in fields }) .map(([name]) => name) ) } // Cohort injection is gated on this derived set because `.strict()` schemas drop events that don't declare `nth_repo_added`. -const COHORT_EXTENDED_SET = eventsWithShapeKey('nth_repo_added') +const COHORT_EXTENDED_SET = eventsDeclaringKey('nth_repo_added') // Compile-time roster guarding the runtime injection set against silent schema drift. type _CohortExtendedRoster = @@ -78,7 +78,7 @@ export function isCohortExtendedEvent(name: EventName): boolean { } // Events whose schema declares `cohort`: the IPC handler injects cohort only for these — a `.strict()` schema without it would reject the event. -const ONBOARDING_COHORT_SET = eventsWithShapeKey('cohort') +const ONBOARDING_COHORT_SET = eventsDeclaringKey('cohort') // `NonNullable` strips `undefined` introduced by `cohortSchema`'s `.optional()`. export type OnboardingCohort = NonNullable> diff --git a/src/shared/zod-salvage-absence.test.ts b/src/shared/zod-salvage-absence.test.ts index 065c6118ec1..5b6ca28f94f 100644 --- a/src/shared/zod-salvage-absence.test.ts +++ b/src/shared/zod-salvage-absence.test.ts @@ -20,7 +20,7 @@ const CONTAINERS: [string, () => z.ZodType, unknown][] = [ ['salvagingArray', () => salvagingArray(z.string()), ['v']] ] -describe('salvaging containers used bare in an object shape', () => { +describe('salvaging containers used bare in an object schema', () => { it.each(CONTAINERS)('%s is neither optional-in nor optional-out', (_name, build) => { const { optin, optout } = optionalityOf(build()) expect(optin).toBeUndefined() @@ -28,12 +28,12 @@ describe('salvaging containers used bare in an object shape', () => { }) it.each(CONTAINERS)('%s rejects an absent key and an explicit undefined', (_name, build, ok) => { - const shape = z.object({ a: build() }) + const schema = z.object({ a: build() }) - expect(shape.safeParse({}).success).toBe(false) - expect(shape.safeParse({ a: undefined }).success).toBe(false) + expect(schema.safeParse({}).success).toBe(false) + expect(schema.safeParse({ a: undefined }).success).toBe(false) // Why: a positive control, so the two rejections above cannot pass by rejecting everything. - expect(shape.safeParse({ a: ok })).toMatchObject({ success: true }) + expect(schema.safeParse({ a: ok })).toMatchObject({ success: true }) }) it.each(CONTAINERS)( diff --git a/tests/e2e/helpers/host-created-terminal-retention-oracle.ts b/tests/e2e/helpers/host-created-terminal-retention-oracle.ts index b3c4d8c2d21..9a1af6a957b 100644 --- a/tests/e2e/helpers/host-created-terminal-retention-oracle.ts +++ b/tests/e2e/helpers/host-created-terminal-retention-oracle.ts @@ -48,7 +48,7 @@ const HOST_TERMINAL_SURFACE_SEPARATOR = '::' /** Daemon session id form. Deliberately excluded from id-shape classification, * which is why a host-created tab needs its own binding to be preserved — * a `serve-`/`ssh-` shaped id would take an already-correct path instead. */ -function isDaemonShapedPtyId(ptyId: string, worktreeId: string): boolean { +function isDaemonPtyIdForm(ptyId: string, worktreeId: string): boolean { return ( ptyId.startsWith(`${worktreeId}@@`) && !ptyId.startsWith('serve-') && @@ -156,7 +156,7 @@ export async function createHostCliTerminal( throw new Error('Host did not report a leaf id for the CLI-created terminal') } expect( - isDaemonShapedPtyId(ptyId, worktreeId), + isDaemonPtyIdForm(ptyId, worktreeId), `CLI terminal ${ptyId} must carry the daemon id shape this seam excludes from classification` ).toBe(true) await expect diff --git a/tests/e2e/terminal-cjk-ime-committed-text.spec.ts b/tests/e2e/terminal-cjk-ime-committed-text.spec.ts index fb98b2d1975..056929de8bb 100644 --- a/tests/e2e/terminal-cjk-ime-committed-text.spec.ts +++ b/tests/e2e/terminal-cjk-ime-committed-text.spec.ts @@ -73,7 +73,7 @@ const SUBSTITUTION_GROUPS = [ * The two ways a substituted keystroke can reach the renderer. Both are real; only the second one * regressed, and only the second one can regress, which is why running both is the point. */ -const SUBSTITUTION_SHAPES: readonly { +const SUBSTITUTION_ROUTES: readonly { name: string slug: string dispatch: (session: CDPSession, keystroke: SubstitutedKeystroke) => Promise @@ -165,9 +165,9 @@ test.describe('Terminal CJK IME committed text', () => { } }) - for (const shape of SUBSTITUTION_SHAPES) { + for (const route of SUBSTITUTION_ROUTES) { for (const group of SUBSTITUTION_GROUPS) { - test(`sends full-width ${group.label} and never their ASCII form when ${shape.name}`, async ({ + test(`sends full-width ${group.label} and never their ASCII form when ${route.name}`, async ({ orcaPage, testRepoPath }, testInfo) => { @@ -179,7 +179,7 @@ test.describe('Terminal CJK IME committed text', () => { try { await startTerminalImeByteReader(orcaPage, arena.ptyId, reader) for (const keystroke of group.keystrokes) { - await shape.dispatch(arena.session, keystroke) + await route.dispatch(arena.session, keystroke) await orcaPage.waitForTimeout(60) } await dispatchPlainEnter(arena.session) @@ -200,7 +200,7 @@ test.describe('Terminal CJK IME committed text', () => { await closeTerminalImePaneArena( arena, testInfo, - `full-width-${group.label}-${shape.slug}`, + `full-width-${group.label}-${route.slug}`, !completed ) removeTerminalImeByteReader(reader) diff --git a/tests/tools/win-crash-survival-e2e/cli-args.mjs b/tests/tools/win-crash-survival-e2e/cli-args.mjs index e3533342e4d..1276d5e2eb4 100644 --- a/tests/tools/win-crash-survival-e2e/cli-args.mjs +++ b/tests/tools/win-crash-survival-e2e/cli-args.mjs @@ -69,7 +69,7 @@ export function parseArgs(argv) { function validate(opts, exePathFlagPresent, argv) { const errors = [] - errors.push(...validateArgShape(argv)) + errors.push(...validateArgSyntax(argv)) if (!opts.expect) { errors.push('Missing --expect ') } else if (!VALID_PROFILES.has(opts.expect)) { @@ -94,7 +94,7 @@ function validate(opts, exePathFlagPresent, argv) { return errors } -function validateArgShape(argv) { +function validateArgSyntax(argv) { const errors = [] const seen = new Set() for (let index = 0; index < argv.length; index++) { From caa465d1da886b2697c85c18bf36f79f2d3b9e68 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:50:00 -0700 Subject: [PATCH 33/34] fix(automations): stop tick latency counting against the missed-run grace (#20819) * fix(automations): stop tick latency counting against the missed-run grace The scheduler compared wall-clock lateness straight against the grace budget, but evaluation runs on a fixed 60s interval that is never aligned to an occurrence. With grace 0, any tick arriving after the scheduled instant -- in practice every tick -- recorded skipped_missed and told the user "Orca was unavailable during the missed-run grace window" while Orca had been up the whole time. A zero-grace automation effectively never ran. Grace is a downtime catch-up budget. An occurrence that came due while the scheduler was running was never missed; it is waiting for the next tick. The service now tracks continuous availability and only charges lateness to grace for occurrences that came due while it was stopped. Downtime behaviour is unchanged, and the new test asserts that half too. The missed-run branch moved to dispatch-refusal.ts, which already owns non-dispatch outcomes, keeping service.ts under max-lines without a disable. Fixes #11299 * fix(automations): use a tick-latency tolerance instead of process liveness Review caught two real defects in the first cut: - availableSince is process liveness, not continuous execution. A suspended process (system sleep) keeps its start time, so an occurrence that came due during a multi-hour sleep skipped the grace check entirely and replayed on wake -- exactly the downtime case grace exists for. - The restart edge: an occurrence due after the last tick but before stop() was reclassified as downtime and skipped with zero grace. Elapsed lateness cannot be faked by suspension and needs no restart bookkeeping, so the budget is now grace + two tick intervals. Both edges disappear rather than being special-cased. Also fixes a hollow test: workspaceId 'wt1' has no worktree separator, so the target refused and the run recorded skipped_unavailable -- a 'not skipped_missed' assertion passed without ever dispatching. Tests now use a valid id and assert 'dispatching' directly, and cover the sleep, tolerance boundary and restart cases. * fix(automations): scope to the verified tolerance and document the stall gap Review found three defects, all real: - The 'as never' cast failed the changed-code casting gate. AutomationRendererChannel is a Pick<> precisely so a test can pass the real shape; cast removed. - The restart test never restarted: evaluateAt advanced 60s internally, so the first pass already dispatched and the second was a no-op. It now evaluates exactly once and asserts no run exists before the second pass. - tickMs * 2 does not bound a pass that holds the re-entrancy guard across a slow serve-mode dispatch. I tried a busy-window fix for the third and could not test it honestly -- the case needs a genuinely slow in-pass dispatch, and both attempts passed with the fix disabled. Rather than ship logic I cannot prove, the tolerance stays at the verified shape and the gap is documented where the next reader will find it, with the reason 'time since last pass' is the wrong bound (a suspended process runs no passes either). Not a regression: on main that automation never ran at all. * fix(automations): name the check for what it does and correct its message Two review points, both fair: - missedDuringDowntime consulted nothing about availability once the liveness flag was removed; it is elapsed lateness against grace plus tolerance. Renamed missedBeyondGrace so callers read the real contract. - The run error still claimed 'Orca was unavailable' -- the same false statement #11299 was filed about, now reachable for a genuinely late run rather than a merely tick-delayed one. It states what was actually observed instead. Also documented the deliberate trade CodeRabbit raised: elapsed lateness cannot tell a short outage from a late tick, so a zero-grace run due during an outage shorter than the tolerance dispatches instead of skipping. The alternative got the far worse case wrong -- a multi-hour sleep replayed on wake. --- ...automation-zero-grace-tick-latency.test.ts | 127 ++++++++++++++++++ src/main/automations/dispatch-refusal.ts | 47 +++++++ src/main/automations/service.ts | 13 +- 3 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 src/main/automations/automation-zero-grace-tick-latency.test.ts diff --git a/src/main/automations/automation-zero-grace-tick-latency.test.ts b/src/main/automations/automation-zero-grace-tick-latency.test.ts new file mode 100644 index 00000000000..4639a872887 --- /dev/null +++ b/src/main/automations/automation-zero-grace-tick-latency.test.ts @@ -0,0 +1,127 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import type { Repo } from '../../shared/repo-types' +import { AutomationService } from './service' +import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' + +const testState = { dir: '' } + +vi.mock('electron', () => ({ + app: { + getPath: () => testState.dir + }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +async function createStore() { + vi.resetModules() + installFakeAppEnvironment({ getPath: () => testState.dir }) + const { Store, initDataPath } = await import('../persistence') + initDataPath() + return new Store() +} + +const makeRepo = (overrides: Partial = {}): Repo => ({ + id: 'r1', + path: '/repo', + displayName: 'test', + badgeColor: '#fff', + addedAt: 1, + ...overrides +}) + +describe('AutomationService zero-grace tick latency', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-automations-test-')) + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + rmSync(testState.dir, { recursive: true, force: true }) + }) + + const DUE = new Date('2026-05-13T09:00:00').getTime() + + const makeZeroGrace = (store: Awaited>) => + store.createAutomation({ + name: 'Zero grace', + prompt: 'Run it', + agentId: 'claude', + projectId: 'r1', + workspaceMode: 'existing', + // Why a separator: without one resolveAutomationRunTarget refuses and the run records + // skipped_unavailable, which would make a "not skipped_missed" assertion pass vacuously. + workspaceId: 'r1::wt1', + timezone: 'UTC', + rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', + dtstart: new Date('2026-05-12T00:00:00').getTime(), + missedRunGraceMinutes: 0 + }) + + /** One evaluation pass at exactly `at` -- start()/setRendererReady() triggers it directly, so + * advancing the timer would silently add a second pass a minute later (and did). */ + const evaluateAt = async ( + store: Awaited>, + at: number + ): Promise => { + vi.setSystemTime(at) + const service = new AutomationService(store, { tickMs: 60_000 }) + service.setWebContents({ isDestroyed: () => false, send: vi.fn() }) + service.start() + service.setRendererReady() + await vi.advanceTimersByTimeAsync(0) + service.stop() + } + + const statusAt = async (lateMs: number): Promise => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + store.addRepo(makeRepo()) + const automation = makeZeroGrace(store) + await evaluateAt(store, DUE + lateMs) + return store.listAutomationRuns(automation.id)[0]?.status + } + + // Why 1ms and 45s: the tick interval is never aligned to an occurrence, so ANY positive + // lateness used to exceed a zero grace budget and skip the run (#11299). + it.each([ + ['1ms late', 1], + ['45s late', 45_000] + ])('dispatches a zero-grace occurrence only the tick was late for (%s)', async (_l, lateMs) => { + // Assert the outcome, not merely "not skipped_missed" -- a refused target would also + // satisfy that while never dispatching. + expect(await statusAt(lateMs)).toBe('dispatching') + }) + + // The other half of the invariant: real downtime still consumes the grace budget. A suspended + // process keeps its start time, so this is the case a liveness flag would have waved through. + it('still skips a zero-grace occurrence that came due during a long sleep', async () => { + expect(await statusAt(4 * 60 * 60 * 1000)).toBe('skipped_missed') + }) + + // Just past the tolerance: the boundary has to bite, or the tolerance is a blanket grace. + it('skips once lateness exceeds the tick-latency tolerance', async () => { + expect(await statusAt(2 * 60_000 + 1)).toBe('skipped_missed') + }) + + // A restart that crosses the occurrence must behave like any other late tick, not like + // downtime -- the elapsed lateness is what decides, so bookkeeping cannot drift. + it('dispatches after a restart that crosses the occurrence within tolerance', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + store.addRepo(makeRepo()) + const automation = makeZeroGrace(store) + await evaluateAt(store, DUE - 30_000) + // Nothing may have run yet, or the second pass is not the one under test. + expect(store.listAutomationRuns(automation.id)).toHaveLength(0) + await evaluateAt(store, DUE + 30_000) + expect(store.listAutomationRuns(automation.id)[0]?.status).toBe('dispatching') + }) +}) diff --git a/src/main/automations/dispatch-refusal.ts b/src/main/automations/dispatch-refusal.ts index 08fd400e8c8..4a5fffa1b16 100644 --- a/src/main/automations/dispatch-refusal.ts +++ b/src/main/automations/dispatch-refusal.ts @@ -119,3 +119,50 @@ export function sendRendererDispatch( }) } } + +/** + * Grace is a downtime catch-up budget. It must not also absorb the scheduler's own tick latency: + * evaluation runs on a fixed interval never aligned to an occurrence, so with zero grace every + * tick arrived "late" and skipped the run, blaming downtime that never happened (#11299). + * + * Why not process liveness: a suspended process (system sleep) keeps its start time, so a + * liveness flag waves through an occurrence that came due during a multi-hour sleep -- exactly + * what grace exists for. Elapsed lateness cannot be faked that way. + * + * Consequence worth knowing: elapsed lateness cannot distinguish a short outage from a late + * tick, so a zero-grace run that came due during an outage shorter than the tolerance is + * dispatched rather than skipped. That is the deliberate trade -- the alternative was a + * liveness flag, which got the far worse case wrong (a multi-hour sleep replayed on wake). + * + * Known remaining gap: an evaluation pass holds the re-entrancy guard across its dispatches, and + * in serve mode a dispatch runs inline (precheck up to 600s, then a worktree create). A pass + * longer than the tolerance drops every intervening tick, so the next automation's lateness is + * the scheduler's stall rather than downtime and can still be mis-skipped. Desktop is + * unaffected -- its dispatch is synchronous IPC. Tracked separately; forgiving "time since the + * last pass" is NOT the fix, because a suspended process runs no passes either. + */ +export function missedBeyondGrace(input: { + automation: Automation + scheduledFor: number + now: number + tickMs: number +}): boolean { + const graceMs = input.automation.missedRunGraceMinutes * 60 * 1000 + // Two intervals: one for the tick that should have caught it, one for ordinary jitter. + const jitterMs = input.tickMs * 2 + return input.now - input.scheduledFor > graceMs + jitterMs +} + +export function recordMissedRun(input: { + runs: AutomationRunWriter + automation: Automation + scheduledFor: number +}): void { + const missed = input.runs.createRun(input.automation, input.scheduledFor) + input.runs.updateRun({ + runId: missed.id, + status: 'skipped_missed', + workspaceId: input.automation.workspaceId, + error: 'This run was past its missed-run grace window when Orca next checked.' + }) +} diff --git a/src/main/automations/service.ts b/src/main/automations/service.ts index 4be15f095db..227a290783b 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -28,6 +28,8 @@ import { createAutomationRunWriter, type AutomationRunWriter } from './automatio import { reportAutomationScheduleDrift } from './schedule-drift-report' import { describeScheduledRefusal, + missedBeyondGrace, + recordMissedRun, recordRefusedAutomationRun, recordUnevaluableAutomation, sendRendererDispatch, @@ -251,15 +253,8 @@ export class AutomationService { this.store.advanceAutomationNextRun(automation.id, now) return } - const graceMs = automation.missedRunGraceMinutes * 60 * 1000 - if (now - scheduledFor > graceMs) { - const missed = this.runs.createRun(automation, scheduledFor) - this.runs.updateRun({ - runId: missed.id, - status: 'skipped_missed', - workspaceId: automation.workspaceId, - error: 'Orca was unavailable during the missed-run grace window.' - }) + if (missedBeyondGrace({ automation, scheduledFor, now, tickMs: this.tickMs })) { + recordMissedRun({ runs: this.runs, automation, scheduledFor }) this.store.advanceAutomationNextRun(automation.id, now) return } From 36ef93a64f3a10b335ffe8a9f9976cc5e6155faa Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:10:32 -0400 Subject: [PATCH 34/34] refactor(mobile): migrate the small domains onto RpcOperation (step 4) (#20705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(mobile): pin each RPC golden to its own mount adapter, not every domain's `recorderSha256` covered the whole recorder directory, mount adapters included, so a domain PR that adds its adapter module moved the header of all 153 goldens. #20568 did exactly that and its merge with main conflicted on that one line in 153 files; every future domain PR would collide with every other in flight the same way. Split the directory at a real seam instead of a filename convention: `adapters/` holds one module per domain, registered in `adapters/mounted-operation-modules.ts`, and `recorderSha256` now covers the engine only. A new `adapterSha256` covers the source of the module that mounts each operation a golden's 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 now re-digests nothing already recorded; editing one fails exactly the goldens mounted through it. `adapter-seam.test.ts` keeps the split from drifting: an engine file inside `adapters/`, an adapter defined in an engine file, a register entry naming the wrong file, and an adapter importing a sibling each fail. The five adapters that were inline in `pilot-mount-adapters.ts` move into their own modules, which leaves that file as the registry and nothing else. `GOLDEN_FORMAT_VERSION` goes to 5 for the new header field; the goldens re-record in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the split recorder/adapter digest Header-only. Every changed line is `recorderSha256` (the engine digest no longer covers `adapters/`), the new `adapterSha256`, or `goldenFormatVersion` 4 -> 5; `baseline` is unchanged and recording ran against the same pinned product tree. git diff -U0 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256|goldenFormatVersion)":' | wc -l 0 The seven `adapterSha256` values partition the 153 goldens by the module each was recorded through: 58 settings, 37 hosted review, 21 source control, 11 new-tab agents, 9 file inventory, 9 tasks, 8 workspace settings. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): stop pinning goldens to recorder inputs no recording can read The adapter split left three per-domain edits still moving all 153 headers: the mutant table, the per-family mutant registry beside it, and the probe-hole witness. None can change a recording -- the loader consults a mutant only when a mutant test asks for one, and no suite but the two recording drivers writes a golden -- so pinning them claimed a provenance the goldens do not have and charged every domain a full re-record for it. `mutants/` now holds the table, the registry, the reference states, the mutant suites and the probe-hole witness, and `recorderSha256` skips it. What makes that sound is that no recording can reach it: `operationModuleLoader` takes a resolved mutation spec instead of importing a table by name, so nothing on the recording path names `mutants/` at all. `mutants/mutant-seam.test.ts` checks exactly that, and fails if an engine file names the directory or anything outside imports from it. `recorderSha256` also pins only the suites in `recording-drivers.ts`, which `scripts/rpc-recording.mts` records from, so the two cannot drift. A suite that reads goldens, or writes one to a scratch directory, is no longer provenance for a recorded file. `OPERATION_EXPOSURES` went the other way, because it does change what a recording loads: withhold the resume-metadata exposure and exactly four goldens fail. Each domain module now declares its own exposures and gets its own loader, so `adapterSha256` pins the ones that reached each golden. Two assertions in the digest boundary test were vacuous: `join(root, '.')` normalises back to `root` and hit `recorderSha256`'s per-root cache, so the prose-is-ignored claim never recomputed anything. Each call now spells the root differently. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens under the mutant and driver exclusions Header-only, and no format bump: the header shape is unchanged. `recorderSha256` moves on all 153 because the engine set shrank, and `adapterSha256` moves on the 58 settings goldens because that module now carries its own exposure declaration. git diff -U0 HEAD~1 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \ | grep -vE '^(\+\+\+|---)' \ | grep -vE '^[+-] "(recorderSha256|adapterSha256)":' | wc -l 0 Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): restore the preferences actions the merge resolution dropped #20568 added `resume` and `trust` actions to the `settings.task-preferences` adapter while it still lived in `pilot-mount-adapters.ts`. This branch had already moved that adapter into `adapters/task-mount-adapters.ts`, so resolving the `pilot-mount-adapters.ts` conflict in favour of the registry merge silently discarded them and `tw-task-preferences-resume-write` failed to record at all ("Missing or completed request: ui.set#1"). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens at main's tip after the merge All 208 goldens, header-only. `baseline` moves from 50e752fc66 to main's tip c6a7216984, `goldenFormatVersion` from 4 to 5, `recorderSha256` to the value of the engine with `adapters/` and `mutants/` carved out, and `adapterSha256` is new on every file. Nine distinct adapter digests over 208 goldens: each golden now pins only the module that mounts it. No observation moved. The whole-diff census against origin/main reports exactly four changed keys and nothing else: 208 "adapterSha256": 416 "baseline": 416 "goldenFormatVersion": 416 "recorderSha256": Recorded in place rather than through the README's detached-baseline dance: this branch changes no product file, so its tree at the merge is byte-identical to c6a7216984 under mobile/src, src/shared and the lockfile, and the parity claim stays non-circular. README says so now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): hold the recording drivers to the engine's mutant-seam rule The name scan exempted every `.test.ts` on the ground that a test cannot change a recording. Two of them can: the recording drivers are the recording path. A driver that read the mutant table by path rather than importing it passed both seam checks — the import scan sees no import, and the name scan waved it through as a test: const table = resolve(import.meta.dirname, 'mutants/operation-mutations.ts') console.log(readFileSync(table, 'utf8').length) at the top of `pilot-recordings.test.ts` gave 2 passed before, and after this change fails with ["pilot-recordings.test.ts"]. Only non-driver tests are exempt now. This file lives in `mutants/`, which `recorderSha256` skips, so no golden moves: the recorder suite is green on the existing 208 with zero dirty. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the registry parameter no caller varies `pilotMountAdapters` took `registered` so a caller could mount a different module set; all six callers take the default. The header-digest tests vary the registry through `goldenRecording`, which keeps its own parameter and is where the stub roots need it. Engine source, so `recorderSha256` moves and the goldens follow in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens after the registry parameter came out All 208, `recorderSha256` only. The re-record against the previous commit moves 416 lines, every one of them that field: 416 "recorderSha256": Against origin/main the picture is unchanged from the merge: 208 goldens, 0 added or deleted, 0 non-header lines, and exactly four keys differing — 208 "adapterSha256" 416 "baseline" 416 "goldenFormatVersion" 416 "recorderSha256" Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): wrap the recording README at the width the rest of it uses Seven lines this branch added ran past 100 columns, worst 124. No wording changed. Markdown is outside `recorderSha256`, so no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): name the worktree overlay, not the archive that cannot work `git archive` was offered alongside a detached checkout as a way to lay this branch's recorder over the pinned baseline. It cannot work: the fence in scripts/rpc-recording.mts runs `git diff --quiet ` and an untracked-file check, both of which need a real `.git`. In an archive tree git exits non-zero for lack of a repository and the script reports "Product sources or lockfile differ from the pinned main baseline", which reads as a product mismatch that is not there. The transport agent lost time to exactly that. Names `git worktree add --detach` only, and says what the misleading failure looks like if someone tries an archive anyway. Markdown is outside `recorderSha256`, so no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): close two ways an adapter module escapes its own digest Two holes, one class: the seam was checked by how an import was spelled and by what the register's values evaluated to, never by where they resolve or where they were written. Inward imports: the scan dropped every specifier starting with `..`, so `'../adapters/settings-mount-adapters'` climbed out of the directory and back into it unseen. A reviewer had `new-tab-agent-mount-adapters.ts` project a value read from the settings module, edited that module, and watched the mounted state change while the new-tab adapter digest held. Specifiers now resolve against the directory and anything landing back inside it fails: ["new-tab-agent-mount-adapters.ts imports ../adapters/settings-mount-adapters"] The register: `adapters/mounted-operation-modules.ts` is pinned by nothing — `recorderSha256` skips the directory and `adapterSha256` reads each entry's `source`. An `exposes` written inline there drives the mounted product module with no digest covering it. The same reviewer replaced the new-tab entry's `exposes` with a literal overriding `loadMobileNewTabAgentOptions`; twelve fence tests passed. Both `mounts` and `exposes` must now be identifiers the register imports from that entry's own module: ["new-tab-agent-mount-adapters.ts writes exposes inline instead of importing it"] Checked on the register's syntax, not its values, because an inline literal and an imported binding are indistinguishable once evaluated. Pinning the register in the engine digest would also close it, and is the wrong trade: every domain adding a register line would re-digest all 208 goldens, which is the conflict this PR exists to remove. Keeping the register an index costs nothing and keeps a domain's line local. Both fixes live in a `.test.ts` outside the drivers, so no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): prove the mutant seam from the drivers out, not by spelling The seam rested on a grep for the literal `mutants`, which the exported `MUTANT_DIRECTORY` spells without containing. A reviewer had `pilot-mount-adapters.ts` read the mutant table through that constant and both checks passed. The README's claim — that nothing on the recording path names the directory — was false as written. Three changes, in order of strength: Reachability is now proved forward. The suite walks the static import graph from the two recording drivers and fails if any module under `mutants/` is in it. That answers the real question, what a golden's bytes can depend on, instead of the old inward scan's question, who mentions this directory. Non-emptiness is asserted on both sides so a graph that resolved nothing cannot pass by reaching nothing. The name scan covers both spellings, for paths a module can be read by rather than imported. The reviewer's probe now fails as ["pilot-mount-adapters.ts"]. `MUTANT_DIRECTORY` is no longer exported. Its two consumers were both tests of the digest, and they now spell the path instead, which is strictly better for them: a test that imports the constant follows a rename silently, while one that spells it fails on a rename — and that specific directory name is the whole soundness argument. This edits `recorder-digest.ts`, so the goldens re-record in the next commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the RPC goldens after MUTANT_DIRECTORY stopped being exported All 208, `recorderSha256` only. Against the previous commit the diff is 416 lines and every one of them is that field: 416 "recorderSha256": Against origin/main, unchanged: 208 goldens, 0 added or deleted, 0 non-header lines, four keys differing — 208 "adapterSha256" 416 "baseline" 416 "goldenFormatVersion" 416 "recorderSha256" Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): state the mutant seam's actual argument, and its edge The README claimed nothing on the recording path names `mutants/`. That was the old inward scan's claim and a reviewer falsified it with the exported constant. It now describes what the check does: a forward walk of the import graph from the two recording drivers, plus a name scan in both spellings for read-by-path, plus the constant no longer being exported. It also names the case neither closes — a path assembled from fragments at runtime. Markdown is outside `recorderSha256`, so no golden moves. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): prove the engine/adapter seam in both directions The inward scan only held adapters to the seam. An engine file importing an adapter executes code its own digest skips and that every golden recorded through another domain leaves out of `adapterSha256`, so the register is now the only crossing allowed from the engine side. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): name what the driver walk missed instead of counting it Seeding `seen` with the drivers made the driver-presence check true by construction, and the size bound compared a graph inflated by `typeof import` product modules against a recorder-sized number. Both go; the walk now reports the recording files it failed to reach, which is empty today and names an orphan engine file the moment one appears. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): reflow four paragraphs left ragged by the rewrap Orphan fragments only, no wording change: the golden-schema field list, the mutant-evidence paragraph, the probe-witness sentence and the re-anchor note. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the small-domain call sites before migrating them Thirteen new families cover the files, new-workspace, host-screen, home and worktree call sites step 4 migrates next: ownership capture, the preview loader and its terminal-artifact grant refresh, the artifact save, the tab doc's three shapes, the drawer's execution target and setup hook, the Codex reset-credit probe, the host view settings, the Home stats card and the three workspace catalog reads. Recorded against main's product code, so these are the parity baseline the refactor must not move. Four new adapter modules under adapters/ and no engine edit, so recorderSha256 is unmoved and every existing golden is byte-identical: 40 files added, none changed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the small-domain reads through RpcOperation Thirty-five of the domain's fifty-five raw-port references now go through a declared operation: the files domain's preview, artifact and tab-doc reads and its ownership capture, the New Workspace drawer, the host screen's metadata and view-settings mirror, the Home stats card, and all three workspace catalog reads. No behaviour change, and the oracle says so: zero goldens move. Acceptance is preserved call site by call site, including two that look like defects and stay that way — a refused worktree.listRetiredNames still settles as an empty registry rather than holding the previous names, and a refused ui.get on a null result still throws into the host screen's own catch. Where two call sites disagreed about one method, both policies are named: files.read and files.readPreview throw for a session file tab and skip for the preview screen, repo.hooks throws for task create and skips for the drawer, and status.get now carries a fourth family for the Codex capability probe's object-or-null rule. The drawer's SSH connect, SSH state and agent detection reuse the workspace-create operations the tasks migration already declared rather than restating them. Two things outside the call sites. requestSingleFlight now shares the params optionality rule that request already had, so an all-optional schema such as preflight.check can omit its params on both helpers instead of only one; that is type-level and puts nothing new on the wire. And the retired-names fixture resolved a reply with no `ok`, a shape no host sends, which read as a refusal once the acceptance policy routed on it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drive the image arm of the preview loader The mutation census found two migrated operations that no recording reached: repointing files.readPreview or files.readTerminalArtifactPreview to a wrong method, and swapping either one's acceptance policy, changed nothing any golden observed. Both preview-load scenarios read a text path, so the loader's image branch was migrated with no wire behind it. Two scenarios now read an image path through each arm, and the adapter takes the path from the scenario instead of a constant. All four mutations die on the new goldens. They are recorded from the pinned baseline with this branch's recorder laid over it, so they are main's behaviour and not the migration's: the candidate run against the refactored tree compares clean. The adapter edit re-digests the nineteen goldens mounted through it. The diff is one adapterSha256 line each and no observation moves, which is what pinning the adapter per golden rather than per suite is for. Two casts also take the SAFETY form the house style asks for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the casts the changed-code gate flags Seven type assertions the gate counted as new, all removed rather than silenced where the type system could already answer. `normalizeMobileFilePreviewResponse` narrows on `ok` instead of asserting each arm; the snapshot adapter narrows on the fetch result's own discriminant; and the drawer's two probe reads go through one overloaded member read that keeps their optional-chaining behaviour. The remaining three keep a cast and now carry the rationale on the asserting line. No behaviour change. The two adapter edits re-digest the sixteen goldens mounted through them, one adapterSha256 line each with no observation moved, recorded from the pinned baseline the same way. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop the import the narrowing left behind RpcSuccess is no longer named once the response reads through its own discriminant. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the host screen's worktree mutations Review of #20705 showed the `use-host-worktree-actions.ts` holdout reason was wrong: its only native call is the pinned-id write, and that sits behind `if (hostId)`, so mounting with no hostId never reaches it. Two scenarios in one new family, recorded from the pinned baseline with the call site still on the raw port. The first drives all three sends so the reply matrix covers each method; the second refuses `worktree.rm` to pin the row restore. The adapter is a new module, so no existing golden's `adapterSha256` moves and none of the 250 goldens already here is rewritten. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): send the host list's worktree mutations through RpcOperation Pin, remove and activate move onto operations in host-screen-operations.ts. All three skip on refusal, which is the policy each site already applied by hand: the pin and activate writes swallow everything in a `.catch`, and the remove restores the row on a refused reply. `worktree.set` therefore carries a second policy next to source-control's `worktree.set-review-link`, which throws; both are named. Zero goldens move. The inventory loses use-host-worktree-actions.ts and states the real reason the drawer's repo list stays: it renders the last-visited-repo hook, whose default import of async-storage the recorder's proxy refuses at module load, before the hostId guard the reviewer expected to save it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the worktree-action fixture row and follow the activation scan The fixture row I recorded from had four fields, which `tsc` rejects as a `Worktree`. Filling it out moves the five goldens of this branch's own new family and nothing else; the recorded sends are unchanged, only the projected row is. `mobile-worktree-activation-source.test.ts` scanned the hook for the literal `sendRequest('worktree.activate'`, which the previous commit replaced. It now asserts the operation call and its two flags in the hook, plus the method in host-screen-operations.ts, so the pair still pins the same wire. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): take the five deletions round-1 review asked for - `fileOwnershipRuntimeStatusRead` was `taskRuntimeStatusRead` field for field. It is now a re-export of it. The goldens are keyed on the scenario family, not the operation name, so `matrix-files.mutation-ownership-status.get-1.json` survives unchanged. - `readProbeMember`'s two overloads asserted shapes nothing checked. Gone; the nested read goes through the same single-signature function. - `normalizeMobileFilePreviewResponse` had no product caller. Deleted with its re-export; its twelve assertions now drive the accepted and refused arms directly. - The three inline copies of the accepted-result union are gone. They name each operation's own `interpret` return instead of importing `RpcAcceptedResult`: importing the contract would pull all three call sites into the cast fence, where their existing SAFETY assertions fail it. - `codex-reset-credit-capability-operation.ts` is now `-operations.ts`. No adapter names it, so no golden re-digests. Zero goldens move. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the skip verdict its own transport module The three settle helpers typed their interpret parameter as `ReturnType.interpret>`, naming one operation while being called with others whose verdicts happen to be structurally identical. Narrowing a named reader would have silently retyped unrelated helpers. `RpcAcceptedResult` moves to `rpc-accepted-result.ts`, outside the cast fence's three region seeds, so a consumer can name the verdict without becoming an operation implementation. `rpc-operation-contract.ts` re-exports it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): drop three sender aliases nothing imports MobileHostScreenRpcSender, MobileNewWorkspaceRpcSender and MobileWorktreeCatalogRpcSender each appeared only in the file that declared them. A named type with no consumer is a cost, not a boundary. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say why a holdout is a holdout in the port inventory A site can be pointed at an operation without being mountable, so "cannot be migrated" was the wrong claim. The rule is record-first: the golden recorded against the old code is the only parity proof, so a site the recorder cannot mount cannot be recorded, and unrecorded sites do not migrate. Stated once in the list's header. codex-reset-credit.ts loads fine under the module loader; probed it, and its attempt-journal access throws on async-storage at call time before the send, with no guard to skip it. The old comment described it as a storage read around the send. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): state the status.get policies without counting them "the fourth policy on this method", "the first of two" and "the second of two" were already wrong after round 1 folded the files family's status read into the tasks one. Each comment now states its own invariant, which no later policy can invalidate. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): stop the activation scan claiming to pin the wire `expect(operations).toContain("method: 'worktree.activate'")` passes whichever operation carries that method, so it survives swapping worktreePinWrite's and worktreeActivate's methods. tsc and the host-worktree-actions-pin-open-delete golden both fail on that swap; the scan keeps only what it can prove, which is that the callback sends through worktreeActivate with the two flags. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../goldens/components-codex-capability.json | 80 ++ .../goldens/components-setup-ask.json | 149 +++ .../goldens/components-target-local.json | 142 ++ .../goldens/components-target-ssh.json | 258 ++++ .../goldens/files-ownership-local.json | 123 ++ .../goldens/files-ownership-ssh.json | 216 +++ .../files-preview-artifact-direct.json | 96 ++ .../goldens/files-preview-artifact-image.json | 93 ++ .../goldens/files-preview-grant-refresh.json | 240 ++++ .../goldens/files-preview-worktree-image.json | 92 ++ .../goldens/files-preview-worktree.json | 95 ++ .../goldens/files-save-blind.json | 87 ++ .../goldens/files-save-verified.json | 174 +++ .../goldens/files-tab-doc-shapes.json | 232 ++++ .../goldens/home-host-stats.json | 133 ++ .../goldens/host-view-settings-sync.json | 224 ++++ ...host-worktree-actions-pin-open-delete.json | 368 ++++++ .../goldens/host-worktree-delete-refused.json | 229 ++++ ...s.codex-reset-capability-status.get-1.json | 539 ++++++++ ...target-local-preflight.detectagents-1.json | 605 +++++++++ ...target-preflight.detectremoteagents-1.json | 729 +++++++++++ ...onents.execution-target-ssh.connect-1.json | 784 +++++++++++ ...nents.execution-target-ssh.getstate-1.json | 804 ++++++++++++ ...-components.setup-script-repo.hooks-1.json | 599 +++++++++ ...les.mutation-ownership-ssh.getstate-1.json | 746 +++++++++++ ...files.mutation-ownership-status.get-1.json | 746 +++++++++++ ...es.mutation-ownership-worktree.show-1.json | 746 +++++++++++ ...iew-load-files.readterminalartifact-1.json | 649 +++++++++ ...iew-load-files.readterminalartifact-2.json | 747 +++++++++++ ...view-load-files.resolveterminalpath-1.json | 757 +++++++++++ ...iew-save-files.readterminalartifact-1.json | 681 ++++++++++ ...ew-save-files.writeterminalartifact-1.json | 691 ++++++++++ .../matrix-files.tab-doc-files.read-1.json | 861 ++++++++++++ ...rix-files.tab-doc-files.readpreview-1.json | 818 ++++++++++++ .../matrix-files.tab-doc-git.diff-1.json | 816 ++++++++++++ ...atrix-home.host-stats-stats.summary-1.json | 656 ++++++++++ .../matrix-host.view-settings-ui.get-1.json | 779 +++++++++++ .../matrix-host.view-settings-ui.set-1.json | 804 ++++++++++++ ....worktree-actions-worktree.activate-1.json | 1158 +++++++++++++++++ ...x-host.worktree-actions-worktree.rm-1.json | 1078 +++++++++++++++ ...-host.worktree-actions-worktree.set-1.json | 1148 ++++++++++++++++ ...rktree.catalog-snapshot-worktree.ps-1.json | 715 ++++++++++ ...x-worktree.home-catalog-worktree.ps-1.json | 665 ++++++++++ ...red-names-worktree.listretirednames-1.json | 583 +++++++++ .../goldens/worktree-catalog-snapshot.json | 164 +++ .../goldens/worktree-home-catalog.json | 165 +++ .../goldens/worktree-retired-names.json | 133 ++ mobile/rpc-foundation/pilot-scenarios.json | 975 ++++++++++++++ ...odex-reset-credit-capability-operations.ts | 33 + .../codex-reset-credit-capability.ts | 12 +- .../components/new-workspace-operations.ts | 40 + .../use-new-workspace-execution-target.ts | 44 +- .../use-new-workspace-runtime-context.ts | 48 +- .../use-new-workspace-setup-script.ts | 14 +- .../files/mobile-file-mutation-ownership.ts | 69 +- .../files/mobile-file-ownership-operations.ts | 35 + .../files/mobile-file-preview-operations.ts | 83 ++ .../files/mobile-file-preview-request.test.ts | 45 +- .../src/files/mobile-file-preview-request.ts | 185 ++- .../src/files/mobile-file-preview-response.ts | 19 +- .../files/mobile-file-tab-doc-operations.ts | 47 + mobile/src/files/mobile-file-tab-doc.ts | 36 +- .../mobile-terminal-artifact-grant-refresh.ts | 27 +- .../src/home/mobile-home-host-operations.ts | 17 + mobile/src/home/mobile-home-host-requests.ts | 29 +- .../src/host-screen/host-screen-operations.ts | 106 ++ .../src/host-screen/use-host-repo-metadata.ts | 56 +- .../src/host-screen/use-host-view-settings.ts | 16 +- .../host-screen/use-host-worktree-actions.ts | 13 +- .../tasks/mobile-task-runtime-operations.ts | 4 +- .../mobile-workspace-create-operations.ts | 4 +- .../adapters/file-request-mount-adapters.ts | 108 ++ .../adapters/host-screen-mount-adapters.ts | 111 ++ .../host-worktree-action-mount-adapters.ts | 90 ++ .../adapters/mounted-operation-modules.ts | 15 +- .../adapters/new-workspace-mount-adapters.ts | 107 ++ .../worktree-catalog-mount-adapters.ts | 89 ++ mobile/src/transport/rpc-accepted-result.ts | 8 + .../src/transport/rpc-operation-contract.ts | 8 +- mobile/src/transport/rpc-operation.ts | 25 +- .../unvalidated-rpc-request-port-inventory.ts | 53 +- .../src/worktree/home-host-worktree-fetch.ts | 13 +- .../mobile-worktree-activation-source.test.ts | 5 +- .../use-retired-worktree-names.test.tsx | 7 +- .../worktree/use-retired-worktree-names.ts | 14 +- .../worktree/worktree-catalog-operations.ts | 35 + .../worktree-catalog-snapshot-client.ts | 17 +- 87 files changed, 25748 insertions(+), 311 deletions(-) create mode 100644 mobile/rpc-foundation/goldens/components-codex-capability.json create mode 100644 mobile/rpc-foundation/goldens/components-setup-ask.json create mode 100644 mobile/rpc-foundation/goldens/components-target-local.json create mode 100644 mobile/rpc-foundation/goldens/components-target-ssh.json create mode 100644 mobile/rpc-foundation/goldens/files-ownership-local.json create mode 100644 mobile/rpc-foundation/goldens/files-ownership-ssh.json create mode 100644 mobile/rpc-foundation/goldens/files-preview-artifact-direct.json create mode 100644 mobile/rpc-foundation/goldens/files-preview-artifact-image.json create mode 100644 mobile/rpc-foundation/goldens/files-preview-grant-refresh.json create mode 100644 mobile/rpc-foundation/goldens/files-preview-worktree-image.json create mode 100644 mobile/rpc-foundation/goldens/files-preview-worktree.json create mode 100644 mobile/rpc-foundation/goldens/files-save-blind.json create mode 100644 mobile/rpc-foundation/goldens/files-save-verified.json create mode 100644 mobile/rpc-foundation/goldens/files-tab-doc-shapes.json create mode 100644 mobile/rpc-foundation/goldens/home-host-stats.json create mode 100644 mobile/rpc-foundation/goldens/host-view-settings-sync.json create mode 100644 mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json create mode 100644 mobile/rpc-foundation/goldens/host-worktree-delete-refused.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json create mode 100644 mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json create mode 100644 mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json create mode 100644 mobile/rpc-foundation/goldens/worktree-home-catalog.json create mode 100644 mobile/rpc-foundation/goldens/worktree-retired-names.json create mode 100644 mobile/src/components/codex-reset-credit-capability-operations.ts create mode 100644 mobile/src/components/new-workspace-operations.ts create mode 100644 mobile/src/files/mobile-file-ownership-operations.ts create mode 100644 mobile/src/files/mobile-file-preview-operations.ts create mode 100644 mobile/src/files/mobile-file-tab-doc-operations.ts create mode 100644 mobile/src/home/mobile-home-host-operations.ts create mode 100644 mobile/src/host-screen/host-screen-operations.ts create mode 100644 mobile/src/test-support/rpc-recording/adapters/file-request-mount-adapters.ts create mode 100644 mobile/src/test-support/rpc-recording/adapters/host-screen-mount-adapters.ts create mode 100644 mobile/src/test-support/rpc-recording/adapters/host-worktree-action-mount-adapters.ts create mode 100644 mobile/src/test-support/rpc-recording/adapters/new-workspace-mount-adapters.ts create mode 100644 mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts create mode 100644 mobile/src/transport/rpc-accepted-result.ts create mode 100644 mobile/src/worktree/worktree-catalog-operations.ts diff --git a/mobile/rpc-foundation/goldens/components-codex-capability.json b/mobile/rpc-foundation/goldens/components-codex-capability.json new file mode 100644 index 00000000000..90c6e92139a --- /dev/null +++ b/mobile/rpc-foundation/goldens/components-codex-capability.json @@ -0,0 +1,80 @@ +{ + "operation": "components.codex-reset-capability", + "family": "components.codex-reset-capability", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "88570b9d2376863c7f88d7ed8c745a5fb771deddbc7409f8944fa861dd4bdce9", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "578b9d38ecc7": { + "supported": true + }, + "6a0093a8288b": { + "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": ["accounts.codex-reset-credit.v1"] + } + } + } + }, + "84e5ca07cb7a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": true + } + }, + "recording": { + "scenario": "components-codex-capability", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["6a0093a8288b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "84e5ca07cb7a" + }, + "state": "578b9d38ecc7", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/components-setup-ask.json b/mobile/rpc-foundation/goldens/components-setup-ask.json new file mode 100644 index 00000000000..894be173ac0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/components-setup-ask.json @@ -0,0 +1,149 @@ +{ + "operation": "components.setup-script", + "family": "components.setup-script", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "d4052f119c7ed68dc922c8beeb0074701f1532b10e48e284fb71aa165a17e437", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "28e75475e9e0": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "3515a8adcd6d": { + "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-1", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": "pnpm install" + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "$rpc": "null" + }, + "source": "repo" + } + } + } + }, + "5d1cf72f4e12": { + "advanced": true, + "command": "pnpm install", + "run": true, + "runPolicy": "ask", + "source": "repo", + "trust": { + "$rpc": "null" + } + }, + "80cf8444e458": { + "advanced": false, + "command": { + "$rpc": "null" + }, + "run": true, + "runPolicy": "run-by-default", + "source": { + "$rpc": "null" + }, + "trust": { + "$rpc": "null" + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f5dc0ce1e7b8": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + } + }, + "recording": { + "scenario": "components-setup-ask", + "checkpoints": [ + { + "id": "hooks-pending", + "observation": { + "sender": ["28e75475e9e0"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["3515a8adcd6d"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "5d1cf72f4e12", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/components-target-local.json b/mobile/rpc-foundation/goldens/components-target-local.json new file mode 100644 index 00000000000..027ee3ffda7 --- /dev/null +++ b/mobile/rpc-foundation/goldens/components-target-local.json @@ -0,0 +1,142 @@ +{ + "operation": "components.execution-target-local", + "family": "components.execution-target-local", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "2e0d3021621698b63117e250dd5e9762b5bfb3dc1911e27c510e9539bd2ee6c9", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e4520fe6576": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": true, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": { + "$rpc": "null" + } + } + }, + "3579737ce1a6": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "6806cee7c59f": { + "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": ["claude"] + } + } + }, + "986a776213e8": { + "detected": ["claude"], + "gate": { + "connectInProgress": true, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": { + "$rpc": "null" + } + } + }, + "cf32edc950ac": { + "name": "preflight.detectAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "components-target-local", + "checkpoints": [ + { + "id": "detect-pending", + "observation": { + "sender": ["3579737ce1a6"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "1e4520fe6576", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["6806cee7c59f"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "986a776213e8", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/components-target-ssh.json b/mobile/rpc-foundation/goldens/components-target-ssh.json new file mode 100644 index 00000000000..41d28f2d1b9 --- /dev/null +++ b/mobile/rpc-foundation/goldens/components-target-ssh.json @@ -0,0 +1,258 @@ +{ + "operation": "components.execution-target", + "family": "components.execution-target", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "82b891c7a7a2f255e2d22a372ee6112c9cc1f650259244e87f2e8c1356e97e5f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0a7094a9a9ac": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "4e2e9a890ced": { + "detected": ["codex"], + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "57095302d8c1": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "6004e75ef39e": { + "name": "preflight.detectRemoteAgents#2", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "66a99391260b": { + "name": "preflight.detectRemoteAgents#2", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "81c9c204b647": { + "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-3", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "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" + } + } + } + } + }, + "9a892112da5b": { + "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-2", + "ok": true, + "result": ["codex"] + } + } + }, + "ca123825be51": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "d23b91bd7660": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": true, + "status": { + "$rpc": "null" + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + } + }, + "recording": { + "scenario": "components-target-ssh", + "checkpoints": [ + { + "id": "state-pending", + "observation": { + "sender": ["ca123825be51"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "d23b91bd7660", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-ownership-local.json b/mobile/rpc-foundation/goldens/files-ownership-local.json new file mode 100644 index 00000000000..816031ef32b --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-ownership-local.json @@ -0,0 +1,123 @@ +{ + "operation": "files.mutation-ownership", + "family": "files.mutation-ownership", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "8d24f52eb4194c3bc5d9f0dcabade6d7a09c066f79f911657647ed21dbecb3b1", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "548f05412e41": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "expectedExecutionHostId": "local" + } + }, + "8bdc2aec524d": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "hostId": "local" + } + } + } + } + }, + "9199aee60486": { + "name": "worktree.show#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" + }, + "a56852d6836b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + } + }, + "bab8756fa040": { + "ownership": { + "expectedExecutionHostId": "local" + } + } + }, + "recording": { + "scenario": "files-ownership-local", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["a56852d6836b", "8bdc2aec524d"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "548f05412e41" + }, + "state": "bab8756fa040", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-ownership-ssh.json b/mobile/rpc-foundation/goldens/files-ownership-ssh.json new file mode 100644 index 00000000000..aa295d7d618 --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-ownership-ssh.json @@ -0,0 +1,216 @@ +{ + "operation": "files.mutation-ownership", + "family": "files.mutation-ownership", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "4cd0be3a1c338b4b68211c717fd10738653c553fdb3b072db6706ff8175a8bd1", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "29bfbe94cca9": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "518ec57c381a": { + "ownership": "uncaptured" + }, + "6116946241ca": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "state": { + "connectionGeneration": 3, + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "target-1" + } + } + } + } + }, + "6ef43f81f7e3": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "hostId": "ssh:target-1" + } + } + } + } + }, + "9199aee60486": { + "name": "worktree.show#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a0341e6a5d84": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"target-1\"}}" + }, + "a56852d6836b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + } + }, + "bc119660f0c1": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "bd84dadd27c7": { + "ownership": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + } + }, + "recording": { + "scenario": "files-ownership-ssh", + "checkpoints": [ + { + "id": "status-pending", + "observation": { + "sender": ["bc119660f0c1"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9270aeb7d9c6" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "6116946241ca"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "29bfbe94cca9" + }, + "state": "bd84dadd27c7", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json b/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json new file mode 100644 index 00000000000..c48ebdc256b --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-direct.json @@ -0,0 +1,96 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "dd8b7a0916a84c7d763a163759c02210ae99f8fbccfccaa98796b8610c5da97c", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "194fabd9b9d8": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 5, + "content": "hello", + "truncated": false + } + } + } + }, + "500d95d47092": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "784ea351e5b2": { + "preview": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + } + }, + "recording": { + "scenario": "files-preview-artifact-direct", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["194fabd9b9d8"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "500d95d47092" + }, + "state": "784ea351e5b2", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-artifact-image.json b/mobile/rpc-foundation/goldens/files-preview-artifact-image.json new file mode 100644 index 00000000000..6cd8c6f0f61 --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-artifact-image.json @@ -0,0 +1,93 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "d15c1f4e0f95b5c49a8f889d2d37458225293d7306c8439690d972d2df4c29c0", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "25b0318e985e": { + "name": "files.readTerminalArtifactPreview#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifactPreview" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/shot.png", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "4a07826edb3b": { + "name": "files.readTerminalArtifactPreview#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifactPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/shot.png\",\"grantId\":\"grant-1\"}}" + }, + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + } + }, + "recording": { + "scenario": "files-preview-artifact-image", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["25b0318e985e"], + "payloads": ["4a07826edb3b"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json b/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json new file mode 100644 index 00000000000..5fd8b8d4c59 --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-grant-refresh.json @@ -0,0 +1,240 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "055f3b45442c1736f10ee98c493e2ece1885fdb68d925ee627e2ba20853537e0", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "25b0d1737c71": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "terminal_file_grant_expired", + "message": "Grant expired" + }, + "id": "frame-1", + "ok": false + } + } + }, + "3c5492779d85": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "exists": true, + "isDirectory": false, + "openTarget": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "kind": "absolute-file" + } + } + } + } + }, + "500d95d47092": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "5f446c109a9a": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "byteLength": 5, + "content": "hello", + "truncated": false + } + } + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "784ea351e5b2": { + "preview": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "813fe48569a6": { + "name": "artifact-source-refreshed", + "value": { + "absolutePath": "/logs/run.txt", + "cwd": "/logs", + "grantId": "grant-2", + "pathText": "run.txt", + "source": "terminalArtifact", + "terminalHandle": "terminal-1", + "worktreeId": "workspace-1" + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a56ffbdd5bf": { + "name": "files.resolveTerminalPath#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.resolveTerminalPath\",\"params\":{\"worktree\":\"id:workspace-1\",\"pathText\":\"run.txt\",\"cwd\":\"/logs\",\"terminal\":\"terminal-1\"}}" + }, + "c283e01480f7": { + "name": "files.readTerminalArtifact#2", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-2\"}}" + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + } + }, + "recording": { + "scenario": "files-preview-grant-refresh", + "checkpoints": [ + { + "id": "read-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "5f446c109a9a"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "500d95d47092" + }, + "state": "784ea351e5b2", + "effects": ["813fe48569a6"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree-image.json b/mobile/rpc-foundation/goldens/files-preview-worktree-image.json new file mode 100644 index 00000000000..bb44448008f --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-worktree-image.json @@ -0,0 +1,92 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "0ffaffb472b663e08a42317d799a2a000211dda5b127fb21cc64f04f73800130", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "3acec737cb08": { + "name": "files.readPreview#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "7659b8b575da": { + "preview": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "f6564bdb4e19": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + } + } + }, + "recording": { + "scenario": "files-preview-worktree-image", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["f6564bdb4e19"], + "payloads": ["3acec737cb08"], + "settlements": { + "load": "eee847a9d90d" + }, + "state": "7659b8b575da", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-preview-worktree.json b/mobile/rpc-foundation/goldens/files-preview-worktree.json new file mode 100644 index 00000000000..b1a1ed527b1 --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-preview-worktree.json @@ -0,0 +1,95 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "bcba4a7d9d929078c5ed80fc7e1acd45859d0b4c559767a919b0396dc6a70a3e", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02ea3f503180": { + "name": "files.read#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "3f8bf3069e3d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "47ef2e397e18": { + "preview": { + "byteLength": 8, + "content": "# readme", + "kind": "markdown", + "status": "ready", + "truncated": false + } + }, + "9babe9503a83": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + } + }, + "recording": { + "scenario": "files-preview-worktree", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["9babe9503a83"], + "payloads": ["02ea3f503180"], + "settlements": { + "load": "3f8bf3069e3d" + }, + "state": "47ef2e397e18", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-save-blind.json b/mobile/rpc-foundation/goldens/files-save-blind.json new file mode 100644 index 00000000000..2603c576c0b --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-save-blind.json @@ -0,0 +1,87 @@ +{ + "operation": "files.preview-save", + "family": "files.preview-save", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "8b5c3e87d989966d7b252f19a537040cd5078ba9355a824e7e49af3424390a3e", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "39c6d10eb5a5": { + "name": "files.writeTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.writeTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\",\"content\":\"next\"}}" + }, + "54a6055a16b5": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "status": "saved" + } + }, + "936b6553a1e7": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "b3f873eb7d0c": { + "saved": { + "status": "saved" + } + } + }, + "recording": { + "scenario": "files-save-blind", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["936b6553a1e7"], + "payloads": ["39c6d10eb5a5"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-save-verified.json b/mobile/rpc-foundation/goldens/files-save-verified.json new file mode 100644 index 00000000000..eb50e796ccd --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-save-verified.json @@ -0,0 +1,174 @@ +{ + "operation": "files.preview-save", + "family": "files.preview-save", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "6e124d16173074d851d58593b20b25889ed13a3d8021c08fbed53b85a7d3196e", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "54a6055a16b5": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "status": "saved" + } + }, + "7875007ef392": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "935100df69e4": { + "saved": "unsaved" + }, + "a3886e3a9791": { + "name": "files.writeTerminalArtifact#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.writeTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\",\"content\":\"next\"}}" + }, + "b3f873eb7d0c": { + "saved": { + "status": "saved" + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e391aec81b96": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 4, + "content": "base", + "truncated": false + } + } + } + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + } + }, + "recording": { + "scenario": "files-save-verified", + "checkpoints": [ + { + "id": "verify-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "9270aeb7d9c6" + }, + "state": "935100df69e4", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["e391aec81b96", "7875007ef392"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json b/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json new file mode 100644 index 00000000000..17f51495f0b --- /dev/null +++ b/mobile/rpc-foundation/goldens/files-tab-doc-shapes.json @@ -0,0 +1,232 @@ +{ + "operation": "files.tab-doc", + "family": "files.tab-doc", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "b636221f719dae19a3af6772b687b0bcb300c9910645d23e98c309578ec1c5c5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02ea3f503180": { + "name": "files.read#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "323bf6059754": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "content": "aGk=", + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "5c610ebe58ed": { + "name": "files.readPreview#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "9babe9503a83": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "b5c68b76c498": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "c8fbe8972330": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "kind": "text", + "modifiedContent": "b\n", + "originalContent": "a\n" + } + } + } + }, + "ed33ecdb4e8e": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "fad4ca11a316": { + "name": "git.diff#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"git.diff\",\"params\":{\"worktree\":\"id:workspace-1\",\"filePath\":\"docs/readme.md\",\"staged\":true}}" + }, + "ffe1c534d459": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + } + } + }, + "recording": { + "scenario": "files-tab-doc-shapes", + "checkpoints": [ + { + "id": "settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "ed33ecdb4e8e", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/home-host-stats.json b/mobile/rpc-foundation/goldens/home-host-stats.json new file mode 100644 index 00000000000..4d240c0b665 --- /dev/null +++ b/mobile/rpc-foundation/goldens/home-host-stats.json @@ -0,0 +1,133 @@ +{ + "operation": "home.host-stats", + "family": "home.host-stats", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", + "scenarioSha256": "bd5f4e5f24a29d96c4c98950691c6571918332f71ebb1f29ee97a9abc857ac29", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0632d58191fb": { + "name": "stats", + "value": { + "host-1": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + } + }, + "0ebcc6f6a4cb": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + } + } + }, + "44136fa355b3": {}, + "7bf81b1e94c5": { + "name": "stats.summary#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"stats.summary\"}" + }, + "9a84a7559023": { + "host-1": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + }, + "a392ac528c2b": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "home-host-stats", + "checkpoints": [ + { + "id": "stats-pending", + "observation": { + "sender": ["a392ac528c2b"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["0ebcc6f6a4cb"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "9a84a7559023", + "effects": ["0632d58191fb"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/host-view-settings-sync.json b/mobile/rpc-foundation/goldens/host-view-settings-sync.json new file mode 100644 index 00000000000..6529922751e --- /dev/null +++ b/mobile/rpc-foundation/goldens/host-view-settings-sync.json @@ -0,0 +1,224 @@ +{ + "operation": "host.view-settings", + "family": "host.view-settings", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", + "scenarioSha256": "1ee6031227fa3efd5b36841afa60f7b2264eacdd9c6126e5851d5acb39ffaadd", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "292b632037a0": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"sortBy\":\"name\"}}" + }, + "3059ce80d86a": { + "name": "collapsedGroups", + "value": [] + }, + "34925f64c9a6": { + "name": "sortMode", + "value": "name" + }, + "5907841fc56d": { + "name": "ui.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.get\"}" + }, + "5fbdd64c75bc": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "78f2fbcd0185": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "7eaddd04bcdd": { + "name": "workspaceStatuses", + "value": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9527461faeb1": { + "name": "filters", + "value": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + } + }, + "a424515cabc9": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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": { + "ui": { + "groupBy": "repo", + "hideSleepingWorkspaces": true, + "sortBy": "name" + } + } + } + } + }, + "ba2035345a68": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + }, + "groupMode": "repo", + "sortMode": "name", + "statuses": [] + }, + "bbdab1a7d122": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": false + }, + "groupMode": "none", + "sortMode": "recent", + "statuses": [] + }, + "e9a3acf203c6": { + "name": "groupMode", + "value": "repo" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "host-view-settings-sync", + "checkpoints": [ + { + "id": "ui-pending", + "observation": { + "sender": ["5fbdd64c75bc"], + "payloads": ["5907841fc56d"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "9270aeb7d9c6" + }, + "state": "bbdab1a7d122", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["a424515cabc9", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json b/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json new file mode 100644 index 00000000000..4faef7d28c0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/host-worktree-actions-pin-open-delete.json @@ -0,0 +1,368 @@ +{ + "operation": "host.worktree-actions", + "family": "host.worktree-actions", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", + "scenarioSha256": "720add498c79425ca8efc9764fd5d8307fe33bc891842604cfc899f770b79811", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "04938673cbf5": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "0ebfc1859f18": { + "name": "pinnedIds", + "value": ["wt-1"] + }, + "2b635c2a4fbb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "4caf7515e224": { + "name": "worktree.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.set\",\"params\":{\"worktree\":\"id:wt-1\",\"isPinned\":true}}" + }, + "56b6d4fb8c56": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "5c186d67b84c": { + "name": "lastKnownWorktrees", + "value": [] + }, + "69d698d4f352": { + "name": "worktree.rm#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.rm\",\"params\":{\"worktree\":\"id:wt-1\",\"force\":true}}" + }, + "6cfdae6737f2": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "6e959a9dd70e": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [], + "optimisticActiveWorktreeIdentity": "|wt-1", + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a9b0d6699b2": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "b1509905fc66": { + "name": "optimisticActiveWorktreeIdentity", + "value": "|wt-1" + }, + "bf2b36bda2d2": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "c3eecb0c6e96": { + "name": "worktree.activate#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.activate\",\"params\":{\"worktree\":\"id:wt-1\",\"notifyClients\":false,\"navigation\":\"caller\"}}" + }, + "cbf8280fdbe7": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "d440bd87d1ce": { + "name": "worktrees", + "value": [] + }, + "e3e3c397a66a": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "host-worktree-actions-pin-open-delete", + "checkpoints": [ + { + "id": "pin-optimistic", + "observation": { + "sender": ["bf2b36bda2d2"], + "payloads": ["4caf7515e224"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a" + }, + "state": "9a9b0d6699b2", + "effects": ["cbf8280fdbe7", "6cfdae6737f2", "0ebfc1859f18"] + } + }, + { + "id": "delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json b/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json new file mode 100644 index 00000000000..a5267cbcdf0 --- /dev/null +++ b/mobile/rpc-foundation/goldens/host-worktree-delete-refused.json @@ -0,0 +1,229 @@ +{ + "operation": "host.worktree-actions", + "family": "host.worktree-actions", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", + "scenarioSha256": "5dd5e7eabaabba1e471b13958f59891c3b28f553087c96315598c83a14ded7e7", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "3fd02e693647": { + "name": "worktree.rm#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.rm\",\"params\":{\"worktree\":\"id:wt-1\",\"force\":true}}" + }, + "5c186d67b84c": { + "name": "lastKnownWorktrees", + "value": [] + }, + "8056533b940f": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": [], + "routeActionState": {}, + "worktrees": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a7c564546e94": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "bcb66b345fcd": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": [], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "c777b17081dd": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "d440bd87d1ce": { + "name": "worktrees", + "value": [] + }, + "e3e3c397a66a": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "e97d1f006b72": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "worktree_busy", + "message": "Worktree is busy" + }, + "id": "frame-1", + "ok": false + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "host-worktree-delete-refused", + "checkpoints": [ + { + "id": "delete-optimistic", + "observation": { + "sender": ["e3e3c397a66a"], + "payloads": ["3fd02e693647"], + "settlements": { + "mount": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "8056533b940f", + "effects": ["d440bd87d1ce", "5c186d67b84c"] + } + }, + { + "id": "restored", + "observation": { + "sender": ["e97d1f006b72"], + "payloads": ["3fd02e693647"], + "settlements": { + "mount": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "bcb66b345fcd", + "effects": ["d440bd87d1ce", "5c186d67b84c", "a7c564546e94", "c777b17081dd"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json new file mode 100644 index 00000000000..321a61e6b23 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.codex-reset-capability-status.get-1.json @@ -0,0 +1,539 @@ +{ + "operation": "components.codex-reset-capability", + "family": "components.codex-reset-capability", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "06c2ad6d4b464f889a640be7a238f6d0ff7c54b0e93fb5ea22aaa856dadb0336", + "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" + } + } + } + }, + "578b9d38ecc7": { + "supported": true + }, + "6a0093a8288b": { + "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": ["accounts.codex-reset-credit.v1"] + } + } + } + }, + "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 + } + } + }, + "7ed3d39f0607": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": false + }, + "84e5ca07cb7a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": true + }, + "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 + } + } + }, + "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 + } + } + }, + "ece4ea3ed179": { + "supported": false + } + }, + "recording": { + "scenario": "matrix-components.codex-reset-capability-status.get-1", + "checkpoints": [ + { + "id": "components-codex-capability.normal:settled", + "observation": { + "sender": ["6a0093a8288b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "84e5ca07cb7a" + }, + "state": "578b9d38ecc7", + "effects": [] + } + }, + { + "id": "components-codex-capability.result-absent:settled", + "observation": { + "sender": ["7d3dd7f9381b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.result-null:settled", + "observation": { + "sender": ["88200d49083c"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.inner-ok-missing:settled", + "observation": { + "sender": ["4451bb95a76e"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.inner-false-string-error:settled", + "observation": { + "sender": ["944bf432f199"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.inner-false-object-error:settled", + "observation": { + "sender": ["89236e432861"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.outer-refused:settled", + "observation": { + "sender": ["16cd464bf664"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.outer-refused-no-message:settled", + "observation": { + "sender": ["9cdf3c107e7b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.method-not-found:settled", + "observation": { + "sender": ["c71b2f8a6993"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.transport-rejection:settled", + "observation": { + "sender": ["de87f6266897"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + }, + { + "id": "components-codex-capability.transport-rejection-no-message:settled", + "observation": { + "sender": ["2698c9770ad3"], + "payloads": ["1e5b32902af7"], + "settlements": { + "probe": "7ed3d39f0607" + }, + "state": "ece4ea3ed179", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json new file mode 100644 index 00000000000..bd210517b12 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-local-preflight.detectagents-1.json @@ -0,0 +1,605 @@ +{ + "operation": "components.execution-target-local", + "family": "components.execution-target-local", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "f88ee2f5d19b19cc53dca5180a9b5936a13a00a82e8a4636a5e895262b669dec", + "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 + } + } + }, + "1e4520fe6576": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": true, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": { + "$rpc": "null" + } + } + }, + "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" + } + } + } + }, + "3579737ce1a6": { + "name": "preflight.detectAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectAgents" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "365a6864b043": { + "detected": [], + "gate": { + "connectInProgress": true, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": { + "$rpc": "null" + } + } + }, + "6806cee7c59f": { + "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": ["claude"] + } + } + }, + "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 + } + } + }, + "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 + } + } + }, + "986a776213e8": { + "detected": ["claude"], + "gate": { + "connectInProgress": true, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": { + "$rpc": "null" + } + } + }, + "cf32edc950ac": { + "name": "preflight.detectAgents#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "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-components.execution-target-local-preflight.detectagents-1", + "checkpoints": [ + { + "id": "components-target-local.prelude:detect-pending", + "observation": { + "sender": ["3579737ce1a6"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "1e4520fe6576", + "effects": [] + } + }, + { + "id": "components-target-local.normal:settled", + "observation": { + "sender": ["6806cee7c59f"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "986a776213e8", + "effects": [] + } + }, + { + "id": "components-target-local.result-absent:settled", + "observation": { + "sender": ["6e5fcf24648d"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.result-null:settled", + "observation": { + "sender": ["1317fc33bdbe"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.inner-ok-missing:settled", + "observation": { + "sender": ["327b46fb8bef"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.inner-false-string-error:settled", + "observation": { + "sender": ["0846bea730cf"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.inner-false-object-error:settled", + "observation": { + "sender": ["00d70c40c34c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.outer-refused:settled", + "observation": { + "sender": ["fb640b2bca4c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.outer-refused-no-message:settled", + "observation": { + "sender": ["163b91b6fe9c"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.method-not-found:settled", + "observation": { + "sender": ["87d7d24a30d2"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.transport-rejection:settled", + "observation": { + "sender": ["fbb9eef78275"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + }, + { + "id": "components-target-local.transport-rejection-no-message:settled", + "observation": { + "sender": ["70d128c20ae4"], + "payloads": ["cf32edc950ac"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "365a6864b043", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json new file mode 100644 index 00000000000..6d1e1894f2f --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-preflight.detectremoteagents-1.json @@ -0,0 +1,729 @@ +{ + "operation": "components.execution-target", + "family": "components.execution-target", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "79a49cddf66935007afb9be8a30593b778f02237894e5fd4d2898b526fc125df", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "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 + } + } + }, + "0a7094a9a9ac": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "245e68137e04": { + "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-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "4e2e9a890ced": { + "detected": ["codex"], + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "51d7ac902696": { + "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-2", + "ok": false + } + } + }, + "57095302d8c1": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "6004e75ef39e": { + "name": "preflight.detectRemoteAgents#2", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "65a3db621845": { + "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-2", + "ok": false + } + } + }, + "66a99391260b": { + "name": "preflight.detectRemoteAgents#2", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "737995ed36c3": { + "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-2", + "ok": false + } + } + }, + "75cd96280963": { + "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-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "81c9c204b647": { + "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-3", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "ssh-1" + } + } + } + } + }, + "88ecd0c754ca": { + "detected": [], + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "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" + } + } + } + } + }, + "8ce8dae8c036": { + "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-2", + "ok": true + } + } + }, + "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 + } + } + }, + "9a892112da5b": { + "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-2", + "ok": true, + "result": ["codex"] + } + } + }, + "c0d4a122ea86": { + "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-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "ca123825be51": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "ce1d236eece4": { + "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-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "d23b91bd7660": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": true, + "status": { + "$rpc": "null" + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + } + }, + "recording": { + "scenario": "matrix-components.execution-target-preflight.detectremoteagents-1", + "checkpoints": [ + { + "id": "components-target-ssh.prelude:state-pending", + "observation": { + "sender": ["ca123825be51"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "d23b91bd7660", + "effects": [] + } + }, + { + "id": "components-target-ssh.normal:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-absent:settled", + "observation": { + "sender": ["89aa7a3bd619", "8ce8dae8c036", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-null:settled", + "observation": { + "sender": ["89aa7a3bd619", "75cd96280963", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["89aa7a3bd619", "ce1d236eece4", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["89aa7a3bd619", "245e68137e04", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["89aa7a3bd619", "c0d4a122ea86", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused:settled", + "observation": { + "sender": ["89aa7a3bd619", "65a3db621845", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["89aa7a3bd619", "51d7ac902696", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.method-not-found:settled", + "observation": { + "sender": ["89aa7a3bd619", "737995ed36c3", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection:settled", + "observation": { + "sender": ["89aa7a3bd619", "07d4c9b0eaf2", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["89aa7a3bd619", "95dee1165f95", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "88ecd0c754ca", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json new file mode 100644 index 00000000000..f9ce5a35c53 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.connect-1.json @@ -0,0 +1,784 @@ +{ + "operation": "components.execution-target", + "family": "components.execution-target", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "8b8f7fe7227d44330e216e0bf5d366c41b54d9bf76acfb24df2c1770984b9f27", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0a7094a9a9ac": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "1d5f374a6378": { + "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-3", + "ok": false + } + } + }, + "2a9fa3de486c": { + "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-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "2d47b46a5872": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "Unknown method", + "requiresConnection": true, + "status": "error" + } + }, + "2fd7109925e5": { + "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-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "33a303634ab9": { + "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-3", + "ok": false + } + } + }, + "3443aa3290c8": { + "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-3", + "ok": false + } + } + }, + "3828494e64df": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "", + "requiresConnection": true, + "status": "error" + } + }, + "467f1a0954f0": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "transport failure", + "requiresConnection": true, + "status": "error" + } + }, + "4e2e9a890ced": { + "detected": ["codex"], + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "57095302d8c1": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "5a241dd7bf9b": { + "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-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "5a628e933aa0": { + "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-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "6004e75ef39e": { + "name": "preflight.detectRemoteAgents#2", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "66a99391260b": { + "name": "preflight.detectRemoteAgents#2", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "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 + } + } + }, + "81c9c204b647": { + "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-3", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "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" + } + } + } + } + }, + "990a404630b4": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "outer refused", + "requiresConnection": true, + "status": "error" + } + }, + "9a892112da5b": { + "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-2", + "ok": true, + "result": ["codex"] + } + } + }, + "c04e51232b36": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "Cannot read properties of null (reading 'state')", + "requiresConnection": true, + "status": "error" + } + }, + "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 + } + } + }, + "c9821a8643be": { + "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-3", + "ok": true + } + } + }, + "ca123825be51": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "d23b91bd7660": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": true, + "status": { + "$rpc": "null" + } + } + }, + "d714ad0ce8fb": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": "Cannot read properties of undefined (reading 'state')", + "requiresConnection": true, + "status": "error" + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + } + }, + "recording": { + "scenario": "matrix-components.execution-target-ssh.connect-1", + "checkpoints": [ + { + "id": "components-target-ssh.prelude:state-pending", + "observation": { + "sender": ["ca123825be51"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "d23b91bd7660", + "effects": [] + } + }, + { + "id": "components-target-ssh.normal:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-absent:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "c9821a8643be"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "d714ad0ce8fb", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-null:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "5a241dd7bf9b"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "c04e51232b36", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "2fd7109925e5", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "2a9fa3de486c", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "5a628e933aa0", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "3443aa3290c8"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "990a404630b4", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "33a303634ab9"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "3828494e64df", + "effects": [] + } + }, + { + "id": "components-target-ssh.method-not-found:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "1d5f374a6378"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "2d47b46a5872", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "c5608f9dd27c"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "467f1a0954f0", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "671db70f932a"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "3828494e64df", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json new file mode 100644 index 00000000000..726067933f4 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.execution-target-ssh.getstate-1.json @@ -0,0 +1,804 @@ +{ + "operation": "components.execution-target", + "family": "components.execution-target", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "6832d23c6500e4fcb20abe7c53bc4f5abe72180dc0ad747a4907b82d99bc75d0", + "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" + } + } + } + }, + "0a7094a9a9ac": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "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 + } + } + }, + "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 + } + } + }, + "4e2e9a890ced": { + "detected": ["codex"], + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "57095302d8c1": { + "name": "ssh.connect#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.connect\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "6004e75ef39e": { + "name": "preflight.detectRemoteAgents#2", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "66a99391260b": { + "name": "preflight.detectRemoteAgents#2", + "json": "{\"id\":\"frame-4\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"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\"}}" + }, + "81c9c204b647": { + "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-3", + "ok": true, + "result": { + "state": { + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "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" + } + } + } + } + }, + "9a892112da5b": { + "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-2", + "ok": true, + "result": ["codex"] + } + } + }, + "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 + } + } + } + }, + "b69a18178af8": { + "name": "preflight.detectRemoteAgents#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectRemoteAgents\",\"params\":{\"connectionId\":\"ssh-1\"}}" + }, + "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 + } + } + }, + "ca123825be51": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "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 + } + } + }, + "d23b91bd7660": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": true, + "status": { + "$rpc": "null" + } + } + }, + "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 + } + } + } + }, + "e314f3903bd3": { + "detected": { + "$rpc": "null" + }, + "gate": { + "connectInProgress": false, + "error": { + "$rpc": "null" + }, + "requiresConnection": false, + "status": "connected" + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f03117831a8e": { + "name": "preflight.detectRemoteAgents#1", + "args": [ + { + "name": "method", + "value": "preflight.detectRemoteAgents" + }, + { + "name": "params", + "value": { + "connectionId": "ssh-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "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 + } + } + }, + "f9dfbe0c0ea7": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"ssh-1\"}}" + }, + "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-components.execution-target-ssh.getstate-1", + "checkpoints": [ + { + "id": "components-target-ssh.prelude:state-pending", + "observation": { + "sender": ["ca123825be51"], + "payloads": ["f9dfbe0c0ea7"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "d23b91bd7660", + "effects": [] + } + }, + { + "id": "components-target-ssh.normal:settled", + "observation": { + "sender": ["89aa7a3bd619", "9a892112da5b", "81c9c204b647", "6004e75ef39e"], + "payloads": ["f9dfbe0c0ea7", "0a7094a9a9ac", "57095302d8c1", "66a99391260b"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "4e2e9a890ced", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-absent:settled", + "observation": { + "sender": ["14db652edf02", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.result-null:settled", + "observation": { + "sender": ["0eabd872f405", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["0a16839c6f87", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["b09dd4915f43", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["e18278fce524", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused:settled", + "observation": { + "sender": ["d0fad8f739ca", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["ff6c3161dcc7", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.method-not-found:settled", + "observation": { + "sender": ["b705ba88a562", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection:settled", + "observation": { + "sender": ["2d910059043a", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + }, + { + "id": "components-target-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["f36f17f8d448", "71d817ffdd81", "f03117831a8e"], + "payloads": ["f9dfbe0c0ea7", "7c9498659f58", "b69a18178af8"], + "settlements": { + "mount": "eb79a9b3682a", + "connect": "eb79a9b3682a" + }, + "state": "e314f3903bd3", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json b/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json new file mode 100644 index 00000000000..2a0ea00dd72 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-components.setup-script-repo.hooks-1.json @@ -0,0 +1,599 @@ +{ + "operation": "components.setup-script", + "family": "components.setup-script", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "5cfbce3c7d97d908fbd447646d611e41a8aa1f61f684b9b710c4b67d6ff023a7", + "scenarioSha256": "a844134d7bad3c7c12107d60dbd298f5cfba778703fb9dd0f7ad454615d26b07", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0a180dd2149f": { + "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-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "170986cae6b4": { + "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-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "1e344a6b5da7": { + "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-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "28e75475e9e0": { + "name": "repo.hooks#1", + "args": [ + { + "name": "method", + "value": "repo.hooks" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "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 + } + } + }, + "3515a8adcd6d": { + "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-1", + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": "pnpm install" + } + }, + "setupRunPolicy": "ask", + "setupTrust": { + "$rpc": "null" + }, + "source": "repo" + } + } + } + }, + "3c9287ca1560": { + "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-1", + "ok": false + } + } + }, + "3cdc23cf6f4a": { + "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-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "5d1cf72f4e12": { + "advanced": true, + "command": "pnpm install", + "run": true, + "runPolicy": "ask", + "source": "repo", + "trust": { + "$rpc": "null" + } + }, + "64c03730d628": { + "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-1", + "ok": false + } + } + }, + "80cf8444e458": { + "advanced": false, + "command": { + "$rpc": "null" + }, + "run": true, + "runPolicy": "run-by-default", + "source": { + "$rpc": "null" + }, + "trust": { + "$rpc": "null" + } + }, + "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 + } + } + }, + "daf213730e62": { + "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-1", + "ok": true + } + } + }, + "e6c72f695b50": { + "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-1", + "ok": false + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f5dc0ce1e7b8": { + "name": "repo.hooks#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"repo.hooks\",\"params\":{\"repo\":\"id:repo-1\"}}" + } + }, + "recording": { + "scenario": "matrix-components.setup-script-repo.hooks-1", + "checkpoints": [ + { + "id": "components-setup-ask.prelude:hooks-pending", + "observation": { + "sender": ["28e75475e9e0"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.normal:settled", + "observation": { + "sender": ["3515a8adcd6d"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "5d1cf72f4e12", + "effects": [] + } + }, + { + "id": "components-setup-ask.result-absent:settled", + "observation": { + "sender": ["daf213730e62"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.result-null:settled", + "observation": { + "sender": ["1e344a6b5da7"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.inner-ok-missing:settled", + "observation": { + "sender": ["3cdc23cf6f4a"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.inner-false-string-error:settled", + "observation": { + "sender": ["0a180dd2149f"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.inner-false-object-error:settled", + "observation": { + "sender": ["170986cae6b4"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.outer-refused:settled", + "observation": { + "sender": ["64c03730d628"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.outer-refused-no-message:settled", + "observation": { + "sender": ["3c9287ca1560"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.method-not-found:settled", + "observation": { + "sender": ["e6c72f695b50"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.transport-rejection:settled", + "observation": { + "sender": ["941b6aeb0d6f"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + }, + { + "id": "components-setup-ask.transport-rejection-no-message:settled", + "observation": { + "sender": ["33cfd55c1890"], + "payloads": ["f5dc0ce1e7b8"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "80cf8444e458", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json new file mode 100644 index 00000000000..c4c55a4f919 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-ssh.getstate-1.json @@ -0,0 +1,746 @@ +{ + "operation": "files.mutation-ownership", + "family": "files.mutation-ownership", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "5815450e8d07f463423ca0bd8237830791c220234201abffc6fa13e698913516", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0cfc3aa2bfb0": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "29bfbe94cca9": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "3bff05e80a36": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-3", + "ok": false + } + } + }, + "504c0e27345c": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "518ec57c381a": { + "ownership": "uncaptured" + }, + "6116946241ca": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "state": { + "connectionGeneration": 3, + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "target-1" + } + } + } + } + }, + "6178f3695366": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Couldn't verify the SSH connection. Reconnect the host and try again.", + "isRpcDeliveryUnknown": false + } + }, + "6ef43f81f7e3": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "hostId": "ssh:target-1" + } + } + } + } + }, + "7ae12fc753a2": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-3", + "ok": false + } + } + }, + "9199aee60486": { + "name": "worktree.show#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "98a7aa1d359d": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a0341e6a5d84": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"target-1\"}}" + }, + "a56852d6836b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + } + }, + "a7e256068a4e": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b8b7e759edc4": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bc119660f0c1": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "bd84dadd27c7": { + "ownership": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "c05abe5bc0bc": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ce2b29907ae3": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'state')", + "isRpcDeliveryUnknown": false + } + }, + "d954a0a142a5": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'state')", + "isRpcDeliveryUnknown": false + } + }, + "dfc84caa8f54": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "e7fd41d8b9e6": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + } + }, + "recording": { + "scenario": "matrix-files.mutation-ownership-ssh.getstate-1", + "checkpoints": [ + { + "id": "files-ownership-ssh.prelude:status-pending", + "observation": { + "sender": ["bc119660f0c1"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9270aeb7d9c6" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.normal:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "6116946241ca"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "29bfbe94cca9" + }, + "state": "bd84dadd27c7", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-absent:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "dfc84caa8f54"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "d954a0a142a5" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-null:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "504c0e27345c"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "ce2b29907ae3" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "c05abe5bc0bc"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "b8b7e759edc4"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "a7e256068a4e"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "7ae12fc753a2"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "32a7c0ae7918" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "e7fd41d8b9e6"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "f3b516f62081" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.method-not-found:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "3bff05e80a36"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "b948e8307e81" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "0cfc3aa2bfb0"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "a947768bc0ed" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "98a7aa1d359d"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "c7584e82c72f" + }, + "state": "518ec57c381a", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json new file mode 100644 index 00000000000..c4f3129c330 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-status.get-1.json @@ -0,0 +1,746 @@ +{ + "operation": "files.mutation-ownership", + "family": "files.mutation-ownership", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "b9cfb187224a4b42efe8ccfcd96145833730d135c4fffa345716f95991a4700f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0b7588536afb": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "0d163aa89099": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "29bfbe94cca9": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "48e2bdc38094": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "4b0fb2833d76": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "518ec57c381a": { + "ownership": "uncaptured" + }, + "6116946241ca": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "state": { + "connectionGeneration": 3, + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "target-1" + } + } + } + } + }, + "68ce4d376250": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'capabilities')", + "isRpcDeliveryUnknown": false + } + }, + "6ef43f81f7e3": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "hostId": "ssh:target-1" + } + } + } + } + }, + "74a9cdb3c227": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "753f8f2aac3b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "848eaee9cd6a": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'capabilities')", + "isRpcDeliveryUnknown": false + } + }, + "90817e8c47cb": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "9199aee60486": { + "name": "worktree.show#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9ce0c7923c41": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Remote file changes require a newer Orca server. Update the HUB and try again.", + "isRpcDeliveryUnknown": false + } + }, + "a0341e6a5d84": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"target-1\"}}" + }, + "a56852d6836b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + } + }, + "a8d9f204690e": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "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 + } + }, + "bc119660f0c1": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "bd84dadd27c7": { + "ownership": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "f2a2b92aa73c": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f68f9c806fb2": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + } + }, + "recording": { + "scenario": "matrix-files.mutation-ownership-status.get-1", + "checkpoints": [ + { + "id": "files-ownership-ssh.prelude:status-pending", + "observation": { + "sender": ["bc119660f0c1"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9270aeb7d9c6" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.normal:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "6116946241ca"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "29bfbe94cca9" + }, + "state": "bd84dadd27c7", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-absent:settled", + "observation": { + "sender": ["90817e8c47cb"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "848eaee9cd6a" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-null:settled", + "observation": { + "sender": ["0d163aa89099"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "68ce4d376250" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["48e2bdc38094"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9ce0c7923c41" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["f2a2b92aa73c"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9ce0c7923c41" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["f68f9c806fb2"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9ce0c7923c41" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused:settled", + "observation": { + "sender": ["0b7588536afb"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "32a7c0ae7918" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["a8d9f204690e"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "f3b516f62081" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.method-not-found:settled", + "observation": { + "sender": ["753f8f2aac3b"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "b948e8307e81" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection:settled", + "observation": { + "sender": ["4b0fb2833d76"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "a947768bc0ed" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["74a9cdb3c227"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "c7584e82c72f" + }, + "state": "518ec57c381a", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json new file mode 100644 index 00000000000..684dfc174ff --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.mutation-ownership-worktree.show-1.json @@ -0,0 +1,746 @@ +{ + "operation": "files.mutation-ownership", + "family": "files.mutation-ownership", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "0bf3b17048bceb0bc8592405facd99cb8086274509206d9e55cd589a05d7415f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "06cbb9a1b167": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "0b4d42954d52": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "1e5b32902af7": { + "name": "status.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"status.get\"}" + }, + "2588fd63a157": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'worktree')", + "isRpcDeliveryUnknown": false + } + }, + "29bfbe94cca9": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "2fa02ab5402f": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "39a0b3c0e319": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "518ec57c381a": { + "ownership": "uncaptured" + }, + "533d020d6123": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "6116946241ca": { + "name": "ssh.getState#1", + "args": [ + { + "name": "method", + "value": "ssh.getState" + }, + { + "name": "params", + "value": { + "targetId": "target-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "state": { + "connectionGeneration": 3, + "error": { + "$rpc": "null" + }, + "reconnectAttempt": 0, + "status": "connected", + "targetId": "target-1" + } + } + } + } + }, + "6178f3695366": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Couldn't verify the SSH connection. Reconnect the host and try again.", + "isRpcDeliveryUnknown": false + } + }, + "6ef43f81f7e3": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "hostId": "ssh:target-1" + } + } + } + } + }, + "8845bcbdc51b": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "9199aee60486": { + "name": "worktree.show#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:workspace-1\"}}" + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a0341e6a5d84": { + "name": "ssh.getState#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"ssh.getState\",\"params\":{\"targetId\":\"target-1\"}}" + }, + "a56852d6836b": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b5447f4dd931": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'worktree')", + "isRpcDeliveryUnknown": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "bc119660f0c1": { + "name": "status.get#1", + "args": [ + { + "name": "method", + "value": "status.get" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "bd84dadd27c7": { + "ownership": { + "expectedExecutionHostId": "ssh:target-1", + "expectedSshConnectionGeneration": 3, + "expectedSshTargetId": "target-1" + } + }, + "c6aa5c0a7bd1": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "cff8b7a5e7ce": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fc05e7103b6c": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "fd50303f30ce": { + "name": "worktree.show#1", + "args": [ + { + "name": "method", + "value": "worktree.show" + }, + { + "name": "params", + "value": { + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 15000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "matrix-files.mutation-ownership-worktree.show-1", + "checkpoints": [ + { + "id": "files-ownership-ssh.prelude:status-pending", + "observation": { + "sender": ["bc119660f0c1"], + "payloads": ["1e5b32902af7"], + "settlements": { + "capture": "9270aeb7d9c6" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.normal:settled", + "observation": { + "sender": ["a56852d6836b", "6ef43f81f7e3", "6116946241ca"], + "payloads": ["1e5b32902af7", "9199aee60486", "a0341e6a5d84"], + "settlements": { + "capture": "29bfbe94cca9" + }, + "state": "bd84dadd27c7", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-absent:settled", + "observation": { + "sender": ["a56852d6836b", "533d020d6123"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "2588fd63a157" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.result-null:settled", + "observation": { + "sender": ["a56852d6836b", "39a0b3c0e319"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "b5447f4dd931" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-ok-missing:settled", + "observation": { + "sender": ["a56852d6836b", "06cbb9a1b167"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-string-error:settled", + "observation": { + "sender": ["a56852d6836b", "8845bcbdc51b"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.inner-false-object-error:settled", + "observation": { + "sender": ["a56852d6836b", "2fa02ab5402f"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "6178f3695366" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused:settled", + "observation": { + "sender": ["a56852d6836b", "cff8b7a5e7ce"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "32a7c0ae7918" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.outer-refused-no-message:settled", + "observation": { + "sender": ["a56852d6836b", "0b4d42954d52"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "f3b516f62081" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.method-not-found:settled", + "observation": { + "sender": ["a56852d6836b", "c6aa5c0a7bd1"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "b948e8307e81" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection:settled", + "observation": { + "sender": ["a56852d6836b", "fd50303f30ce"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "a947768bc0ed" + }, + "state": "518ec57c381a", + "effects": [] + } + }, + { + "id": "files-ownership-ssh.transport-rejection-no-message:settled", + "observation": { + "sender": ["a56852d6836b", "fc05e7103b6c"], + "payloads": ["1e5b32902af7", "9199aee60486"], + "settlements": { + "capture": "c7584e82c72f" + }, + "state": "518ec57c381a", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json new file mode 100644 index 00000000000..58cde4d1342 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-1.json @@ -0,0 +1,649 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "dbd20e271999641affbd4b52635c8864e25f08aa6db820a46d0773faa09770c6", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "139c55987ba6": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "194fabd9b9d8": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 5, + "content": "hello", + "truncated": false + } + } + } + }, + "500d95d47092": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "67427c41b324": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "68b5d189bcca": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "7500d091ea19": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "784ea351e5b2": { + "preview": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "7886fcdc8065": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9d9aa1c01790": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ac130adfeffb": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e088aa7f81b8": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "e2791dca552b": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "f488aff81e98": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-load-files.readterminalartifact-1", + "checkpoints": [ + { + "id": "files-preview-grant-refresh.prelude:read-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.normal:settled", + "observation": { + "sender": ["194fabd9b9d8"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "500d95d47092" + }, + "state": "784ea351e5b2", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.result-absent:settled", + "observation": { + "sender": ["139c55987ba6"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.result-null:settled", + "observation": { + "sender": ["7500d091ea19"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-ok-missing:settled", + "observation": { + "sender": ["f488aff81e98"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-string-error:settled", + "observation": { + "sender": ["e088aa7f81b8"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-object-error:settled", + "observation": { + "sender": ["67427c41b324"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused:settled", + "observation": { + "sender": ["68b5d189bcca"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused-no-message:settled", + "observation": { + "sender": ["e2791dca552b"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.method-not-found:settled", + "observation": { + "sender": ["ac130adfeffb"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection:settled", + "observation": { + "sender": ["7886fcdc8065"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection-no-message:settled", + "observation": { + "sender": ["9d9aa1c01790"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json new file mode 100644 index 00000000000..8f6a42df363 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.readterminalartifact-2.json @@ -0,0 +1,747 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "c4a09003352ba4e97a17ec4109cc125298cf7123b317265cc5eca06e8dcc0615", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "23897314fbe2": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "25b0d1737c71": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "terminal_file_grant_expired", + "message": "Grant expired" + }, + "id": "frame-1", + "ok": false + } + } + }, + "3c5492779d85": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "exists": true, + "isDirectory": false, + "openTarget": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "kind": "absolute-file" + } + } + } + } + }, + "500d95d47092": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "5f446c109a9a": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "byteLength": 5, + "content": "hello", + "truncated": false + } + } + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "68b4cb95d67a": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-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 + } + } + } + }, + "70356f9cd814": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "784ea351e5b2": { + "preview": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "813fe48569a6": { + "name": "artifact-source-refreshed", + "value": { + "absolutePath": "/logs/run.txt", + "cwd": "/logs", + "grantId": "grant-2", + "pathText": "run.txt", + "source": "terminalArtifact", + "terminalHandle": "terminal-1", + "worktreeId": "workspace-1" + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a56ffbdd5bf": { + "name": "files.resolveTerminalPath#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.resolveTerminalPath\",\"params\":{\"worktree\":\"id:workspace-1\",\"pathText\":\"run.txt\",\"cwd\":\"/logs\",\"terminal\":\"terminal-1\"}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b1c3cb621eff": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-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 + } + } + }, + "c01a147cb225": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "c283e01480f7": { + "name": "files.readTerminalArtifact#2", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-2\"}}" + }, + "c727a49c2e15": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "ca6bf3108851": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-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 + } + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "ebf2a6ee078d": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "edcd6a3e98cd": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-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 + } + } + } + }, + "f444aa03ba44": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-load-files.readterminalartifact-2", + "checkpoints": [ + { + "id": "files-preview-grant-refresh.prelude:read-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.normal:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "5f446c109a9a"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "500d95d47092" + }, + "state": "784ea351e5b2", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.result-absent:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "f444aa03ba44"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.result-null:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "23897314fbe2"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.inner-ok-missing:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "c01a147cb225"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-string-error:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "68b4cb95d67a"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-object-error:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "edcd6a3e98cd"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "b1c3cb621eff"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused-no-message:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "c727a49c2e15"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.method-not-found:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "ca6bf3108851"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "ebf2a6ee078d"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection-no-message:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "70356f9cd814"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": ["813fe48569a6"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json new file mode 100644 index 00000000000..c5b0724a1cc --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-load-files.resolveterminalpath-1.json @@ -0,0 +1,757 @@ +{ + "operation": "files.preview-load", + "family": "files.preview-load", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "ecdeff2713e454925158dde09782713d76f39455729bb25688a6ffcfff154f30", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "05c972dfc190": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "25b0d1737c71": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "terminal_file_grant_expired", + "message": "Grant expired" + }, + "id": "frame-1", + "ok": false + } + } + }, + "30954e0c83e6": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "3c5492779d85": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "exists": true, + "isDirectory": false, + "openTarget": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "kind": "absolute-file" + } + } + } + } + }, + "500d95d47092": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "5f446c109a9a": { + "name": "files.readTerminalArtifact#2", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-2", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "byteLength": 5, + "content": "hello", + "truncated": false + } + } + } + }, + "645c5754be42": { + "preview": "unloaded" + }, + "784ea351e5b2": { + "preview": { + "byteLength": 5, + "content": "hello", + "kind": "text", + "status": "ready", + "truncated": false + } + }, + "7f321cd7152f": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "813fe48569a6": { + "name": "artifact-source-refreshed", + "value": { + "absolutePath": "/logs/run.txt", + "cwd": "/logs", + "grantId": "grant-2", + "pathText": "run.txt", + "source": "terminalArtifact", + "terminalHandle": "terminal-1", + "worktreeId": "workspace-1" + } + }, + "8ce900375525": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-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 + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a56ffbdd5bf": { + "name": "files.resolveTerminalPath#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.resolveTerminalPath\",\"params\":{\"worktree\":\"id:workspace-1\",\"pathText\":\"run.txt\",\"cwd\":\"/logs\",\"terminal\":\"terminal-1\"}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ad2583c82bfe": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-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 + } + } + }, + "c283e01480f7": { + "name": "files.readTerminalArtifact#2", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-2\"}}" + }, + "c469c3b9bfe7": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "c657a3f0e02b": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "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 + } + }, + "d9baab0b6b3c": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-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 + } + } + } + }, + "de1ef0907023": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "efd7ff51072f": { + "name": "files.resolveTerminalPath#1", + "args": [ + { + "name": "method", + "value": "files.resolveTerminalPath" + }, + { + "name": "params", + "value": { + "cwd": "/logs", + "pathText": "run.txt", + "terminal": "terminal-1", + "worktree": "id:workspace-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 + } + } + }, + "fba5e3c89244": { + "preview": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + } + }, + "recording": { + "scenario": "matrix-files.preview-load-files.resolveterminalpath-1", + "checkpoints": [ + { + "id": "files-preview-grant-refresh.prelude:read-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.normal:settled", + "observation": { + "sender": ["25b0d1737c71", "3c5492779d85", "5f446c109a9a"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf", "c283e01480f7"], + "settlements": { + "load": "500d95d47092" + }, + "state": "784ea351e5b2", + "effects": ["813fe48569a6"] + } + }, + { + "id": "files-preview-grant-refresh.result-absent:settled", + "observation": { + "sender": ["25b0d1737c71", "05c972dfc190"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.result-null:settled", + "observation": { + "sender": ["25b0d1737c71", "c469c3b9bfe7"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-ok-missing:settled", + "observation": { + "sender": ["25b0d1737c71", "c657a3f0e02b"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-string-error:settled", + "observation": { + "sender": ["25b0d1737c71", "8ce900375525"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.inner-false-object-error:settled", + "observation": { + "sender": ["25b0d1737c71", "d9baab0b6b3c"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused:settled", + "observation": { + "sender": ["25b0d1737c71", "ad2583c82bfe"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.outer-refused-no-message:settled", + "observation": { + "sender": ["25b0d1737c71", "30954e0c83e6"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.method-not-found:settled", + "observation": { + "sender": ["25b0d1737c71", "efd7ff51072f"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "15467bba2d60" + }, + "state": "fba5e3c89244", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection:settled", + "observation": { + "sender": ["25b0d1737c71", "7f321cd7152f"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "a947768bc0ed" + }, + "state": "645c5754be42", + "effects": [] + } + }, + { + "id": "files-preview-grant-refresh.transport-rejection-no-message:settled", + "observation": { + "sender": ["25b0d1737c71", "de1ef0907023"], + "payloads": ["e0401d205ea2", "9a56ffbdd5bf"], + "settlements": { + "load": "c7584e82c72f" + }, + "state": "645c5754be42", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json new file mode 100644 index 00000000000..e8073399b83 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.readterminalartifact-1.json @@ -0,0 +1,681 @@ +{ + "operation": "files.preview-save", + "family": "files.preview-save", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "1a4f7dff351be244712bedb8f495c83a531cfbfc5b5923267d1ed46bf2d6d11b", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "139c55987ba6": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "54a6055a16b5": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "status": "saved" + } + }, + "5ac141a87b5a": { + "saved": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "67427c41b324": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "68b5d189bcca": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "7500d091ea19": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "7875007ef392": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "7886fcdc8065": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "935100df69e4": { + "saved": "unsaved" + }, + "9d9aa1c01790": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a3886e3a9791": { + "name": "files.writeTerminalArtifact#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.writeTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\",\"content\":\"next\"}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ac130adfeffb": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "b3f873eb7d0c": { + "saved": { + "status": "saved" + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e088aa7f81b8": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "e2791dca552b": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e391aec81b96": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 4, + "content": "base", + "truncated": false + } + } + } + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "f488aff81e98": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + } + }, + "recording": { + "scenario": "matrix-files.preview-save-files.readterminalartifact-1", + "checkpoints": [ + { + "id": "files-save-verified.prelude:verify-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "9270aeb7d9c6" + }, + "state": "935100df69e4", + "effects": [] + } + }, + { + "id": "files-save-verified.normal:settled", + "observation": { + "sender": ["e391aec81b96", "7875007ef392"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.result-absent:settled", + "observation": { + "sender": ["139c55987ba6"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.result-null:settled", + "observation": { + "sender": ["7500d091ea19"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-ok-missing:settled", + "observation": { + "sender": ["f488aff81e98"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-false-string-error:settled", + "observation": { + "sender": ["e088aa7f81b8"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-false-object-error:settled", + "observation": { + "sender": ["67427c41b324"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.outer-refused:settled", + "observation": { + "sender": ["68b5d189bcca"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.outer-refused-no-message:settled", + "observation": { + "sender": ["e2791dca552b"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.method-not-found:settled", + "observation": { + "sender": ["ac130adfeffb"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.transport-rejection:settled", + "observation": { + "sender": ["7886fcdc8065"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "a947768bc0ed" + }, + "state": "935100df69e4", + "effects": [] + } + }, + { + "id": "files-save-verified.transport-rejection-no-message:settled", + "observation": { + "sender": ["9d9aa1c01790"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "c7584e82c72f" + }, + "state": "935100df69e4", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json new file mode 100644 index 00000000000..61bf334c6fa --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.preview-save-files.writeterminalartifact-1.json @@ -0,0 +1,691 @@ +{ + "operation": "files.preview-save", + "family": "files.preview-save", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "8d443bda91fe5a1fa74910d525bb2ec40f639109bc71afbd42bccd949b3d463f", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "15467bba2d60": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "24195166cf4d": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "2e3484ef7995": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "3a281590ccf7": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "4db44f048a4d": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + }, + "54a6055a16b5": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "status": "saved" + } + }, + "5ac141a87b5a": { + "saved": { + "message": "Unable to load preview", + "reconnect": false, + "status": "error" + } + }, + "7875007ef392": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "78be845b88ca": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "82f499ca91c8": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "935100df69e4": { + "saved": "unsaved" + }, + "a1ef4333ebe3": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "a3886e3a9791": { + "name": "files.writeTerminalArtifact#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.writeTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\",\"content\":\"next\"}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "b3f873eb7d0c": { + "saved": { + "status": "saved" + } + }, + "bddbfe5ee1aa": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-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 + } + } + } + }, + "c09d56029b09": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c889a22fe351": { + "name": "files.writeTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.writeTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "content": "next", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "e0401d205ea2": { + "name": "files.readTerminalArtifact#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.readTerminalArtifact\",\"params\":{\"worktree\":\"id:workspace-1\",\"absolutePath\":\"/logs/run.txt\",\"grantId\":\"grant-1\"}}" + }, + "e391aec81b96": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 4, + "content": "base", + "truncated": false + } + } + } + }, + "e81d5596c201": { + "name": "files.readTerminalArtifact#1", + "args": [ + { + "name": "method", + "value": "files.readTerminalArtifact" + }, + { + "name": "params", + "value": { + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + } + }, + "recording": { + "scenario": "matrix-files.preview-save-files.writeterminalartifact-1", + "checkpoints": [ + { + "id": "files-save-verified.prelude:verify-pending", + "observation": { + "sender": ["e81d5596c201"], + "payloads": ["e0401d205ea2"], + "settlements": { + "save": "9270aeb7d9c6" + }, + "state": "935100df69e4", + "effects": [] + } + }, + { + "id": "files-save-verified.normal:settled", + "observation": { + "sender": ["e391aec81b96", "7875007ef392"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.result-absent:settled", + "observation": { + "sender": ["e391aec81b96", "a1ef4333ebe3"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.result-null:settled", + "observation": { + "sender": ["e391aec81b96", "c889a22fe351"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-ok-missing:settled", + "observation": { + "sender": ["e391aec81b96", "78be845b88ca"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-false-string-error:settled", + "observation": { + "sender": ["e391aec81b96", "82f499ca91c8"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.inner-false-object-error:settled", + "observation": { + "sender": ["e391aec81b96", "bddbfe5ee1aa"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "54a6055a16b5" + }, + "state": "b3f873eb7d0c", + "effects": [] + } + }, + { + "id": "files-save-verified.outer-refused:settled", + "observation": { + "sender": ["e391aec81b96", "2e3484ef7995"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.outer-refused-no-message:settled", + "observation": { + "sender": ["e391aec81b96", "c09d56029b09"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.method-not-found:settled", + "observation": { + "sender": ["e391aec81b96", "4db44f048a4d"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "15467bba2d60" + }, + "state": "5ac141a87b5a", + "effects": [] + } + }, + { + "id": "files-save-verified.transport-rejection:settled", + "observation": { + "sender": ["e391aec81b96", "3a281590ccf7"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "a947768bc0ed" + }, + "state": "935100df69e4", + "effects": [] + } + }, + { + "id": "files-save-verified.transport-rejection-no-message:settled", + "observation": { + "sender": ["e391aec81b96", "24195166cf4d"], + "payloads": ["e0401d205ea2", "a3886e3a9791"], + "settlements": { + "save": "c7584e82c72f" + }, + "state": "935100df69e4", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json new file mode 100644 index 00000000000..fdbab858db9 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.read-1.json @@ -0,0 +1,861 @@ +{ + "operation": "files.tab-doc", + "family": "files.tab-doc", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "65895c028637185238baf4fd4f1297c11f528a289fe8173a41c48dc5a0b37c26", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02ea3f503180": { + "name": "files.read#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "02ee7655cfac": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "22c63b806ef5": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "3073ceba86bd": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "323bf6059754": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "content": "aGk=", + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "4771cbfc0dfc": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-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 + } + } + }, + "5a33eeedb90f": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": { + "$rpc": "undefined" + }, + "content": { + "$rpc": "undefined" + }, + "kind": "file", + "status": "ready", + "truncated": { + "$rpc": "undefined" + } + } + }, + "5c610ebe58ed": { + "name": "files.readPreview#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "65af9a3f5ad4": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "6aca770498a6": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "8595b3c0f792": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "987f853ccbc2": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-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 + } + } + } + }, + "9babe9503a83": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "a7c7f43265d5": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'content')", + "isRpcDeliveryUnknown": false + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ae1baf99acb7": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-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 + } + } + } + }, + "b5a9ffe4c713": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "b5c68b76c498": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "ba9332ae7bb1": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'content')", + "isRpcDeliveryUnknown": false + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8fbe8972330": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "kind": "text", + "modifiedContent": "b\n", + "originalContent": "a\n" + } + } + } + }, + "cb1b85f12e0a": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-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 + } + } + }, + "ed33ecdb4e8e": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fad4ca11a316": { + "name": "git.diff#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"git.diff\",\"params\":{\"worktree\":\"id:workspace-1\",\"filePath\":\"docs/readme.md\",\"staged\":true}}" + }, + "fb58498a8798": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": { + "$rpc": "undefined" + }, + "content": { + "$rpc": "undefined" + }, + "kind": "file", + "status": "ready", + "truncated": { + "$rpc": "undefined" + } + } + }, + "ffe1c534d459": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + } + } + }, + "recording": { + "scenario": "matrix-files.tab-doc-files.read-1", + "checkpoints": [ + { + "id": "files-tab-doc-shapes.normal:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "ed33ecdb4e8e", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-absent:settled", + "observation": { + "sender": ["b5a9ffe4c713", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "ba9332ae7bb1", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-null:settled", + "observation": { + "sender": ["6aca770498a6", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "a7c7f43265d5", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-ok-missing:settled", + "observation": { + "sender": ["8595b3c0f792", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "5a33eeedb90f", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "fb58498a8798", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-string-error:settled", + "observation": { + "sender": ["987f853ccbc2", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "5a33eeedb90f", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "fb58498a8798", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-object-error:settled", + "observation": { + "sender": ["ae1baf99acb7", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "5a33eeedb90f", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "fb58498a8798", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused:settled", + "observation": { + "sender": ["4771cbfc0dfc", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "32a7c0ae7918", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused-no-message:settled", + "observation": { + "sender": ["02ee7655cfac", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "f3b516f62081", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.method-not-found:settled", + "observation": { + "sender": ["cb1b85f12e0a", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b948e8307e81", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection:settled", + "observation": { + "sender": ["22c63b806ef5", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "a947768bc0ed", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection-no-message:settled", + "observation": { + "sender": ["65af9a3f5ad4", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "c7584e82c72f", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "3073ceba86bd", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json new file mode 100644 index 00000000000..51458db4a34 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-files.readpreview-1.json @@ -0,0 +1,818 @@ +{ + "operation": "files.tab-doc", + "family": "files.tab-doc", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "2ceca0ecd901fd78838fcbdc789cbb6b96f04674b850a0994c7611f5db804915", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02ea3f503180": { + "name": "files.read#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "0fef03f57c61": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "2e3bb1c16607": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'isImage')", + "isRpcDeliveryUnknown": false + } + }, + "323bf6059754": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "content": "aGk=", + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "43465946206b": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "47bde408cf1e": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "5521ad94c331": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "5c610ebe58ed": { + "name": "files.readPreview#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "62a16026dfb1": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-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 + } + } + } + }, + "63bda5bd024b": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "9babe9503a83": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "a5ebcad292b7": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "a9fc8a97c98b": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-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 + } + } + } + }, + "b5c68b76c498": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c2ba83cacd09": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-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 + } + } + }, + "c38abcaf69dd": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "binary_file", + "isRpcDeliveryUnknown": false + } + }, + "c47f8da1be2f": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "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 + } + }, + "c8fbe8972330": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "kind": "text", + "modifiedContent": "b\n", + "originalContent": "a\n" + } + } + } + }, + "d6234620430f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'isImage')", + "isRpcDeliveryUnknown": false + } + }, + "ed33ecdb4e8e": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "f53a3ae32692": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-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 + } + } + }, + "fad4ca11a316": { + "name": "git.diff#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"git.diff\",\"params\":{\"worktree\":\"id:workspace-1\",\"filePath\":\"docs/readme.md\",\"staged\":true}}" + }, + "ffe1c534d459": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + } + } + }, + "recording": { + "scenario": "matrix-files.tab-doc-files.readpreview-1", + "checkpoints": [ + { + "id": "files-tab-doc-shapes.normal:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "ed33ecdb4e8e", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-absent:settled", + "observation": { + "sender": ["9babe9503a83", "47bde408cf1e", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "2e3bb1c16607", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-null:settled", + "observation": { + "sender": ["9babe9503a83", "0fef03f57c61", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "d6234620430f", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-ok-missing:settled", + "observation": { + "sender": ["9babe9503a83", "c47f8da1be2f", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "c38abcaf69dd", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-string-error:settled", + "observation": { + "sender": ["9babe9503a83", "a9fc8a97c98b", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "c38abcaf69dd", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-object-error:settled", + "observation": { + "sender": ["9babe9503a83", "62a16026dfb1", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "c38abcaf69dd", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused:settled", + "observation": { + "sender": ["9babe9503a83", "c2ba83cacd09", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "32a7c0ae7918", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused-no-message:settled", + "observation": { + "sender": ["9babe9503a83", "63bda5bd024b", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "f3b516f62081", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.method-not-found:settled", + "observation": { + "sender": ["9babe9503a83", "f53a3ae32692", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "b948e8307e81", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection:settled", + "observation": { + "sender": ["9babe9503a83", "a5ebcad292b7", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "a947768bc0ed", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection-no-message:settled", + "observation": { + "sender": ["9babe9503a83", "43465946206b", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "c7584e82c72f", + "diff": "ffe1c534d459" + }, + "state": "5521ad94c331", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json new file mode 100644 index 00000000000..7bb502e9db7 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-files.tab-doc-git.diff-1.json @@ -0,0 +1,816 @@ +{ + "operation": "files.tab-doc", + "family": "files.tab-doc", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "b93fc8b6f1ece041acaa8cc5699852085e67d7e2f39db13145e483f854ea3309", + "scenarioSha256": "c4db2424b8a35fd97b3fea00b4dd91a3c8d50c6fb73795811ef5402e3d14f8df", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02ea3f503180": { + "name": "files.read#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"files.read\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/readme.md\"}}" + }, + "0aa81de503dc": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "0ddde941c38a": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-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 + } + } + }, + "0fa28155e34e": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-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 + } + } + }, + "323bf6059754": { + "name": "files.readPreview#1", + "args": [ + { + "name": "method", + "value": "files.readPreview" + }, + { + "name": "params", + "value": { + "relativePath": "docs/logo.png", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "content": "aGk=", + "isImage": true, + "mimeType": "image/png" + } + } + } + }, + "32a7c0ae7918": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "outer refused", + "isRpcDeliveryUnknown": false + } + }, + "3a9e5c87d18b": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of undefined (reading 'kind')", + "isRpcDeliveryUnknown": false + } + }, + "3eca7222b2d0": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "5225d2d0d430": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "559c313a79f9": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "TypeError", + "message": "Cannot read properties of null (reading 'kind')", + "isRpcDeliveryUnknown": false + } + }, + "5c610ebe58ed": { + "name": "files.readPreview#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"files.readPreview\",\"params\":{\"worktree\":\"id:workspace-1\",\"relativePath\":\"docs/logo.png\"}}" + }, + "6f5e5f7888b7": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-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 + } + } + } + }, + "9babe9503a83": { + "name": "files.read#1", + "args": [ + { + "name": "method", + "value": "files.read" + }, + { + "name": "params", + "value": { + "relativePath": "docs/readme.md", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "byteLength": 8, + "content": "# readme", + "truncated": false + } + } + } + }, + "9cf9915b1ff3": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "af88d9765fd0": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "b5c68b76c498": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "b948e8307e81": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Unknown method", + "isRpcDeliveryUnknown": false + } + }, + "c38abcaf69dd": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "binary_file", + "isRpcDeliveryUnknown": false + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "c8fbe8972330": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "kind": "text", + "modifiedContent": "b\n", + "originalContent": "a\n" + } + } + } + }, + "e3995cc146f1": { + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "e56bb4eec9ad": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "ed33ecdb4e8e": { + "diff": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + }, + "image": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + }, + "text": { + "byteLength": 8, + "content": "# readme", + "kind": "file", + "status": "ready", + "truncated": false + } + }, + "eee847a9d90d": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "dataUri": "data:image/png;base64,aGk=", + "kind": "image", + "status": "ready" + } + }, + "f3b516f62081": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": false + } + }, + "fad4ca11a316": { + "name": "git.diff#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"git.diff\",\"params\":{\"worktree\":\"id:workspace-1\",\"filePath\":\"docs/readme.md\",\"staged\":true}}" + }, + "fdb82a72967a": { + "name": "git.diff#1", + "args": [ + { + "name": "method", + "value": "git.diff" + }, + { + "name": "params", + "value": { + "filePath": "docs/readme.md", + "staged": true, + "worktree": "id:workspace-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 + } + } + } + }, + "ffe1c534d459": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "kind": "diff", + "lines": [ + { + "kind": "delete", + "oldLineNumber": 1, + "text": "a" + }, + { + "kind": "add", + "newLineNumber": 1, + "text": "b" + } + ], + "status": "ready", + "truncated": false + } + } + }, + "recording": { + "scenario": "matrix-files.tab-doc-git.diff-1", + "checkpoints": [ + { + "id": "files-tab-doc-shapes.normal:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "c8fbe8972330"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "ffe1c534d459" + }, + "state": "ed33ecdb4e8e", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-absent:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "e56bb4eec9ad"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "3a9e5c87d18b" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.result-null:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "0aa81de503dc"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "559c313a79f9" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-ok-missing:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "af88d9765fd0"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "c38abcaf69dd" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-string-error:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "6f5e5f7888b7"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "c38abcaf69dd" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.inner-false-object-error:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "fdb82a72967a"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "c38abcaf69dd" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "0fa28155e34e"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "32a7c0ae7918" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.outer-refused-no-message:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "3eca7222b2d0"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "f3b516f62081" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.method-not-found:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "0ddde941c38a"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "b948e8307e81" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "9cf9915b1ff3"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "a947768bc0ed" + }, + "state": "e3995cc146f1", + "effects": [] + } + }, + { + "id": "files-tab-doc-shapes.transport-rejection-no-message:settled", + "observation": { + "sender": ["9babe9503a83", "323bf6059754", "5225d2d0d430"], + "payloads": ["02ea3f503180", "5c610ebe58ed", "fad4ca11a316"], + "settlements": { + "text": "b5c68b76c498", + "image": "eee847a9d90d", + "diff": "c7584e82c72f" + }, + "state": "e3995cc146f1", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json b/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json new file mode 100644 index 00000000000..3524680c22e --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-home.host-stats-stats.summary-1.json @@ -0,0 +1,656 @@ +{ + "operation": "home.host-stats", + "family": "home.host-stats", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", + "scenarioSha256": "5518e08c1b20f0ddd4cb6bc81ff9af032b1a38daf24f3d3497bac5df8b2d0ec5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "003f84d10dd0": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "0632d58191fb": { + "name": "stats", + "value": { + "host-1": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + } + }, + "0ebcc6f6a4cb": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + } + } + }, + "2a8c0c9ced05": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "2f0d38ebf67c": { + "name": "stats", + "value": { + "host-1": { + "$rpc": "undefined" + } + } + }, + "44136fa355b3": {}, + "556edbbc8712": { + "host-1": { + "$rpc": "null" + } + }, + "5d85cea3ab32": { + "name": "stats", + "value": { + "host-1": { + "error": "inner refused", + "ok": false + } + } + }, + "6fe0cca6ed2a": { + "name": "stats", + "value": { + "host-1": { + "error": "refused" + } + } + }, + "7b564b8fe22e": { + "name": "stats", + "value": { + "host-1": { + "$rpc": "null" + } + } + }, + "7bf81b1e94c5": { + "name": "stats.summary#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"stats.summary\"}" + }, + "9a84a7559023": { + "host-1": { + "activeWorktrees": 1, + "totalWorktrees": 3 + } + }, + "9b2c7b6a4f74": { + "name": "stats", + "value": { + "host-1": { + "error": { + "message": "inner refused" + }, + "ok": false + } + } + }, + "9c34abfa17e7": { + "host-1": { + "error": { + "message": "inner refused" + }, + "ok": false + } + }, + "9e3e7d14abf9": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "a392ac528c2b": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "b474d2a02a6a": { + "host-1": { + "error": "refused" + } + }, + "bf78e405c5d4": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "c3fad9087af1": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "c5bfd5f18460": { + "host-1": { + "$rpc": "undefined" + } + }, + "dc03021bee85": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "e05986a4b6e2": { + "host-1": { + "error": "inner refused", + "ok": false + } + }, + "e180f1e7839f": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "e20353973dc1": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f9b87a5a7a70": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$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-1", + "ok": false + } + } + }, + "fa16031454a3": { + "name": "stats.summary#1", + "args": [ + { + "name": "method", + "value": "stats.summary" + }, + { + "name": "params", + "value": { + "$rpc": "undefined" + } + }, + { + "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 + } + } + } + } + }, + "recording": { + "scenario": "matrix-home.host-stats-stats.summary-1", + "checkpoints": [ + { + "id": "home-host-stats.prelude:stats-pending", + "observation": { + "sender": ["a392ac528c2b"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "home-host-stats.normal:settled", + "observation": { + "sender": ["0ebcc6f6a4cb"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "9a84a7559023", + "effects": ["0632d58191fb"] + } + }, + { + "id": "home-host-stats.result-absent:settled", + "observation": { + "sender": ["e180f1e7839f"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "c5bfd5f18460", + "effects": ["2f0d38ebf67c"] + } + }, + { + "id": "home-host-stats.result-null:settled", + "observation": { + "sender": ["c3fad9087af1"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "556edbbc8712", + "effects": ["7b564b8fe22e"] + } + }, + { + "id": "home-host-stats.inner-ok-missing:settled", + "observation": { + "sender": ["2a8c0c9ced05"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "b474d2a02a6a", + "effects": ["6fe0cca6ed2a"] + } + }, + { + "id": "home-host-stats.inner-false-string-error:settled", + "observation": { + "sender": ["003f84d10dd0"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "e05986a4b6e2", + "effects": ["5d85cea3ab32"] + } + }, + { + "id": "home-host-stats.inner-false-object-error:settled", + "observation": { + "sender": ["fa16031454a3"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "9c34abfa17e7", + "effects": ["9b2c7b6a4f74"] + } + }, + { + "id": "home-host-stats.outer-refused:settled", + "observation": { + "sender": ["9e3e7d14abf9"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "home-host-stats.outer-refused-no-message:settled", + "observation": { + "sender": ["dc03021bee85"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "home-host-stats.method-not-found:settled", + "observation": { + "sender": ["f9b87a5a7a70"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "home-host-stats.transport-rejection:settled", + "observation": { + "sender": ["bf78e405c5d4"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "home-host-stats.transport-rejection-no-message:settled", + "observation": { + "sender": ["e20353973dc1"], + "payloads": ["7bf81b1e94c5"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "44136fa355b3", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json new file mode 100644 index 00000000000..95587f33b4c --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.get-1.json @@ -0,0 +1,779 @@ +{ + "operation": "host.view-settings", + "family": "host.view-settings", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", + "scenarioSha256": "a9a3191e2e8c36870ce2769a7bb972813f435267fdc6ed9e42632a57a227bbd6", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0039f2221403": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "114056cffd39": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + }, + "1308c5012cf9": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + }, + "17aa61c35dcf": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + } + }, + "292b632037a0": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"sortBy\":\"name\"}}" + }, + "3059ce80d86a": { + "name": "collapsedGroups", + "value": [] + }, + "34925f64c9a6": { + "name": "sortMode", + "value": "name" + }, + "3650379e5c37": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + }, + "5907841fc56d": { + "name": "ui.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.get\"}" + }, + "5fbdd64c75bc": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "61275c3082ca": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "757d36f7d7c1": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + }, + "78f2fbcd0185": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "7eaddd04bcdd": { + "name": "workspaceStatuses", + "value": [] + }, + "8ab3467032ef": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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 + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9527461faeb1": { + "name": "filters", + "value": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + } + }, + "96e59623cbb8": { + "name": "groupMode", + "value": "none" + }, + "993945d30ef2": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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" + } + } + } + }, + "9a285e681215": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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" + } + } + } + }, + "9d2d4e824476": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": false + }, + "groupMode": "none", + "sortMode": "name", + "statuses": [] + }, + "a0a3277ca732": { + "name": "filters", + "value": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": false + } + }, + "a424515cabc9": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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": { + "ui": { + "groupBy": "repo", + "hideSleepingWorkspaces": true, + "sortBy": "name" + } + } + } + } + }, + "ba2035345a68": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + }, + "groupMode": "repo", + "sortMode": "name", + "statuses": [] + }, + "bbdab1a7d122": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": false + }, + "groupMode": "none", + "sortMode": "recent", + "statuses": [] + }, + "e9a3acf203c6": { + "name": "groupMode", + "value": "repo" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "matrix-host.view-settings-ui.get-1", + "checkpoints": [ + { + "id": "host-view-settings-sync.prelude:ui-pending", + "observation": { + "sender": ["5fbdd64c75bc"], + "payloads": ["5907841fc56d"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "9270aeb7d9c6" + }, + "state": "bbdab1a7d122", + "effects": [] + } + }, + { + "id": "host-view-settings-sync.normal:settled", + "observation": { + "sender": ["a424515cabc9", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.result-absent:settled", + "observation": { + "sender": ["61275c3082ca", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.result-null:settled", + "observation": { + "sender": ["993945d30ef2", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.inner-ok-missing:settled", + "observation": { + "sender": ["9a285e681215", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.inner-false-string-error:settled", + "observation": { + "sender": ["17aa61c35dcf", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.inner-false-object-error:settled", + "observation": { + "sender": ["8ab3467032ef", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.outer-refused:settled", + "observation": { + "sender": ["1308c5012cf9", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.outer-refused-no-message:settled", + "observation": { + "sender": ["3650379e5c37", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.method-not-found:settled", + "observation": { + "sender": ["114056cffd39", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.transport-rejection:settled", + "observation": { + "sender": ["757d36f7d7c1", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + }, + { + "id": "host-view-settings-sync.transport-rejection-no-message:settled", + "observation": { + "sender": ["0039f2221403", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "9d2d4e824476", + "effects": [ + "96e59623cbb8", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "a0a3277ca732" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json new file mode 100644 index 00000000000..94dcec7f6c4 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-host.view-settings-ui.set-1.json @@ -0,0 +1,804 @@ +{ + "operation": "host.view-settings", + "family": "host.view-settings", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "a9e0780298a1443664e7ae02056168aa34d67556c9c056d51a82c7b4a73ad35b", + "scenarioSha256": "30cbeff90a845ab5dd576e302156e859338357b608e87e9fadeddf18ae93d9ca", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "0658e20f47f5": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "25d97d355299": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "292b632037a0": { + "name": "ui.set#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"ui.set\",\"params\":{\"sortBy\":\"name\"}}" + }, + "3059ce80d86a": { + "name": "collapsedGroups", + "value": [] + }, + "344b9ddf6cfb": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "34925f64c9a6": { + "name": "sortMode", + "value": "name" + }, + "44e91172f4a0": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "53a6789707e6": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "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 + } + } + }, + "5907841fc56d": { + "name": "ui.get#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"ui.get\"}" + }, + "5fbdd64c75bc": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.get" + }, + { + "name": "params", + "value": { + "$rpc": "absent" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "733b56879f90": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "78f2fbcd0185": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "7eaddd04bcdd": { + "name": "workspaceStatuses", + "value": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9527461faeb1": { + "name": "filters", + "value": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + } + }, + "a234a06a4465": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "a424515cabc9": { + "name": "ui.get#1", + "args": [ + { + "name": "method", + "value": "ui.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": { + "ui": { + "groupBy": "repo", + "hideSleepingWorkspaces": true, + "sortBy": "name" + } + } + } + } + }, + "ba2035345a68": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": true + }, + "groupMode": "repo", + "sortMode": "name", + "statuses": [] + }, + "bbdab1a7d122": { + "collapsed": [], + "filters": { + "alwaysShowDefaultBranch": true, + "filterRepoIds": [], + "hideDefaultBranch": false, + "hideSleeping": false + }, + "groupMode": "none", + "sortMode": "recent", + "statuses": [] + }, + "c6e108d0fcc5": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "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 + } + } + } + }, + "d991b0c4e961": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "e9a3acf203c6": { + "name": "groupMode", + "value": "repo" + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "fa3d32a591e8": { + "name": "ui.set#1", + "args": [ + { + "name": "method", + "value": "ui.set" + }, + { + "name": "params", + "value": { + "sortBy": "name" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-2", + "ok": false + } + } + } + }, + "recording": { + "scenario": "matrix-host.view-settings-ui.set-1", + "checkpoints": [ + { + "id": "host-view-settings-sync.prelude:ui-pending", + "observation": { + "sender": ["5fbdd64c75bc"], + "payloads": ["5907841fc56d"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "9270aeb7d9c6" + }, + "state": "bbdab1a7d122", + "effects": [] + } + }, + { + "id": "host-view-settings-sync.normal:settled", + "observation": { + "sender": ["a424515cabc9", "78f2fbcd0185"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.result-absent:settled", + "observation": { + "sender": ["a424515cabc9", "733b56879f90"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.result-null:settled", + "observation": { + "sender": ["a424515cabc9", "d991b0c4e961"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.inner-ok-missing:settled", + "observation": { + "sender": ["a424515cabc9", "a234a06a4465"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.inner-false-string-error:settled", + "observation": { + "sender": ["a424515cabc9", "344b9ddf6cfb"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.inner-false-object-error:settled", + "observation": { + "sender": ["a424515cabc9", "c6e108d0fcc5"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.outer-refused:settled", + "observation": { + "sender": ["a424515cabc9", "fa3d32a591e8"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.outer-refused-no-message:settled", + "observation": { + "sender": ["a424515cabc9", "0658e20f47f5"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.method-not-found:settled", + "observation": { + "sender": ["a424515cabc9", "53a6789707e6"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.transport-rejection:settled", + "observation": { + "sender": ["a424515cabc9", "44e91172f4a0"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + }, + { + "id": "host-view-settings-sync.transport-rejection-no-message:settled", + "observation": { + "sender": ["a424515cabc9", "25d97d355299"], + "payloads": ["5907841fc56d", "292b632037a0"], + "settlements": { + "mount": "eb79a9b3682a", + "sync": "eb79a9b3682a", + "sort": "eb79a9b3682a" + }, + "state": "ba2035345a68", + "effects": [ + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1", + "e9a3acf203c6", + "34925f64c9a6", + "7eaddd04bcdd", + "3059ce80d86a", + "9527461faeb1" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json new file mode 100644 index 00000000000..f779c166b27 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.activate-1.json @@ -0,0 +1,1158 @@ +{ + "operation": "host.worktree-actions", + "family": "host.worktree-actions", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", + "scenarioSha256": "7e842584620018d5ec5560711d63a472302e8da80cfe65dfcd2952aebb509af2", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "04938673cbf5": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "0ebfc1859f18": { + "name": "pinnedIds", + "value": ["wt-1"] + }, + "2b635c2a4fbb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "2d7fff77e4e1": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "2d8ad3ab13bb": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, + "2eb4a9090f6f": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "403256b7ebef": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-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 + } + } + } + }, + "4caf7515e224": { + "name": "worktree.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.set\",\"params\":{\"worktree\":\"id:wt-1\",\"isPinned\":true}}" + }, + "56b6d4fb8c56": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "5c186d67b84c": { + "name": "lastKnownWorktrees", + "value": [] + }, + "69d698d4f352": { + "name": "worktree.rm#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.rm\",\"params\":{\"worktree\":\"id:wt-1\",\"force\":true}}" + }, + "6cfdae6737f2": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "6e959a9dd70e": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [], + "optimisticActiveWorktreeIdentity": "|wt-1", + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a9b0d6699b2": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "a0393e57105c": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "b1509905fc66": { + "name": "optimisticActiveWorktreeIdentity", + "value": "|wt-1" + }, + "bea1b89d2581": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, + "bf2b36bda2d2": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "c3eecb0c6e96": { + "name": "worktree.activate#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.activate\",\"params\":{\"worktree\":\"id:wt-1\",\"notifyClients\":false,\"navigation\":\"caller\"}}" + }, + "cbf8280fdbe7": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "d440bd87d1ce": { + "name": "worktrees", + "value": [] + }, + "d4d67a091d31": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-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 + } + } + }, + "db04b3f07cf4": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-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 + } + } + } + }, + "e3e3c397a66a": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ef3b6a5ee1f2": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "f7f6b21128d9": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-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 + } + } + } + }, + "recording": { + "scenario": "matrix-host.worktree-actions-worktree.activate-1", + "checkpoints": [ + { + "id": "host-worktree-actions-pin-open-delete.prelude:pin-optimistic", + "observation": { + "sender": ["bf2b36bda2d2"], + "payloads": ["4caf7515e224"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a" + }, + "state": "9a9b0d6699b2", + "effects": ["cbf8280fdbe7", "6cfdae6737f2", "0ebfc1859f18"] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.normal:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.normal:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-absent:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "2d8ad3ab13bb", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-absent:settled", + "observation": { + "sender": ["56b6d4fb8c56", "2d8ad3ab13bb", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-null:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "ef3b6a5ee1f2", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-null:settled", + "observation": { + "sender": ["56b6d4fb8c56", "ef3b6a5ee1f2", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-ok-missing:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "2eb4a9090f6f", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-ok-missing:settled", + "observation": { + "sender": ["56b6d4fb8c56", "2eb4a9090f6f", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-string-error:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "db04b3f07cf4", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-string-error:settled", + "observation": { + "sender": ["56b6d4fb8c56", "db04b3f07cf4", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-object-error:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "403256b7ebef", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-object-error:settled", + "observation": { + "sender": ["56b6d4fb8c56", "403256b7ebef", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "d4d67a091d31", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused:settled", + "observation": { + "sender": ["56b6d4fb8c56", "d4d67a091d31", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused-no-message:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "bea1b89d2581", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused-no-message:settled", + "observation": { + "sender": ["56b6d4fb8c56", "bea1b89d2581", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.method-not-found:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "f7f6b21128d9", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.method-not-found:settled", + "observation": { + "sender": ["56b6d4fb8c56", "f7f6b21128d9", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "2d7fff77e4e1", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection:settled", + "observation": { + "sender": ["56b6d4fb8c56", "2d7fff77e4e1", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection-no-message:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "a0393e57105c", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection-no-message:settled", + "observation": { + "sender": ["56b6d4fb8c56", "a0393e57105c", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json new file mode 100644 index 00000000000..7ca72bac4ca --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.rm-1.json @@ -0,0 +1,1078 @@ +{ + "operation": "host.worktree-actions", + "family": "host.worktree-actions", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", + "scenarioSha256": "c47656a4ca21762e4b5a247ddf9a96efb746bec81e942ffa308a774bab1449e2", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "04938673cbf5": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "064a538f6c1c": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": "|wt-1", + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "0ebfc1859f18": { + "name": "pinnedIds", + "value": ["wt-1"] + }, + "11e971c034ae": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-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 + } + } + } + }, + "2b635c2a4fbb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "2c0740d2cefb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-3", + "ok": false + } + } + }, + "4caf7515e224": { + "name": "worktree.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.set\",\"params\":{\"worktree\":\"id:wt-1\",\"isPinned\":true}}" + }, + "4f1b109adcc0": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-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 + } + } + } + }, + "56b6d4fb8c56": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "5c186d67b84c": { + "name": "lastKnownWorktrees", + "value": [] + }, + "60cb8c68db7d": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + }, + "69d698d4f352": { + "name": "worktree.rm#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.rm\",\"params\":{\"worktree\":\"id:wt-1\",\"force\":true}}" + }, + "6cfdae6737f2": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "6e959a9dd70e": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [], + "optimisticActiveWorktreeIdentity": "|wt-1", + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [] + }, + "7ce7ff16ad9e": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-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 + } + } + }, + "86754b292acd": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-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 + } + } + }, + "909c8bc23636": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "972099c06c75": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true + } + } + }, + "9a9b0d6699b2": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "a7c564546e94": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "b1509905fc66": { + "name": "optimisticActiveWorktreeIdentity", + "value": "|wt-1" + }, + "bf2b36bda2d2": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "c3eecb0c6e96": { + "name": "worktree.activate#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.activate\",\"params\":{\"worktree\":\"id:wt-1\",\"notifyClients\":false,\"navigation\":\"caller\"}}" + }, + "c777b17081dd": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": false, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "cbf8280fdbe7": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "ce97d2eedacb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "cf69e8a7e125": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "d440bd87d1ce": { + "name": "worktrees", + "value": [] + }, + "e3e3c397a66a": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "ff4c661d50a3": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "error": "refused" + } + } + } + } + }, + "recording": { + "scenario": "matrix-host.worktree-actions-worktree.rm-1", + "checkpoints": [ + { + "id": "host-worktree-actions-pin-open-delete.prelude:pin-optimistic", + "observation": { + "sender": ["bf2b36bda2d2"], + "payloads": ["4caf7515e224"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a" + }, + "state": "9a9b0d6699b2", + "effects": ["cbf8280fdbe7", "6cfdae6737f2", "0ebfc1859f18"] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.prelude:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.prelude:cleanup", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "60cb8c68db7d"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.normal:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-absent:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "972099c06c75"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-null:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "909c8bc23636"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-ok-missing:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "ff4c661d50a3"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-string-error:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "4f1b109adcc0"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-object-error:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "11e971c034ae"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "7ce7ff16ad9e"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "064a538f6c1c", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused-no-message:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "2c0740d2cefb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "064a538f6c1c", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.method-not-found:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "86754b292acd"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "064a538f6c1c", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "ce97d2eedacb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "064a538f6c1c", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection-no-message:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "cf69e8a7e125"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "064a538f6c1c", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c", + "a7c564546e94", + "c777b17081dd" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json new file mode 100644 index 00000000000..4e8a29c868e --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-host.worktree-actions-worktree.set-1.json @@ -0,0 +1,1148 @@ +{ + "operation": "host.worktree-actions", + "family": "host.worktree-actions", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "92c29bd78ca0c0d5917e9386fc447bb9a1698b1d1ffaba0db7546eaac60da639", + "scenarioSha256": "054f1b1380fc6cfd4b0f4a85d6f143822a12a0a732d550dd85eef64a6556b3ba", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "04938673cbf5": { + "name": "worktree.activate#1", + "args": [ + { + "name": "method", + "value": "worktree.activate" + }, + { + "name": "params", + "value": { + "navigation": "caller", + "notifyClients": false, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "0ebfc1859f18": { + "name": "pinnedIds", + "value": ["wt-1"] + }, + "1266cec86f6a": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "275536711343": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "2b635c2a4fbb": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-3", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "44ff929f3c43": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-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 + } + } + }, + "4caf7515e224": { + "name": "worktree.set#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.set\",\"params\":{\"worktree\":\"id:wt-1\",\"isPinned\":true}}" + }, + "55d53d27027d": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-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 + } + } + } + }, + "56b6d4fb8c56": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "ok": true + } + } + } + }, + "5c186d67b84c": { + "name": "lastKnownWorktrees", + "value": [] + }, + "6307d17334bd": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "651653c526f2": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-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 + } + } + } + }, + "66469585a5b3": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "69d698d4f352": { + "name": "worktree.rm#1", + "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.rm\",\"params\":{\"worktree\":\"id:wt-1\",\"force\":true}}" + }, + "6b3c3497e633": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "6cfdae6737f2": { + "name": "lastKnownWorktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "6e959a9dd70e": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [], + "optimisticActiveWorktreeIdentity": "|wt-1", + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [] + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "9a9b0d6699b2": { + "confirmRemoveHost": false, + "lastKnownWorktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ], + "optimisticActiveWorktreeIdentity": { + "$rpc": "null" + }, + "pinnedIds": ["wt-1"], + "routeActionState": {}, + "worktrees": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "b1509905fc66": { + "name": "optimisticActiveWorktreeIdentity", + "value": "|wt-1" + }, + "ba44a37bda16": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-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 + } + } + }, + "bf2b36bda2d2": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "c3eecb0c6e96": { + "name": "worktree.activate#1", + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.activate\",\"params\":{\"worktree\":\"id:wt-1\",\"notifyClients\":false,\"navigation\":\"caller\"}}" + }, + "cbf8280fdbe7": { + "name": "worktrees", + "value": [ + { + "branch": "feature/pin", + "displayName": "marlin", + "hasAttachedPty": false, + "isPinned": true, + "linkedPR": { + "$rpc": "null" + }, + "liveTerminalCount": 0, + "path": "/repos/marlin/wt-1", + "preview": "", + "repo": "marlin", + "repoId": "repo-1", + "unread": false, + "worktreeId": "wt-1" + } + ] + }, + "d440bd87d1ce": { + "name": "worktrees", + "value": [] + }, + "e3e3c397a66a": { + "name": "worktree.rm#1", + "args": [ + { + "name": "method", + "value": "worktree.rm" + }, + { + "name": "params", + "value": { + "force": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f1b199e21211": { + "name": "worktree.set#1", + "args": [ + { + "name": "method", + "value": "worktree.set" + }, + { + "name": "params", + "value": { + "isPinned": true, + "worktree": "id:wt-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + } + }, + "recording": { + "scenario": "matrix-host.worktree-actions-worktree.set-1", + "checkpoints": [ + { + "id": "host-worktree-actions-pin-open-delete.prelude:pin-optimistic", + "observation": { + "sender": ["bf2b36bda2d2"], + "payloads": ["4caf7515e224"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a" + }, + "state": "9a9b0d6699b2", + "effects": ["cbf8280fdbe7", "6cfdae6737f2", "0ebfc1859f18"] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.normal:delete-optimistic", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.normal:settled", + "observation": { + "sender": ["56b6d4fb8c56", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-absent:delete-optimistic", + "observation": { + "sender": ["6b3c3497e633", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-absent:settled", + "observation": { + "sender": ["6b3c3497e633", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-null:delete-optimistic", + "observation": { + "sender": ["66469585a5b3", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.result-null:settled", + "observation": { + "sender": ["66469585a5b3", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-ok-missing:delete-optimistic", + "observation": { + "sender": ["275536711343", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-ok-missing:settled", + "observation": { + "sender": ["275536711343", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-string-error:delete-optimistic", + "observation": { + "sender": ["651653c526f2", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-string-error:settled", + "observation": { + "sender": ["651653c526f2", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-object-error:delete-optimistic", + "observation": { + "sender": ["55d53d27027d", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.inner-false-object-error:settled", + "observation": { + "sender": ["55d53d27027d", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused:delete-optimistic", + "observation": { + "sender": ["44ff929f3c43", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused:settled", + "observation": { + "sender": ["44ff929f3c43", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused-no-message:delete-optimistic", + "observation": { + "sender": ["6307d17334bd", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.outer-refused-no-message:settled", + "observation": { + "sender": ["6307d17334bd", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.method-not-found:delete-optimistic", + "observation": { + "sender": ["ba44a37bda16", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.method-not-found:settled", + "observation": { + "sender": ["ba44a37bda16", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection:delete-optimistic", + "observation": { + "sender": ["1266cec86f6a", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection:settled", + "observation": { + "sender": ["1266cec86f6a", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection-no-message:delete-optimistic", + "observation": { + "sender": ["f1b199e21211", "04938673cbf5", "e3e3c397a66a"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "9270aeb7d9c6" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + }, + { + "id": "host-worktree-actions-pin-open-delete.transport-rejection-no-message:settled", + "observation": { + "sender": ["f1b199e21211", "04938673cbf5", "2b635c2a4fbb"], + "payloads": ["4caf7515e224", "c3eecb0c6e96", "69d698d4f352"], + "settlements": { + "mount": "eb79a9b3682a", + "toggle-pin": "eb79a9b3682a", + "open-session": "eb79a9b3682a", + "delete": "eb79a9b3682a" + }, + "state": "6e959a9dd70e", + "effects": [ + "cbf8280fdbe7", + "6cfdae6737f2", + "0ebfc1859f18", + "b1509905fc66", + "d440bd87d1ce", + "5c186d67b84c" + ] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json new file mode 100644 index 00000000000..990edb8d529 --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.catalog-snapshot-worktree.ps-1.json @@ -0,0 +1,715 @@ +{ + "operation": "worktree.catalog-snapshot", + "family": "worktree.catalog-snapshot", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "95ca47f382997c412da974e564a46b1ae0c20d6e0f3ca14258d33c8d8b51a160", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "08dde29706df": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "0ce4a7117a8d": { + "admitted": { + "$rpc": "null" + }, + "fetched": { + "code": "refused", + "kind": "request_failed" + } + }, + "0d9bf2f46a5e": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "code": "refused", + "kind": "request_failed" + } + }, + "227f9e3de4fa": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + } + } + } + }, + "253b98015b8d": { + "admitted": "unadmitted", + "fetched": "unfetched" + }, + "2d40d1d38104": { + "name": "unhandled-rejection", + "value": { + "category": "Error", + "isRpcDeliveryUnknown": false, + "message": "Unsupported observation: function" + } + }, + "4262ba495b1b": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "50c4271e912d": { + "admitted": { + "$rpc": "null" + }, + "fetched": { + "kind": "response", + "pending": { + "admission": { + "kind": "invalid" + }, + "client": "logical-client", + "hostId": "host-1" + } + } + }, + "5a288976750e": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "5d54bccfc557": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "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 + } + } + }, + "8e2c2fbe7e94": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "code": "method_not_found", + "kind": "request_failed" + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a0fab6bf1fb0": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "a87f1f91dc98": { + "name": "worktree.ps#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.ps\",\"params\":{\"limit\":10000,\"afterSnapshotId\":null,\"supportsWorktreeVisibilitySourceDefaults\":true}}" + }, + "a947768bc0ed": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + }, + "ab1a9ba6301c": { + "admitted": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ], + "fetched": { + "kind": "response", + "pending": { + "admission": { + "kind": "full", + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + }, + "client": "logical-client", + "hostId": "host-1" + } + } + }, + "ad584cc963bb": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "b14360b67647": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "b1ae1170d95b": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "b670d230caf2": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "ba9be29baf40": { + "admitted": { + "$rpc": "null" + }, + "fetched": { + "code": "method_not_found", + "kind": "request_failed" + } + }, + "c7584e82c72f": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + }, + "f5d207eddd1d": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "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 + } + } + } + }, + "f97b6b46b1d5": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + } + }, + "recording": { + "scenario": "matrix-worktree.catalog-snapshot-worktree.ps-1", + "checkpoints": [ + { + "id": "worktree-catalog-snapshot.prelude:catalog-pending", + "observation": { + "sender": ["f97b6b46b1d5"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "253b98015b8d", + "effects": [] + } + }, + { + "id": "worktree-catalog-snapshot.normal:settled", + "observation": { + "sender": ["227f9e3de4fa"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "ab1a9ba6301c", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.result-absent:settled", + "observation": { + "sender": ["ad584cc963bb"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "50c4271e912d", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.result-null:settled", + "observation": { + "sender": ["b670d230caf2"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "50c4271e912d", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.inner-ok-missing:settled", + "observation": { + "sender": ["4262ba495b1b"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "50c4271e912d", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.inner-false-string-error:settled", + "observation": { + "sender": ["5a288976750e"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "50c4271e912d", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.inner-false-object-error:settled", + "observation": { + "sender": ["f5d207eddd1d"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "50c4271e912d", + "effects": ["2d40d1d38104"] + } + }, + { + "id": "worktree-catalog-snapshot.outer-refused:settled", + "observation": { + "sender": ["b14360b67647"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "0d9bf2f46a5e" + }, + "state": "0ce4a7117a8d", + "effects": [] + } + }, + { + "id": "worktree-catalog-snapshot.outer-refused-no-message:settled", + "observation": { + "sender": ["a0fab6bf1fb0"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "0d9bf2f46a5e" + }, + "state": "0ce4a7117a8d", + "effects": [] + } + }, + { + "id": "worktree-catalog-snapshot.method-not-found:settled", + "observation": { + "sender": ["5d54bccfc557"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "8e2c2fbe7e94" + }, + "state": "ba9be29baf40", + "effects": [] + } + }, + { + "id": "worktree-catalog-snapshot.transport-rejection:settled", + "observation": { + "sender": ["b1ae1170d95b"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "a947768bc0ed" + }, + "state": "253b98015b8d", + "effects": [] + } + }, + { + "id": "worktree-catalog-snapshot.transport-rejection-no-message:settled", + "observation": { + "sender": ["08dde29706df"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "c7584e82c72f" + }, + "state": "253b98015b8d", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json new file mode 100644 index 00000000000..73fefd560ef --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.home-catalog-worktree.ps-1.json @@ -0,0 +1,665 @@ +{ + "operation": "worktree.home-catalog", + "family": "worktree.home-catalog", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "fa0e28a167a5fba6fe7ffebb9f4ad28dd413d601c07116a24a7781d156f54beb", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "111018d23b6c": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "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 + } + } + }, + "2e82f8bbb1f1": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + { + "displayName": "Two", + "repo": "Repo", + "status": "idle", + "worktreeId": "w-2" + } + ] + } + } + } + }, + "39b7354b00f4": { + "host-1": { + "activeCount": 1, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + "totalWorktrees": 2 + } + }, + "430d32843438": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "44136fa355b3": {}, + "481a5e96b319": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "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 + } + } + } + }, + "4912be5d956f": { + "name": "worktree.ps#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.ps\",\"params\":{\"limit\":10000,\"supportsWorktreeVisibilitySourceDefaults\":true}}" + }, + "4fa9e403a3c8": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "51d64b49c1ab": { + "host-1": { + "activeCount": 0, + "catalogUnavailable": true, + "hostId": "host-1", + "lastActiveWorktree": { + "$rpc": "null" + }, + "totalWorktrees": 0 + } + }, + "6ed6d686b491": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "8944b50988b9": { + "name": "info", + "value": { + "host-1": { + "activeCount": 1, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + "totalWorktrees": 2 + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "97177805ceb8": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "993fb2bd3f3e": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "9f1a49cd671e": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "a3336f998e22": { + "name": "info", + "value": { + "host-1": { + "activeCount": 0, + "catalogUnavailable": true, + "hostId": "host-1", + "lastActiveWorktree": { + "$rpc": "null" + }, + "totalWorktrees": 0 + } + } + }, + "bc1a8e138f82": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "cca1bdaf2563": { + "name": "info", + "value": { + "host-1": { + "activeCount": 0, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "$rpc": "null" + }, + "totalWorktrees": 0 + } + } + }, + "d0ec31ab66d5": { + "host-1": { + "activeCount": 0, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "$rpc": "null" + }, + "totalWorktrees": 0 + } + }, + "e904502f2359": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + }, + "f2257f595504": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "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-worktree.home-catalog-worktree.ps-1", + "checkpoints": [ + { + "id": "worktree-home-catalog.prelude:catalog-pending", + "observation": { + "sender": ["bc1a8e138f82"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "worktree-home-catalog.normal:settled", + "observation": { + "sender": ["2e82f8bbb1f1"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "39b7354b00f4", + "effects": ["8944b50988b9"] + } + }, + { + "id": "worktree-home-catalog.result-absent:settled", + "observation": { + "sender": ["6ed6d686b491"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.result-null:settled", + "observation": { + "sender": ["430d32843438"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.inner-ok-missing:settled", + "observation": { + "sender": ["e904502f2359"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "d0ec31ab66d5", + "effects": ["cca1bdaf2563"] + } + }, + { + "id": "worktree-home-catalog.inner-false-string-error:settled", + "observation": { + "sender": ["f2257f595504"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "d0ec31ab66d5", + "effects": ["cca1bdaf2563"] + } + }, + { + "id": "worktree-home-catalog.inner-false-object-error:settled", + "observation": { + "sender": ["481a5e96b319"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "d0ec31ab66d5", + "effects": ["cca1bdaf2563"] + } + }, + { + "id": "worktree-home-catalog.outer-refused:settled", + "observation": { + "sender": ["993fb2bd3f3e"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.outer-refused-no-message:settled", + "observation": { + "sender": ["97177805ceb8"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.method-not-found:settled", + "observation": { + "sender": ["111018d23b6c"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.transport-rejection:settled", + "observation": { + "sender": ["4fa9e403a3c8"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + }, + { + "id": "worktree-home-catalog.transport-rejection-no-message:settled", + "observation": { + "sender": ["9f1a49cd671e"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "51d64b49c1ab", + "effects": ["a3336f998e22"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json new file mode 100644 index 00000000000..7d25dad36cb --- /dev/null +++ b/mobile/rpc-foundation/goldens/matrix-worktree.retired-names-worktree.listretirednames-1.json @@ -0,0 +1,583 @@ +{ + "operation": "worktree.retired-names", + "family": "worktree.retired-names", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "d321d6c17e67ae90f6ceefb775495ff765a86a35423ed33a212e71ec5e9e94aa", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "02fe07399476": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "097832ba0321": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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" + } + } + } + }, + "0c1342cbe912": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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" + } + } + } + }, + "2b135054eb20": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + } + }, + "569633c0c5c5": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "5e2e60e145d8": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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": { + "retiredNameTiersByRepo": { + "repo-1": 2 + }, + "retiredNamesByRepo": { + "repo-1": ["marlin", "orca"] + } + } + } + } + }, + "62049c27970e": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "65d2ebb48892": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "85d826c606ff": { + "registry": { + "exhaustedTiers": 2, + "names": ["marlin", "orca"] + } + }, + "ba7d8283433b": { + "name": "worktree.listRetiredNames#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.listRetiredNames\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "be9540321e8c": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "cb76d0017b96": { + "registry": { + "exhaustedTiers": 0, + "names": [] + } + }, + "dfdfe583f72c": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + } + }, + "e30ddb3bb2da": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "ea14357ab74a": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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 + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "matrix-worktree.retired-names-worktree.listretirednames-1", + "checkpoints": [ + { + "id": "worktree-retired-names.prelude:names-pending", + "observation": { + "sender": ["569633c0c5c5"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.normal:settled", + "observation": { + "sender": ["5e2e60e145d8"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "85d826c606ff", + "effects": [] + } + }, + { + "id": "worktree-retired-names.result-absent:settled", + "observation": { + "sender": ["62049c27970e"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.result-null:settled", + "observation": { + "sender": ["097832ba0321"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.inner-ok-missing:settled", + "observation": { + "sender": ["0c1342cbe912"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.inner-false-string-error:settled", + "observation": { + "sender": ["2b135054eb20"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.inner-false-object-error:settled", + "observation": { + "sender": ["dfdfe583f72c"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.outer-refused:settled", + "observation": { + "sender": ["65d2ebb48892"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.outer-refused-no-message:settled", + "observation": { + "sender": ["be9540321e8c"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.method-not-found:settled", + "observation": { + "sender": ["e30ddb3bb2da"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.transport-rejection:settled", + "observation": { + "sender": ["ea14357ab74a"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "worktree-retired-names.transport-rejection-no-message:settled", + "observation": { + "sender": ["02fe07399476"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json new file mode 100644 index 00000000000..a1bf90b2b94 --- /dev/null +++ b/mobile/rpc-foundation/goldens/worktree-catalog-snapshot.json @@ -0,0 +1,164 @@ +{ + "operation": "worktree.catalog-snapshot", + "family": "worktree.catalog-snapshot", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "d2947158840576cbd0f0604ed3d37b0f446c63c6d439b4d1b7def7fe8524523d", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "227f9e3de4fa": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + } + } + } + }, + "253b98015b8d": { + "admitted": "unadmitted", + "fetched": "unfetched" + }, + "2d40d1d38104": { + "name": "unhandled-rejection", + "value": { + "category": "Error", + "isRpcDeliveryUnknown": false, + "message": "Unsupported observation: function" + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a87f1f91dc98": { + "name": "worktree.ps#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.ps\",\"params\":{\"limit\":10000,\"afterSnapshotId\":null,\"supportsWorktreeVisibilitySourceDefaults\":true}}" + }, + "ab1a9ba6301c": { + "admitted": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ], + "fetched": { + "kind": "response", + "pending": { + "admission": { + "kind": "full", + "snapshotId": "snapshot-1", + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "worktreeId": "w-1" + } + ] + }, + "client": "logical-client", + "hostId": "host-1" + } + } + }, + "f97b6b46b1d5": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "afterSnapshotId": { + "$rpc": "null" + }, + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + } + }, + "recording": { + "scenario": "worktree-catalog-snapshot", + "checkpoints": [ + { + "id": "catalog-pending", + "observation": { + "sender": ["f97b6b46b1d5"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "253b98015b8d", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["227f9e3de4fa"], + "payloads": ["a87f1f91dc98"], + "settlements": { + "fetch": "9270aeb7d9c6" + }, + "state": "ab1a9ba6301c", + "effects": ["2d40d1d38104"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/worktree-home-catalog.json b/mobile/rpc-foundation/goldens/worktree-home-catalog.json new file mode 100644 index 00000000000..955191bccc8 --- /dev/null +++ b/mobile/rpc-foundation/goldens/worktree-home-catalog.json @@ -0,0 +1,165 @@ +{ + "operation": "worktree.home-catalog", + "family": "worktree.home-catalog", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "4749bb3b871275ba08f026f9b6bcfd383605f443e7bba70a7f89175b91db6fa5", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "2e82f8bbb1f1": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "worktrees": [ + { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + { + "displayName": "Two", + "repo": "Repo", + "status": "idle", + "worktreeId": "w-2" + } + ] + } + } + } + }, + "39b7354b00f4": { + "host-1": { + "activeCount": 1, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + "totalWorktrees": 2 + } + }, + "44136fa355b3": {}, + "4912be5d956f": { + "name": "worktree.ps#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.ps\",\"params\":{\"limit\":10000,\"supportsWorktreeVisibilitySourceDefaults\":true}}" + }, + "8944b50988b9": { + "name": "info", + "value": { + "host-1": { + "activeCount": 1, + "countsProvenAt": 1767225600000, + "hostId": "host-1", + "lastActiveWorktree": { + "displayName": "One", + "repo": "Repo", + "status": "working", + "worktreeId": "w-1" + }, + "totalWorktrees": 2 + } + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "bc1a8e138f82": { + "name": "worktree.ps#1", + "args": [ + { + "name": "method", + "value": "worktree.ps" + }, + { + "name": "params", + "value": { + "limit": 10000 + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "worktree-home-catalog", + "checkpoints": [ + { + "id": "catalog-pending", + "observation": { + "sender": ["bc1a8e138f82"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "9270aeb7d9c6" + }, + "state": "44136fa355b3", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["2e82f8bbb1f1"], + "payloads": ["4912be5d956f"], + "settlements": { + "load": "eb79a9b3682a" + }, + "state": "39b7354b00f4", + "effects": ["8944b50988b9"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/worktree-retired-names.json b/mobile/rpc-foundation/goldens/worktree-retired-names.json new file mode 100644 index 00000000000..56c92ffeebb --- /dev/null +++ b/mobile/rpc-foundation/goldens/worktree-retired-names.json @@ -0,0 +1,133 @@ +{ + "operation": "worktree.retired-names", + "family": "worktree.retired-names", + "namedDeltas": [], + "runnerVersion": 1, + "baseline": "c6a72169843ececf3a21da370ac50c5c5a4e6462", + "lockfileSha256": "788c1234b38a61fc882fca292341fce6690deee9cc6695673b0575cf2c4cf571", + "recorderSha256": "2e90933db32efc17d752cf5d506ecdd9ec54ce53ea30ba55021ea3d1bc1d3fb2", + "adapterSha256": "6119d409e1958877e4a04b3901f94a3609ac0420f5ca02af20eb8dae5cbf3408", + "scenarioSha256": "2faa07ee5f12b3ed584117359d3b7aeb9c78a04e8fc49e753372f8c3927a3740", + "platform": "darwin", + "scenarioVersion": 1, + "projectionVersion": 2, + "goldenFormatVersion": 5, + "values": { + "569633c0c5c5": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "name": "params", + "value": { + "repo": "id:repo-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "5e2e60e145d8": { + "name": "worktree.listRetiredNames#1", + "args": [ + { + "name": "method", + "value": "worktree.listRetiredNames" + }, + { + "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": { + "retiredNameTiersByRepo": { + "repo-1": 2 + }, + "retiredNamesByRepo": { + "repo-1": ["marlin", "orca"] + } + } + } + } + }, + "85d826c606ff": { + "registry": { + "exhaustedTiers": 2, + "names": ["marlin", "orca"] + } + }, + "ba7d8283433b": { + "name": "worktree.listRetiredNames#1", + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.listRetiredNames\",\"params\":{\"repo\":\"id:repo-1\"}}" + }, + "cb76d0017b96": { + "registry": { + "exhaustedTiers": 0, + "names": [] + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "worktree-retired-names", + "checkpoints": [ + { + "id": "names-pending", + "observation": { + "sender": ["569633c0c5c5"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "cb76d0017b96", + "effects": [] + } + }, + { + "id": "settled", + "observation": { + "sender": ["5e2e60e145d8"], + "payloads": ["ba7d8283433b"], + "settlements": { + "mount": "eb79a9b3682a" + }, + "state": "85d826c606ff", + "effects": [] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 1443895a1a8..ab58e1425ff 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -6753,6 +6753,981 @@ "checkpoint": "awaited-trust-write-refused" } ] + }, + { + "id": "files-ownership-ssh", + "operation": "files.mutation-ownership", + "version": 1, + "family": "files.mutation-ownership", + "sites": ["mobile/src/files/mobile-file-mutation-ownership.ts"], + "schedules": [], + "steps": [ + { + "action": "capture", + "id": "capture" + }, + { + "checkpoint": "status-pending" + }, + { + "complete": "status.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + }, + { + "complete": "worktree.show#1", + "params": { + "worktree": "id:workspace-1" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "hostId": "ssh:target-1" + } + } + } + }, + { + "complete": "ssh.getState#1", + "params": { + "targetId": "target-1" + }, + "reply": { + "ok": true, + "result": { + "state": { + "targetId": "target-1", + "status": "connected", + "error": null, + "reconnectAttempt": 0, + "connectionGeneration": 3 + } + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-ownership-local", + "operation": "files.mutation-ownership", + "version": 1, + "family": "files.mutation-ownership", + "sites": ["mobile/src/files/mobile-file-mutation-ownership.ts"], + "schedules": [], + "steps": [ + { + "action": "capture", + "id": "capture" + }, + { + "complete": "status.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["files.mutation-ownership.v1"] + } + } + }, + { + "complete": "worktree.show#1", + "params": { + "worktree": "id:workspace-1" + }, + "reply": { + "ok": true, + "result": { + "worktree": { + "hostId": "local" + } + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-grant-refresh", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-load", + "sites": [ + "mobile/src/files/mobile-file-preview-request.ts", + "mobile/src/files/mobile-terminal-artifact-grant-refresh.ts" + ], + "schedules": [], + "steps": [ + { + "action": "artifact", + "id": "load" + }, + { + "checkpoint": "read-pending" + }, + { + "complete": "files.readTerminalArtifact#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-1" + }, + "reply": { + "ok": false, + "error": { + "code": "terminal_file_grant_expired", + "message": "Grant expired" + } + } + }, + { + "complete": "files.resolveTerminalPath#1", + "params": { + "worktree": "id:workspace-1", + "pathText": "run.txt", + "cwd": "/logs", + "terminal": "terminal-1" + }, + "reply": { + "ok": true, + "result": { + "exists": true, + "isDirectory": false, + "openTarget": { + "kind": "absolute-file", + "absolutePath": "/logs/run.txt", + "grantId": "grant-2" + } + } + } + }, + { + "complete": "files.readTerminalArtifact#2", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-2" + }, + "reply": { + "ok": true, + "result": { + "content": "hello", + "truncated": false, + "byteLength": 5 + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-artifact-direct", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-load", + "sites": [ + "mobile/src/files/mobile-file-preview-request.ts", + "mobile/src/files/mobile-terminal-artifact-grant-refresh.ts" + ], + "schedules": [], + "steps": [ + { + "action": "artifact", + "id": "load" + }, + { + "complete": "files.readTerminalArtifact#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-1" + }, + "reply": { + "ok": true, + "result": { + "content": "hello", + "truncated": false, + "byteLength": 5 + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-worktree", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-load", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "worktree", + "id": "load" + }, + { + "complete": "files.read#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/readme.md" + }, + "reply": { + "ok": true, + "result": { + "content": "# readme", + "truncated": false, + "byteLength": 8 + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-worktree-image", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-load", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "worktree", + "id": "load", + "args": { + "path": "docs/logo.png" + } + }, + { + "complete": "files.readPreview#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/logo.png" + }, + "reply": { + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-preview-artifact-image", + "operation": "files.preview-load", + "version": 1, + "family": "files.preview-load", + "sites": ["mobile/src/files/mobile-file-preview-request.ts"], + "schedules": [], + "steps": [ + { + "action": "artifact", + "id": "load", + "args": { + "path": "/logs/shot.png" + } + }, + { + "complete": "files.readTerminalArtifactPreview#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/shot.png", + "grantId": "grant-1" + }, + "reply": { + "ok": true, + "result": { + "content": "aGk=", + "isBinary": true, + "isImage": true, + "mimeType": "image/png" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-save-verified", + "operation": "files.preview-save", + "version": 1, + "family": "files.preview-save", + "sites": [ + "mobile/src/files/mobile-file-preview-request.ts", + "mobile/src/files/mobile-terminal-artifact-grant-refresh.ts" + ], + "schedules": [], + "steps": [ + { + "action": "verified", + "id": "save" + }, + { + "checkpoint": "verify-pending" + }, + { + "complete": "files.readTerminalArtifact#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-1" + }, + "reply": { + "ok": true, + "result": { + "content": "base", + "truncated": false, + "byteLength": 4 + } + } + }, + { + "complete": "files.writeTerminalArtifact#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "content": "next" + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-save-blind", + "operation": "files.preview-save", + "version": 1, + "family": "files.preview-save", + "sites": [ + "mobile/src/files/mobile-file-preview-request.ts", + "mobile/src/files/mobile-terminal-artifact-grant-refresh.ts" + ], + "schedules": [], + "steps": [ + { + "action": "blind", + "id": "save" + }, + { + "complete": "files.writeTerminalArtifact#1", + "params": { + "worktree": "id:workspace-1", + "absolutePath": "/logs/run.txt", + "grantId": "grant-1", + "content": "next" + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "files-tab-doc-shapes", + "operation": "files.tab-doc", + "version": 1, + "family": "files.tab-doc", + "sites": ["mobile/src/files/mobile-file-tab-doc.ts"], + "schedules": [], + "steps": [ + { + "action": "text", + "id": "text" + }, + { + "complete": "files.read#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/readme.md" + }, + "reply": { + "ok": true, + "result": { + "content": "# readme", + "truncated": false, + "byteLength": 8 + } + } + }, + { + "action": "image", + "id": "image" + }, + { + "complete": "files.readPreview#1", + "params": { + "worktree": "id:workspace-1", + "relativePath": "docs/logo.png" + }, + "reply": { + "ok": true, + "result": { + "content": "aGk=", + "isImage": true, + "mimeType": "image/png" + } + } + }, + { + "action": "diff", + "id": "diff" + }, + { + "complete": "git.diff#1", + "params": { + "worktree": "id:workspace-1", + "filePath": "docs/readme.md", + "staged": true + }, + "reply": { + "ok": true, + "result": { + "kind": "text", + "originalContent": "a\n", + "modifiedContent": "b\n" + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "components-target-ssh", + "operation": "components.execution-target", + "version": 1, + "family": "components.execution-target", + "sites": ["mobile/src/components/use-new-workspace-execution-target.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "checkpoint": "state-pending" + }, + { + "complete": "ssh.getState#1", + "params": { + "targetId": "ssh-1" + }, + "reply": { + "ok": true, + "result": { + "state": { + "targetId": "ssh-1", + "status": "connected", + "error": null, + "reconnectAttempt": 0 + } + } + } + }, + { + "complete": "preflight.detectRemoteAgents#1", + "params": { + "connectionId": "ssh-1" + }, + "reply": { + "ok": true, + "result": ["codex"] + } + }, + { + "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": "settled" + } + ] + }, + { + "id": "components-target-local", + "operation": "components.execution-target-local", + "version": 1, + "family": "components.execution-target-local", + "sites": ["mobile/src/components/use-new-workspace-execution-target.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "checkpoint": "detect-pending" + }, + { + "complete": "preflight.detectAgents#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": ["claude"] + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "components-setup-ask", + "operation": "components.setup-script", + "version": 1, + "family": "components.setup-script", + "sites": ["mobile/src/components/use-new-workspace-setup-script.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "checkpoint": "hooks-pending" + }, + { + "complete": "repo.hooks#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "hooks": { + "scripts": { + "setup": "pnpm install" + } + }, + "source": "repo", + "setupRunPolicy": "ask", + "setupTrust": null + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "components-codex-capability", + "operation": "components.codex-reset-capability", + "version": 1, + "family": "components.codex-reset-capability", + "sites": ["mobile/src/components/codex-reset-credit-capability.ts"], + "schedules": [], + "steps": [ + { + "action": "probe", + "id": "probe" + }, + { + "complete": "status.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "capabilities": ["accounts.codex-reset-credit.v1"] + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "host-view-settings-sync", + "operation": "host.view-settings", + "version": 1, + "family": "host.view-settings", + "sites": ["mobile/src/host-screen/use-host-view-settings.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "sync", + "id": "sync" + }, + { + "checkpoint": "ui-pending" + }, + { + "complete": "ui.get#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "ui": { + "sortBy": "name", + "groupBy": "repo", + "hideSleepingWorkspaces": true + } + } + } + }, + { + "action": "sort", + "id": "sort" + }, + { + "complete": "ui.set#1", + "params": { + "sortBy": "name" + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "home-host-stats", + "operation": "home.host-stats", + "version": 1, + "family": "home.host-stats", + "sites": ["mobile/src/home/mobile-home-host-requests.ts"], + "schedules": [], + "steps": [ + { + "action": "load", + "id": "load" + }, + { + "checkpoint": "stats-pending" + }, + { + "complete": "stats.summary#1", + "params": { + "$undefined": true + }, + "reply": { + "ok": true, + "result": { + "totalWorktrees": 3, + "activeWorktrees": 1 + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "worktree-home-catalog", + "operation": "worktree.home-catalog", + "version": 1, + "family": "worktree.home-catalog", + "sites": ["mobile/src/worktree/home-host-worktree-fetch.ts"], + "schedules": [], + "steps": [ + { + "action": "load", + "id": "load" + }, + { + "checkpoint": "catalog-pending" + }, + { + "complete": "worktree.ps#1", + "params": { + "limit": 10000, + "supportsWorktreeVisibilitySourceDefaults": true + }, + "reply": { + "ok": true, + "result": { + "worktrees": [ + { + "worktreeId": "w-1", + "displayName": "One", + "repo": "Repo", + "status": "working" + }, + { + "worktreeId": "w-2", + "displayName": "Two", + "repo": "Repo", + "status": "idle" + } + ] + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "worktree-catalog-snapshot", + "operation": "worktree.catalog-snapshot", + "version": 1, + "family": "worktree.catalog-snapshot", + "sites": ["mobile/src/worktree/worktree-catalog-snapshot-client.ts"], + "schedules": [], + "steps": [ + { + "action": "fetch", + "id": "fetch" + }, + { + "checkpoint": "catalog-pending" + }, + { + "complete": "worktree.ps#1", + "params": { + "limit": 10000, + "supportsWorktreeVisibilitySourceDefaults": true, + "afterSnapshotId": null + }, + "reply": { + "ok": true, + "result": { + "snapshotId": "snapshot-1", + "worktrees": [ + { + "worktreeId": "w-1", + "displayName": "One", + "repo": "Repo" + } + ] + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "worktree-retired-names", + "operation": "worktree.retired-names", + "version": 1, + "family": "worktree.retired-names", + "sites": ["mobile/src/worktree/use-retired-worktree-names.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "checkpoint": "names-pending" + }, + { + "complete": "worktree.listRetiredNames#1", + "params": { + "repo": "id:repo-1" + }, + "reply": { + "ok": true, + "result": { + "retiredNamesByRepo": { + "repo-1": ["marlin", "orca"] + }, + "retiredNameTiersByRepo": { + "repo-1": 2 + } + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "host-worktree-actions-pin-open-delete", + "operation": "host.worktree-actions", + "version": 1, + "family": "host.worktree-actions", + "sites": ["mobile/src/host-screen/use-host-worktree-actions.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "toggle-pin", + "id": "toggle-pin" + }, + { + "checkpoint": "pin-optimistic" + }, + { + "complete": "worktree.set#1", + "params": { + "worktree": "id:wt-1", + "isPinned": true + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "action": "open-session", + "id": "open-session" + }, + { + "complete": "worktree.activate#1", + "params": { + "worktree": "id:wt-1", + "notifyClients": false, + "navigation": "caller" + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "action": "delete", + "id": "delete" + }, + { + "checkpoint": "delete-optimistic" + }, + { + "complete": "worktree.rm#1", + "params": { + "worktree": "id:wt-1", + "force": true + }, + "reply": { + "ok": true, + "result": { + "ok": true + } + } + }, + { + "checkpoint": "settled" + } + ] + }, + { + "id": "host-worktree-delete-refused", + "operation": "host.worktree-actions", + "version": 1, + "family": "host.worktree-actions", + "sites": ["mobile/src/host-screen/use-host-worktree-actions.ts"], + "schedules": [], + "steps": [ + { + "action": "mount", + "id": "mount" + }, + { + "action": "delete", + "id": "delete" + }, + { + "checkpoint": "delete-optimistic" + }, + { + "complete": "worktree.rm#1", + "params": { + "worktree": "id:wt-1", + "force": true + }, + "reply": { + "ok": false, + "error": { + "code": "worktree_busy", + "message": "Worktree is busy" + } + } + }, + { + "checkpoint": "restored" + } + ] } ] } diff --git a/mobile/src/components/codex-reset-credit-capability-operations.ts b/mobile/src/components/codex-reset-credit-capability-operations.ts new file mode 100644 index 00000000000..71502c945e2 --- /dev/null +++ b/mobile/src/components/codex-reset-credit-capability-operations.ts @@ -0,0 +1,33 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import type { RpcCompatibleReader } from '../transport/rpc-operation-contract' +import { rpcReadUnchecked } from '../transport/rpc-reader-payload' + +// Reads the capability list off a status the object policy already admitted, so a non-object +// result reads as no capabilities rather than throwing — which is what the probe's `catch` did. +const capabilityListReader: RpcCompatibleReader< + Record, + 'capabilities', + unknown +> = (raw) => rpcReadUnchecked('capabilities', raw.capabilities) + +/** + * status.get read for the Codex reset-credit probe, with its own policy on that method. + * + * The probe treats a refusal, a null result and a non-object result identically as "unsupported", + * which only `object-result-or-null` expresses, and which is what `rpcObjectResultOrNull` already + * spelled at this call site. + */ +export const codexResetCreditCapabilityRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'status.codex-reset-credit-capability', + method: 'status.get', + acceptance: 'object-result-or-null', + barrier: 'after-caller-barrier', + read: capabilityListReader + }) +) + +/** What the probe sends with, named from an operation so no module names the raw port. */ +export type MobileCodexResetCapabilityRpcSender = Parameters< + typeof codexResetCreditCapabilityRead.request +>[0] diff --git a/mobile/src/components/codex-reset-credit-capability.ts b/mobile/src/components/codex-reset-credit-capability.ts index 8e88f74f2f9..5246860a928 100644 --- a/mobile/src/components/codex-reset-credit-capability.ts +++ b/mobile/src/components/codex-reset-credit-capability.ts @@ -2,18 +2,22 @@ import { useEffect, useState } from 'react' import { CODEX_RESET_CREDIT_RUNTIME_CAPABILITY } from '../../../src/shared/protocol-version' import type { RpcClient } from '../transport/rpc-client' import { startRuntimeCapabilityProbe } from '../transport/runtime-capability-probe' -import { rpcObjectResultOrNull } from '../transport/rpc-acceptance-policies' +import { + codexResetCreditCapabilityRead, + type MobileCodexResetCapabilityRpcSender +} from './codex-reset-credit-capability-operations' // Why: source the capability string from the shared contract so a host bump can never // silently drift from the mobile probe. export const MOBILE_CODEX_RESET_CREDIT_CAPABILITY = CODEX_RESET_CREDIT_RUNTIME_CAPABILITY export async function readCodexResetCreditCapability( - client: Pick + client: MobileCodexResetCapabilityRpcSender ): Promise { try { - const response = await client.sendRequest('status.get') - const capabilities = rpcObjectResultOrNull(response)?.capabilities + const capabilities = codexResetCreditCapabilityRead.interpret( + await codexResetCreditCapabilityRead.request(client) + ) return ( Array.isArray(capabilities) && capabilities.includes(MOBILE_CODEX_RESET_CREDIT_CAPABILITY) ) diff --git a/mobile/src/components/new-workspace-operations.ts b/mobile/src/components/new-workspace-operations.ts new file mode 100644 index 00000000000..bd3a6a24990 --- /dev/null +++ b/mobile/src/components/new-workspace-operations.ts @@ -0,0 +1,40 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import type { RpcCompatibleReader } from '../transport/rpc-operation-contract' +import { rpcReadUnchecked, rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +// The New Workspace drawer's own reads. Its SSH connect, SSH state and agent detection are the +// workspace-create operations in ../tasks/mobile-workspace-source-operations.ts, asked with the +// same acceptance by the same flow, so the drawer sends those rather than restating them. + +/** + * repo.hooks read for the drawer, the second of two policies on this method. + * + * The tasks create path (`repo.setup-hooks`) throws the host's message because it cannot decide + * whether to run setup without an answer. The drawer only decorates a form: a refusal leaves the + * advanced section on its defaults and the message is never shown, so refusal is a skip here. + */ +export const newWorkspaceSetupHooksRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.drawer-setup-hooks-or-skip', + method: 'repo.hooks', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('repo-hooks') + }) +) + +// Reads `ui` the way the drawer always has: through optional chaining, so a null or absent result +// is untrusted-but-not-fatal rather than the property-read throw the Tasks screen's reader keeps. +const optionalUiMemberReader: RpcCompatibleReader = (raw) => + rpcReadUnchecked('optional-ui-member', raw == null ? undefined : Object(raw).ui) + +/** Persisted UI state, read for the trusted-hooks record only. A refused read trusts nothing. */ +export const newWorkspaceUiStateRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ui.new-workspace-trust-or-skip', + method: 'ui.get', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: optionalUiMemberReader + }) +) diff --git a/mobile/src/components/use-new-workspace-execution-target.ts b/mobile/src/components/use-new-workspace-execution-target.ts index 27302d9ecb2..1a3c7798931 100644 --- a/mobile/src/components/use-new-workspace-execution-target.ts +++ b/mobile/src/components/use-new-workspace-execution-target.ts @@ -1,7 +1,12 @@ import { useEffect, useState } from 'react' import type { SshConnectionState } from '../../../src/shared/ssh-types' import type { RpcClient } from '../transport/rpc-client' -import type { RpcSuccess } from '../transport/types' +import { + localAgentDetectionRead, + remoteAgentDetectionRead, + sshRepoConnectRun, + sshRepoStateRead +} from '../tasks/mobile-workspace-source-operations' import { deriveWorkspaceSshGate, type WorkspaceSshGate } from '../tasks/workspace-ssh-gate' type DetectedAgentIdsState = { @@ -48,17 +53,15 @@ export function useNewWorkspaceExecutionTarget(args: { return } let stale = false - void client - .sendRequest('ssh.getState', { targetId: connectionId }) - .then((response) => { + void sshRepoStateRead + .request(client, { targetId: connectionId }) + .then((reply) => { if (stale) { return } - if (!response.ok) { - throw new Error(response.error.message) - } - const state = (response as RpcSuccess).result as { state?: SshConnectionState | null } - setSshState(state.state ?? fallbackSshState(connectionId, 'disconnected', null)) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const state = sshRepoStateRead.interpret(reply) as SshConnectionState | null | undefined + setSshState(state ?? fallbackSshState(connectionId, 'disconnected', null)) }) .catch((error) => { if (!stale) { @@ -83,13 +86,16 @@ export function useNewWorkspaceExecutionTarget(args: { let stale = false void (async () => { try { - const response = connectionId - ? await client.sendRequest('preflight.detectRemoteAgents', { connectionId }) - : await client.sendRequest('preflight.detectAgents') + const detected = connectionId + ? remoteAgentDetectionRead.interpret( + await remoteAgentDetectionRead.request(client, { connectionId }) + ) + : localAgentDetectionRead.interpret(await localAgentDetectionRead.request(client)) if (!stale) { setDetectedAgentIdsState({ connectionId, - ids: response.ok ? new Set((response as RpcSuccess).result as string[]) : new Set() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + ids: detected.accepted ? new Set(detected.value as string[]) : new Set() }) } } catch { @@ -110,16 +116,14 @@ export function useNewWorkspaceExecutionTarget(args: { setConnectingTargetId(connectionId) setSshState(fallbackSshState(connectionId, 'connecting', null)) try { - const response = await client.sendRequest( - 'ssh.connect', + const reply = await sshRepoConnectRun.request( + client, { targetId: connectionId }, { timeoutMs: 120_000 } ) - if (!response.ok) { - throw new Error(response.error.message) - } - const result = (response as RpcSuccess).result as { state?: SshConnectionState | null } - setSshState(result.state ?? fallbackSshState(connectionId, 'connected', null)) + // 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 + setSshState(state ?? fallbackSshState(connectionId, 'connected', null)) } catch (error) { setSshState( fallbackSshState( diff --git a/mobile/src/components/use-new-workspace-runtime-context.ts b/mobile/src/components/use-new-workspace-runtime-context.ts index b7d3b142675..a4540eb2ebb 100644 --- a/mobile/src/components/use-new-workspace-runtime-context.ts +++ b/mobile/src/components/use-new-workspace-runtime-context.ts @@ -1,19 +1,33 @@ import { optionalSettingsRead } from '../transport/settings-read-operations' import { useEffect, useState } from 'react' import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' +import type { RpcAcceptedResult } from '../transport/rpc-accepted-result' import type { RpcClient } from '../transport/rpc-client' -import type { RpcResponse, RpcSuccess } from '../transport/types' +import type { RpcResponse } from '../transport/types' +import { taskLinearStatusRead, taskPreflightRead } from '../tasks/mobile-task-runtime-operations' import { filterAvailableTaskProviders, normalizeVisibleTaskProviders, type TaskProvider } from '../tasks/mobile-task-providers' import type { NewWorktreeRuntimeSettings } from './new-worktree-agent-selection' +import { newWorkspaceUiStateRead } from './new-workspace-operations' -type UiGetResult = { ui?: { trustedOrcaHooks?: PersistedTrustedOrcaHooks } } | null | undefined +/** One member off a probe payload the drawer only re-typed, keeping its optional-chaining read. */ +function readProbeMember(payload: unknown, key: string): unknown { + return payload == null ? undefined : Object(payload)[key] +} -function settledSuccess(entry: PromiseSettledResult): RpcSuccess | null { - return entry.status === 'fulfilled' && entry.value.ok ? (entry.value as RpcSuccess) : null +/** A settled probe's accepted payload, or undefined when it never landed or was refused. */ +function settledValue( + entry: PromiseSettledResult, + interpret: (reply: RpcResponse) => RpcAcceptedResult +): unknown { + if (entry.status !== 'fulfilled') { + return undefined + } + const verdict = interpret(entry.value) + return verdict.accepted ? verdict.value : undefined } export function useNewWorkspaceRuntimeContext( @@ -38,12 +52,12 @@ export function useNewWorkspaceRuntimeContext( let stale = false void (async () => { const probes = Promise.allSettled([ - client.sendRequest('preflight.check'), - client.sendRequest('linear.status') + taskPreflightRead.request(client), + taskLinearStatusRead.request(client) ]) const [settingsRes, uiRes] = await Promise.allSettled([ optionalSettingsRead.request(client), - client.sendRequest('ui.get') + newWorkspaceUiStateRead.request(client) ]) if (stale) { return @@ -60,11 +74,13 @@ export function useNewWorkspaceRuntimeContext( if (settingsValue) { setRuntimeSettings(settingsValue) } - const uiResult = settledSuccess(uiRes) - if (uiResult) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary; a missing result reads as untrusted. - const ui = (uiResult.result as UiGetResult)?.ui - setTrustedOrcaHooks(ui?.trustedOrcaHooks ?? {}) + if (uiRes.status === 'fulfilled') { + const ui = newWorkspaceUiStateRead.interpret(uiRes.value) + if (ui.accepted) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary; a missing result reads as untrusted. + const trust = ui.value as { trustedOrcaHooks?: PersistedTrustedOrcaHooks } | undefined + setTrustedOrcaHooks(trust?.trustedOrcaHooks ?? {}) + } } const [preflightRes, linearRes] = await probes @@ -72,10 +88,12 @@ export function useNewWorkspaceRuntimeContext( return } const glabInstalled = - (settledSuccess(preflightRes)?.result as { glab?: { installed?: boolean } } | undefined) - ?.glab?.installed === true + readProbeMember( + readProbeMember(settledValue(preflightRes, taskPreflightRead.interpret), 'glab'), + 'installed' + ) === true const linearConnected = - (settledSuccess(linearRes)?.result as { connected?: boolean } | undefined)?.connected === + readProbeMember(settledValue(linearRes, taskLinearStatusRead.interpret), 'connected') === true const visibleProviders = normalizeVisibleTaskProviders(settingsValue?.visibleTaskProviders) setAvailableProviders( diff --git a/mobile/src/components/use-new-workspace-setup-script.ts b/mobile/src/components/use-new-workspace-setup-script.ts index e564b8365e3..2df9067c0e8 100644 --- a/mobile/src/components/use-new-workspace-setup-script.ts +++ b/mobile/src/components/use-new-workspace-setup-script.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import type { RpcClient } from '../transport/rpc-client' -import type { RpcSuccess } from '../transport/types' import { normalizeSetupHookTrust } from '../tasks/setup-hook-trust' +import { newWorkspaceSetupHooksRead } from './new-workspace-operations' import type { WorkspaceCreateSetupDecision } from '../tasks/workspace-create-params' import type { MobileWorkspaceRepo, @@ -39,13 +39,15 @@ export function useNewWorkspaceSetupScript(args: { return } let stale = false - void client - .sendRequest('repo.hooks', { repo: `id:${selectedRepo.id}` }) - .then((response) => { - if (stale || !response.ok) { + void newWorkspaceSetupHooksRead + .request(client, { repo: `id:${selectedRepo.id}` }) + .then((reply) => { + const hooks = newWorkspaceSetupHooksRead.interpret(reply) + if (stale || !hooks.accepted) { return } - const result = (response as RpcSuccess).result as RepoHooksResponse + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = hooks.value as RepoHooksResponse const command = result.hooks?.scripts?.setup?.trim() || null const runPolicy = result.setupRunPolicy ?? 'run-by-default' setDetails({ diff --git a/mobile/src/files/mobile-file-mutation-ownership.ts b/mobile/src/files/mobile-file-mutation-ownership.ts index e5a1cbbeb65..978cb0796d8 100644 --- a/mobile/src/files/mobile-file-mutation-ownership.ts +++ b/mobile/src/files/mobile-file-mutation-ownership.ts @@ -2,8 +2,12 @@ import { parseExecutionHostId } from '../../../src/shared/execution-host' import { assertFileMutationOwnershipCapability } from '../../../src/shared/file-mutation-ownership' import type { RuntimeStatus } from '../../../src/shared/runtime-types' import type { SshConnectionState, SshMutationExpectation } from '../../../src/shared/ssh-types' -import type { RpcClient } from '../transport/rpc-client' -import type { RpcFailure, RpcSuccess } from '../transport/types' +import { + fileOwnershipRuntimeStatusRead, + fileOwnershipSshStateRead, + fileOwnershipWorktreeRead, + type MobileFileOwnershipRpcSender +} from './mobile-file-ownership-operations' const FILE_MUTATION_TIMEOUT_MS = 15_000 const SSH_OWNER_CHANGED_MESSAGE = @@ -35,47 +39,42 @@ export function buildMobileFileMutationOwnership( } export async function captureMobileFileMutationOwnership( - client: Pick, + client: MobileFileOwnershipRpcSender, worktree: string ): Promise { - const status = await requestResult>( - client, - 'status.get', - undefined - ) + const statusReply = await fileOwnershipRuntimeStatusRead.request(client, undefined, { + timeoutMs: FILE_MUTATION_TIMEOUT_MS + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const status = fileOwnershipRuntimeStatusRead.interpret(statusReply) as Pick< + RuntimeStatus, + 'capabilities' + > assertFileMutationOwnershipCapability(status) - const result = await requestResult<{ worktree?: { hostId?: string | null } }>( + const worktreeReply = await fileOwnershipWorktreeRead.request( client, - 'worktree.show', - { worktree } + { worktree }, + { timeoutMs: FILE_MUTATION_TIMEOUT_MS } ) - if (!result.worktree) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const summary = fileOwnershipWorktreeRead.interpret(worktreeReply) as + | { hostId?: string | null } + | undefined + if (!summary) { throw new Error(SSH_OWNER_CHANGED_MESSAGE) } - const host = parseExecutionHostId(result.worktree.hostId) - const sshState = - host?.kind === 'ssh' - ? ( - await requestResult<{ state: SshConnectionState | null }>(client, 'ssh.getState', { - targetId: host.targetId - }) - ).state - : null - return buildMobileFileMutationOwnership(result.worktree.hostId, sshState) -} - -async function requestResult( - client: Pick, - method: string, - params: unknown -): Promise { - const response = await client.sendRequest(method, params, { - timeoutMs: FILE_MUTATION_TIMEOUT_MS - }) - if (!response.ok) { - throw new Error((response as RpcFailure).error.message) + const host = parseExecutionHostId(summary.hostId) + let sshState: SshConnectionState | null = null + if (host?.kind === 'ssh') { + const stateReply = await fileOwnershipSshStateRead.request( + client, + { targetId: host.targetId }, + { timeoutMs: FILE_MUTATION_TIMEOUT_MS } + ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + sshState = fileOwnershipSshStateRead.interpret(stateReply) as SshConnectionState | null } - return (response as RpcSuccess).result as TResult + return buildMobileFileMutationOwnership(summary.hostId, sshState) } diff --git a/mobile/src/files/mobile-file-ownership-operations.ts b/mobile/src/files/mobile-file-ownership-operations.ts new file mode 100644 index 00000000000..82084993729 --- /dev/null +++ b/mobile/src/files/mobile-file-ownership-operations.ts @@ -0,0 +1,35 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedMemberReader } from '../transport/rpc-reader-payload' + +// The three reads that pin which execution host owns a workspace before a file mutation is sent. +// All three share one acceptance because the capture is all-or-nothing: any refusal aborts the +// mutation with the host's own message rather than letting a write land on the wrong host. + +// The runtime status this gate needs is the one the Tasks screen already asks for, field for +// field. A second operation would only be a second name for the same wire. +export { taskRuntimeStatusRead as fileOwnershipRuntimeStatusRead } from '../tasks/mobile-task-runtime-operations' + +/** The workspace row the mutation targets. A null result throws where `result.worktree` did. */ +export const fileOwnershipWorktreeRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.file-mutation-owner', + method: 'worktree.show', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('worktree-summary', 'worktree') + }) +) + +/** The SSH connection generation the mutation is expected to still be running on. */ +export const fileOwnershipSshStateRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ssh.file-mutation-owner-state', + method: 'ssh.getState', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('ssh-connection-state', 'state') + }) +) + +/** What an ownership capture sends with, named from an operation so no module names the raw port. */ +export type MobileFileOwnershipRpcSender = Parameters[0] diff --git a/mobile/src/files/mobile-file-preview-operations.ts b/mobile/src/files/mobile-file-preview-operations.ts new file mode 100644 index 00000000000..ac679e61bce --- /dev/null +++ b/mobile/src/files/mobile-file-preview-operations.ts @@ -0,0 +1,83 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +/** + * The preview screen's reads and writes. + * + * Every one of them is a skip rather than a throw, because a refused preview is not an error the + * screen raises: it is a result the screen renders. The refusal itself stays at the call site, + * which maps the host's code and message into display copy (`previewError`) and decides whether + * the failure is a stale terminal-artifact grant worth refreshing. No acceptance policy exposes a + * refusal code, and only these two consumers want one. + * + * The payloads are unchecked here because the shape depends on the path, not on the method: + * `normalizeMobileFilePreviewResult` picks the image or text projection from the file name, which + * a module-level reader cannot see. + */ + +/** files.read for a preview. The tab doc asks the same method under a throwing policy. */ +export const filePreviewTextRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.preview-text-or-skip', + method: 'files.read', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-preview') + }) +) + +/** files.readPreview for a preview; the tab doc's image read is the other policy on it. */ +export const filePreviewImageRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.preview-image-or-skip', + method: 'files.readPreview', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-preview') + }) +) + +export const terminalArtifactTextRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.terminal-artifact-text-or-skip', + method: 'files.readTerminalArtifact', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-preview') + }) +) + +export const terminalArtifactImageRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.terminal-artifact-image-or-skip', + method: 'files.readTerminalArtifactPreview', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-preview') + }) +) + +/** The save. Its reply body is never read: a success is the whole answer. */ +export const terminalArtifactWrite = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.write-terminal-artifact-or-skip', + method: 'files.writeTerminalArtifact', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('artifact-written') + }) +) + +/** Re-resolves a terminal path to mint a fresh grant. A refusal leaves the stale grant in place. */ +export const terminalArtifactPathResolve = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.resolve-terminal-path-or-skip', + method: 'files.resolveTerminalPath', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('terminal-path-resolution') + }) +) + +/** What a preview send takes, named from an operation so no module names the raw port. */ +export type MobileFilePreviewRpcSender = Parameters[0] diff --git a/mobile/src/files/mobile-file-preview-request.test.ts b/mobile/src/files/mobile-file-preview-request.test.ts index fe15cad0b07..9c8692e5a78 100644 --- a/mobile/src/files/mobile-file-preview-request.test.ts +++ b/mobile/src/files/mobile-file-preview-request.test.ts @@ -4,9 +4,12 @@ import { createMobileFilePreviewRequest, formatPreviewByteLength, loadMobileFilePreview, - normalizeMobileFilePreviewResponse, saveMobileTerminalArtifactPreview } from './mobile-file-preview-request' +import { + normalizeMobileFilePreviewResult, + previewErrorFromRefusal +} from './mobile-file-preview-response' function ok(result: unknown): RpcSuccess { return { id: '1', ok: true, result, _meta: { runtimeId: 'runtime-1' } } @@ -592,7 +595,7 @@ describe('mobile-file-preview-request', () => { ['missing mimeType', { content: 'aW1hZ2U=', isBinary: true, isImage: true }], ['empty content', { content: '', isBinary: true, isImage: true, mimeType: 'image/png' }] ])('rejects invalid image preview results: %s', (_label, result) => { - expect(normalizeMobileFilePreviewResponse('assets/logo.png', ok(result))).toEqual({ + expect(normalizeMobileFilePreviewResult('assets/logo.png', result)).toEqual({ status: 'error', message: 'Binary preview unavailable', reconnect: false @@ -601,10 +604,11 @@ describe('mobile-file-preview-request', () => { it('normalizes markdown, html, text, empty, and truncated reads', () => { expect( - normalizeMobileFilePreviewResponse( - 'README.md', - ok({ content: '# Hi', truncated: false, byteLength: 4 }) - ) + normalizeMobileFilePreviewResult('README.md', { + content: '# Hi', + truncated: false, + byteLength: 4 + }) ).toEqual({ status: 'ready', kind: 'markdown', @@ -613,16 +617,18 @@ describe('mobile-file-preview-request', () => { byteLength: 4 }) expect( - normalizeMobileFilePreviewResponse( - 'index.html', - ok({ content: '

Hi

', truncated: false, byteLength: 11 }) - ) + normalizeMobileFilePreviewResult('index.html', { + content: '

Hi

', + truncated: false, + byteLength: 11 + }) ).toMatchObject({ status: 'ready', kind: 'html' }) expect( - normalizeMobileFilePreviewResponse( - 'src/app.ts', - ok({ content: 'const a = 1', truncated: true, byteLength: 700_000 }) - ) + normalizeMobileFilePreviewResult('src/app.ts', { + content: 'const a = 1', + truncated: true, + byteLength: 700_000 + }) ).toEqual({ status: 'ready', kind: 'text', @@ -631,10 +637,11 @@ describe('mobile-file-preview-request', () => { byteLength: 700_000 }) expect( - normalizeMobileFilePreviewResponse( - 'empty.txt', - ok({ content: '', truncated: false, byteLength: 0 }) - ) + normalizeMobileFilePreviewResult('empty.txt', { + content: '', + truncated: false, + byteLength: 0 + }) ).toEqual({ status: 'empty', kind: 'text' }) }) @@ -651,7 +658,7 @@ describe('mobile-file-preview-request', () => { ['terminal_file_grant_stale', 'Reload preview before saving', false], ['permission denied', 'Unable to load preview', false] ])('maps preview failure %s', (message, expected, reconnect) => { - expect(normalizeMobileFilePreviewResponse('src/app.ts', fail(message))).toEqual({ + expect(previewErrorFromRefusal(fail(message).error)).toEqual({ status: 'error', message: expected, reconnect diff --git a/mobile/src/files/mobile-file-preview-request.ts b/mobile/src/files/mobile-file-preview-request.ts index 63b61326654..5a76dc33560 100644 --- a/mobile/src/files/mobile-file-preview-request.ts +++ b/mobile/src/files/mobile-file-preview-request.ts @@ -1,9 +1,18 @@ import { classifyMobileArtifact } from '../session/mobile-artifact-kind' +import type { RpcAcceptedResult } from '../transport/rpc-accepted-result' import type { RpcFailure, RpcResponse } from '../transport/types' -import type { RpcClient } from '../transport/rpc-client' import { - normalizeMobileFilePreviewResponse, + filePreviewImageRead, + filePreviewTextRead, + terminalArtifactImageRead, + terminalArtifactTextRead, + terminalArtifactWrite, + type MobileFilePreviewRpcSender +} from './mobile-file-preview-operations' +import { + normalizeMobileFilePreviewResult, previewError, + previewErrorFromRefusal, type MobileFilePreviewResult } from './mobile-file-preview-response' import { @@ -12,11 +21,8 @@ import { type TerminalArtifactRetryOptions } from './mobile-terminal-artifact-grant-refresh' -export { - formatPreviewByteLength, - normalizeMobileFilePreviewResponse, - previewError -} from './mobile-file-preview-response' +export { formatPreviewByteLength, previewError } from './mobile-file-preview-response' + export type { MobileFilePreviewResult, MobileFilePreviewTextKind @@ -35,17 +41,26 @@ export type MobileFilePreviewSource = } | MobileTerminalArtifactPreviewSource -export type MobileFilePreviewRequest = { - method: MobileFilePreviewReadMethod | MobileTerminalArtifactPreviewReadMethod - params: { - worktree: string - relativePath?: string - absolutePath?: string - grantId?: string - } -} +/** Which read the path selects, and the params that read takes. */ +export type MobileFilePreviewRequest = + | { + method: MobileFilePreviewReadMethod + params: { worktree: string; relativePath: string } + } + | { + method: MobileTerminalArtifactPreviewReadMethod + params: { worktree: string; absolutePath: string; grantId: string } + } + +/** + * A settled preview send. The refusal is carried rather than interpreted because the preview + * screen's fallback copy is the host's `code`, which no acceptance policy exposes, and the grant + * refresh reads the same code to decide whether a stale grant is worth re-minting. + */ +type MobileFilePreviewOutcome = + | { accepted: true; payload: unknown } + | { accepted: false; refusal: RpcFailure['error'] } -type MobileFilePreviewClient = Pick type TerminalArtifactSource = MobileTerminalArtifactPreviewSource type TerminalArtifactSaveOptions = TerminalArtifactRetryOptions & { baseContent?: string @@ -83,35 +98,80 @@ export function createMobileFilePreviewRequest( } } +async function sendMobileFilePreviewRead( + client: MobileFilePreviewRpcSender, + request: MobileFilePreviewRequest +): Promise { + switch (request.method) { + case 'files.read': + return settlePreviewSend( + await filePreviewTextRead.request(client, request.params), + filePreviewTextRead.interpret + ) + case 'files.readPreview': + return settlePreviewSend( + await filePreviewImageRead.request(client, request.params), + filePreviewImageRead.interpret + ) + case 'files.readTerminalArtifact': + return settlePreviewSend( + await terminalArtifactTextRead.request(client, request.params), + terminalArtifactTextRead.interpret + ) + case 'files.readTerminalArtifactPreview': + return settlePreviewSend( + await terminalArtifactImageRead.request(client, request.params), + terminalArtifactImageRead.interpret + ) + } +} + +function settlePreviewSend( + reply: RpcResponse, + interpret: (reply: RpcResponse) => RpcAcceptedResult +): MobileFilePreviewOutcome { + const verdict = interpret(reply) + return verdict.accepted + ? { accepted: true, payload: verdict.value } + : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. + { accepted: false, refusal: (reply as RpcFailure).error } +} + export async function loadMobileFilePreview( - client: MobileFilePreviewClient, + client: MobileFilePreviewRpcSender, worktreeIdOrSource: string | MobileFilePreviewSource, relativePath?: string, options: TerminalArtifactRetryOptions = {} ): Promise { let source = worktreeIdOrSource - let request = createMobileFilePreviewRequest(source, relativePath) - let response = await client.sendRequest(request.method, request.params) - if (!response.ok && typeof source !== 'string' && source.source === 'terminalArtifact') { + let read = await sendMobileFilePreviewRead( + client, + createMobileFilePreviewRequest(source, relativePath) + ) + if (!read.accepted && typeof source !== 'string' && source.source === 'terminalArtifact') { const refreshed = await refreshTerminalArtifactSourceAfterGrantFailure( client, source, - response, + read.refusal, options ) if (refreshed) { source = refreshed options.onTerminalArtifactSourceRefreshed?.(refreshed) - request = createMobileFilePreviewRequest(source, relativePath) - response = await client.sendRequest(request.method, request.params) + read = await sendMobileFilePreviewRead( + client, + createMobileFilePreviewRequest(source, relativePath) + ) } } const previewPath = typeof source === 'string' ? relativePath! : previewPathForSource(source) - return normalizeMobileFilePreviewResponse(previewPath, response) + return read.accepted + ? normalizeMobileFilePreviewResult(previewPath, read.payload) + : previewErrorFromRefusal(read.refusal) } export async function saveMobileTerminalArtifactPreview( - client: MobileFilePreviewClient, + client: MobileFilePreviewRpcSender, source: TerminalArtifactSource, content: string, options: TerminalArtifactSaveOptions = {} @@ -135,26 +195,22 @@ export async function saveMobileTerminalArtifactPreview( options.onTerminalArtifactSourceRefreshed?.(verified.source) } } - let response = await writeTerminalArtifactPreview(client, writeSource, content) - if (response.ok) { + let write = await writeTerminalArtifactPreview(client, writeSource, content) + if (write.accepted) { return { status: 'saved' } } if (typeof options.baseContent !== 'string') { - return previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) + return previewErrorFromRefusal(write.refusal) } const refreshed = await refreshTerminalArtifactSourceAfterGrantFailure( client, writeSource, - response, + write.refusal, options ) if (!refreshed) { - return previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) + return previewErrorFromRefusal(write.refusal) } const verified = await verifyTerminalArtifactBaseContent(client, refreshed, options.baseContent, { refreshGrant: false @@ -164,17 +220,15 @@ export async function saveMobileTerminalArtifactPreview( } options.onTerminalArtifactSourceRefreshed?.(refreshed) writeSource = verified.source - response = await writeTerminalArtifactPreview(client, writeSource, content) - if (!response.ok) { - return previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) + write = await writeTerminalArtifactPreview(client, writeSource, content) + if (!write.accepted) { + return previewErrorFromRefusal(write.refusal) } return { status: 'saved' } } async function verifyTerminalArtifactBaseContent( - client: MobileFilePreviewClient, + client: MobileFilePreviewRpcSender, source: TerminalArtifactSource, baseContent: string, options: TerminalArtifactRetryOptions @@ -183,38 +237,26 @@ async function verifyTerminalArtifactBaseContent( | { status: 'error'; error: MobileFilePreviewResult } > { let readSource = source - let request = createMobileFilePreviewRequest(readSource) - let response = await client.sendRequest(request.method, request.params) + let read = await sendMobileFilePreviewRead(client, createMobileFilePreviewRequest(readSource)) let refreshed = false - if (!response.ok) { + if (!read.accepted) { const nextSource = await refreshTerminalArtifactSourceAfterGrantFailure( client, readSource, - response, + read.refusal, options ) if (!nextSource) { - return { - status: 'error', - error: previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) - } + return { status: 'error', error: previewErrorFromRefusal(read.refusal) } } readSource = nextSource refreshed = true - request = createMobileFilePreviewRequest(readSource) - response = await client.sendRequest(request.method, request.params) + read = await sendMobileFilePreviewRead(client, createMobileFilePreviewRequest(readSource)) } - if (!response.ok) { - return { - status: 'error', - error: previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) - } + if (!read.accepted) { + return { status: 'error', error: previewErrorFromRefusal(read.refusal) } } - const latest = normalizeMobileFilePreviewResponse(readSource.absolutePath, response) + const latest = normalizeMobileFilePreviewResult(readSource.absolutePath, read.payload) if (latest.status === 'error' || latest.status === 'waiting') { return { status: 'error', error: latest } } @@ -231,17 +273,20 @@ async function verifyTerminalArtifactBaseContent( return { status: 'ok', source: readSource, refreshed } } -function writeTerminalArtifactPreview( - client: MobileFilePreviewClient, +async function writeTerminalArtifactPreview( + client: MobileFilePreviewRpcSender, source: TerminalArtifactSource, content: string -): Promise { - return client.sendRequest('files.writeTerminalArtifact', { - worktree: `id:${source.worktreeId}`, - absolutePath: source.absolutePath, - grantId: source.grantId, - content - }) +): Promise { + return settlePreviewSend( + await terminalArtifactWrite.request(client, { + worktree: `id:${source.worktreeId}`, + absolutePath: source.absolutePath, + grantId: source.grantId, + content + }), + terminalArtifactWrite.interpret + ) } function terminalArtifactPreviewMatchesBase( diff --git a/mobile/src/files/mobile-file-preview-response.ts b/mobile/src/files/mobile-file-preview-response.ts index 0b7b1b23be8..fba691dd2c4 100644 --- a/mobile/src/files/mobile-file-preview-response.ts +++ b/mobile/src/files/mobile-file-preview-response.ts @@ -1,5 +1,5 @@ import { classifyMobileArtifact } from '../session/mobile-artifact-kind' -import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types' +import type { RpcFailure } from '../transport/types' import { isMarkdownPath } from './file-tree' import { isTerminalArtifactGrantError } from './terminal-artifact-grant-error' @@ -37,23 +37,22 @@ export type MobileFilePreviewResult = reconnect: boolean } -export function normalizeMobileFilePreviewResponse( +/** The accepted arm, for a call site whose acceptance policy already admitted the payload. */ +export function normalizeMobileFilePreviewResult( relativePath: string, - response: RpcResponse + result: unknown ): MobileFilePreviewResult { - if (!response.ok) { - return previewError( - (response as RpcFailure).error.message || (response as RpcFailure).error.code - ) - } - - const result = (response as RpcSuccess).result if (classifyMobileArtifact(relativePath) === 'image') { return normalizeImagePreviewResult(result) } return normalizeTextPreviewResult(relativePath, result) } +/** The refused arm. The code is the fallback copy, which is why the refusal itself is needed. */ +export function previewErrorFromRefusal(error: RpcFailure['error']): MobileFilePreviewResult { + return previewError(error.message || error.code) +} + export function previewError(message: string): MobileFilePreviewResult { const normalized = message.toLowerCase() if (normalized === 'binary_file' || normalized.includes('binary_file')) { diff --git a/mobile/src/files/mobile-file-tab-doc-operations.ts b/mobile/src/files/mobile-file-tab-doc-operations.ts new file mode 100644 index 00000000000..244d44e5e39 --- /dev/null +++ b/mobile/src/files/mobile-file-tab-doc-operations.ts @@ -0,0 +1,47 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +/** + * What a session file tab reads to render one document. + * + * All three throw the host's message on refusal, which is the opposite of the preview screen's + * policy on the same two file methods: a tab maps the throw to an error doc and keeps the tab, + * while the preview screen renders the refusal as body copy. Two policies, two families, named + * here and in mobile-file-preview-operations.ts so neither can drift onto the other. + * + * The payloads stay unchecked: the tab picks its projection from the path, and moving a shape + * check into a reader would reject replies the tab renders today. + */ + +export const fileTabDiffRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'git.file-tab-diff', + method: 'git.diff', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-tab-diff') + }) +) + +export const fileTabTextRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.file-tab-text', + method: 'files.read', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-tab-text') + }) +) + +export const fileTabImageRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'files.file-tab-image', + method: 'files.readPreview', + acceptance: 'require-result-or-throw-message', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('file-tab-image') + }) +) + +/** What a file tab reads with, named from an operation so no module names the raw port. */ +export type MobileFileTabDocRpcSender = Parameters[0] diff --git a/mobile/src/files/mobile-file-tab-doc.ts b/mobile/src/files/mobile-file-tab-doc.ts index 78977b5fd30..1e9d43abbba 100644 --- a/mobile/src/files/mobile-file-tab-doc.ts +++ b/mobile/src/files/mobile-file-tab-doc.ts @@ -1,11 +1,13 @@ import { buildImageDataUri } from '../../../src/shared/image-data-uri' import { classifyMobileArtifact } from '../session/mobile-artifact-kind' import { buildMobileDiffLines, type MobileDiffLine } from '../session/mobile-diff-lines' -import type { RpcClient } from '../transport/rpc-client' -import type { RpcFailure, RpcSuccess } from '../transport/types' import { mobileDiffImageDataUri, type MobileBinaryDiffResult } from './mobile-diff-image-preview' - -type FileTabDocClient = Pick +import { + fileTabDiffRead, + fileTabImageRead, + fileTabTextRead, + type MobileFileTabDocRpcSender +} from './mobile-file-tab-doc-operations' // The ready doc a session file tab renders. Mirrors the ready arm of the route's // FileDocState; kept in src so the loader stays testable without the route. @@ -24,21 +26,19 @@ export type MobileFileTabDocRequest = { // Throws 'binary_file'/'file_too_large'/the RPC error message; callers map those // to error docs. export async function resolveMobileFileTabDoc( - client: FileTabDocClient, + client: MobileFileTabDocRpcSender, request: MobileFileTabDocRequest ): Promise { const worktree = `id:${request.worktreeId}` const { relativePath } = request if (request.diffSource === 'staged' || request.diffSource === 'unstaged') { - const response = await client.sendRequest('git.diff', { + const reply = await fileTabDiffRead.request(client, { worktree, filePath: relativePath, staged: request.diffSource === 'staged' }) - if (!response.ok) { - throw new Error((response as RpcFailure).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 = fileTabDiffRead.interpret(reply) as | { kind: 'text'; originalContent: string; modifiedContent: string } | MobileBinaryDiffResult if (result.kind !== 'text') { @@ -56,11 +56,9 @@ export async function resolveMobileFileTabDoc( const artifactKind = classifyMobileArtifact(relativePath) if (artifactKind === 'image') { - const preview = await client.sendRequest('files.readPreview', { worktree, relativePath }) - if (!preview.ok) { - throw new Error((preview as RpcFailure).error.message) - } - const result = (preview as RpcSuccess).result as { + const preview = await fileTabImageRead.request(client, { worktree, relativePath }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = fileTabImageRead.interpret(preview) as { content: string isImage?: boolean mimeType?: string @@ -72,11 +70,9 @@ export async function resolveMobileFileTabDoc( return { status: 'ready', kind: 'image', dataUri } } - const response = await client.sendRequest('files.read', { worktree, relativePath }) - if (!response.ok) { - throw new Error((response as RpcFailure).error.message) - } - const result = (response as RpcSuccess).result as { + const reply = await fileTabTextRead.request(client, { worktree, relativePath }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = fileTabTextRead.interpret(reply) as { content: string truncated: boolean byteLength: number diff --git a/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts b/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts index cbe5ffdebd8..0e79764437b 100644 --- a/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts +++ b/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts @@ -1,10 +1,11 @@ import type { RuntimeNativeChatFileContext } from '../../../src/shared/runtime-types' -import type { RpcClient } from '../transport/rpc-client' -import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types' +import type { RpcFailure } from '../transport/types' +import { + terminalArtifactPathResolve, + type MobileFilePreviewRpcSender +} from './mobile-file-preview-operations' import { isTerminalArtifactGrantError } from './terminal-artifact-grant-error' -type MobileFilePreviewClient = Pick - export type MobileTerminalArtifactPreviewSource = { source: 'terminalArtifact' worktreeId: string @@ -22,26 +23,28 @@ export type TerminalArtifactRetryOptions = { refreshGrant?: boolean } +/** Takes the refusal rather than the envelope: every caller already routed on its own acceptance. */ export async function refreshTerminalArtifactSourceAfterGrantFailure( - client: MobileFilePreviewClient, + client: MobileFilePreviewRpcSender, source: MobileTerminalArtifactPreviewSource, - response: RpcResponse, + refusal: RpcFailure['error'], options: TerminalArtifactRetryOptions = {} ): Promise { - if (response.ok || !isTerminalArtifactGrantFailure(response, options)) { + if (!isTerminalArtifactGrantFailure(refusal, options)) { return null } - const refreshed = await client.sendRequest('files.resolveTerminalPath', { + const reply = await terminalArtifactPathResolve.request(client, { worktree: `id:${source.worktreeId}`, pathText: source.pathText ?? source.absolutePath, ...(source.cwd ? { cwd: source.cwd } : {}), ...(source.terminalHandle ? { terminal: source.terminalHandle } : {}), ...(source.nativeChatContext ? { nativeChatContext: source.nativeChatContext } : {}) }) - if (!refreshed.ok) { + const resolved = terminalArtifactPathResolve.interpret(reply) + if (!resolved.accepted) { return null } - const result = (refreshed as RpcSuccess).result + const result = resolved.value if (!isTerminalArtifactResolution(result)) { return null } @@ -62,13 +65,13 @@ export async function refreshTerminalArtifactSourceAfterGrantFailure( } function isTerminalArtifactGrantFailure( - response: RpcFailure, + refusal: RpcFailure['error'], options: TerminalArtifactRetryOptions ): boolean { if (options.refreshGrant === false) { return false } - return isTerminalArtifactGrantError(`${response.error.code} ${response.error.message}`) + return isTerminalArtifactGrantError(`${refusal.code} ${refusal.message}`) } function isTerminalArtifactResolution(result: unknown): result is { diff --git a/mobile/src/home/mobile-home-host-operations.ts b/mobile/src/home/mobile-home-host-operations.ts new file mode 100644 index 00000000000..9bd77db3710 --- /dev/null +++ b/mobile/src/home/mobile-home-host-operations.ts @@ -0,0 +1,17 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +/** + * The Home card's per-host counts. Decorative: a refused summary leaves the card on whatever it + * already showed, so refusal is a skip. Its glab and Linear probes are the task-tooling reads in + * ../tasks/mobile-task-runtime-operations.ts — the same question, asked by a second screen. + */ +export const homeHostStatsRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'stats.home-summary-or-skip', + method: 'stats.summary', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('home-stats-summary') + }) +) diff --git a/mobile/src/home/mobile-home-host-requests.ts b/mobile/src/home/mobile-home-host-requests.ts index cf4c45cf4a8..70ba1f93c20 100644 --- a/mobile/src/home/mobile-home-host-requests.ts +++ b/mobile/src/home/mobile-home-host-requests.ts @@ -1,6 +1,7 @@ import { settingsRead } from '../transport/settings-read-operations' import { decodeAccountsSnapshot, type AccountsSnapshot } from '../components/AccountUsage' import type { HomeStatsSummary } from '../stats/home-stats-total' +import { taskLinearStatusRead, taskPreflightRead } from '../tasks/mobile-task-runtime-operations' import { filterAvailableTaskProviders, normalizeVisibleTaskProviders, @@ -8,6 +9,7 @@ import { } from '../tasks/mobile-task-providers' import type { RpcClient } from '../transport/rpc-client' import { sendSingleFlightRequest } from '../transport/request-single-flight' +import { homeHostStatsRead } from './mobile-home-host-operations' type HomeTaskSettings = { visibleTaskProviders?: unknown @@ -39,12 +41,15 @@ export function fetchMobileHomeStats( setStats: HomeStatsSetter, disposed: () => boolean ): void { - sendSingleFlightRequest(client, hostId, 'stats.summary') - .then((response) => { - if (!disposed() && response.ok) { + homeHostStatsRead + .requestSingleFlight(client, hostId) + .then((reply) => { + const summary = homeHostStatsRead.interpret(reply) + if (!disposed() && summary.accepted) { setStats((previous) => ({ ...previous, - [hostId]: response.result as HomeStatsSummary + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + [hostId]: summary.value as HomeStatsSummary })) } }) @@ -75,8 +80,8 @@ export function fetchMobileHomeTaskProviders( ): void { Promise.all([ settingsRead.requestSingleFlight(client, hostId), - sendSingleFlightRequest(client, hostId, 'preflight.check'), - sendSingleFlightRequest(client, hostId, 'linear.status') + taskPreflightRead.requestSingleFlight(client, hostId), + taskLinearStatusRead.requestSingleFlight(client, hostId) ]) .then(([settingsResponse, preflightResponse, linearResponse]) => { if (disposed()) { @@ -87,10 +92,16 @@ export function fetchMobileHomeTaskProviders( ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. ((settingsResult.value ?? {}) as HomeTaskSettings) : {} - const preflight = preflightResponse.ok - ? (preflightResponse.result as HomePreflightStatus) + const preflightResult = taskPreflightRead.interpret(preflightResponse) + const preflight = preflightResult.accepted + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (preflightResult.value as HomePreflightStatus) + : null + const linearResult = taskLinearStatusRead.interpret(linearResponse) + const linear = linearResult.accepted + ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + (linearResult.value as HomeLinearStatus) : null - const linear = linearResponse.ok ? (linearResponse.result as HomeLinearStatus) : null const providers = filterAvailableTaskProviders( normalizeVisibleTaskProviders(settings.visibleTaskProviders), { diff --git a/mobile/src/host-screen/host-screen-operations.ts b/mobile/src/host-screen/host-screen-operations.ts new file mode 100644 index 00000000000..4c0d3e46055 --- /dev/null +++ b/mobile/src/host-screen/host-screen-operations.ts @@ -0,0 +1,106 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { + rpcUncheckedMemberReader, + rpcUncheckedPayloadReader +} from '../transport/rpc-reader-payload' + +// What the host screen reads to label its rows and to mirror the desktop's workspace view store. +// Every read here is decorative: a refusal leaves the screen on what it already has and the next +// refresh retries, so all of them skip rather than throw. + +export const hostRepoCatalogRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'repo.host-catalog-or-skip', + method: 'repo.list', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('repo-catalog') + }) +) + +/** Row labels for a catalog that spans hosts. Absent on a host that predates the method. */ +export const hostSshTargetSummariesRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ssh.host-target-summaries-or-skip', + method: 'ssh.listTargetSummaries', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('ssh-target-summaries') + }) +) + +export const hostPlatformRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'host.platform-or-skip', + method: 'host.platform', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('host-platform') + }) +) + +/** + * The desktop's shared workspace view settings, a third family on ui.get. + * + * It keeps the Tasks screen's property-read throw on a null result — the screen's own try/catch is + * what that throw has always landed in — where the New Workspace drawer's reader degrades instead. + */ +export const hostViewSettingsRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ui.host-view-settings-or-skip', + method: 'ui.get', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedMemberReader('ui-view-settings', 'ui') + }) +) + +/** Patching the same store. Best-effort: the local state already moved, and no reply is read. */ +export const hostViewSettingsWrite = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'ui.set-host-view-settings-or-skip', + method: 'ui.set', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('ui-view-settings-written') + }) +) + +/** + * The host list's three row mutations. + * + * All three skip on refusal, which is not the policy `worktree.set-review-link` uses on the same + * method in source-control: a review link throws so the composer can report it, where a pin write + * is optimistic and its `.catch` already swallowed everything. Two policies, both named. + */ +export const worktreePinWrite = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.set-pinned-or-skip', + method: 'worktree.set', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('pin-written') + }) +) + +/** Deleting a row. Only acceptance is read: a refusal is what puts the row back. */ +export const worktreeRemove = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.remove-or-skip', + method: 'worktree.rm', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('worktree-removed') + }) +) + +/** Telling the host which workspace the phone opened. Best-effort; navigation does not wait. */ +export const worktreeActivate = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.activate-or-skip', + method: 'worktree.activate', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('worktree-activated') + }) +) diff --git a/mobile/src/host-screen/use-host-repo-metadata.ts b/mobile/src/host-screen/use-host-repo-metadata.ts index 64ece7b700e..d87d917b467 100644 --- a/mobile/src/host-screen/use-host-repo-metadata.ts +++ b/mobile/src/host-screen/use-host-repo-metadata.ts @@ -2,32 +2,47 @@ import { optionalSettingsRead } from '../transport/settings-read-operations' import { useCallback } from 'react' import { getRepoExecutionHostId } from '../../../src/shared/execution-host' import { setCachedRepos } from '../cache/repo-cache' +import type { RpcAcceptedResult } from '../transport/rpc-accepted-result' import type { RpcClient } from '../transport/rpc-client' -import type { ConnectionState, RpcResponse, RpcSuccess } from '../transport/types' +import type { ConnectionState, RpcResponse } from '../transport/types' import type { RepoSummary } from '../worktree/host-worktree-rpc-types' import { repoColor } from '../worktree/repo-color' import { buildHostLabelById, buildRepoHostIdByRepoId } from '../worktree/worktree-host-context-labels' +import { + hostPlatformRead, + hostRepoCatalogRead, + hostSshTargetSummariesRead +} from './host-screen-operations' import type { HostScreenState } from './use-host-screen-state' const REPO_METADATA_REFRESH_MS = 60_000 type SshTargetSummaryRow = { id: string; label: string } -async function requestMetadataResponse( - client: RpcClient, - method: 'repo.list' | 'ssh.listTargetSummaries' | 'host.platform' -): Promise { +async function settledMetadataReply(send: () => Promise): Promise { try { - return await client.sendRequest(method) + return await send() } catch { // Best-effort: hosts that predate a method still list repos; labels degrade to host ids. return null } } +/** An accepted metadata payload, or null for a refusal or a send that never landed. */ +function acceptedMetadata( + reply: RpcResponse | null, + interpret: (reply: RpcResponse) => RpcAcceptedResult +): unknown { + if (!reply) { + return null + } + const verdict = interpret(reply) + return verdict.accepted ? verdict.value : null +} + function readSshTargets(result: unknown): SshTargetSummaryRow[] { const targets = (result as { targets?: unknown } | null)?.targets if (!Array.isArray(targets)) { @@ -93,15 +108,18 @@ export function useHostRepoMetadata(args: { try { do { fetchRepoMetadataPendingRef.current.delete(requestClient) - const repoResponse = await requestMetadataResponse(requestClient, 'repo.list') - if ( - clientRef.current !== requestClient || - hostId !== requestHostId || - !repoResponse?.ok - ) { + const repoReply = await settledMetadataReply(() => + hostRepoCatalogRead.request(requestClient) + ) + if (clientRef.current !== requestClient || hostId !== requestHostId) { return } - const repoResult = (repoResponse as RpcSuccess).result as { repos: RepoSummary[] } + const repos = repoReply && hostRepoCatalogRead.interpret(repoReply) + if (!repos || !repos.accepted) { + return + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const repoResult = repos.value as { repos: RepoSummary[] } repoMetadataFetchedAtRef.current = Date.now() setCachedRepos(requestHostId, repoResult.repos) setRepoColorsByName( @@ -127,9 +145,9 @@ export function useHostRepoMetadata(args: { const hostIds = new Set(repoResult.repos.map((repo) => getRepoExecutionHostId(repo))) if (hostIds.size > 1) { const [sshTargets, hostSettings, hostPlatform] = await Promise.all([ - requestMetadataResponse(requestClient, 'ssh.listTargetSummaries'), + settledMetadataReply(() => hostSshTargetSummariesRead.request(requestClient)), optionalSettingsRead.request(requestClient).catch(() => null), - requestMetadataResponse(requestClient, 'host.platform') + settledMetadataReply(() => hostPlatformRead.request(requestClient)) ]) if (clientRef.current !== requestClient || hostId !== requestHostId) { return @@ -139,13 +157,17 @@ export function useHostRepoMetadata(args: { : null setHostLabelById( buildHostLabelById({ - sshTargets: readSshTargets(sshTargets?.ok ? sshTargets.result : null), + sshTargets: readSshTargets( + acceptedMetadata(sshTargets, hostSshTargetSummariesRead.interpret) + ), hostSettingOverrides: readHostSettingOverrides( hostSettingsResult?.accepted ? hostSettingsResult.value : undefined ) }) ) - setHostPlatform(readHostPlatform(hostPlatform?.ok ? hostPlatform.result : null)) + setHostPlatform( + readHostPlatform(acceptedMetadata(hostPlatform, hostPlatformRead.interpret)) + ) } } while (fetchRepoMetadataPendingRef.current.has(requestClient)) } catch { diff --git a/mobile/src/host-screen/use-host-view-settings.ts b/mobile/src/host-screen/use-host-view-settings.ts index 877aa5f3c9f..9fb192b76ab 100644 --- a/mobile/src/host-screen/use-host-view-settings.ts +++ b/mobile/src/host-screen/use-host-view-settings.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo } from 'react' import type { RpcClient } from '../transport/rpc-client' -import type { ConnectionState, RpcSuccess } from '../transport/types' +import type { ConnectionState } from '../transport/types' import { getMobileWorkspaceLineageGroupKey } from '../worktree/mobile-workspace-lineage' import { WORKSPACE_SORT_OPTIONS as SORT_OPTIONS } from '../worktree/workspace-list-picker-options' import { @@ -12,6 +12,7 @@ import { type WorkspaceViewSettings } from '../worktree/workspace-view-settings' import type { Worktree } from '../worktree/workspace-list-sections' +import { hostViewSettingsRead, hostViewSettingsWrite } from './host-screen-operations' import type { HostScreenState } from './use-host-screen-state' export function useHostViewSettings(args: { @@ -79,7 +80,7 @@ export function useHostViewSettings(args: { if (Object.keys(payload).length === 0) { return } - void client.sendRequest('ui.set', payload).catch(() => { + void hostViewSettingsWrite.request(client, payload).catch(() => { // Best-effort: view settings are a convenience preference. }) }, @@ -94,11 +95,16 @@ export function useHostViewSettings(args: { const requestClient = client const requestHostId = hostId try { - const response = await requestClient.sendRequest('ui.get') - if (clientRef.current !== requestClient || hostId !== requestHostId || !response.ok) { + const reply = await hostViewSettingsRead.request(requestClient) + if (clientRef.current !== requestClient || hostId !== requestHostId) { return } - const ui = ((response as RpcSuccess).result as { ui?: WorkspaceViewSettings }).ui + const settings = hostViewSettingsRead.interpret(reply) + if (!settings.accepted) { + return + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const ui = settings.value as WorkspaceViewSettings | undefined if (!ui) { return } diff --git a/mobile/src/host-screen/use-host-worktree-actions.ts b/mobile/src/host-screen/use-host-worktree-actions.ts index 21f49a4f306..61956476209 100644 --- a/mobile/src/host-screen/use-host-worktree-actions.ts +++ b/mobile/src/host-screen/use-host-worktree-actions.ts @@ -11,6 +11,7 @@ import { setHostRouteNewWorktreeVisible } from '../host-route-action-state' import { leaveHostRoute } from '../host-route-exit' import { getWorktreeRowIdentity, removeWorktreeRow } from '../worktree/worktree-host-row-identity' import { isWorktreePinned, type Worktree } from '../worktree/workspace-list-sections' +import { worktreeActivate, worktreePinWrite, worktreeRemove } from './host-screen-operations' import type { HostScreenState } from './use-host-screen-state' export function useHostWorktreeActions(args: { @@ -101,8 +102,8 @@ export function useHostWorktreeActions(args: { updateLocalPins(worktreeId, newPinned) if (client) { - client - .sendRequest('worktree.set', { + worktreePinWrite + .request(client, { worktree: `id:${worktreeId}`, isPinned: newPinned }) @@ -123,11 +124,11 @@ export function useHostWorktreeActions(args: { setLastKnownWorktrees(removeFromList) try { - const response = await client.sendRequest('worktree.rm', { + const reply = await worktreeRemove.request(client, { worktree: `id:${item.worktreeId}`, force: true }) - if (!response.ok) { + if (!worktreeRemove.interpret(reply).accepted) { setWorktrees((prev) => [...prev, item]) setLastKnownWorktrees((prev) => [...prev, item]) } @@ -176,8 +177,8 @@ export function useHostWorktreeActions(args: { (item: Worktree) => { setOptimisticActiveWorktreeIdentity(getWorktreeRowIdentity(item)) if (client && connState === 'connected') { - void client - .sendRequest('worktree.activate', { + void worktreeActivate + .request(client, { worktree: `id:${item.worktreeId}`, notifyClients: false, navigation: 'caller' diff --git a/mobile/src/tasks/mobile-task-runtime-operations.ts b/mobile/src/tasks/mobile-task-runtime-operations.ts index c69651af666..bf77d496df6 100644 --- a/mobile/src/tasks/mobile-task-runtime-operations.ts +++ b/mobile/src/tasks/mobile-task-runtime-operations.ts @@ -7,8 +7,8 @@ import { // 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 + * status.get read for task hydration, with its own policy on that method: a refused status stops + * hydration with the host's own message, where the create-time probe in * mobile-workspace-create-operations.ts degrades instead. One reader serves both. */ export const taskRuntimeStatusRead = bindDeferredRpcOperation( diff --git a/mobile/src/tasks/mobile-workspace-create-operations.ts b/mobile/src/tasks/mobile-workspace-create-operations.ts index 55cad2f373f..186d04b1498 100644 --- a/mobile/src/tasks/mobile-workspace-create-operations.ts +++ b/mobile/src/tasks/mobile-workspace-create-operations.ts @@ -46,9 +46,9 @@ export const worktreeMrBaseResolve = bindDeferredRpcOperation( ) /** - * status.get read for create-time capabilities, the second of two policies on this method. + * status.get read for create-time capabilities, with its own policy on that method. * - * Both policies named because the two callers disagree about what a refused status means: the + * Separately named because the 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. diff --git a/mobile/src/test-support/rpc-recording/adapters/file-request-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/file-request-mount-adapters.ts new file mode 100644 index 00000000000..334ecde80ed --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/file-request-mount-adapters.ts @@ -0,0 +1,108 @@ +import type { MountAdapter } from '../recording-scenario' +import type { operationModuleLoader } from '../operation-module-loader' + +const WORKSPACE = 'workspace-1' +/** The artifact a scenario reads; the path decides which of the two artifact methods it asks. */ +function artifactSource(absolutePath: string) { + return { + source: 'terminalArtifact' as const, + worktreeId: WORKSPACE, + absolutePath, + grantId: 'grant-1', + terminalHandle: 'terminal-1', + pathText: absolutePath.slice(absolutePath.lastIndexOf('/') + 1), + cwd: '/logs' + } +} + +const ARTIFACT = artifactSource('/logs/run.txt') + +/** + * The file reads and writes a session file tab runs: ownership capture before a mutation, the + * preview loader with its terminal-artifact grant refresh, the artifact save, and the tab doc's + * three shapes. Each is an exported async function taking a client, so the recorded state is the + * function's own answer and no React host is needed. + */ +export function fileRequestMountAdapters( + modules: ReturnType +): Record { + return { + 'files.mutation-ownership': ({ client }) => { + const capture = modules.load( + 'mobile/src/files/mobile-file-mutation-ownership.ts' + ).captureMobileFileMutationOwnership + let ownership: unknown = 'uncaptured' + return { + action: () => + capture(client, `id:${WORKSPACE}`).then((value: unknown) => { + ownership = value + return value + }), + state: () => ({ ownership }), + dispose: () => {} + } + }, + 'files.preview-load': ({ client, effect }) => { + const load = modules.load( + 'mobile/src/files/mobile-file-preview-request.ts' + ).loadMobileFilePreview + let preview: unknown = 'unloaded' + return { + action(name, args) { + const request = + name === 'worktree' + ? load(client, WORKSPACE, String(args.path ?? 'docs/readme.md')) + : load(client, artifactSource(String(args.path ?? '/logs/run.txt')), undefined, { + onTerminalArtifactSourceRefreshed: (source: unknown) => + effect('artifact-source-refreshed', source) + }) + return request.then((value: unknown) => { + preview = value + return value + }) + }, + state: () => ({ preview }), + dispose: () => {} + } + }, + 'files.preview-save': ({ client, effect }) => { + const save = modules.load( + 'mobile/src/files/mobile-file-preview-request.ts' + ).saveMobileTerminalArtifactPreview + let saved: unknown = 'unsaved' + return { + action: (name) => + save(client, ARTIFACT, 'next', { + onTerminalArtifactSourceRefreshed: (source: unknown) => + effect('artifact-source-refreshed', source), + // The verified arm re-reads the artifact first; the blind arm writes straight away. + ...(name === 'blind' ? {} : { baseContent: 'base' }) + }).then((value: unknown) => { + saved = value + return value + }), + state: () => ({ saved }), + dispose: () => {} + } + }, + 'files.tab-doc': ({ client }) => { + const resolve = modules.load( + 'mobile/src/files/mobile-file-tab-doc.ts' + ).resolveMobileFileTabDoc + const docs: Record = {} + return { + action: (name) => + resolve(client, { + worktreeId: WORKSPACE, + relativePath: name === 'image' ? 'docs/logo.png' : 'docs/readme.md', + ...(name === 'diff' ? { diffSource: 'staged' as const } : {}) + }).then((value: unknown) => { + docs[name] = value + return value + }), + state: () => ({ ...docs }), + dispose: () => {} + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/host-screen-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/host-screen-mount-adapters.ts new file mode 100644 index 00000000000..6c10c79aa3f --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/host-screen-mount-adapters.ts @@ -0,0 +1,111 @@ +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 HOST = 'host-1' + +const INITIAL_VIEW_STATE = { + groupMode: 'none', + sortMode: 'recent', + hideSleeping: false, + hideDefaultBranch: false, + alwaysShowDefaultBranch: true, + filterRepoIds: [], + collapsedGroups: [], + workspaceStatuses: [] +} + +/** + * The host screen's shared view settings, and the Home card's per-host stats read. Both belong to + * the host list: one mirrors the desktop's workspace view store, the other fills the card's counts. + */ +export function hostScreenMountAdapters( + modules: ReturnType +): Record { + return { + 'host.view-settings': (context) => { + const useViewSettings = modules.load< + typeof import('../../../host-screen/use-host-view-settings') + >('mobile/src/host-screen/use-host-view-settings.ts').useHostViewSettings + const state = observableModel(context, { + clientRef: { current: context.client }, + collapsedGroups: new Set(), + filters: { + filterRepoIds: new Set(), + hideSleeping: false, + hideDefaultBranch: false, + alwaysShowDefaultBranch: true + }, + groupMode: 'none', + sortMode: 'recent', + viewStateRef: { current: { ...INITIAL_VIEW_STATE } }, + workspaceStatuses: [] + }) + let actions: ReturnType + const hook = hookMount(() => { + actions = useViewSettings({ + client: context.client, + connState: 'connected', + hostId: HOST, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + state: state as unknown as Parameters[0]['state'] + }) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'sync') { + return actions.syncViewSettingsFromDesktop() + } + if (name === 'sort') { + return performHookAction(() => actions.handleSortChange('name')) + } + if (name === 'hide-sleeping') { + return performHookAction(() => actions.toggleHideSleeping()) + } + throw new Error(`Unknown view settings action: ${name}`) + }, + state: () => + projectObservable({ + groupMode: state.groupMode, + sortMode: state.sortMode, + filters: state.filters, + collapsed: state.collapsedGroups, + statuses: state.workspaceStatuses + }), + dispose: hook.unmount + } + }, + 'home.host-stats': (context) => { + const fetchStats = modules.load( + 'mobile/src/home/mobile-home-host-requests.ts' + ).fetchMobileHomeStats + let stats: Record = {} + let disposed = false + return { + action(name) { + if (name === 'unmount') { + disposed = true + return + } + return fetchStats( + context.client, + HOST, + (update: (value: Record) => Record) => { + stats = update(stats) + context.effect('stats', stats) + }, + () => disposed + ) + }, + state: () => ({ ...stats }), + dispose: () => { + disposed = true + } + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/host-worktree-action-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/host-worktree-action-mount-adapters.ts new file mode 100644 index 00000000000..562f642f039 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/host-worktree-action-mount-adapters.ts @@ -0,0 +1,90 @@ +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 ROW = { + worktreeId: 'wt-1', + repoId: 'repo-1', + repo: 'marlin', + branch: 'feature/pin', + displayName: 'marlin', + path: '/repos/marlin/wt-1', + liveTerminalCount: 0, + hasAttachedPty: false, + preview: '', + unread: false, + isPinned: false, + linkedPR: null +} + +/** + * The host screen's three worktree mutations: pin, delete and open. + * + * Mounted with no hostId, which is the only thing that keeps the hook off native storage: the + * pinned-id write is the sole native call and it sits behind `if (hostId)`. + */ +export function hostWorktreeActionMountAdapters( + modules: ReturnType +): Record { + return { + 'host.worktree-actions': (context) => { + const useActions = modules.load< + typeof import('../../../host-screen/use-host-worktree-actions') + >('mobile/src/host-screen/use-host-worktree-actions.ts').useHostWorktreeActions + const state = observableModel(context, { + newWorktreeModalRef: { current: null }, + newWorktreeModalVisibleRef: { current: false }, + pinnedIds: new Set(), + worktrees: [ROW], + lastKnownWorktrees: [ROW], + confirmRemoveHost: false, + optimisticActiveWorktreeIdentity: null, + routeActionState: {} + }) + let actions: ReturnType + const hook = hookMount(() => { + actions = useActions({ + client: context.client, + connState: 'connected', + embedded: false, + fetchWorktrees: async () => {}, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook only forwards this to host removal, which no scenario drives. + forgetHostClient: (() => {}) as unknown as Parameters< + typeof useActions + >[0]['forgetHostClient'], + hostId: undefined, + pathname: '/h/host-1', + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: navigation is observed through the recorded sends, not the router. + router: { push: () => {}, replace: () => {} } as unknown as Parameters< + typeof useActions + >[0]['router'], + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the recorder supplies only the members the hook reads. + state: state as unknown as Parameters[0]['state'] + }) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'toggle-pin') { + return performHookAction(() => actions.togglePin(ROW.worktreeId)) + } + if (name === 'delete') { + return performHookAction(() => actions.handleDeleteWorktree(ROW)) + } + if (name === 'open-session') { + return performHookAction(() => actions.openWorktreeSession(ROW)) + } + throw new Error(`Unknown worktree action: ${name}`) + }, + state: () => + projectObservable( + Object.fromEntries(Object.entries(state).filter(([key]) => !key.endsWith('Ref'))) + ), + dispose: hook.unmount + } + } + } +} 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 index 137ee683f8e..516e941eaef 100644 --- a/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts +++ b/mobile/src/test-support/rpc-recording/adapters/mounted-operation-modules.ts @@ -1,12 +1,17 @@ import { fileInventoryMountAdapters } from './file-inventory-mount-adapters' +import { fileRequestMountAdapters } from './file-request-mount-adapters' +import { hostScreenMountAdapters } from './host-screen-mount-adapters' +import { hostWorktreeActionMountAdapters } from './host-worktree-action-mount-adapters' import { hostedReviewMountAdapters } from './hosted-review-mount-adapters' import { newTabAgentMountAdapters } from './new-tab-agent-mount-adapters' +import { newWorkspaceMountAdapters } from './new-workspace-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 { worktreeCatalogMountAdapters } from './worktree-catalog-mount-adapters' import type { MountedOperationModule } from '../mounted-operation-module' /** @@ -16,8 +21,15 @@ import type { MountedOperationModule } from '../mounted-operation-module' */ export const MOUNTED_OPERATION_MODULES: readonly MountedOperationModule[] = [ { source: 'file-inventory-mount-adapters.ts', mounts: fileInventoryMountAdapters }, + { source: 'file-request-mount-adapters.ts', mounts: fileRequestMountAdapters }, + { source: 'host-screen-mount-adapters.ts', mounts: hostScreenMountAdapters }, + { + source: 'host-worktree-action-mount-adapters.ts', + mounts: hostWorktreeActionMountAdapters + }, { source: 'hosted-review-mount-adapters.ts', mounts: hostedReviewMountAdapters }, { source: 'new-tab-agent-mount-adapters.ts', mounts: newTabAgentMountAdapters }, + { source: 'new-workspace-mount-adapters.ts', mounts: newWorkspaceMountAdapters }, { source: 'settings-mount-adapters.ts', mounts: settingsMountAdapters, @@ -33,5 +45,6 @@ export const MOUNTED_OPERATION_MODULES: readonly MountedOperationModule[] = [ source: 'task-workspace-sender-mount-adapters.ts', mounts: taskWorkspaceSenderMountAdapters }, - { source: 'workspace-settings-mounts.ts', mounts: workspaceSettingsMounts } + { source: 'workspace-settings-mounts.ts', mounts: workspaceSettingsMounts }, + { source: 'worktree-catalog-mount-adapters.ts', mounts: worktreeCatalogMountAdapters } ] diff --git a/mobile/src/test-support/rpc-recording/adapters/new-workspace-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/new-workspace-mount-adapters.ts new file mode 100644 index 00000000000..83195fa6c80 --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/new-workspace-mount-adapters.ts @@ -0,0 +1,107 @@ +import type { MountAdapter } from '../recording-scenario' +import { hookMount, performHookAction } from '../hook-mount' +import { projectObservable } from '../observable-model' +import type { operationModuleLoader } from '../operation-module-loader' + +const REPO = { id: 'repo-1', displayName: 'Repo' } + +/** + * The host screen's New Workspace drawer: the SSH/agent execution target, the repo's setup hook, + * and the Codex reset-credit capability probe the account rows gate on. The drawer's own repo list + * is absent because its hook also reads native storage, which no recording may reach. + */ +export function newWorkspaceMountAdapters( + modules: ReturnType +): Record { + // `connectionId` picks the arm the detection effect takes: an SSH repo detects remote agents, + // a repo without a connection detects local ones. + function executionTargetAdapter(connectionId: string | null): MountAdapter { + return ({ client }) => { + const useTarget = modules.load< + typeof import('../../../components/use-new-workspace-execution-target') + >( + 'mobile/src/components/use-new-workspace-execution-target.ts' + ).useNewWorkspaceExecutionTarget + let state: ReturnType + let visible = true + const hook = hookMount(() => { + state = useTarget({ client, connectionId, visible }) + }) + return { + action(name) { + if (name === 'mount' || name === 'remount') { + return hook.mount() + } + if (name === 'unmount') { + return hook.unmount() + } + if (name === 'blur') { + visible = false + return hook.update() + } + if (name === 'connect') { + return performHookAction(() => state.connect()) + } + throw new Error(`Unknown execution target action: ${name}`) + }, + state: () => + projectObservable({ + gate: state?.sshGate, + detected: state?.detectedAgentIds + }), + dispose: hook.unmount + } + } + } + + return { + 'components.execution-target': executionTargetAdapter('ssh-1'), + 'components.execution-target-local': executionTargetAdapter(null), + 'components.setup-script': ({ client }) => { + const useSetup = modules.load< + typeof import('../../../components/use-new-workspace-setup-script') + >('mobile/src/components/use-new-workspace-setup-script.ts').useNewWorkspaceSetupScript + let state: ReturnType + const hook = hookMount(() => { + state = useSetup({ + client, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reads only the repo's id. + selectedRepo: REPO as Parameters[0]['selectedRepo'] + }) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + throw new Error(`Unknown setup script action: ${name}`) + }, + state: () => + projectObservable({ + command: state?.setupCommand, + source: state?.setupSource, + trust: state?.setupTrust, + runPolicy: state?.setupRunPolicy, + advanced: state?.showAdvanced, + run: state?.runSetup + }), + dispose: hook.unmount + } + }, + 'components.codex-reset-capability': ({ client }) => { + const read = modules.load( + 'mobile/src/components/codex-reset-credit-capability.ts' + ).readCodexResetCreditCapability + let supported: unknown = 'unprobed' + return { + action: () => + read(client).then((value: unknown) => { + supported = value + return value + }), + state: () => ({ supported }), + dispose: () => {} + } + } + } +} diff --git a/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts new file mode 100644 index 00000000000..41283b2a41d --- /dev/null +++ b/mobile/src/test-support/rpc-recording/adapters/worktree-catalog-mount-adapters.ts @@ -0,0 +1,89 @@ +import type { MountAdapter } from '../recording-scenario' +import { hookMount } from '../hook-mount' +import { projectObservable } from '../observable-model' +import type { operationModuleLoader } from '../operation-module-loader' + +const HOST = 'host-1' +const REPO = 'repo-1' + +/** + * The workspace catalog reads: the Home card's per-host summary, the snapshot client the host + * screen polls with, and the retired-name registry the create sheet asks for per repo. + */ +export function worktreeCatalogMountAdapters( + modules: ReturnType +): Record { + return { + 'worktree.home-catalog': (context) => { + const fetchInfo = modules.load( + 'mobile/src/worktree/home-host-worktree-fetch.ts' + ).fetchHomeHostWorktreeInfo + let info: Record = {} + let disposed = false + return { + action(name) { + if (name === 'unmount') { + disposed = true + return + } + return fetchInfo( + context.client, + HOST, + (update: (value: Record) => Record) => { + info = update(info) + context.effect('info', projectObservable(info)) + }, + () => disposed + ) + }, + state: () => projectObservable(info), + dispose: () => { + disposed = true + } + } + }, + 'worktree.catalog-snapshot': ({ client }) => { + const SnapshotClient = modules.load< + typeof import('../../../worktree/worktree-catalog-snapshot-client') + >('mobile/src/worktree/worktree-catalog-snapshot-client.ts').WorktreeCatalogSnapshotClient + const snapshots = new SnapshotClient() + let fetched: unknown = 'unfetched' + let admitted: unknown = 'unadmitted' + return { + action: () => + snapshots.fetch(client, HOST).then((result) => { + fetched = result + // Admitting is what advances the snapshot token a later poll sends back. + admitted = snapshots.admit(result.kind === 'response' ? result.pending : null) + return result + }), + state: () => projectObservable({ fetched, admitted }), + dispose: () => {} + } + }, + 'worktree.retired-names': ({ client }) => { + const useRetired = modules.load< + typeof import('../../../worktree/use-retired-worktree-names') + >('mobile/src/worktree/use-retired-worktree-names.ts').useRetiredWorktreeNames + let registry: unknown + let refreshKey = 1 + const hook = hookMount(() => { + registry = useRetired(client, REPO, refreshKey) + }) + return { + action(name) { + if (name === 'mount') { + return hook.mount() + } + if (name === 'refresh') { + refreshKey++ + return hook.update() + } + throw new Error(`Unknown retired names action: ${name}`) + }, + state: () => projectObservable({ registry }), + dispose: hook.unmount + } + } + } +} diff --git a/mobile/src/transport/rpc-accepted-result.ts b/mobile/src/transport/rpc-accepted-result.ts new file mode 100644 index 00000000000..a21f2a229e2 --- /dev/null +++ b/mobile/src/transport/rpc-accepted-result.ts @@ -0,0 +1,8 @@ +// Its own module because a consumer that only names this verdict is not an operation +// implementation: importing rpc-operation-contract would pull it into the cast fence's region +// and ban the assertions it legitimately still makes on the raw envelope. + +/** A skip-policy verdict: refusal is distinct from an accepted null/undefined payload. */ +export type RpcAcceptedResult = + | { readonly accepted: false } + | { readonly accepted: true; readonly value: Value } diff --git a/mobile/src/transport/rpc-operation-contract.ts b/mobile/src/transport/rpc-operation-contract.ts index 23bc012319c..2c60546d3b7 100644 --- a/mobile/src/transport/rpc-operation-contract.ts +++ b/mobile/src/transport/rpc-operation-contract.ts @@ -1,6 +1,9 @@ +import type { RpcAcceptedResult } from './rpc-accepted-result' import type { RpcMethodName } from './rpc-params-contract' import type { RpcFailure, RpcResponse, RpcSuccess } from './types' +export type { RpcAcceptedResult } + // An operation descriptor fixes the method, the acceptance policy and the interpretation // barrier at definition time. Per-call freedom over those three is what produced acceptance // drift and settlement-order drift across mobile's RPC call sites, so none of them is a @@ -157,11 +160,6 @@ export type StreamOpenerRpcDefinition< read?: never } -/** Refusal is distinct from an accepted null/undefined payload. */ -export type RpcAcceptedResult = - | { readonly accepted: false } - | { readonly accepted: true; readonly value: Value } - export type RpcReaderAcceptance = | 'require-result-or-throw' | 'object-result-or-null' diff --git a/mobile/src/transport/rpc-operation.ts b/mobile/src/transport/rpc-operation.ts index 97f27143e93..8bdce8ebd88 100644 --- a/mobile/src/transport/rpc-operation.ts +++ b/mobile/src/transport/rpc-operation.ts @@ -248,18 +248,25 @@ export async function interpretAtRpcBarrier< } /** - * Preserves omitted sender arguments as well as explicit undefined. + * Whether a sender may omit the params argument entirely. * - * 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. + * A params type with no required field may be omitted as well as `void`, 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. Shared by both + * send helpers, so single-flight and direct sends cannot disagree about which methods that covers. */ -type RpcSendArguments = +type RpcParamsOmittable = void extends RpcSendParams - ? [params?: RpcSendParams, options?: SendRequestOptions] + ? true : Record extends RpcSendParams - ? [params?: RpcSendParams, options?: SendRequestOptions] - : [params: RpcSendParams, options?: SendRequestOptions] + ? true + : false + +/** Preserves omitted sender arguments as well as explicit undefined. */ +type RpcSendArguments = + RpcParamsOmittable extends true + ? [params?: RpcSendParams, options?: SendRequestOptions] + : [params: RpcSendParams, options?: SendRequestOptions] /** Binds sending and interpretation while preserving the transport promise identity. */ export function bindDeferredRpcOperation< @@ -277,7 +284,7 @@ export function bindDeferredRpcOperation< requestSingleFlight( client: RpcClient, hostId: string, - ...args: void extends RpcSendParams + ...args: RpcParamsOmittable extends true ? [params?: RpcSendParams] : [params: RpcSendParams] ) { diff --git a/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts b/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts index 722c4e58310..6fb2f460611 100644 --- a/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts +++ b/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts @@ -12,6 +12,11 @@ * step-4 migration backlog and shares one reason, stated once here instead of 144 times: * the call site predates the typed contract and still picks its own method string, its own * acceptance rule and its own decoding. Replacing one with an RpcOperation deletes its line. + * + * Where a group below names a blocker, it is a recording blocker, not a migration blocker. + * Pointing a site at an operation is mechanical; the golden recorded against the old code before + * the refactor is the only parity proof this migration has. So a site the recorder cannot mount + * cannot be recorded, and unrecorded sites do not migrate. */ export type UnvalidatedRpcRequestPortEntry = { readonly file: string @@ -60,37 +65,43 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques { file: 'src/browser/use-mobile-browser-commands.ts', references: 5 }, { file: 'src/browser/use-mobile-browser-request.ts', references: 1 }, - // src/components/ — shared widgets that fetch their own data - { file: 'src/components/codex-reset-credit-capability.ts', references: 2 }, + // src/components/ — shared widgets that fetch their own data. The New Workspace drawer's + // execution target, setup hook, runtime context and Codex capability probe migrated in step 4: + // see new-workspace-operations.ts, codex-reset-credit-capability-operations.ts, and the SSH and + // agent-detection operations in tasks/mobile-workspace-source-operations.ts. Two remain, neither + // recordable. codex-reset-credit.ts loads under the module loader; its attempt-journal access + // throws on async-storage at call time, before the send, and nothing guards it away. The repo + // list fails one module further out: it renders use-last-visited-worktree-repo.ts, whose default + // import of async-storage is a property read the loader's proxy refuses. { file: 'src/components/codex-reset-credit.ts', references: 3 }, - { file: 'src/components/use-new-workspace-execution-target.ts', references: 4 }, { file: 'src/components/use-new-workspace-repositories.ts', references: 1 }, - { file: 'src/components/use-new-workspace-runtime-context.ts', references: 3 }, - { file: 'src/components/use-new-workspace-setup-script.ts', references: 1 }, // src/dictation/ — dictation session control { file: 'src/dictation/mobile-dictation-setup.ts', references: 10 }, - // src/files/ — file read, write and preview - { file: 'src/files/mobile-file-mutation-ownership.ts', references: 3 }, - { file: 'src/files/mobile-file-preview-request.ts', references: 6 }, - { file: 'src/files/mobile-file-tab-doc.ts', references: 4 }, - { file: 'src/files/mobile-terminal-artifact-grant-refresh.ts', references: 2 }, + // src/files/ — file read, write and preview. The preview loader, the terminal-artifact grant + // refresh and save, the session file tab and the mutation-ownership capture migrated in step 4: + // see mobile-file-preview-operations.ts, mobile-file-tab-doc-operations.ts and + // mobile-file-ownership-operations.ts. The explorer panel's two sends sit inline in a React + // Native screen, which the recorder cannot mount and so cannot record. { file: 'src/files/MobileFileExplorerPanel.tsx', references: 2 }, - // src/home/ — home screen host reads - { file: 'src/home/mobile-home-host-requests.ts', references: 5 }, + // src/home/ — home screen host reads. The stats card and both task-provider probes migrated in + // step 4 (mobile-home-host-operations.ts, plus the shared task-tooling reads in + // tasks/mobile-task-runtime-operations.ts). The accounts read stays: its decoder is re-exported + // through a React Native screen module, which no recording can load. + { file: 'src/home/mobile-home-host-requests.ts', references: 2 }, // src/hooks/ — cross-screen data hooks { file: 'src/hooks/mobile-dictation-audio-chunk.ts', references: 1 }, { file: 'src/hooks/mobile-dictation-desktop-start.ts', references: 4 }, { file: 'src/hooks/use-mobile-dictation.ts', references: 4 }, - // src/host-screen/ — host screen catalog and actions + // src/host-screen/ — host screen catalog and actions. The repo and label metadata reads, the + // desktop view-settings mirror and the list's pin, remove and activate mutations migrated in + // step 4; see host-screen-operations.ts. What is left sends from inside a React Native screen, + // which the recorder cannot mount. { file: 'src/host-screen/host-screen-overlays.tsx', references: 1 }, - { file: 'src/host-screen/use-host-repo-metadata.ts', references: 1 }, - { file: 'src/host-screen/use-host-view-settings.ts', references: 2 }, - { file: 'src/host-screen/use-host-worktree-actions.ts', references: 3 }, // src/notifications/ — push registration and delivery { file: 'src/notifications/mobile-notifications.ts', references: 1 }, @@ -146,7 +157,8 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques { file: 'src/session/use-mobile-terminal-paste.ts', references: 1 }, { file: 'src/session/use-quick-commands.ts', references: 2 }, - // src/settings/ — settings screen actions + // src/settings/ — settings screen actions. Its one reference is the client parameter it forwards + // to dictation/mobile-dictation-setup.ts, so it can only drop when that file migrates. { file: 'src/settings/native-voice-settings-operations.ts', references: 1 }, // src/settings/ — notification display probe @@ -210,10 +222,5 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques { file: 'src/transport/pairing-candidate-race.ts', references: 1 }, { file: 'src/transport/pairing-relay-candidate.ts', references: 4 }, { file: 'src/transport/pre-profile-pairing-coordinator.ts', references: 2 }, - { file: 'src/transport/runtime-capability-probe.ts', references: 2 }, - - // src/worktree/ — worktree activation and resume - { file: 'src/worktree/home-host-worktree-fetch.ts', references: 2 }, - { file: 'src/worktree/use-retired-worktree-names.ts', references: 1 }, - { file: 'src/worktree/worktree-catalog-snapshot-client.ts', references: 1 } + { file: 'src/transport/runtime-capability-probe.ts', references: 2 } ] diff --git a/mobile/src/worktree/home-host-worktree-fetch.ts b/mobile/src/worktree/home-host-worktree-fetch.ts index 72b9e572ba1..5e119518a7d 100644 --- a/mobile/src/worktree/home-host-worktree-fetch.ts +++ b/mobile/src/worktree/home-host-worktree-fetch.ts @@ -1,5 +1,4 @@ import { setCachedWorktrees } from '../cache/worktree-cache' -import { sendSingleFlightRequest } from '../transport/request-single-flight' import type { RpcClient } from '../transport/rpc-client' import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import { @@ -8,6 +7,7 @@ import { type HostWorktreeInfo } from './home-worktree-info' import { pickResumeWorktree } from './resume-worktree' +import { worktreeCatalogRead } from './worktree-catalog-operations' import { WORKTREE_PS_FULL_LIMIT } from './worktree-catalog-snapshot-client' const ACTIVE_STATUSES = new Set(['working', 'active', 'permission']) @@ -36,16 +36,19 @@ export function fetchHomeHostWorktreeInfo( } const attempt = (cutoverRetriesLeft: number): Promise => - sendSingleFlightRequest(client, hostId, 'worktree.ps', { limit: WORKTREE_PS_FULL_LIMIT }) - .then((response) => { + worktreeCatalogRead + .requestSingleFlight(client, hostId, { limit: WORKTREE_PS_FULL_LIMIT }) + .then((reply) => { if (disposed()) { return } - if (!response.ok) { + const catalog = worktreeCatalogRead.interpret(reply) + if (!catalog.accepted) { markUnavailable() return } - const result = response.result as { worktrees?: HomeWorktreeSummary[] } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. + const result = catalog.value as { worktrees?: HomeWorktreeSummary[] } const worktrees = result.worktrees ?? [] setCachedWorktrees(hostId, worktrees, { proven: true }) const active = worktrees.filter((w) => w.status && ACTIVE_STATUSES.has(w.status)) diff --git a/mobile/src/worktree/mobile-worktree-activation-source.test.ts b/mobile/src/worktree/mobile-worktree-activation-source.test.ts index 6d6497de67f..d7eaddfacfd 100644 --- a/mobile/src/worktree/mobile-worktree-activation-source.test.ts +++ b/mobile/src/worktree/mobile-worktree-activation-source.test.ts @@ -5,7 +5,6 @@ const source = readFileSync( new URL('../host-screen/use-host-worktree-actions.ts', import.meta.url), 'utf8' ) - function sliceBetween(startPattern: string, endPattern: string): string { const start = source.indexOf(startPattern) expect(start).toBeGreaterThanOrEqual(0) @@ -21,8 +20,10 @@ describe('mobile worktree activation', () => { 'const openFloatingWorkspace = useCallback' ) - expect(openSession).toContain("sendRequest('worktree.activate'") + expect(openSession).toContain('worktreeActivate') expect(openSession).toContain('notifyClients: false') expect(openSession).toContain("navigation: 'caller'") + // The method is no longer in this file: tsc pins the params to worktreeActivate's shape and + // the host-worktree-actions-pin-open-delete golden pins the bytes. }) }) diff --git a/mobile/src/worktree/use-retired-worktree-names.test.tsx b/mobile/src/worktree/use-retired-worktree-names.test.tsx index 660b2f018d9..848353e8592 100644 --- a/mobile/src/worktree/use-retired-worktree-names.test.tsx +++ b/mobile/src/worktree/use-retired-worktree-names.test.tsx @@ -55,7 +55,12 @@ function mountNames() { retiredNameTiersByRepo: Record = {} ) { await act(async () => { - pending[index]!.resolve({ result: { retiredNamesByRepo, retiredNameTiersByRepo } }) + // `ok` is what the host always sends and what the read's acceptance policy routes on; + // a reply without it read as a refusal, which is not a shape any host produces. + pending[index]!.resolve({ + ok: true, + result: { retiredNamesByRepo, retiredNameTiersByRepo } + }) await Promise.resolve() }) }, diff --git a/mobile/src/worktree/use-retired-worktree-names.ts b/mobile/src/worktree/use-retired-worktree-names.ts index 3158110f82c..5da29f4cb26 100644 --- a/mobile/src/worktree/use-retired-worktree-names.ts +++ b/mobile/src/worktree/use-retired-worktree-names.ts @@ -7,6 +7,7 @@ import { } from '../../../src/shared/worktree/retired-name-cache' import type { RetiredNameRegistry } from '../../../src/shared/worktree/retired-name-registry' import type { RpcClient } from '../transport/rpc-client' +import { retiredWorktreeNamesRead } from './worktree-catalog-operations' export function buildRetiredWorktreeNamesRefreshKey( existingWorktreePaths: readonly string[] | undefined @@ -42,13 +43,16 @@ export function useRetiredWorktreeNames( setLoaded((previous) => retiredNamesAfterRefresh(previous, activeRepoId, registry)) } } - void client - .sendRequest('worktree.listRetiredNames', { repo: `id:${activeRepoId}` }) - .then((response) => + void retiredWorktreeNamesRead + .request(client, { repo: `id:${activeRepoId}` }) + .then((reply) => { + const names = retiredWorktreeNamesRead.interpret(reply) + // A refusal is not a failure here: it settles as an empty registry, which un-retires the + // repo's names until the next refresh. Preserved from main, not repaired. settle( - readRetiredNameRegistryForRepo((response as { result?: unknown }).result, activeRepoId) + readRetiredNameRegistryForRepo(names.accepted ? names.value : undefined, activeRepoId) ) - ) + }) .catch(() => settle(null)) return () => { cancelled = true diff --git a/mobile/src/worktree/worktree-catalog-operations.ts b/mobile/src/worktree/worktree-catalog-operations.ts new file mode 100644 index 00000000000..067f5ef3773 --- /dev/null +++ b/mobile/src/worktree/worktree-catalog-operations.ts @@ -0,0 +1,35 @@ +import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation' +import { rpcUncheckedPayloadReader } from '../transport/rpc-reader-payload' + +// The two workspace-catalog reads, both best-effort: a refused catalog leaves the last proven +// counts and the last confirmed rows in place rather than rendering a host as empty (STA-3123). + +/** + * worktree.ps. One family for both readers — the Home card's summary and the host screen's + * snapshot poll — because they ask the same question with the same acceptance. The payload stays + * unchecked: the snapshot client admits an `unchanged` envelope the card never sees. + */ +export const worktreeCatalogRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.catalog-or-skip', + method: 'worktree.ps', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('worktree-catalog') + }) +) + +/** + * Names already spent in one repo. The payload is unchecked because the call site projects it + * through `readRetiredNameRegistryForRepo`, which reads a refusal as an empty registry — the + * behaviour a skip preserves, and not the same thing as the failure a rejection means here. + */ +export const retiredWorktreeNamesRead = bindDeferredRpcOperation( + defineRpcOperation({ + name: 'worktree.retired-names-or-skip', + method: 'worktree.listRetiredNames', + acceptance: 'success-result-or-skip', + barrier: 'after-caller-barrier', + read: rpcUncheckedPayloadReader('retired-names') + }) +) diff --git a/mobile/src/worktree/worktree-catalog-snapshot-client.ts b/mobile/src/worktree/worktree-catalog-snapshot-client.ts index 9e804c39b34..13de6453a6d 100644 --- a/mobile/src/worktree/worktree-catalog-snapshot-client.ts +++ b/mobile/src/worktree/worktree-catalog-snapshot-client.ts @@ -1,6 +1,7 @@ import type { RpcClient } from '../transport/rpc-client' -import type { RpcFailure, RpcSuccess } from '../transport/types' +import type { RpcFailure } from '../transport/types' import type { Worktree } from './workspace-list-sections' +import { worktreeCatalogRead } from './worktree-catalog-operations' // Why: worktree.ps silently truncates at 200; use a high cap so large hosts don't drop workspaces. export const WORKTREE_PS_FULL_LIMIT = 10_000 @@ -71,12 +72,15 @@ export class WorktreeCatalogSnapshotClient { this.confirmedWorktrees = null } const requestedSnapshotId = this.snapshotId - const response = await client.sendRequest('worktree.ps', { + const reply = await worktreeCatalogRead.request(client, { limit: WORKTREE_PS_FULL_LIMIT, afterSnapshotId: requestedSnapshotId }) - if (!response.ok) { - const code = (response as RpcFailure).error?.code + const catalog = worktreeCatalogRead.interpret(reply) + if (!catalog.accepted) { + // The refusal code the caller reports lives on the envelope; no acceptance policy carries it. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this policy skips only a refusal, so an unaccepted reply is a failure envelope. + const code = (reply as RpcFailure).error?.code return { kind: 'request_failed', code: typeof code === 'string' && code.length > 0 ? code : 'request_failed' @@ -85,10 +89,7 @@ export class WorktreeCatalogSnapshotClient { return { kind: 'response', pending: { - admission: admitWorktreeCatalogResponse( - (response as RpcSuccess).result, - requestedSnapshotId - ), + admission: admitWorktreeCatalogResponse(catalog.value, requestedSnapshotId), client, hostId }