diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 398bd61bd04..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 @@ -170,6 +173,9 @@ jobs: - name: Check reliability gate manifest run: pnpm run check:reliability-gates + - name: Enforce dead design-system classes + run: pnpm run check:dead-classes + - name: Check VM runtime rollback compatibility env: BASE_SHA: ${{ github.event.pull_request.base.sha }} @@ -326,40 +332,59 @@ jobs: # Why: the 2.25.5 lane is a source build of a pinned tarball, so it produced the # same binary on every PR for minutes of runner time. The key carries the version # because that is the only input; the sha256 assertion below still guards the - # tarball on the miss path that actually builds. + # tarball on the miss path that actually builds. Only this PR's own later pushes + # can restore it — GitHub scopes a cache written from a pull_request run to that + # ref — so a first push always takes the build path below. - name: Cache baseline Git build uses: actions/cache@v5 with: path: ~/.cache/orca-git-compat/git-2.25.5 key: git-compat-baseline-${{ runner.os }}-${{ runner.arch }}-2.25.5 + # Why its own step: this is `make -j$(nproc)` on every core, and the lanes below + # spend their wall clock waiting on container starts, not on Git. Sharing a runner + # with the build stretched one ~1.5s boundary case past Vitest's 30s timeout, so + # the build has to finish before anything timed starts. + - name: Build the baseline Git binary + run: | + archive="$RUNNER_TEMP/git-2.25.5.tar.gz" + source="$HOME/.cache/orca-git-compat/git-2.25.5" + if [ -x "$source/git" ]; then + exit 0 + fi + curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" + echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \ + | sha256sum --check + mkdir -p "$source" + tar -xzf "$archive" -C "$source" --strip-components=1 + make -C "$source" -j"$(nproc)" \ + NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git + # Why: the linked binaries are what the next run needs; the objects that + # produced them are most of the tree and would bloat the cache entry. + find "$source" -name '*.o' -delete + - name: Verify Git binary compatibility matrix run: | + specs=( + "alpine/git:edge-2.38.1|2.38.1" + "alpine/git:v2.49.1|2.49.1" + ) + # Why pull up front: a lane's first `docker run` otherwise pulls its image + # while the sibling lane is mid-test, and that stall is charged to the test. + for spec in "${specs[@]}"; do + docker pull --quiet "${spec%%|*}" + done + pids=() ( - archive="$RUNNER_TEMP/git-2.25.5.tar.gz" - source="$HOME/.cache/orca-git-compat/git-2.25.5" - if [ ! -x "$source/git" ]; then - curl -fsSL https://www.kernel.org/pub/software/scm/git/git-2.25.5.tar.gz -o "$archive" - echo "41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf $archive" \ - | sha256sum --check - mkdir -p "$source" - tar -xzf "$archive" -C "$source" --strip-components=1 - make -C "$source" -j"$(nproc)" \ - NO_GETTEXT=YesPlease NO_TCLTK=YesPlease NO_PYTHON=YesPlease git - # Why: the linked binaries are what the next run needs; the objects that - # produced them are most of the tree and would bloat the cache entry. - find "$source" -name '*.o' -delete - fi - ORCA_GIT_COMPAT_BINARY="$source/git" ORCA_GIT_COMPAT_VERSION="2.25.5" \ + ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \ + ORCA_GIT_COMPAT_VERSION="2.25.5" \ pnpm exec vitest run --config config/vitest.config.ts \ src/shared/git-binary-compatibility.test.ts ) & pids+=("$!") - for spec in \ - "alpine/git:edge-2.38.1|2.38.1" \ - "alpine/git:v2.49.1|2.49.1"; do + for spec in "${specs[@]}"; do ( image="${spec%%|*}" version="${spec#*|}" diff --git a/.github/workflows/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' 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/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-anti-slop.json b/config/oxlint-anti-slop.json new file mode 100644 index 00000000000..bba8588d949 --- /dev/null +++ b/config/oxlint-anti-slop.json @@ -0,0 +1,110 @@ +{ + "$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": "error", + "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", + "anti-slop/no-runtime-typeof": "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", + "anti-slop/no-unsafe-dictionary-type": "off", + "anti-slop/no-widen-then-assert": "error", + "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" + } + }, + // 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/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/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/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/archive-hook-removal-repro.mjs b/config/scripts/archive-hook-removal-repro.mjs new file mode 100644 index 00000000000..3f392ac830d --- /dev/null +++ b/config/scripts/archive-hook-removal-repro.mjs @@ -0,0 +1,526 @@ +/** + * Real-repo verification for #19334 — run with: + * node config/scripts/archive-hook-removal-repro.mjs + * + * Requires a prior `build:cli` and `build:electron-vite`; it drives the BUILT CLI against the + * BUILT headless runtime, so it proves the shipped artifacts rather than the test harness. + *: a failed archive hook must BLOCK a destructive + * worktree removal, and the checkout, its git registration and its files must all survive. + * + * Boots the BUILT headless runtime (`out/main/index.js --serve`), pairs the BUILT CLI to it, + * and drives `orca worktree rm` end to end against real git worktrees on disk. + */ +import { spawn, spawnSync } from 'node:child_process' +import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, dirname, resolve } from 'node:path' +import { randomBytes } from 'node:crypto' + +const projectDir = resolve(import.meta.dirname, '../..') +const serveEntry = join(projectDir, 'out', 'main', 'index.js') +const cliEntry = join(projectDir, 'out', 'cli', 'index.js') +const PORT = 6900 + Math.floor(Math.random() * 400) +const READY_TIMEOUT_MS = 180_000 + +const control = mkdtempSync(join(tmpdir(), 'agh-control-')) +const modeFile = join(control, 'mode') +const ranFile = join(control, 'ran') +const setMode = (m) => writeFileSync(modeFile, m) +const hookRuns = () => (existsSync(ranFile) ? readFileSync(ranFile, 'utf8').trim().split('\n') : []) + +let failures = 0 +const out = (s) => process.stdout.write(`${s}\n`) +const banner = (s) => out(`\n${'='.repeat(78)}\n${s}\n${'='.repeat(78)}`) +function check(label, ok, detail = '') { + out(` ${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` -- ${detail}` : ''}`) + if (!ok) { + failures++ + } +} + +let pairingCode = null + +/** Run the real CLI against the booted server. Returns the raw process result. */ +function cli(args, { json = true } = {}) { + return spawnSync( + process.execPath, + [cliEntry, ...args, '--pairing-code', pairingCode, ...(json ? ['--json'] : [])], + { encoding: 'utf8', shell: false } + ) +} + +/** Run the CLI and require success, returning result payload. */ +function ok(args) { + const r = cli(args) + const parsed = parseJsonLine(r) + if (!parsed) { + throw new Error(`orca ${args.join(' ')} produced no JSON:\n${r.stdout}\n${r.stderr}`) + } + if (parsed.ok === false) { + throw new Error(`orca ${args.join(' ')} failed: ${parsed.error?.code} ${parsed.error?.message}`) + } + return parsed.result +} + +/** The CLI pretty-prints one JSON document to stdout. */ +function parseJsonLine(r) { + const text = (r.stdout ?? '').trim() + const start = text.indexOf('{') + if (start === -1) { + return null + } + try { + return JSON.parse(text.slice(start)) + } catch { + return null + } +} + +function git(cwd, ...args) { + const r = spawnSync('git', args, { cwd, encoding: 'utf8' }) + if (r.status !== 0) { + throw new Error(`git ${args.join(' ')}: ${r.stderr || r.stdout}`) + } + return r.stdout +} + +const ARCHIVE_HOOK = `echo "[archive-hook] running in $PWD" +echo "$PWD" >> ${JSON.stringify(ranFile).slice(1, -1)} +mode=$(cat ${JSON.stringify(modeFile).slice(1, -1)}) +case "$mode" in + ok) echo "[archive-hook] archived OK"; exit 0 ;; + fail) echo "[archive-hook] backup target unreachable" >&2; exit 23 ;; + signal) echo "[archive-hook] losing the execution host now"; kill -KILL $$ ;; +esac +echo "unknown mode $mode" >&2; exit 99 +` + +/** A throwaway git repo with one commit; optionally an orca.yaml archive hook. */ +function seedGitRepo(label, withHook, githubSlug) { + const dir = mkdtempSync(join(tmpdir(), `agh-repo-${label}-`)) + writeFileSync(join(dir, 'README.md'), `# ${label}\n`) + if (withHook) { + writeFileSync( + join(dir, 'orca.yaml'), + `scripts:\n archive: |\n${ARCHIVE_HOOK.split('\n') + .map((l) => ` ${l}`) + .join('\n')}\n` + ) + } + git(dir, 'init', '-b', 'main') + git(dir, 'config', 'user.email', 'verify@orca.test') + git(dir, 'config', 'user.name', 'Archive Gate Verify') + if (githubSlug) { + git(dir, 'remote', 'add', 'origin', `https://github.com/agh-owner/${githubSlug}.git`) + } + git(dir, 'add', '-A') + git(dir, 'commit', '-m', 'seed') + return dir +} + +function waitForReady(child) { + return new Promise((res, rej) => { + let buffered = '' + let serverErr = '' + const timer = setTimeout( + () => rej(new Error(`no ready payload in ${READY_TIMEOUT_MS}ms\n${serverErr}`)), + READY_TIMEOUT_MS + ) + child.stderr.setEncoding('utf8') + child.stderr.on('data', (c) => { + serverErr += c + }) + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk) => { + buffered += chunk + for (const line of buffered.split('\n')) { + if (!line.startsWith('{')) { + continue + } + try { + const p = JSON.parse(line) + if (p.type === 'orca_server_ready') { + clearTimeout(timer) + res(p) + return + } + } catch { + /* partial */ + } + } + }) + child.on('exit', (code) => { + clearTimeout(timer) + rej(new Error(`server exited ${code} before ready:\n${serverErr}`)) + }) + }) +} + +/** Filesystem + git truth about a worktree, read directly rather than through Orca. */ +function evidence(repoPath, wtPath) { + const ls = spawnSync('ls', ['-la', wtPath], { encoding: 'utf8' }) + const list = spawnSync('git', ['worktree', 'list'], { cwd: repoPath, encoding: 'utf8' }) + return { + dirExists: existsSync(wtPath), + fileExists: existsSync(join(wtPath, 'PRECIOUS.txt')), + fileBody: existsSync(join(wtPath, 'PRECIOUS.txt')) + ? readFileSync(join(wtPath, 'PRECIOUS.txt'), 'utf8').trim() + : null, + registered: (list.stdout ?? '').includes(wtPath), + ls: (ls.stdout ?? '').trim(), + worktreeList: (list.stdout ?? '').trim() + } +} + +function showEvidence(e) { + out(' --- ls -la ---') + out( + e.ls + .split('\n') + .map((l) => ` ${l}`) + .join('\n') + ) + out(' --- git worktree list (in the repo) ---') + out( + e.worktreeList + .split('\n') + .map((l) => ` ${l}`) + .join('\n') + ) +} + +async function main() { + const userDataDir = mkdtempSync(join(tmpdir(), 'agh-userdata-')) + out(`booting headless runtime on port ${PORT}, userData ${userDataDir}`) + const child = spawn( + 'npx', + [ + 'electron', + serveEntry, + '--serve', + '--serve-port', + String(PORT), + '--serve-json', + `--user-data-dir=${userDataDir}` + ], + { + cwd: projectDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' } + } + ) + const created = [] + + try { + const ready = await waitForReady(child) + pairingCode = new URL(ready.pairing.url).searchParams.get('code') + out(`ready: ${ready.advertisedEndpoint}`) + + // ---------------------------------------------------------------- setup + const hookRepoPath = seedGitRepo('hooked', true) + const folderProjectSlug = `agh-folder-proof-${randomBytes(3).toString('hex')}` + const bareRepoPath = seedGitRepo('nohook', false, folderProjectSlug) + const hookRepo = ok(['repo', 'add', '--path', hookRepoPath]).repo + const bareRepo = ok(['repo', 'add', '--path', bareRepoPath]).repo + out(`repo with archive hook: ${hookRepoPath} (${hookRepo.id})`) + out(`repo without archive hook: ${bareRepoPath} (${bareRepo.id})`) + + const makeWorktree = (repo, repoPath, name) => { + const wt = ok([ + 'worktree', + 'create', + '--repo', + `id:${repo.id}`, + '--name', + name, + '--setup', + 'skip' + ]).worktree + created.push(wt) + const unarchivedBody = `unarchived work for ${name}` + writeFileSync(join(wt.path, 'PRECIOUS.txt'), `${unarchivedBody}\n`) + return { ...wt, repoPath, unarchivedBody } + } + + // ============================================================ SCENARIO 1 + banner('SCENARIO 1 — archive hook exits 23: removal MUST be refused, nothing deleted') + setMode('fail') + const wt1 = makeWorktree(hookRepo, hookRepoPath, `gate-fail-${randomBytes(3).toString('hex')}`) + out(`worktree: ${wt1.path}`) + const before = evidence(wt1.repoPath, wt1.path) + + out('\n$ orca worktree rm --worktree --run-hooks (human output)') + const human = cli(['worktree', 'rm', '--worktree', wt1.id, '--run-hooks'], { json: false }) + out(` exit code: ${human.status}`) + out(' --- stdout ---') + out( + (human.stdout ?? '') + .trimEnd() + .split('\n') + .map((l) => ` ${l}`) + .join('\n') + ) + out(' --- stderr ---') + out( + (human.stderr ?? '') + .trimEnd() + .split('\n') + .map((l) => ` ${l}`) + .join('\n') + ) + + out( + '\n$ orca worktree rm --worktree --force --run-hooks --json (--force must NOT waive)' + ) + const forced = cli(['worktree', 'rm', '--worktree', wt1.id, '--force', '--run-hooks']) + const forcedJson = parseJsonLine(forced) + out(` exit code: ${forced.status}`) + out(` ${JSON.stringify(forcedJson)}`) + + const after1 = evidence(wt1.repoPath, wt1.path) + showEvidence(after1) + + check('CLI exits non-zero', human.status !== 0, `got ${human.status}`) + check( + 'human stderr names the archive hook', + /Archive hook failed for worktree/.test(human.stderr ?? '') + ) + check('--force also refused (non-zero)', forced.status !== 0, `got ${forced.status}`) + check( + 'typed error code', + forcedJson?.error?.code === 'worktree_archive_hook_failed', + JSON.stringify(forcedJson?.error?.code) + ) + check( + "error data outcome is 'exited'", + forcedJson?.error?.data?.outcome === 'exited', + JSON.stringify(forcedJson?.error?.data) + ) + check('error data carries exitCode 23', forcedJson?.error?.data?.exitCode === 23) + check('checkout directory still exists', after1.dirExists) + // Assert the CONTENTS, not just the path: a file that survived as an empty stub would prove + // nothing about the work the archive hook was supposed to rescue. + check( + 'unarchived file PRECIOUS.txt survives with its contents', + after1.fileExists && after1.fileBody === wt1.unarchivedBody, + `exists=${after1.fileExists} body=${JSON.stringify(after1.fileBody)}` + ) + check('git worktree registration survives', after1.registered) + check( + 'nothing changed vs. before the attempt', + before.dirExists === after1.dirExists && before.registered === after1.registered + ) + const shown = ok(['worktree', 'show', '--worktree', wt1.id]).worktree + check('Orca still resolves the worktree', shown?.id === wt1.id) + check( + 'the hook really ran (twice: plain + --force)', + hookRuns().length >= 2, + `runs=${hookRuns().length}` + ) + // The checkout is dirty (untracked PRECIOUS.txt). The plain run reported the ARCHIVE failure, + // not the dirty-preflight failure, so the gate is evaluated before that preflight. + check( + 'archive gate precedes the dirty preflight (dirty checkout, archive error reported)', + /Archive hook failed/.test(human.stderr ?? '') && + !/\?\? PRECIOUS\.txt/.test(human.stderr ?? '') + ) + + // ============================================================ SCENARIO 2 + banner('SCENARIO 2 — --allow-failed-archive-hook: removal proceeds, waiver recorded') + // --force here waives only the DIRTY preflight (PRECIOUS.txt is untracked on purpose); + // scenario 1 already proved it does not waive the archive gate. + const waived = cli([ + 'worktree', + 'rm', + '--worktree', + wt1.id, + '--force', + '--run-hooks', + '--allow-failed-archive-hook' + ]) + const waivedJson = parseJsonLine(waived) + out(` exit code: ${waived.status}`) + out(` ${JSON.stringify(waivedJson)}`) + const after2 = evidence(wt1.repoPath, wt1.path) + out(` checkout still on disk: ${after2.dirExists}`) + out( + ` --- git worktree list ---\n${after2.worktreeList + .split('\n') + .map((l) => ` ${l}`) + .join('\n')}` + ) + check('override exits zero', waived.status === 0, `got ${waived.status}`) + check('removal reported', waivedJson?.result?.removed === true) + check('checkout is GONE', !after2.dirExists) + check('git registration is gone', !after2.registered) + check( + 'archiveHookOverride recorded', + waivedJson?.result?.archiveHookOverride?.overridden === true, + JSON.stringify(waivedJson?.result?.archiveHookOverride) + ) + check( + 'override records exit 23 / exited', + waivedJson?.result?.archiveHookOverride?.exitCode === 23 && + waivedJson?.result?.archiveHookOverride?.outcome === 'exited' + ) + + // ============================================================ SCENARIO 3 + banner('SCENARIO 3 — archive hook exits 0: removal proceeds') + setMode('ok') + const wt3 = makeWorktree(hookRepo, hookRepoPath, `gate-ok-${randomBytes(3).toString('hex')}`) + out(`worktree: ${wt3.path}`) + const okRm = cli(['worktree', 'rm', '--worktree', wt3.id, '--force', '--run-hooks']) + const okJson = parseJsonLine(okRm) + out(` exit code: ${okRm.status}`) + out(` ${JSON.stringify(okJson)}`) + const after3 = evidence(wt3.repoPath, wt3.path) + check('exits zero', okRm.status === 0) + check('checkout deleted', !after3.dirExists) + check('git registration gone', !after3.registered) + check( + 'no archiveHookOverride on a clean run', + okJson?.result?.archiveHookOverride === undefined + ) + + // ============================================================ SCENARIO 4 + banner('SCENARIO 4 — no archive hook configured: removal proceeds unchanged') + const wt4 = makeWorktree( + bareRepo, + bareRepoPath, + `gate-nohook-${randomBytes(3).toString('hex')}` + ) + out(`worktree: ${wt4.path}`) + const runsBefore = hookRuns().length + const noHook = cli(['worktree', 'rm', '--worktree', wt4.id, '--force', '--run-hooks']) + const noHookJson = parseJsonLine(noHook) + out(` exit code: ${noHook.status}`) + out(` ${JSON.stringify(noHookJson)}`) + const after4 = evidence(wt4.repoPath, wt4.path) + check('exits zero', noHook.status === 0) + check('checkout deleted', !after4.dirExists) + check('no hook was run', hookRuns().length === runsBefore) + + // ============================================================ SCENARIO 5 + banner('SCENARIO 5 — hook never reports an exit (killed): must BLOCK as `unverifiable`') + setMode('signal') + const wt5 = makeWorktree(hookRepo, hookRepoPath, `gate-unver-${randomBytes(3).toString('hex')}`) + out(`worktree: ${wt5.path}`) + const unver = cli(['worktree', 'rm', '--worktree', wt5.id, '--run-hooks']) + const unverJson = parseJsonLine(unver) + out(` exit code: ${unver.status}`) + out(` ${JSON.stringify(unverJson)}`) + const after5 = evidence(wt5.repoPath, wt5.path) + showEvidence(after5) + check('blocked (non-zero)', unver.status !== 0, `got ${unver.status}`) + check('typed error code', unverJson?.error?.code === 'worktree_archive_hook_failed') + check( + "outcome is 'unverifiable', NOT 'exited'", + unverJson?.error?.data?.outcome === 'unverifiable', + JSON.stringify(unverJson?.error?.data) + ) + check( + 'exit code is WITHHELD (never read as a pass)', + unverJson?.error?.data?.exitCode === undefined + ) + check('checkout survives', after5.dirExists && after5.fileExists) + check('git registration survives', after5.registered) + + // clean up scenario 5 with the waiver so the temp dirs go away + setMode('ok') + cli(['worktree', 'rm', '--worktree', wt5.id, '--force']) + + // ============================================================ SCENARIO 6 + banner('SCENARIO 6 — folder workspace removal (the boundary that runs no hook) is unchanged') + const folderDir = mkdtempSync(join(tmpdir(), 'agh-folder-')) + mkdirSync(join(folderDir, 'src')) + writeFileSync(join(folderDir, 'src', 'app.txt'), 'folder workspace content\n') + // A folder workspace is imported against an existing project identity, so anchor it on the + // hookless repo's GitHub-derived project. + const folderProjectId = `github:agh-owner/${folderProjectSlug}` + ok([ + 'project', + 'setup-existing-folder', + '--project', + folderProjectId, + '--host', + 'local', + '--path', + folderDir, + '--kind', + 'folder' + ]) + const folderRepo = (ok(['repo', 'list']).repos ?? []).find((r) => r.path === folderDir) ?? null + out(`folder repo: ${folderDir} (${folderRepo?.id}) kind=${folderRepo?.kind}`) + check('registered repo kind is folder', folderRepo?.kind === 'folder', String(folderRepo?.kind)) + // The project ROOT of a folder project is not deletable (pre-existing rule, unrelated to the + // gate); the deletable folder workspace is a child created under it. + const folderRoot = ok(['worktree', 'show', '--worktree', `path:${folderDir}`]).worktree + const rootRm = cli(['worktree', 'rm', '--worktree', folderRoot.id, '--force', '--run-hooks']) + out( + ` root refusal (unchanged): exit ${rootRm.status} ${parseJsonLine(rootRm)?.error?.code} -- ${parseJsonLine(rootRm)?.error?.message}` + ) + check( + 'folder project root still refuses for its own reason, not the archive gate', + rootRm.status !== 0 && parseJsonLine(rootRm)?.error?.code !== 'worktree_archive_hook_failed' + ) + + const folderChild = ok([ + 'worktree', + 'create', + '--repo', + `id:${folderRepo.id}`, + '--name', + 'agh-folder-child', + '--setup', + 'skip' + ]).worktree + out(`folder workspace: ${folderChild.id}`) + const runsBeforeFolder = hookRuns().length + out(`\n$ orca worktree rm --worktree --force --run-hooks`) + const folderRm = cli(['worktree', 'rm', '--worktree', folderChild.id, '--force', '--run-hooks']) + const folderJson = parseJsonLine(folderRm) + out(` exit code: ${folderRm.status}`) + out(` ${JSON.stringify(folderJson)}`) + const stillThere = cli(['worktree', 'show', '--worktree', folderChild.id]) + check( + 'folder workspace removal exits zero', + folderRm.status === 0, + `${folderRm.status} ${folderRm.stderr}` + ) + check('folder removal ran no archive hook', hookRuns().length === runsBeforeFolder) + check( + 'folder contents left on disk (forget, not delete)', + existsSync(join(folderDir, 'src', 'app.txt')) + ) + check( + 'folder workspace is deregistered', + stillThere.status !== 0 && parseJsonLine(stillThere)?.error?.code === 'selector_not_found' + ) + rmSync(folderDir, { recursive: true, force: true }) + + banner(failures === 0 ? 'ALL CHECKS PASSED' : `${failures} CHECK(S) FAILED`) + } catch (error) { + out(`\nHARNESS ERROR: ${error instanceof Error ? error.stack : String(error)}`) + failures++ + } finally { + for (const wt of created) { + if (existsSync(wt.path)) { + cli(['worktree', 'rm', '--worktree', wt.id, '--force']) + rmSync(dirname(wt.path), { recursive: true, force: true }) + } + } + if (child.exitCode === null && child.signalCode === null) { + child.kill('SIGTERM') + await Promise.race([ + new Promise((r) => child.on('exit', r)), + new Promise((r) => setTimeout(r, 15_000)) + ]) + child.kill('SIGKILL') + } + rmSync(userDataDir, { recursive: true, force: true }) + } + process.exitCode = failures === 0 ? 0 : 1 +} + +setMode('ok') +main() diff --git a/config/scripts/check-changed-code-quality.mjs b/config/scripts/check-changed-code-quality.mjs index a1b5b2fc88a..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 @@ -29,6 +31,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'] } ] @@ -324,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) { @@ -396,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/git-binary-compatibility-workflow.test.mjs b/config/scripts/git-binary-compatibility-workflow.test.mjs index afe5615bb44..35d2b5c60dc 100644 --- a/config/scripts/git-binary-compatibility-workflow.test.mjs +++ b/config/scripts/git-binary-compatibility-workflow.test.mjs @@ -2,42 +2,63 @@ import { readFileSync } from 'node:fs' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' +const BASELINE_DIR = '~/.cache/orca-git-compat/git-2.25.5' + +const gateSteps = () => + parse(readFileSync('.github/workflows/pr.yml', 'utf8')).jobs.git_compatibility.steps + +const stepNamed = (name) => gateSteps().find((step) => step.name === name) + describe('Git binary compatibility PR gate', () => { it('runs the real-binary contract at each compatibility boundary', () => { - const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) - const step = workflow.jobs.git_compatibility.steps.find( - (candidate) => candidate.name === 'Verify Git binary compatibility matrix' - ) + const run = stepNamed('Verify Git binary compatibility matrix')?.run - expect(step?.run).toContain('git-2.25.5.tar.gz') + expect(run).toContain('ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git"') + expect(run).toContain('alpine/git:edge-2.38.1|2.38.1') + expect(run).toContain('alpine/git:v2.49.1|2.49.1') + expect(run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"') + expect(run).toContain('src/shared/git-binary-compatibility.test.ts') + expect(run).toContain('pids+=("$!")') + expect(run).toContain('wait "$pid" || status=1') + }) + + it('builds the pinned baseline tarball into the cached directory', () => { + const run = stepNamed('Build the baseline Git binary')?.run + + expect(run).toContain('git-2.25.5.tar.gz') // Why asserted: the sha256 check only runs on the build path, so a cached binary // must come from a key that pins the same version the tarball line declares. - expect(step?.run).toContain('if [ ! -x "$source/git" ]; then') - expect(step?.run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf') - expect(step?.run).toContain('ORCA_GIT_COMPAT_BINARY="$source/git"') - expect(step?.run).toContain('alpine/git:edge-2.38.1|2.38.1') - expect(step?.run).toContain('alpine/git:v2.49.1|2.49.1') - expect(step?.run).toContain('ORCA_GIT_COMPAT_IMAGE="$image"') - expect(step?.run).toContain('src/shared/git-binary-compatibility.test.ts') - expect(step?.run).toContain('-j"$(nproc)"') - expect(step?.run).toContain('pids+=("$!")') - expect(step?.run).toContain('wait "$pid" || status=1') - }) - - it('restores the baseline Git build before the matrix runs', () => { - const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) - const steps = workflow.jobs.git_compatibility.steps - const cacheIndex = steps.findIndex((step) => step.name === 'Cache baseline Git build') - const matrixIndex = steps.findIndex( - (step) => step.name === 'Verify Git binary compatibility matrix' - ) - - expect(cacheIndex).toBeGreaterThanOrEqual(0) - expect(cacheIndex).toBeLessThan(matrixIndex) + expect(run).toContain('if [ -x "$source/git" ]; then') + expect(run).toContain('41662c52fc16fec4963bfc41075e71f8ead6b5e386797eb6f9a1111ff95a8ddf') + expect(run).toContain('-j"$(nproc)"') // The cached path and the build path must be the same directory or the guard // above would rebuild on every run while still reporting a cache hit. - expect(steps[cacheIndex].with.path).toBe('~/.cache/orca-git-compat/git-2.25.5') - expect(steps[matrixIndex].run).toContain('source="$HOME/.cache/orca-git-compat/git-2.25.5"') + expect(run).toContain('source="$HOME/.cache/orca-git-compat/git-2.25.5"') + }) + + it('finishes the baseline build before the timed lanes start', () => { + const steps = gateSteps() + const names = steps.map((step) => step.name) + const cacheIndex = names.indexOf('Cache baseline Git build') + const buildIndex = names.indexOf('Build the baseline Git binary') + const matrixIndex = names.indexOf('Verify Git binary compatibility matrix') + + expect(cacheIndex).toBeGreaterThanOrEqual(0) + expect(cacheIndex).toBeLessThan(buildIndex) + expect(buildIndex).toBeLessThan(matrixIndex) + // Why asserted: each lane is bounded by Vitest's per-test timeout while it waits on + // container starts, so a `make -j$(nproc)` sharing the runner shows up as a timeout + // in whichever boundary case is running rather than as a slow build. + expect(steps[matrixIndex].run).not.toContain('make -C') + expect(steps[cacheIndex].with.path).toBe(BASELINE_DIR) expect(steps[cacheIndex].with.key).toContain('2.25.5') }) + + it('pulls every matrix image before any lane runs', () => { + const run = stepNamed('Verify Git binary compatibility matrix')?.run + // A lazy pull inside one lane stalls whatever test the sibling lane is timing. + const [beforeLanes] = run.split('pids=()') + + expect(beforeLanes).toContain('docker pull --quiet "${spec%%|*}"') + }) }) diff --git a/config/scripts/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/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/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/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/package-electron-install-owner.test.mjs b/config/scripts/package-electron-install-owner.test.mjs new file mode 100644 index 00000000000..3e1e3cf0d09 --- /dev/null +++ b/config/scripts/package-electron-install-owner.test.mjs @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' + +const projectDir = resolve(import.meta.dirname, '../..') +const readProject = (file) => readFileSync(join(projectDir, file), 'utf8') +const packageJson = JSON.parse(readProject('package.json')) +const pnpmWorkspace = parse(readProject('pnpm-workspace.yaml')) + +const OWNED_ELECTRON_REBUILD = 'node config/scripts/rebuild-native-deps.mjs' +// Why exact tokens and not /electron/i or a substring: the owner's own path has no "electron" +// in it, so a keyword check waves a duplicated rebuild through -- the case this contract is +// named for (#20787). Substring matching has the opposite fault: `install-app-deps` would also +// reject a `check-install-app-deps-version.mjs` that installs nothing. `rebuild:electron` is +// package.json's alias for the owned script, so running it is the same takeover. +const ELECTRON_INSTALL_COMMANDS = [ + OWNED_ELECTRON_REBUILD, + 'config/scripts/rebuild-native-deps.mjs', + 'rebuild:electron', + 'electron-rebuild', + 'electron-builder', + 'install-app-deps' +] +const tokenize = (step) => step.split(/[\s]+/).flatMap((word) => [word, ...word.split(/[@]/)]) +const takesOverElectronInstall = (step) => { + if (step.includes(OWNED_ELECTRON_REBUILD)) { + return true + } + const tokens = new Set(tokenize(step)) + return ELECTRON_INSTALL_COMMANDS.some((command) => tokens.has(command)) +} + +describe('Electron binary install ownership', () => { + it('keeps root postinstall as the single Electron binary install owner', () => { + // The invariant is that the root postinstall owns the Electron binary install, not that + // nothing may run after it -- pinning the whole string broke every open PR (#20726). + const steps = packageJson.scripts.postinstall.split('&&').map((step) => step.trim()) + expect(steps[0]).toBe(OWNED_ELECTRON_REBUILD) + for (const step of steps.slice(1)) { + expect(takesOverElectronInstall(step)).toBe(false) + } + expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') + }) + + // Why a separate case: the assertion above only reads the real postinstall, so it cannot show + // a bad chain would be caught. #20787 shipped a keyword check that missed a duplicated + // rebuild; these fixtures pin the rejections themselves. + it('rejects a chained step that would take over the Electron install', () => { + expect(takesOverElectronInstall(OWNED_ELECTRON_REBUILD)).toBe(true) + expect(takesOverElectronInstall('npx electron-rebuild')).toBe(true) + expect(takesOverElectronInstall('npx electron-builder install-app-deps')).toBe(true) + expect(takesOverElectronInstall('node config/scripts/sync-anti-slop-plugin.mjs')).toBe(false) + expect(takesOverElectronInstall('node config/scripts/check-electron-version.mjs')).toBe(false) + expect(takesOverElectronInstall('pnpm run rebuild:electron')).toBe(true) + expect(takesOverElectronInstall('node config/scripts/check-install-app-deps-version.mjs')).toBe( + false + ) + }) +}) diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs index 31d9b7a8d57..7ede0fc6208 100644 --- a/config/scripts/package-electron-runtime-contract.test.mjs +++ b/config/scripts/package-electron-runtime-contract.test.mjs @@ -24,11 +24,6 @@ describe('Electron runtime package contract', () => { linux: createPackagedRuntimeNodeModuleResources('linux') } - it('keeps root postinstall as the single Electron binary install owner', () => { - expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs') - expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') - }) - it('keeps the native Windows registry addon optional and platform-gated', () => { const rebuildScript = readProject('config/scripts/rebuild-native-deps.mjs') const ensureScript = readProject('config/scripts/ensure-native-runtime.mjs') 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/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' ? ' { calls += 1 - return Reflect.apply(nativeByteLength, Buffer, args) + return nativeByteLength.call(Buffer, ...args) } try { return { output: fn(), calls } diff --git a/config/scripts/tool-preview-whitespace-benchmark.mjs b/config/scripts/tool-preview-whitespace-benchmark.mjs index df590e52520..a98fdef7f8b 100644 --- a/config/scripts/tool-preview-whitespace-benchmark.mjs +++ b/config/scripts/tool-preview-whitespace-benchmark.mjs @@ -77,7 +77,7 @@ for (const input of [ ]) { assert.deepEqual(display(after, input), display(before, input)) } -for (const [shape, input] of [ +for (const [caseName, input] of [ ['tiny', 'ls -la'], ['100KB', 'a b\n\t'.repeat(15000)], ['1MB', 'a b\n\t'.repeat(150000)], @@ -110,5 +110,5 @@ for (const [shape, input] of [ samples[label].push((cpu.user + cpu.system) / 20000) } } - console.log(JSON.stringify({ shape, samples })) + console.log(JSON.stringify({ caseName, samples })) } diff --git a/config/scripts/verify-skill-update-roundtrip.mjs b/config/scripts/verify-skill-update-roundtrip.mjs index bfb10a50494..bc7a9c5e998 100644 --- a/config/scripts/verify-skill-update-roundtrip.mjs +++ b/config/scripts/verify-skill-update-roundtrip.mjs @@ -22,7 +22,7 @@ function option(name) { const cliVersion = option('cli') const autocrlf = option('autocrlf') -const shape = option('shape') +const placement = option('shape') // Why: PR branch names are untrusted workflow input. Keep them out of the // generated shell command and pass them to Node through the environment. const source = option('source') ?? process.env.SKILL_UPDATE_SOURCE @@ -30,7 +30,7 @@ const ref = option('ref') ?? process.env.SKILL_UPDATE_REF if ( !cliVersion || (autocrlf !== 'true' && autocrlf !== 'false') || - (shape !== 'symlink' && shape !== 'copy') || + (placement !== 'symlink' && placement !== 'copy') || !source || !ref || !/^[^/\s]+\/[^/\s]+$/.test(source) @@ -103,7 +103,7 @@ async function seedPlacement(name, tag) { const providerRoot = path.join(home, '.claude', 'skills') const provider = path.join(providerRoot, name) await mkdir(providerRoot, { recursive: true }) - await (shape === 'copy' + await (placement === 'copy' ? cp(canonical, provider, { recursive: true }) : symlink(canonical, provider, process.platform === 'win32' ? 'junction' : 'dir')) } @@ -211,20 +211,20 @@ try { await assertCurrentCanonical(targetName) const targetProviderAfter = await packageDigestAt(await realpath(targetProvider)) const targetProviderStat = await lstat(targetProvider) - if (shape === 'symlink' && !targetProviderStat.isSymbolicLink()) { + if (placement === 'symlink' && !targetProviderStat.isSymbolicLink()) { throw new Error(`${targetName} provider alias was replaced with an independent copy`) } - if (shape === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) { + if (placement === 'symlink' && targetProviderAfter !== currentSkill(targetName).packageDigest) { throw new Error(`${targetName} provider alias did not converge with the canonical update`) } if ( - shape === 'copy' && + placement === 'copy' && targetProviderAfter !== targetProviderBefore && targetProviderAfter !== currentSkill(targetName).packageDigest ) { throw new Error('Independent provider copy changed to an unexpected package identity') } - if (shape === 'copy') { + if (placement === 'copy') { // Why: hosted 1.5.17 replaces copies with aliases while equivalent local runs // retain the copy. Both prove this input topology must remain ineligible. const outcome = targetProviderStat.isSymbolicLink() @@ -241,7 +241,7 @@ try { throw new Error('Targeted update changed the non-targeted control provider placement') } const controlProviderStat = await lstat(controlProvider) - if (shape === 'symlink' && !controlProviderStat.isSymbolicLink()) { + if (placement === 'symlink' && !controlProviderStat.isSymbolicLink()) { throw new Error('Targeted update changed the non-targeted control topology') } } finally { diff --git a/config/scripts/wsl-git-shell-benchmark.mjs b/config/scripts/wsl-git-shell-benchmark.mjs index ca63c291165..2ae1f09876d 100644 --- a/config/scripts/wsl-git-shell-benchmark.mjs +++ b/config/scripts/wsl-git-shell-benchmark.mjs @@ -358,7 +358,7 @@ async function main() { sampleAggregation: BENCHMARK_SAMPLE_AGGREGATION, injectedLoginDelayMs: options.loginDelayMs, loginProbePreambleBytes: Buffer.byteLength(probeText.split('__ORCA_PATH__', 1)[0]), - guestProcessShape: { + guestProcessChain: { login: 'sh -> interactive login shell -> git', fast: 'env -> git' }, diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index bd7a18f8488..8a4e9697938 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 54m + + downloads: 58m @@ -15,7 +15,7 @@ downloads downloads - 54m - 54m + 58m + 58m diff --git a/docs/reference/git-compatibility.md b/docs/reference/git-compatibility.md index 1e19860385e..d537c4d94de 100644 --- a/docs/reference/git-compatibility.md +++ b/docs/reference/git-compatibility.md @@ -69,6 +69,12 @@ PR checks run the capability contract against real Git 2.25.5, 2.38.1, and 2.49.1 binaries. This spans the pre-2.29 serialized `FETCH_HEAD` fallback, the transitional `merge-tree --write-tree` behavior before `--merge-base`, and current Git. +The three lanes run in parallel and each Git call in the container lanes costs a +container start, so their wall clock is runner contention, not Git. Build the +2.25.5 binary and pull the images before the lanes start: anything heavy left +running alongside them is charged to whichever boundary case is in flight and +surfaces as a Vitest timeout rather than as a slow setup step. + Keep the unit tests alongside that matrix. They cover concurrent probes, native/WSL/SSH/relay isolation, and error-stream shapes that a single real binary invocation cannot exercise deterministically. diff --git a/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 c7420ede6cd..0a13a238efd 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -6754,6 +6754,981 @@ } ] }, + { + "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" + } + ] + }, { "id": "transport-host-status-gates-ready", "operation": "transport.host-status-gates", 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/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/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/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/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/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/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/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 d1a5e6b383e..20436844fce 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,6 +1,10 @@ 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 { pairingJournalMountAdapters } from './pairing-journal-mount-adapters' import { relayCredentialMountAdapters } from './relay-credential-mount-adapters' import { settingsMountAdapters, settingsMountExposures } from './settings-mount-adapters' @@ -10,6 +14,7 @@ import { taskWorkspaceHookMountAdapters } from './task-workspace-hook-mount-adap import { taskWorkspaceSenderMountAdapters } from './task-workspace-sender-mount-adapters' import { transportStatusMountAdapters } from './transport-status-mount-adapters' import { workspaceSettingsMounts } from './workspace-settings-mounts' +import { worktreeCatalogMountAdapters } from './worktree-catalog-mount-adapters' import type { MountedOperationModule } from '../mounted-operation-module' /** @@ -19,8 +24,17 @@ 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: 'pairing-journal-mount-adapters.ts', mounts: pairingJournalMountAdapters }, + { source: 'relay-credential-mount-adapters.ts', mounts: relayCredentialMountAdapters }, { source: 'settings-mount-adapters.ts', mounts: settingsMountAdapters, @@ -37,7 +51,6 @@ export const MOUNTED_OPERATION_MODULES: readonly MountedOperationModule[] = [ mounts: taskWorkspaceSenderMountAdapters }, { source: 'transport-status-mount-adapters.ts', mounts: transportStatusMountAdapters }, - { source: 'relay-credential-mount-adapters.ts', mounts: relayCredentialMountAdapters }, - { source: 'pairing-journal-mount-adapters.ts', mounts: pairingJournalMountAdapters }, - { 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/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/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 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/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-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/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/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/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts b/mobile/src/transport/unvalidated-rpc-request-port-inventory.ts index fe304110fff..a30db71353b 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 @@ -218,10 +230,5 @@ export const UNVALIDATED_RPC_REQUEST_PORT_PENDING: readonly UnvalidatedRpcReques // Sends through hostStatusProbe; the one reference left is its parameter type. Its callers do // not share a client type — push-registration.ts holds only the sender — so the parameter names // the port itself. It reaches zero when the last such caller migrates. - { file: 'src/transport/runtime-capability-probe.ts', references: 1 }, - - // 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: 1 } ] 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 } diff --git a/package.json b/package.json index a9f12759f74..9d37cb13cba 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: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", "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", @@ -162,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", @@ -197,8 +201,10 @@ "@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", "@stablyai/playwright-test": "^2.1.14", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-virtual": "^3.14.10", @@ -262,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 b2f6425aafd..6c9c98349d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,12 +213,18 @@ 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 '@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) @@ -408,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 @@ -718,6 +727,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 +993,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 +1053,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 +1233,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 +1416,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 +1501,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 +1515,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 +1529,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 +1543,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 +1557,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 +1571,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 +1585,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 +1599,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 +1629,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 +1669,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} @@ -1743,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'} @@ -2644,6 +2855,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 +3502,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 +3576,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 +3835,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 +3861,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 +4039,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 +4153,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 +4470,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 +4726,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 +4771,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 +4829,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 +4872,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 +4890,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 +5009,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 +5066,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 +5130,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 +5221,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 +5419,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 +5459,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 +5482,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 +5579,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 +5935,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 +6034,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 +6056,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} @@ -5703,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} @@ -5740,6 +6114,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 +6270,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 +6353,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 +6364,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 +7040,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 +7069,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 +7167,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 +7310,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 +7685,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 +7943,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 +8002,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 +8168,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 +8364,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 +8467,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 @@ -8086,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 @@ -8988,6 +9529,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 +10174,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 +10251,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 +10474,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 +10488,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 +10721,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 +10827,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cn@0.2.6: {} + code-block-writer@13.0.3: {} color-convert@2.0.1: @@ -10500,6 +11128,8 @@ snapshots: dedent@1.7.2: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -10791,8 +11421,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 +11432,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 +11497,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} eventemitter3@5.0.4: {} @@ -10912,6 +11592,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 +11630,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 +11658,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 +11781,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 +11864,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 +12002,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 +12109,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 +12246,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 +12283,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 +12299,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 +12379,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 +12980,8 @@ snapshots: nanoid@3.3.18: {} + natural-compare@1.4.0: {} + negotiator@1.0.0: {} node-abi@4.33.0: @@ -12339,6 +13073,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 +13135,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 @@ -12416,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 @@ -12469,6 +13241,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 +13383,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 +13503,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 +14312,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 +14337,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 +14460,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 +14578,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/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/cli/handlers/worktree-removal-warnings.ts b/src/cli/handlers/worktree-removal-warnings.ts new file mode 100644 index 00000000000..08079c51a0d --- /dev/null +++ b/src/cli/handlers/worktree-removal-warnings.ts @@ -0,0 +1,37 @@ +import { + formatArchiveHookOverride, + type ArchiveHookOverride +} from '../../shared/worktree/archive-hook-removal-gate' + +type HookWarningResult = { + warning?: string + archiveHookOverride?: ArchiveHookOverride +} + +type PreservedBranchResult = { + preservedBranch?: { + branchName: string + } +} + +export function printHookWarning(result: HookWarningResult, json: boolean): void { + if (json) { + return + } + if (result.warning) { + console.error(`warning: ${result.warning}`) + } + // Why (#19334): a waived archive-hook failure is the one case where Orca deleted a checkout + // whose archive step did not succeed. It has to stay visible in human output. + if (result.archiveHookOverride) { + console.error(`warning: ${formatArchiveHookOverride(result.archiveHookOverride)}`) + } +} + +export function printPreservedBranchWarning(result: PreservedBranchResult, json: boolean): void { + if (!json && result.preservedBranch) { + console.error( + `warning: local branch "${result.preservedBranch.branchName}" was kept because Git could not safely delete it` + ) + } +} diff --git a/src/cli/handlers/worktree.ts b/src/cli/handlers/worktree.ts index 262599234f0..62ddd27155a 100644 --- a/src/cli/handlers/worktree.ts +++ b/src/cli/handlers/worktree.ts @@ -6,6 +6,7 @@ import type { RuntimeWorktreeRemoveResult } from '../../shared/runtime-types' import type { CommandHandler } from '../dispatch' +import { printHookWarning, printPreservedBranchWarning } from './worktree-removal-warnings' import { formatWorktreeList, formatWorktreePs, formatWorktreeShow, printResult } from '../format' import { annotateOmittedHostScope, @@ -38,30 +39,6 @@ import { } from './worktree-create-parent-selector' import { getOptionalLinearIssueLinkFlag } from './worktree-linear-issue-link' -type HookWarningResult = { - warning?: string -} - -type PreservedBranchResult = { - preservedBranch?: { - branchName: string - } -} - -function printHookWarning(result: HookWarningResult, json: boolean): void { - if (!json && result.warning) { - console.error(`warning: ${result.warning}`) - } -} - -function printPreservedBranchWarning(result: PreservedBranchResult, json: boolean): void { - if (!json && result.preservedBranch) { - console.error( - `warning: local branch "${result.preservedBranch.branchName}" was kept because Git could not safely delete it` - ) - } -} - function assertParentWorktreeFlagsCompatible(flags: Map): void { if (flags.has('parent-worktree') && flags.get('no-parent') === true) { throw new RuntimeClientError( @@ -305,13 +282,24 @@ export const WORKTREE_HANDLERS: Record = { 'Orca cannot tell which host owns this workspace. Refresh projects and try again.' ) } + // Why (#19334): the waiver only ever applies to a hook that ran, so without --run-hooks it + // silently does nothing. Rejecting it beats letting someone believe they waived something. + if (flags.get('allow-failed-archive-hook') === true && flags.get('run-hooks') !== true) { + throw new RuntimeClientError( + 'invalid_argument', + '--allow-failed-archive-hook waives a FAILED archive hook, but without --run-hooks no hook runs at all. Pass --run-hooks too, or drop the waiver.' + ) + } const result = await client.call('worktree.rm', { worktree, hostId, force: flags.get('force') === true, // Why (#11960): --force is explicit here, so it may also waive PTY-stop proof. allowUnverifiedPtyStop: flags.get('force') === true, - runHooks: flags.get('run-hooks') === true + runHooks: flags.get('run-hooks') === true, + // Why (#19334): deliberately NOT coupled to --force, which above already waives PTY-stop + // proof. Waiving a failed archive hook is a separate decision about the user's data. + allowFailedArchiveHook: flags.get('allow-failed-archive-hook') === true }) printHookWarning(result.result, json) printPreservedBranchWarning(result.result, json) diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 7308d39dac6..f63e3832429 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -135,6 +135,83 @@ describe('command aliases dispatch to the canonical handler', () => { } }) + // #19334: a failed archive hook blocks removal, so the CLI must exit non-zero rather than + // report a delete that did not happen — and the waiver must ride its own flag, never --force. + it('exits non-zero when worktree removal is refused by a failed archive hook', async () => { + queueFixtures(callMock, okFixture('req_show', { worktree: { hostId: 'local' } })) + callMock.mockRejectedValueOnce( + Object.assign(new Error('Archive hook failed for worktree: /tmp/wt — exited 23.'), { + code: 'worktree_archive_hook_failed' + }) + ) + const priorExitCode = process.exitCode + + try { + await main( + ['worktree', 'rm', '--worktree', 'id:wt-1', '--force', '--run-hooks', '--json'], + '/tmp/repo' + ) + + expect(process.exitCode).toBe(1) + expect(callMock).toHaveBeenNthCalledWith( + 2, + 'worktree.rm', + expect.objectContaining({ + runHooks: true, + allowFailedArchiveHook: false + }) + ) + } finally { + process.exitCode = priorExitCode + } + }) + + // #19334 S4: the waiver only applies to a hook that ran, so alone it silently does nothing. + it('rejects the archive-hook waiver without --run-hooks instead of ignoring it', async () => { + queueFixtures(callMock, okFixture('req_show', { worktree: { hostId: 'local' } })) + const priorExitCode = process.exitCode + + try { + await main( + ['worktree', 'rm', '--worktree', 'id:wt-1', '--allow-failed-archive-hook', '--json'], + '/tmp/repo' + ) + + expect(process.exitCode).toBe(1) + // The removal must never have been attempted. + expect(callMock).not.toHaveBeenCalledWith('worktree.rm', expect.anything()) + } finally { + process.exitCode = priorExitCode + } + }) + + it('forwards the explicit archive-hook waiver on worktree rm', async () => { + queueFixtures( + callMock, + okFixture('req_show', { worktree: { hostId: 'local' } }), + okFixture('req', { removed: true }) + ) + + await main( + [ + 'worktree', + 'rm', + '--worktree', + 'id:wt-1', + '--run-hooks', + '--allow-failed-archive-hook', + '--json' + ], + '/tmp/repo' + ) + + expect(callMock).toHaveBeenNthCalledWith( + 2, + 'worktree.rm', + expect.objectContaining({ runHooks: true, allowFailedArchiveHook: true }) + ) + }) + it('still runs `terminal focus` after the handler de-duplication', async () => { queueFixtures(callMock, okFixture('req', { focus: { ok: true } })) diff --git a/src/cli/root-help-text-secondary.ts b/src/cli/root-help-text-secondary.ts index 50a1a76de7d..324fe847855 100644 --- a/src/cli/root-help-text-secondary.ts +++ b/src/cli/root-help-text-secondary.ts @@ -52,7 +52,7 @@ export const ROOT_HELP_TEXT_SECONDARY = [ ' orca worktree show --worktree [--json]', ' orca worktree current [--json]', ' orca worktree set --worktree [--display-name ] [--issue ] [--linear-issue ] [--comment ] [--workspace-status ] [--parent-worktree |--no-parent] [--json]', - ' orca worktree rm --worktree [--force] [--run-hooks] [--json]', + ' orca worktree rm --worktree [--force] [--run-hooks] [--allow-failed-archive-hook] [--json]', ' orca worktree ps [--limit ] [--json]', ' orca file open [--worktree ] [--json]', ' orca file diff [--staged] [--worktree ] [--json]', diff --git a/src/cli/specs/core.ts b/src/cli/specs/core.ts index 2cd3b5f3869..d029b0cd43d 100644 --- a/src/cli/specs/core.ts +++ b/src/cli/specs/core.ts @@ -172,10 +172,13 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [ ], destructive: true, summary: 'Remove a worktree from Orca and git', - usage: 'orca worktree rm --worktree [--force] [--run-hooks] [--json]', - allowedFlags: [...GLOBAL_FLAGS, 'worktree', 'force', 'run-hooks'], + usage: + 'orca worktree rm --worktree [--force] [--run-hooks] [--allow-failed-archive-hook] [--json]', + allowedFlags: [...GLOBAL_FLAGS, 'worktree', 'force', 'run-hooks', 'allow-failed-archive-hook'], notes: [ 'Repo-defined orca.yaml archive hooks are skipped unless --run-hooks is passed.', + 'With --run-hooks, a failed archive hook blocks the removal: nothing is stopped, deleted or deregistered, and the command exits non-zero with error code worktree_archive_hook_failed. --force does not waive this.', + 'Pass --allow-failed-archive-hook to delete anyway after the hook has run and failed; the waived failure is reported back on result.archiveHookOverride. It requires --run-hooks and is rejected without it, because with no hook running there is no failure to waive.', 'For Git worktrees, removal also attempts to delete the checked-out local branch, with or without --force. Orca retains branches it knows predated the worktree and any branch whose changes it cannot prove are already merged.' ] }, 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-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/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/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/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/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/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..227a290783b 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -25,8 +25,11 @@ import { type AutomationRunTerminalObserver } from './run-completion-watcher' import { createAutomationRunWriter, type AutomationRunWriter } from './automation-run-writer' +import { reportAutomationScheduleDrift } from './schedule-drift-report' import { describeScheduledRefusal, + missedBeyondGrace, + recordMissedRun, recordRefusedAutomationRun, recordUnevaluableAutomation, sendRendererDispatch, @@ -115,6 +118,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) { @@ -249,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 } 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/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/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/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-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-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/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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/hooks-archive-exit-observation.test.ts b/src/main/hooks-archive-exit-observation.test.ts index f924a5291cf..7b046e844c8 100644 --- a/src/main/hooks-archive-exit-observation.test.ts +++ b/src/main/hooks-archive-exit-observation.test.ts @@ -118,6 +118,11 @@ describe('archive hook exit observation', () => { ).resolves.toMatchObject({ success: true }) }) + it('names a signalled exit as one rather than reporting "exit code null"', async () => { + const result = await runArchiveWith({ code: null, signal: 'SIGKILL' }) + expect(result.output).toContain('terminated without reporting an exit code') + }) + it.each([ ['was killed by a signal', { code: null, signal: 'SIGKILL' as const }], // A real spawn failure carries a STRING code; the guard under test is `typeof code === diff --git a/src/main/hooks.ts b/src/main/hooks.ts index 8ea68c8f5fa..00cf8ea7408 100644 --- a/src/main/hooks.ts +++ b/src/main/hooks.ts @@ -44,7 +44,12 @@ function classifyHookProcessResult( return { success: false, output: `${streams}\n${message}`.trim() } } if (result.code !== 0) { - const message = `Command failed with exit code ${result.code}.` + // `null` means signalled: there is no exit code, and saying "exit code null" reads as a + // reporting glitch rather than the `unverifiable` verdict the gate is about to give it. + const message = + result.code === null + ? 'Command was terminated without reporting an exit code.' + : `Command failed with exit code ${result.code}.` console.error(`[hooks] ${context.hookName} hook failed in ${context.cwd}:`, message) return { success: false, 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/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) } 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/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/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/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/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/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-remove-archive-hooks.test.ts b/src/main/ipc/worktrees-remove-archive-hooks.test.ts index cb1b39c8d6e..38c243f95e5 100644 --- a/src/main/ipc/worktrees-remove-archive-hooks.test.ts +++ b/src/main/ipc/worktrees-remove-archive-hooks.test.ts @@ -14,6 +14,13 @@ import { } from './worktrees-test-module-mocks' import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness' import { mockKnownFeatureWorktree } from './worktrees-test-fixtures' +import { + ARCHIVE_HOOK_FAILED_REMOVAL_CODE, + asArchiveHookRefusal, + type WorktreeArchiveHookFailedError +} from '../../shared/worktree/archive-hook-removal-gate' +import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' +import type { RemoveWorktreeArgs } from './worktrees/ipc-context-schemas' import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub' vi.mock('electron', async () => @@ -98,6 +105,25 @@ vi.mock('../runtime/worktree-teardown', async () => ) vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock()) +// The shared IPC surface types every handler as returning `unknown`; removal's contract is +// narrower, and #19334's whole point is that a caller can name and branch on it. +async function removeWorktreeViaIpc(args: RemoveWorktreeArgs): Promise { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the registry types every handler as `(...) => unknown`, so this is the only place the real `worktrees:remove` return shape can be named; the production caller in worktree-ipc.ts declares the same type. + return (await handlers['worktrees:remove'](null, args)) as RemoveWorktreeResult +} + +/** Narrows through the exported error class — the same branch a real caller would write. */ +async function expectArchiveHookRefusal( + args: RemoveWorktreeArgs +): Promise { + try { + await removeWorktreeViaIpc(args) + } catch (error) { + return asArchiveHookRefusal(error) + } + throw new Error(`expected removal of ${args.worktreeId} to be refused by the archive hook`) +} + describe('registerWorktreeHandlers', () => { let runtimeStub: WorktreeRuntimeStub @@ -443,7 +469,8 @@ describe('registerWorktreeHandlers', () => { expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', true) }) - it('continues SSH worktree removal when the archive hook fails', async () => { + // Was "continues SSH worktree removal when the archive hook fails" (#19334): it now refuses. + it('refuses SSH worktree removal when the remote archive hook exits non-zero', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', @@ -453,7 +480,6 @@ describe('registerWorktreeHandlers', () => { connectionId: 'conn-1', worktreeBaseRef: null } - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const provider = { listWorktrees: vi.fn().mockResolvedValue([ { @@ -492,21 +518,18 @@ describe('registerWorktreeHandlers', () => { getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'exit 7' } }) - try { - await handlers['worktrees:remove'](null, { - worktreeId: 'repo-ssh::/remote/feature-wt' - }) - expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) - expect(consoleErrorSpy).toHaveBeenCalledWith( - '[hooks] archive hook failed for /remote/feature-wt:', - expect.stringContaining('archive hook exited 7') - ) - } finally { - consoleErrorSpy.mockRestore() - } + const refusal = await expectArchiveHookRefusal({ + worktreeId: 'repo-ssh::/remote/feature-wt' + }) + + expect(refusal.code).toBe(ARCHIVE_HOOK_FAILED_REMOVAL_CODE) + expect(refusal.data).toMatchObject({ outcome: 'exited', exitCode: 7 }) + expect(provider.worktreeIsClean).not.toHaveBeenCalled() + expect(provider.removeWorktree).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) - it('continues SSH worktree removal when archive hook execution rejects', async () => { + it('does not read a lost SSH connection as an archive hook that passed', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', @@ -516,7 +539,6 @@ describe('registerWorktreeHandlers', () => { connectionId: 'conn-1', worktreeBaseRef: null } - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const provider = { listWorktrees: vi.fn().mockResolvedValue([ { @@ -550,18 +572,15 @@ describe('registerWorktreeHandlers', () => { getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) - try { - await handlers['worktrees:remove'](null, { - worktreeId: 'repo-ssh::/remote/feature-wt' - }) - expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) - expect(consoleErrorSpy).toHaveBeenCalledWith( - '[hooks] archive hook failed for /remote/feature-wt:', - 'relay disconnected' - ) - } finally { - consoleErrorSpy.mockRestore() - } + const refusal = await expectArchiveHookRefusal({ + worktreeId: 'repo-ssh::/remote/feature-wt' + }) + + // Loss of contact is `unverifiable`, never evidence the hook succeeded. + expect(refusal.data).toMatchObject({ outcome: 'unverifiable' }) + expect(refusal.data.exitCode).toBeUndefined() + expect(provider.removeWorktree).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('uses cmd.exe for archive hooks on Windows-like SSH worktree paths', async () => { @@ -681,4 +700,116 @@ describe('registerWorktreeHandlers', () => { expect(provider.execNonInteractive).not.toHaveBeenCalled() expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) }) + + // Regression cover for #19334: a failed archive hook is a blocking precondition, not an advisory. + it('refuses removal and mutates nothing when the local archive hook exits 23', async () => { + mockKnownFeatureWorktree() + removeWorktreeMock.mockResolvedValue(undefined) + getEffectiveHooksMock.mockReturnValue({ + scripts: { archive: 'echo archived' } + }) + runHookMock.mockResolvedValue({ + success: false, + output: 'backup target unreachable', + exitCode: 23 + }) + + const refusal = await expectArchiveHookRefusal({ + worktreeId: 'repo-1::/workspace/feature-wt' + }) + + expect(refusal.code).toBe(ARCHIVE_HOOK_FAILED_REMOVAL_CODE) + expect(refusal.data).toEqual({ + worktreePath: '/workspace/feature-wt', + outcome: 'exited', + exitCode: 23, + output: 'backup target unreachable' + }) + expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() + expect(assertWorktreeCleanForRemovalMock).not.toHaveBeenCalled() + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() + }) + + it('classifies a local archive hook that never reported an exit as unverifiable', async () => { + mockKnownFeatureWorktree() + getEffectiveHooksMock.mockReturnValue({ + scripts: { archive: 'echo archived' } + }) + runHookMock.mockResolvedValue({ + success: false, + output: 'Hook timed out after 120000ms.' + }) + + const refusal = await expectArchiveHookRefusal({ + worktreeId: 'repo-1::/workspace/feature-wt' + }) + + expect(refusal.data).toEqual({ + worktreePath: '/workspace/feature-wt', + outcome: 'unverifiable', + output: 'Hook timed out after 120000ms.' + }) + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() + }) + + it('removes and records the waiver when a failed archive hook is explicitly overridden', async () => { + mockKnownFeatureWorktree() + removeWorktreeMock.mockResolvedValue({}) + getEffectiveHooksMock.mockReturnValue({ + scripts: { archive: 'echo archived' } + }) + runHookMock.mockResolvedValue({ + success: false, + output: 'boom', + exitCode: 23 + }) + + const result = await removeWorktreeViaIpc({ + worktreeId: 'repo-1::/workspace/feature-wt', + allowFailedArchiveHook: true + }) + + expect(result.archiveHookOverride).toEqual({ + worktreePath: '/workspace/feature-wt', + outcome: 'exited', + exitCode: 23, + output: 'boom', + overridden: true + }) + expect(removeWorktreeMock).toHaveBeenCalled() + }) + + // The folder-workspace path runs no archive hook at all (no Git removal step), so the gate has + // nothing to evaluate there. Pinned so a future hook added to that path is a deliberate change. + it('removes a folder workspace without consulting the archive hook', async () => { + const repo = { + id: 'repo-folder', + path: '/workspace/folder-project', + displayName: 'folder', + badgeColor: '#000', + addedAt: 0, + kind: 'folder' as const, + worktreeBaseRef: null + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'exit 23' } }) + runHookMock.mockResolvedValue({ + success: false, + output: 'boom', + exitCode: 23 + }) + + const result = await removeWorktreeViaIpc({ + worktreeId: 'repo-folder::/workspace/folder-project/nested' + }) + + expect(result).toEqual({}) + expect(runHookMock).not.toHaveBeenCalled() + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).toHaveBeenCalled() + }) }) 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/ipc/worktrees/ipc-context-schemas.ts b/src/main/ipc/worktrees/ipc-context-schemas.ts index f29f09b39d4..26f38473a1a 100644 --- a/src/main/ipc/worktrees/ipc-context-schemas.ts +++ b/src/main/ipc/worktrees/ipc-context-schemas.ts @@ -21,6 +21,9 @@ export type RemoveWorktreeArgs = { /** Explicit Force Delete only — `force` alone is set by the ordinary confirmation (#11960). */ allowUnverifiedPtyStop?: boolean skipArchive?: boolean + /** Explicit waiver for a FAILED archive hook (#19334). Distinct from `skipArchive`, which + * never runs the hook at all, and never implied by `force`. */ + allowFailedArchiveHook?: boolean snapshotPruneBatchId?: string } diff --git a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts index 443e05627ca..057dfec420c 100644 --- a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts +++ b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts @@ -12,6 +12,8 @@ import { isPrunableGitFileWorktree } from '../../../worktree-prunable-git-file' import { findRegisteredDeletableWorktree } from '../../../worktree-removal-safety' import { removeStaleLocalWorktreeRegistration } from '../../../local-worktree-removal-recovery' import { runHook } from '../../../hooks' +import type { ArchiveHookOverride } from '../../../../shared/worktree/archive-hook-removal-gate' +import { gateWorktreeRemovalOnArchiveHook } from '../../../worktree-archive-hook-gate' import { withWorktreeRemoveStageSpan } from '../../../observability/instrumentation' import { cleanupUnusedWorktreePushTargetRemote, @@ -84,6 +86,10 @@ export async function executeWorktreeRemoval( throw new Error(formatWorktreeRemovalError(error, canonicalWorktreePath, args.force ?? false)) } + // Ahead of the archive-hook gate below, and that ordering is right: both arms describe a + // registration with no checkout behind it — a row whose path IS a `.git` file, or a tree already + // gone from disk. There is nothing to archive, and running the hook would fail on the missing + // cwd and block a cleanup that has no user data to lose. if ( !repo.connectionId && ((await isPrunableGitFileWorktree(registeredWorktree, localWorktreeGitOptions)) || @@ -126,10 +132,18 @@ export async function executeWorktreeRemoval( return removalResult ?? {} } + // No connectionId override here, deliberately: this path derives its host from the repo row + // (`getRepoExecutionHostId` in register-worktree-removal-handlers) and resolves its provider, git + // options, listing and dispatch from `repo.connectionId` alone. Passing a different owner to the + // hook reader would read one host's orca.yaml while running the other host's git. The runtime's + // SSH path is the one that carries a route owner separate from the row, and it passes it. const hooks = await getArchiveHooksForRemoval(repo) const archiveScript = hooks?.scripts.archive + // Precondition, not an advisory (#19334): both branches below stop PTYs and delete the + // checkout, so a hook failure has to throw here — before either is reached. + let archiveHookOverride: ArchiveHookOverride | undefined if (archiveScript && !args.skipArchive) { // Why the branch on connectionId: this block is shared by both flows, so a hardcoded // 'remote' would file every local archive hook under the SSH breakdown. @@ -146,38 +160,40 @@ export async function executeWorktreeRemoval( undefined, localWorktreeGitOptions ) - if (!result.success) { - console.error(`[hooks] archive hook failed for ${canonicalWorktreePath}:`, result.output) - } + archiveHookOverride = gateWorktreeRemovalOnArchiveHook({ + worktreePath: canonicalWorktreePath, + result, + allowFailure: args.allowFailedArchiveHook === true + }) } ) } const remoteConnectionId = repo.connectionId ?? undefined - if (remoteConnectionId) { - return removeRegisteredRemoteWorktree( - context, - args, - repo, - repoId, - canonicalWorktreePath, - removalHostId, - registeredWorktree, - removedPushTarget, - provider!, - deleteBranch - ) - } - return removeRegisteredLocalWorktree( - context, - args, - repo, - repoId, - canonicalWorktreePath, - removalHostId, - removedPushTarget, - localWorktreeGitOptions, - hasLocalWorktreeGitOptions, - deleteBranch - ) + const result = remoteConnectionId + ? await removeRegisteredRemoteWorktree( + context, + args, + repo, + repoId, + canonicalWorktreePath, + removalHostId, + registeredWorktree, + removedPushTarget, + provider!, + deleteBranch + ) + : await removeRegisteredLocalWorktree( + context, + args, + repo, + repoId, + canonicalWorktreePath, + removalHostId, + removedPushTarget, + localWorktreeGitOptions, + hasLocalWorktreeGitOptions, + deleteBranch + ) + return archiveHookOverride ? { ...result, archiveHookOverride } : result } diff --git a/src/main/ipc/worktrees/removal/worktree-archive-hook.test.ts b/src/main/ipc/worktrees/removal/worktree-archive-hook.test.ts new file mode 100644 index 00000000000..5058cc6445b --- /dev/null +++ b/src/main/ipc/worktrees/removal/worktree-archive-hook.test.ts @@ -0,0 +1,93 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo } from '../../../../shared/repo-types' +import type * as HooksModule from '../../../hooks' + +const { getSshFilesystemProviderMock, getEffectiveHooksMock } = vi.hoisted(() => ({ + getSshFilesystemProviderMock: vi.fn(), + getEffectiveHooksMock: vi.fn() +})) +vi.mock('../../../providers/ssh-filesystem-dispatch', () => ({ + getSshFilesystemProvider: getSshFilesystemProviderMock +})) +// Only `getEffectiveHooks` is stubbed: the module under test also imports `parseOrcaYaml` from +// here, and replacing it wholesale made the parse throw into the fail-open catch — which answers +// "no hook", so the test saw an empty result rather than an error. +vi.mock('../../../hooks', async () => ({ + ...(await vi.importActual('../../../hooks')), + getEffectiveHooks: getEffectiveHooksMock +})) + +import { getArchiveHooksForRemoval } from './worktree-archive-hook' + +const REMOTE_REPO: Repo = { + id: 'r', + path: '/home/orca/repo', + displayName: 'r', + badgeColor: '#000', + addedAt: 0 +} + +// Why (#19334): a worktree row can name its owner only as `executionHostId: 'ssh:'`, leaving +// `repo.connectionId` null. Resolving hooks off the row alone then reads THIS machine's disk for a +// repo that lives on an SSH host — the committed archive hook goes unseen and the removal proceeds +// as though none were configured, which is the bug the gate exists to stop. +describe('getArchiveHooksForRemoval owner resolution', () => { + beforeEach(() => { + vi.clearAllMocks() + getSshFilesystemProviderMock.mockReturnValue(undefined) + getEffectiveHooksMock.mockReturnValue(null) + }) + + // Why this reads a file rather than just checking the lookup key: SSH owner resolution has been + // wrong twice on this path, and both times the fix looked right. Asserting only that + // `'ssh-target'` was passed stops short of the thing that broke — whether the hook actually comes + // from the REMOTE orca.yaml. This drives a stubbed provider holding real content and asserts the + // returned script is the remote one. + it('returns the hook from the execution host\u2019s orca.yaml, not the local disk', async () => { + const readFile = vi.fn().mockResolvedValue({ + isBinary: false, + content: 'scripts:\n archive: remote-archive.sh\n' + }) + getSshFilesystemProviderMock.mockReturnValue({ readFile }) + // If the local reader were consulted it would answer with a DIFFERENT script, so a wrong + // resolution shows up as the wrong value rather than as a silent absence. + getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'local-archive.sh' } }) + + const hooks = await getArchiveHooksForRemoval(REMOTE_REPO, 'ssh-target') + + expect(getSshFilesystemProviderMock).toHaveBeenCalledWith('ssh-target') + expect(readFile).toHaveBeenCalledWith('/home/orca/repo/orca.yaml') + expect(hooks?.scripts.archive).toBe('remote-archive.sh') + expect(getEffectiveHooksMock).not.toHaveBeenCalled() + }) + + it('falls back to the repo row when the caller names no owner', async () => { + await getArchiveHooksForRemoval({ ...REMOTE_REPO, connectionId: 'row-connection' }) + + expect(getSshFilesystemProviderMock).toHaveBeenCalledWith('row-connection') + expect(getEffectiveHooksMock).not.toHaveBeenCalled() + }) + + it('reads locally only when neither names a connection', async () => { + await getArchiveHooksForRemoval({ ...REMOTE_REPO, path: '/local/repo' }) + + expect(getSshFilesystemProviderMock).not.toHaveBeenCalled() + expect(getEffectiveHooksMock).toHaveBeenCalled() + }) + + // Known limitation, pinned so it is a decision rather than a surprise: the relay rewrites a + // non-numeric error code to -32000, so a missing orca.yaml and an unreachable host arrive + // identically. Both answer "no hook", which lets the removal proceed. Reporting them apart needs + // a provider contract that returns absence as a successful outcome — tracked in #20196. + it('answers "no hook" when the host cannot be read, missing or unreachable alike', async () => { + getSshFilesystemProviderMock.mockReturnValue({ + readFile: vi.fn().mockRejectedValue( + Object.assign(new Error('transport closed'), { + code: -32000 + }) + ) + }) + + await expect(getArchiveHooksForRemoval(REMOTE_REPO, 'ssh-target')).resolves.toEqual(null) + }) +}) diff --git a/src/main/ipc/worktrees/removal/worktree-archive-hook.ts b/src/main/ipc/worktrees/removal/worktree-archive-hook.ts index f59ac4a2ebb..4df6bedd49f 100644 --- a/src/main/ipc/worktrees/removal/worktree-archive-hook.ts +++ b/src/main/ipc/worktrees/removal/worktree-archive-hook.ts @@ -7,16 +7,42 @@ import { getSshFilesystemProvider } from '../../../providers/ssh-filesystem-disp import { requireSshGitProvider } from '../../../providers/ssh-git-dispatch' import { joinWorktreeRelativePath } from '../../../runtime/runtime-relative-paths' import { getSetupRunnerEnvVars } from '../../../setup-hook-env-vars' +import { + ARCHIVE_HOOK_TIMEOUT_MS, + type ArchiveHookRunResult +} from '../../../../shared/worktree/archive-hook-removal-gate' -const WORKTREE_ARCHIVE_HOOK_TIMEOUT_MS = 120_000 - -export async function getArchiveHooksForRemoval(repo: Repo): Promise { - if (!repo.connectionId) { +/** + * Resolve the archive hook against the host that owns the worktree. + * + * A failed read is answered as "no hook", which is a known limitation rather than a judgement: a + * missing `orca.yaml` is indistinguishable from an unreachable one here, because the relay rewrites + * a non-numeric error code to `-32000` (`src/relay/dispatcher-rpc-routing.ts`), so nothing survives + * to tell ENOENT from a transport failure. Reporting it as unreadable fired on every SSH repo that + * simply has no orca.yaml; blocking on it would refuse those deletes outright. Distinguishing the + * two needs a provider contract that reports absence as a successful outcome — tracked in #20196. + * + * @param connectionId Overrides `repo.connectionId`, which answers null for a row that names its + * owner only as `executionHostId: 'ssh:'`. Callers holding a resolved removal route must + * pass it, or an SSH-hosted repo is read on the local disk and its archive hook goes unseen. + */ +export async function getArchiveHooksForRemoval( + repo: Repo, + connectionId?: string +): Promise { + const owner = connectionId ?? repo.connectionId + if (!owner) { return getEffectiveHooks(repo) } - const fsProvider = getSshFilesystemProvider(repo.connectionId) + const fsProvider = getSshFilesystemProvider(owner) if (!fsProvider) { + // Fail-open, and the one case here we can name confidently: no provider means the host's + // orca.yaml was never even looked at, so "no archive hook" is an assumption. Logged rather + // than surfaced, because the removal that follows fails on its own missing provider anyway. + console.warn( + `[hooks] no SSH filesystem provider for ${owner}; treating ${repo.path} as having no archive hook` + ) return getEffectiveHooksFromConfig(repo, null) } @@ -24,7 +50,16 @@ export async function getArchiveHooksForRemoval(repo: Repo): Promise { +): Promise { if (!repo.connectionId) { return { success: true, output: '' } } @@ -46,7 +81,7 @@ export async function runRemoteArchiveHook( isWindowsRemote ? 'cmd.exe' : '/bin/bash', isWindowsRemote ? ['/d', '/s', '/c', script] : ['-lc', script], worktreePath, - WORKTREE_ARCHIVE_HOOK_TIMEOUT_MS, + ARCHIVE_HOOK_TIMEOUT_MS, undefined, env ) @@ -70,8 +105,15 @@ export async function runRemoteArchiveHook( .join('\n') .trim() + // Why (#19334): a spawn error or timeout means the host never reported an exit for this run, so + // the code is withheld and the gate classifies the failure `unverifiable` rather than `exited`. + const observedExit = + !result.spawnError && !result.timedOut && typeof result.exitCode === 'number' + ? result.exitCode + : undefined return { - success: !result.spawnError && !result.timedOut && result.exitCode === 0, - output + success: observedExit === 0, + output, + ...(observedExit !== undefined ? { exitCode: observedExit } : {}) } } diff --git a/src/main/ipc/worktrees/removal/worktree-removal-coordinator.ts b/src/main/ipc/worktrees/removal/worktree-removal-coordinator.ts index 3c52c4cabae..66fc769e046 100644 --- a/src/main/ipc/worktrees/removal/worktree-removal-coordinator.ts +++ b/src/main/ipc/worktrees/removal/worktree-removal-coordinator.ts @@ -8,14 +8,21 @@ export type WorktreeRemovalInFlight = { } export function getWorktreeRemovalOptionsKey( - args: Pick + args: Pick< + RemoveWorktreeArgs, + 'force' | 'allowUnverifiedPtyStop' | 'skipArchive' | 'allowFailedArchiveHook' + > ): string { const forceKey = args.force === true ? 'force' : 'normal' const archiveKey = args.skipArchive === true ? 'skip-archive' : 'run-archive' // Why: a Force Delete retry must not coalesce onto the in-flight attempt that // just failed the PTY gate — it would inherit that failure instead of retrying. const ptyKey = args.allowUnverifiedPtyStop === true ? 'allow-unverified-pty' : 'require-pty-stop' - return `${forceKey}:${archiveKey}:${ptyKey}` + // Same reason for the archive waiver: a retry that waives the failed hook must not coalesce + // onto the in-flight attempt that is about to refuse on it. + const archiveFailureKey = + args.allowFailedArchiveHook === true ? 'allow-failed-archive' : 'require-archive' + return `${forceKey}:${archiveKey}:${ptyKey}:${archiveFailureKey}` } export function getWorktreeRemovalInFlightKey( 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-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/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/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/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/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/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/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/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/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/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..d9d063172c5 --- /dev/null +++ b/src/main/providers/local-pty-child-process-verdict.test.ts @@ -0,0 +1,218 @@ +import * 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 { isRetiredPtyMaster } from '../pty/node-pty-master-fd-retirement' +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' + +const POSIX_SHELL = '/bin/sh' + +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) + } +} + +/** + * 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' }) +}) + +afterEach(() => { + ptyProcesses.clear() + 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( + 'pty-closed', + () => { + throw new Error('EBADF: bad file descriptor') + }, + '/bin/zsh' + ) + 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 () => { + 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-retired')).resolves.toBe(false) + }, 15000) +}) + +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') + }) + + 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 d4a717a9de2..d376124d7db 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' @@ -6,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, @@ -21,23 +23,36 @@ import { import { readWindowsConsoleAttachedProcessIds } from './windows-console-attached-processes' import { isWindowsPtyJobReadable, readWindowsPtyJobProcessIds } from './windows-pty-job-membership' -export async function hasLocalPtyChildProcesses(id: string): Promise { +/** + * 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 false + return 'no-children' + } + if (isRetiredPtyMaster(proc)) { + return 'unverifiable' } 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..8dad9843ab6 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,27 @@ export class LocalPtyProvider implements IPtyProvider { return hasLocalPtyChildProcesses(id) } + 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, + hasChildProcesses: childProcessEvidence === 'children', + childProcessEvidence + } + } + getForegroundProcess(id: string): Promise { return getLocalPtyForegroundProcess(id) } 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 +} 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/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-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-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/orca-runtime-remove-managed-worktree.ts b/src/main/runtime/orca-runtime-remove-managed-worktree.ts index 9cb95e5d354..10f9dd91ae1 100644 --- a/src/main/runtime/orca-runtime-remove-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-remove-managed-worktree.ts @@ -7,7 +7,10 @@ import { import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host' import { preservedBranchCleanupScopeKey } from '../../shared/preserved-branch-cleanup' -import { getRuntimeWorktreeRemovalOptionsKey } from './runtime-worktree-selection' +import { + getRuntimeWorktreeRemovalOptionsKey, + type RemoveManagedWorktreeOptions +} from './runtime-worktree-selection' import { withWorktreeSpan } from '../observability/instrumentation' import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth' import { resolveWorktreeRemovalRoute } from '../worktree-removal-execution-host-route' @@ -30,11 +33,15 @@ import { deleteRemoteWorktreeHistory } from '../remote-worktree-history-cleanup' export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateManagedRemoteWorktree { async removeManagedWorktree( worktreeSelector: string, - force = false, - runHooks = false, - allowUnverifiedPtyStop = false, - hostId?: string + options: RemoveManagedWorktreeOptions = {} ): Promise { + const { + force = false, + runHooks = false, + allowUnverifiedPtyStop = false, + allowFailedArchiveHook = false, + hostId + } = options if (!this.store) { throw new Error('runtime_unavailable') } @@ -45,7 +52,12 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM worktreeId: removalTarget.id, hostId: cleanupHostId }) - const optionsKey = getRuntimeWorktreeRemovalOptionsKey(force, runHooks, allowUnverifiedPtyStop) + const optionsKey = getRuntimeWorktreeRemovalOptionsKey({ + force, + runHooks, + allowUnverifiedPtyStop, + allowFailedArchiveHook + }) const inFlightRemoval = this.removeManagedWorktreeInFlight.get( cleanupScopeKey, removalTarget.id, @@ -190,6 +202,8 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM } if (route.kind === 'ssh') { return removeRuntimeRegisteredRemoteWorktree({ + runHooks, + allowFailedArchiveHook, repo, target: removalTarget, registeredWorktree, @@ -240,6 +254,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM hasLocalOptions: hasLocalWorktreeGitOptions, force, runHooks, + allowFailedArchiveHook, allowUnverifiedPtyStop, deleteBranch, acquireWatcherRemoval: this.acquireFileWatcherRemoval, 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) diff --git a/src/main/runtime/orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec.ts index 7af4da3957c..678ea23eb1d 100644 --- a/src/main/runtime/orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec.ts @@ -396,7 +396,7 @@ describe('OrcaRuntimeService', () => { }) try { - await runtime.removeManagedWorktree('path:/remote/feature', true, false) + await runtime.removeManagedWorktree('path:/remote/feature', { force: true, runHooks: false }) } finally { unregisterSshGitProvider('ssh-1') } @@ -472,7 +472,7 @@ describe('OrcaRuntimeService', () => { runtime.registerPty('pty-local-same-id', `${TEST_REPO_ID}::/remote/feature`, null) try { - await runtime.removeManagedWorktree('path:/remote/feature', true, false) + await runtime.removeManagedWorktree('path:/remote/feature', { force: true, runHooks: false }) } finally { unregisterSshGitProvider('ssh-1') } @@ -519,9 +519,9 @@ describe('OrcaRuntimeService', () => { const runtime = new OrcaRuntimeService(remoteStore as never) try { - await expect(runtime.removeManagedWorktree('path:/remote/repo', true)).rejects.toThrow( - 'Refusing to delete protected worktree path: /remote/repo' - ) + await expect( + runtime.removeManagedWorktree('path:/remote/repo', { force: true }) + ).rejects.toThrow('Refusing to delete protected worktree path: /remote/repo') } finally { unregisterSshGitProvider('ssh-1') } 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/orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec.ts index 16ad2bb818e..0c7f99da477 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec.ts @@ -298,7 +298,7 @@ describe('OrcaRuntimeService', () => { .mockResolvedValue([]) try { - const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) + const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true }) expect(result).toEqual({ preservedBranch: { branchName: 'feature/foo', head: 'abc' }, @@ -340,7 +340,9 @@ describe('OrcaRuntimeService', () => { ) try { - await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true)).rejects.toThrow( + await expect( + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true }) + ).rejects.toThrow( `Failed to force delete worktree at ${TEST_WORKTREE_PATH}. error: failed to delete deep/file.txt: Filename too long` ) expect(removePathSpy).not.toHaveBeenCalled() @@ -387,7 +389,7 @@ describe('OrcaRuntimeService', () => { }) try { - const result = await runtime.removeManagedWorktree(worktreeId, true) + const result = await runtime.removeManagedWorktree(worktreeId, { force: true }) expect(result).toEqual({ preservedBranch: { branchName: 'feature/foo', head: 'abc' } @@ -425,9 +427,9 @@ describe('OrcaRuntimeService', () => { vi.mocked(listWorktreesStrict).mockResolvedValue(registeredWorktrees) vi.mocked(removeWorktree).mockResolvedValue({}) - await expect(runtime.removeManagedWorktree(worktreeId, true, false)).rejects.toThrow( - 'Worktree is locked by Git. Lock reason: active agent session' - ) + await expect( + runtime.removeManagedWorktree(worktreeId, { force: true, runHooks: false }) + ).rejects.toThrow('Worktree is locked by Git. Lock reason: active agent session') expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts index 06982cf179e..9006502ad73 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts @@ -94,7 +94,12 @@ describe('OrcaRuntimeService', () => { const runtime = createWorktreeRemovalRuntime(runtimeStore) try { - await runtime.removeManagedWorktree(TEST_WORKTREE_ID, false, false, false, 'ssh:ssh-1') + await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:ssh-1' + }) expect(provider.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, false) expect(metaById[TEST_WORKTREE_ID]?.hostId).toBe('local') const result = await runtime.forceDeletePreservedBranch( @@ -181,8 +186,8 @@ describe('OrcaRuntimeService', () => { return {} }) - const first = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) - const second = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true) + const first = runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true }) + const second = runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true }) await removeStarted.promise await Promise.resolve() @@ -238,14 +243,18 @@ describe('OrcaRuntimeService', () => { registerSshGitProvider('host-b', provider as never) try { - const local = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'local') - const remote = runtime.removeManagedWorktree( - TEST_WORKTREE_ID, - true, - false, - false, - 'ssh:host-b' - ) + const local = runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'local' + }) + const remote = runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:host-b' + }) await bothStarted.promise expect(removeWorktree).toHaveBeenCalledTimes(1) @@ -271,7 +280,7 @@ describe('OrcaRuntimeService', () => { const first = runtime.removeManagedWorktree(TEST_WORKTREE_ID) await removeStarted.promise - await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true)).rejects.toThrow( + await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true })).rejects.toThrow( 'Worktree deletion already in progress' ) @@ -292,7 +301,7 @@ describe('OrcaRuntimeService', () => { try { vi.mocked(listWorktrees).mockResolvedValue([]) - await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) + await expect(runtime.removeManagedWorktree(worktreeId, { force: true })).resolves.toEqual({}) expect(removeWorktree).not.toHaveBeenCalled() // The repo resolved to the local host, so the metadata purge names it — @@ -458,7 +467,9 @@ describe('OrcaRuntimeService', () => { }) try { - await expect(runtime.removeManagedWorktree(`id:${worktreeId}`, true)).resolves.toEqual({}) + await expect( + runtime.removeManagedWorktree(`id:${worktreeId}`, { force: true }) + ).resolves.toEqual({}) } finally { unregisterSshGitProvider(repo.connectionId) unregisterSshFilesystemProvider(repo.connectionId) @@ -513,7 +524,7 @@ describe('OrcaRuntimeService', () => { try { vi.mocked(listWorktrees).mockResolvedValue([]) - await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) + await expect(runtime.removeManagedWorktree(worktreeId, { force: true })).resolves.toEqual({}) await expect(lstat(orphanPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(closeLocalWatcherForWorktreePathMock).toHaveBeenCalledWith( @@ -586,7 +597,7 @@ describe('OrcaRuntimeService', () => { expect(removeWorktree).not.toHaveBeenCalled() expect(removeWorktreeMeta).not.toHaveBeenCalled() - await expect(runtime.removeManagedWorktree(worktreeId, true)).resolves.toEqual({}) + await expect(runtime.removeManagedWorktree(worktreeId, { force: true })).resolves.toEqual({}) await expect(lstat(leftoverPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() @@ -642,7 +653,7 @@ describe('OrcaRuntimeService', () => { try { vi.mocked(listWorktrees).mockResolvedValue([]) - await expect(runtime.removeManagedWorktree(worktreeId, true)).rejects.toThrow( + await expect(runtime.removeManagedWorktree(worktreeId, { force: true })).rejects.toThrow( `Refusing to delete unregistered worktree path: ${standalonePath}` ) diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts index efc9efc57b5..78c4f072bb3 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts @@ -90,9 +90,9 @@ describe('OrcaRuntimeService', () => { const runtime = createWorktreeRemovalRuntime(runtimeStore) try { - await expect(runtime.removeManagedWorktree(`id:${worktreeId}`, true)).rejects.toThrow( - 'SSH filesystem provider unavailable' - ) + await expect( + runtime.removeManagedWorktree(`id:${worktreeId}`, { force: true }) + ).rejects.toThrow('SSH filesystem provider unavailable') await expect(lstat(localPath)).resolves.toBeTruthy() expect(removeWorktree).not.toHaveBeenCalled() @@ -112,7 +112,7 @@ describe('OrcaRuntimeService', () => { try { vi.mocked(listWorktrees).mockResolvedValue([]) - await expect(runtime.removeManagedWorktree(worktreeId, true)).rejects.toThrow( + await expect(runtime.removeManagedWorktree(worktreeId, { force: true })).rejects.toThrow( 'Refusing to delete unregistered worktree path' ) @@ -177,7 +177,9 @@ describe('OrcaRuntimeService', () => { } }) - await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( + await expect( + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true, runHooks: true }) + ).rejects.toThrow( `Refusing to delete worktree because it contains another registered worktree: ${TEST_WORKTREE_PATH}/child` ) @@ -238,7 +240,9 @@ describe('OrcaRuntimeService', () => { } ]) - await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( + await expect( + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true, runHooks: true }) + ).rejects.toThrow( `Failed to force delete worktree at ${TEST_WORKTREE_PATH}. Worktree is locked by Git.` ) @@ -278,9 +282,9 @@ describe('OrcaRuntimeService', () => { } ]) - await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, true)).rejects.toThrow( - 'Worktree is locked by Git' - ) + await expect( + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: true, runHooks: true }) + ).rejects.toThrow('Worktree is locked by Git') expect(runHook).toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() @@ -377,7 +381,7 @@ describe('OrcaRuntimeService', () => { vi.mocked(runHook).mockResolvedValue({ success: true, output: '' }) vi.mocked(removeWorktree).mockResolvedValue({}) - await runtime.removeManagedWorktree(TEST_WORKTREE_ID, false, true) + await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { force: false, runHooks: true }) expect(runHook).toHaveBeenCalledWith( 'archive', diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts index 630eaebf007..aa976375801 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts @@ -612,7 +612,12 @@ describe('OrcaRuntimeService', () => { }) await expect( - runtime.removeManagedWorktree(TEST_WORKTREE_ID, false, false, false, 'runtime:env-b') + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'runtime:env-b' + }) ).rejects.toThrow('no longer belongs to runtime:env-b') expect(localProvider.listProcesses).not.toHaveBeenCalled() diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts new file mode 100644 index 00000000000..1333d6bd469 --- /dev/null +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-archive-hook-gate.spec.ts @@ -0,0 +1,242 @@ +// Regression cover for #19334: a failed archive hook used to be logged and stepped over, so the +// checkout was deleted with nothing archived. The hook is a blocking precondition now. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + assertWorktreeCleanForRemoval, + deleteWorktreeHistoryDirMock, + getEffectiveHooks, + invalidateAuthorizedRootsCacheMock, + listWorktreesStrict, + removeWorktree, + removeWorktreeLinkedPathsMock, + runHook +} from '../orca-runtime-test-mocks.spec' +import { + TEST_REPO_PATH, + TEST_WORKTREE_ID, + TEST_WORKTREE_PATH, + createStaleRuntimeWorktreeStore, + deferred +} from '../orca-runtime-test-fixtures.spec' +import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec' +import { + ARCHIVE_HOOK_FAILED_REMOVAL_CODE, + asArchiveHookRefusal +} from '../../../shared/worktree/archive-hook-removal-gate' + +function withArchiveHook(): void { + vi.mocked(getEffectiveHooks).mockReturnValue({ + scripts: { archive: 'pnpm worktree:archive' } + }) +} + +function expectNothingMutated(removeWorktreeMeta: ReturnType): void { + // The checkout, its Git registration, its agents and Orca's ownership evidence all survive. + expect(removeWorktree).not.toHaveBeenCalled() + expect(removeWorktreeMeta).not.toHaveBeenCalled() + expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() + expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled() + expect(invalidateAuthorizedRootsCacheMock).not.toHaveBeenCalled() + // The gate runs before the registration re-read, so even the preflights never start. The one + // listing is the orchestrator's own lookup ahead of the hook; the post-hook refresh never runs. + expect(listWorktreesStrict).toHaveBeenCalledTimes(1) + expect(assertWorktreeCleanForRemoval).not.toHaveBeenCalled() +} + +describe('archive hook removal gate', () => { + // These specs are imported into one aggregate test file, so the module-level mocks arrive with + // calls from earlier specs. Clear counts here and restore the shared defaults afterwards. + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.mocked(getEffectiveHooks).mockReturnValue(null) + vi.mocked(runHook).mockResolvedValue({ success: true, output: '' }) + }) + + it('refuses removal and mutates nothing when the archive hook exits 23', async () => { + const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(TEST_WORKTREE_ID) + const runtime = createWorktreeRemovalRuntime(runtimeStore) + withArchiveHook() + vi.mocked(runHook).mockResolvedValue({ + success: false, + output: 'backup target unreachable', + exitCode: 23 + }) + + const failure = await runtime + .removeManagedWorktree(TEST_WORKTREE_ID, { force: false, runHooks: true }) + .catch((error: unknown) => error) + + const refusal = asArchiveHookRefusal(failure) + expect(refusal.code).toBe(ARCHIVE_HOOK_FAILED_REMOVAL_CODE) + expect(refusal.data).toEqual({ + worktreePath: TEST_WORKTREE_PATH, + outcome: 'exited', + exitCode: 23, + output: 'backup target unreachable' + }) + expectNothingMutated(removeWorktreeMeta) + }) + + it('refuses removal when the hook never reported an exit, without claiming it passed', async () => { + const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(TEST_WORKTREE_ID) + const runtime = createWorktreeRemovalRuntime(runtimeStore) + withArchiveHook() + // A timeout or a lost execution host yields no exit code: `unverifiable`, never a pass. + vi.mocked(runHook).mockResolvedValue({ + success: false, + output: 'Hook timed out after 120000ms.' + }) + + const failure = await runtime + .removeManagedWorktree(TEST_WORKTREE_ID, { force: false, runHooks: true }) + .catch((error: unknown) => error) + + const refusal = asArchiveHookRefusal(failure) + expect(refusal.data).toEqual({ + worktreePath: TEST_WORKTREE_PATH, + outcome: 'unverifiable', + output: 'Hook timed out after 120000ms.' + }) + expectNothingMutated(removeWorktreeMeta) + }) + + it('does not let --force waive a failed archive hook', async () => { + const { runtimeStore, removeWorktreeMeta } = createStaleRuntimeWorktreeStore(TEST_WORKTREE_ID) + const runtime = createWorktreeRemovalRuntime(runtimeStore) + withArchiveHook() + vi.mocked(runHook).mockResolvedValue({ + success: false, + output: 'boom', + exitCode: 23 + }) + + await expect( + // force + the PTY-stop waiver, i.e. everything the desktop Force Delete sets. + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: true, + allowUnverifiedPtyStop: true + }) + ).rejects.toMatchObject({ code: ARCHIVE_HOOK_FAILED_REMOVAL_CODE }) + expectNothingMutated(removeWorktreeMeta) + }) + + it('removes and records the waiver when the failure is explicitly overridden', async () => { + const runtime = createWorktreeRemovalRuntime() + withArchiveHook() + vi.mocked(runHook).mockResolvedValue({ + success: false, + output: 'boom', + exitCode: 23 + }) + vi.mocked(removeWorktree).mockResolvedValue({}) + + const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: true, + allowUnverifiedPtyStop: false, + allowFailedArchiveHook: true + }) + + expect(result.archiveHookOverride).toEqual({ + worktreePath: TEST_WORKTREE_PATH, + outcome: 'exited', + exitCode: 23, + output: 'boom', + overridden: true + }) + expect(removeWorktree).toHaveBeenCalledWith( + TEST_REPO_PATH, + TEST_WORKTREE_PATH, + false, + expect.objectContaining({ + knownRemovedWorktree: expect.objectContaining({ + path: TEST_WORKTREE_PATH + }) + }) + ) + }) + + it('removes without an override record when the hook succeeds', async () => { + const runtime = createWorktreeRemovalRuntime() + withArchiveHook() + vi.mocked(runHook).mockResolvedValue({ + success: true, + output: '', + exitCode: 0 + }) + vi.mocked(removeWorktree).mockResolvedValue({}) + + const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: true + }) + + expect(result.archiveHookOverride).toBeUndefined() + expect(removeWorktree).toHaveBeenCalled() + }) + + it('removes when the hook is configured but not requested', async () => { + const runtime = createWorktreeRemovalRuntime() + withArchiveHook() + vi.mocked(removeWorktree).mockResolvedValue({}) + + const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID) + + expect(runHook).not.toHaveBeenCalled() + expect(result.warning).toContain('archive hook skipped') + expect(removeWorktree).toHaveBeenCalled() + }) + + it('removes when no archive hook is configured', async () => { + const runtime = createWorktreeRemovalRuntime() + vi.mocked(getEffectiveHooks).mockReturnValue(null) + vi.mocked(removeWorktree).mockResolvedValue({}) + + const result = await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: true + }) + + expect(runHook).not.toHaveBeenCalled() + expect(result.warning).toBeUndefined() + expect(removeWorktree).toHaveBeenCalled() + }) + + it('does not coalesce an override retry onto the refusal already in flight', async () => { + const runtime = createWorktreeRemovalRuntime() + withArchiveHook() + const hookRun = deferred<{ + success: boolean + output: string + exitCode?: number + }>() + vi.mocked(runHook).mockReturnValue(hookRun.promise) + vi.mocked(removeWorktree).mockResolvedValue({}) + + const refused = runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: true + }) + await vi.waitFor(() => expect(runHook).toHaveBeenCalled()) + + // The waiver is part of the in-flight options key, so a concurrent waived retry is refused + // outright rather than handed the in-flight attempt that is about to reject on the hook. + await expect( + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: false, + runHooks: true, + allowUnverifiedPtyStop: false, + allowFailedArchiveHook: true + }) + ).rejects.toThrow('Worktree deletion already in progress') + + hookRun.resolve({ success: false, output: 'boom', exitCode: 23 }) + await expect(refused).rejects.toMatchObject({ + code: ARCHIVE_HOOK_FAILED_REMOVAL_CODE + }) + }) +}) diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts index 8ec38e0e5e8..4370d02e217 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts @@ -102,7 +102,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { vi.spyOn(runtime, 'acquireFileWatcherRemoval').mockResolvedValue({ finish: vi.fn() }) try { - await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a') + await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:target-a' + }) expect(provider.listWorktrees).toHaveBeenCalledWith(REMOTE_REPO_PATH) expect(provider.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, true) @@ -127,7 +132,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { try { await expect( - runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a') + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:target-a' + }) ).resolves.toEqual({}) expect(provider.listWorktrees).toHaveBeenCalledWith(REMOTE_REPO_PATH) @@ -152,7 +162,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { vi.spyOn(runtime, 'acquireFileWatcherRemoval').mockResolvedValue({ finish: vi.fn() }) try { - await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-b') + await runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:target-b' + }) expect(providerB.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, true) expect(providerA.listWorktrees).not.toHaveBeenCalled() @@ -168,7 +183,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { const runtime = createWorktreeRemovalRuntime(runtimeStore) await expect( - runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a') + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'ssh:target-a' + }) ).rejects.toThrow('Remote connection dropped') expect(listWorktreesStrict).not.toHaveBeenCalled() @@ -181,7 +201,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { const runtime = createWorktreeRemovalRuntime(runtimeStore) await expect( - runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'runtime:env-1') + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'runtime:env-1' + }) ).rejects.toThrow('not dispatched by this process') expect(listWorktreesStrict).not.toHaveBeenCalled() @@ -201,7 +226,12 @@ describe('OrcaRuntimeService worktree removal execution host', () => { try { await expect( - runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'runtime:env-1') + runtime.removeManagedWorktree(TEST_WORKTREE_ID, { + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + hostId: 'runtime:env-1' + }) ).rejects.toThrow('not dispatched by this process') // Selector resolution still lists through the raw field before removal begins — a read on diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 7ec1fd5fc35..d9e187817bf 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -110,6 +110,7 @@ await import('./orca-runtime-tests/worktree-removal-and-reconciliation.spec') await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec') await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec') await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec') +await import('./orca-runtime-tests/worktree-removal-archive-hook-gate.spec') await import('./orca-runtime-tests/worktree-removal-execution-host.spec') await import('./orca-runtime-tests/targeting-and-resilience.spec') await import('./orca-runtime-tests/worktree-scan-cache-ttl.spec') 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/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/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/rpc/errors.ts b/src/main/runtime/rpc/errors.ts index 5bb9dc28343..5081236f09a 100644 --- a/src/main/runtime/rpc/errors.ts +++ b/src/main/runtime/rpc/errors.ts @@ -22,6 +22,7 @@ import { } from '../../../shared/skill-install-failure' import { GIT_DIFF_TOO_LARGE_CODE } from '../../../shared/git-diff-transport-budget' import { AUTOMATION_OWNER_CONFLICT_CODES } from '../../../shared/automation-owner-conflict' +import { ARCHIVE_HOOK_FAILED_REMOVAL_CODE } from '../../../shared/worktree/archive-hook-removal-gate' import { NESTED_WORKER_DEPTH_EXCEEDED_CODE } from '../../../shared/nested-worker-depth' export function successResponse(id: string, meta: RpcEnvelopeMeta, result: unknown): RpcSuccess { @@ -126,6 +127,9 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet = new Set([ 'stale_delivery', 'waiter_exists', 'invalid_argument', + // Why (#19334): "your archive hook failed, nothing was deleted" is a distinct decision — retry, + // waive, or skip the hook. Flattened to runtime_error a caller can only pattern-match the text. + ARCHIVE_HOOK_FAILED_REMOVAL_CODE, NESTED_WORKER_DEPTH_EXCEEDED_CODE, GIT_DIFF_TOO_LARGE_CODE, ARTIFACT_SHARING_DISABLED_CODE, diff --git a/src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts b/src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts index 68f8b040479..d96fa04c521 100644 --- a/src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts +++ b/src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts @@ -20,6 +20,15 @@ function makeRequest(params: unknown): RpcRequest { return { id: 'req-1', authToken: 'tok', method: 'worktree.rm', params } } +/** The removal options every case forwards; only the resolved host differs. */ +const forwarded = (hostId?: string): Record => ({ + force: true, + runHooks: false, + allowUnverifiedPtyStop: false, + allowFailedArchiveHook: false, + ...(hostId ? { hostId } : {}) +}) + describe('worktree.rm host qualification', () => { it('routes an explicitly qualified removal to that host', async () => { const runtime = makeRuntime() @@ -29,13 +38,7 @@ describe('worktree.rm host qualification', () => { makeRequest({ worktree: 'id:wt-1', hostId: 'local', force: true, runHooks: false }) ) - expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( - 'id:wt-1', - true, - false, - false, - 'local' - ) + expect(runtime.removeManagedWorktree).toHaveBeenCalledWith('id:wt-1', forwarded('local')) expect(response).toMatchObject({ ok: true, result: { removed: true } }) }) @@ -54,10 +57,7 @@ describe('worktree.rm host qualification', () => { expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( `id:${WORKTREE_ID}`, - true, - false, - false, - 'local' + forwarded('local') ) expect(response).toMatchObject({ ok: true, result: { removed: true } }) }) @@ -77,10 +77,7 @@ describe('worktree.rm host qualification', () => { expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( `id:${WORKTREE_ID}`, - true, - false, - false, - 'runtime:env-1' + forwarded('runtime:env-1') ) }) @@ -119,10 +116,7 @@ describe('worktree.rm host qualification', () => { expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( `id:${WORKTREE_ID}`, - true, - false, - false, - 'ssh:target-a' + forwarded('ssh:target-a') ) }) @@ -151,13 +145,7 @@ describe('worktree.rm host qualification', () => { ) expect(runtime.showManagedWorktree).toHaveBeenCalledWith('id:wt-1') - expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( - 'id:wt-1', - true, - false, - false, - 'local' - ) + expect(runtime.removeManagedWorktree).toHaveBeenCalledWith('id:wt-1', forwarded('local')) expect(response).toMatchObject({ ok: true, result: { removed: true } }) }) @@ -205,13 +193,7 @@ describe('worktree.rm host qualification', () => { expect(response).toMatchObject({ ok: true, result: { removed: true } }) // Unqualified on purpose: removeManagedWorktree owns the stale-row path and // still refuses on its own if the id turns out to have two owners. - expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( - 'id:wt-gone', - true, - false, - false, - undefined - ) + expect(runtime.removeManagedWorktree).toHaveBeenCalledWith('id:wt-gone', forwarded()) }) it('propagates a non-missing lookup failure instead of deleting unqualified', async () => { diff --git a/src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts b/src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts index 7f4a6c7751c..0c912db53be 100644 --- a/src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts +++ b/src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts @@ -14,74 +14,75 @@ function makeRuntime(): OrcaRuntimeService { } as unknown as OrcaRuntimeService } -// Why (#11960): waiving the proof that every PTY stopped must ride its own field. -// The desktop sets `force` for an ordinary confirmed delete, so keying the waiver -// off `force` would silently disable the gate on the primary delete path. -describe('worktree.rm PTY-stop waiver', () => { - it('forwards an explicit waiver to the runtime', async () => { +/** The dispatcher validates against the Zod schema, so the test spells the wire shape. */ +type RmParams = { + hostId?: string + force?: boolean + runHooks?: boolean + allowUnverifiedPtyStop?: boolean + allowFailedArchiveHook?: boolean +} + +async function dispatchRm(runtime: OrcaRuntimeService, params: RmParams): Promise { + const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) + const request: RpcRequest = { + id: 'req-1', + authToken: 'tok', + method: 'worktree.rm', + params: { worktree: 'id:wt-1', ...params } + } + await dispatcher.dispatch(request) +} + +/** Every waiver off unless a case turns it on — the defaults are the assertion. */ +const forwarded = (overrides: Partial> = {}): Record => ({ + force: false, + runHooks: false, + allowUnverifiedPtyStop: false, + allowFailedArchiveHook: false, + hostId: 'local', + ...overrides +}) + +// Why (#11960 and #19334): each waiver rides its own field. The desktop sets `force` for an +// ordinary confirmed delete, so keying either waiver off `force` would silently disable that gate +// on the primary delete path. These cases exist to keep `force` from acquiring a second meaning. +describe('worktree.rm waivers travel on their own fields', () => { + it.each([ + [ + 'an explicit PTY-stop waiver reaches the runtime', + { hostId: 'local', force: true, allowUnverifiedPtyStop: true, runHooks: false }, + forwarded({ force: true, allowUnverifiedPtyStop: true }) + ], + [ + 'force alone does NOT waive the PTY-stop proof', + { hostId: 'local', force: true, runHooks: false }, + forwarded({ force: true }) + ], + [ + 'an explicit archive-hook waiver reaches the runtime', + { hostId: 'local', runHooks: true, allowFailedArchiveHook: true }, + forwarded({ runHooks: true, allowFailedArchiveHook: true }) + ], + [ + 'force plus a PTY waiver does NOT waive a failed archive hook', + { hostId: 'local', force: true, allowUnverifiedPtyStop: true, runHooks: true }, + forwarded({ force: true, runHooks: true, allowUnverifiedPtyStop: true }) + ] + ])('%s', async (_name, params, expected) => { const runtime = makeRuntime() - const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) - - await dispatcher.dispatch({ - id: 'req-1', - authToken: 'tok', - method: 'worktree.rm', - params: { - worktree: 'id:wt-1', - hostId: 'local', - force: true, - allowUnverifiedPtyStop: true, - runHooks: false - } - } satisfies RpcRequest) - - expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( - 'id:wt-1', - true, - false, - true, - 'local' - ) - }) - - it('does not infer a waiver from force alone', async () => { - const runtime = makeRuntime() - const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) - - await dispatcher.dispatch({ - id: 'req-1', - authToken: 'tok', - method: 'worktree.rm', - params: { worktree: 'id:wt-1', hostId: 'local', force: true, runHooks: false } - } satisfies RpcRequest) - - expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( - 'id:wt-1', - true, - false, - false, - 'local' - ) + await dispatchRm(runtime, params) + expect(runtime.removeManagedWorktree).toHaveBeenCalledWith('id:wt-1', expected) }) it('resolves the host before forwarding an unqualified removal', async () => { const runtime = makeRuntime() - const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) - - await dispatcher.dispatch({ - id: 'req-1', - authToken: 'tok', - method: 'worktree.rm', - params: { worktree: 'id:wt-1', force: true, runHooks: false } - } satisfies RpcRequest) + await dispatchRm(runtime, { force: true, runHooks: false }) expect(runtime.showManagedWorktree).toHaveBeenCalledWith('id:wt-1') expect(runtime.removeManagedWorktree).toHaveBeenCalledWith( 'id:wt-1', - true, - false, - false, - 'ssh:builder' + forwarded({ force: true, hostId: 'ssh:builder' }) ) }) }) diff --git a/src/main/runtime/rpc/methods/worktree.ts b/src/main/runtime/rpc/methods/worktree.ts index be8a0983036..3d7c407254c 100644 --- a/src/main/runtime/rpc/methods/worktree.ts +++ b/src/main/runtime/rpc/methods/worktree.ts @@ -235,13 +235,13 @@ export const WORKTREE_METHODS = [ } } } - const removalArgs = [ - params.worktree, - params.force === true, - params.runHooks === true, - params.allowUnverifiedPtyStop === true - ] as const - const result = await runtime.removeManagedWorktree(...removalArgs, resolvedHostId) + const result = await runtime.removeManagedWorktree(params.worktree, { + force: params.force === true, + runHooks: params.runHooks === true, + allowUnverifiedPtyStop: params.allowUnverifiedPtyStop === true, + allowFailedArchiveHook: params.allowFailedArchiveHook === true, + ...(resolvedHostId ? { hostId: resolvedHostId } : {}) + }) return { removed: true, ...result } } }), 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/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 63b053f45df..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 @@ -47,6 +50,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) } }) @@ -54,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 @@ -66,7 +70,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-registered-local-worktree-removal.ts b/src/main/runtime/runtime-registered-local-worktree-removal.ts index d5df7e52132..8f34b1df393 100644 --- a/src/main/runtime/runtime-registered-local-worktree-removal.ts +++ b/src/main/runtime/runtime-registered-local-worktree-removal.ts @@ -1,5 +1,7 @@ import type { GitPushTarget, GitWorktreeInfo } from '../../shared/worktree/types' import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' +import type { ArchiveHookOverride } from '../../shared/worktree/archive-hook-removal-gate' +import { gateWorktreeRemovalOnArchiveHook } from '../worktree-archive-hook-gate' import type { Repo } from '../../shared/repo-types' import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options' @@ -40,6 +42,8 @@ export async function removeRuntimeRegisteredLocalWorktree(args: { hasLocalOptions: boolean force: boolean runHooks: boolean + /** Explicit waiver for a FAILED archive hook. Never implied by `force` — see #19334. */ + allowFailedArchiveHook: boolean allowUnverifiedPtyStop: boolean deleteBranch: boolean acquireWatcherRemoval: (path: string) => Promise<{ finish: (removed: boolean) => Promise }> @@ -60,6 +64,9 @@ export async function removeRuntimeRegisteredLocalWorktree(args: { const canonicalPath = registeredWorktree.path const hooks = getEffectiveHooks(repo) let warning: string | undefined + // Precondition, not an advisory: this runs before the registration refresh, the preflights, the + // PTY stop and `removeWorktree`, so a throw here leaves every one of them untouched (#19334). + let archiveHookOverride: ArchiveHookOverride | undefined if (hooks?.scripts.archive && args.runHooks) { const result = await runHook( 'archive', @@ -68,9 +75,11 @@ export async function removeRuntimeRegisteredLocalWorktree(args: { undefined, args.hasLocalOptions ? localOptions : undefined ) - if (!result.success) { - console.error(`[hooks] archive hook failed for ${canonicalPath}:`, result.output) - } + archiveHookOverride = gateWorktreeRemovalOnArchiveHook({ + worktreePath: canonicalPath, + result, + allowFailure: args.allowFailedArchiveHook + }) } else if (hooks?.scripts.archive) { warning = `orca.yaml archive hook skipped for ${canonicalPath}; pass --run-hooks to run it.` console.warn(`[hooks] ${warning}`) @@ -151,7 +160,10 @@ export async function removeRuntimeRegisteredLocalWorktree(args: { await cleanupPushTarget(args) args.finishRemoval(undefined, false, refreshed.head) completed = true - return warning ? { warning } : {} + return { + ...(archiveHookOverride ? { archiveHookOverride } : {}), + ...(warning ? { warning } : {}) + } } else { throw new Error(formatWorktreeRemovalError(error, canonicalPath, args.force)) } @@ -162,7 +174,11 @@ export async function removeRuntimeRegisteredLocalWorktree(args: { } await cleanupPushTarget(args) args.finishRemoval(removalResult, true, refreshed.head) - return { ...removalResult, ...(warning ? { warning } : {}) } + return { + ...removalResult, + ...(archiveHookOverride ? { archiveHookOverride } : {}), + ...(warning ? { warning } : {}) + } } async function cleanupOrphanedDirectory( diff --git a/src/main/runtime/runtime-registered-remote-worktree-removal.ts b/src/main/runtime/runtime-registered-remote-worktree-removal.ts index 6eec18d52c8..b971072e979 100644 --- a/src/main/runtime/runtime-registered-remote-worktree-removal.ts +++ b/src/main/runtime/runtime-registered-remote-worktree-removal.ts @@ -5,6 +5,7 @@ import type { SshGitProvider } from '../providers/ssh-git-provider' import { cleanupUnusedWorktreePushTargetRemoteSsh } from '../ipc/worktree-remote' import type { RuntimeStore } from './runtime-store-contract' import type { RuntimeWorktreeRemovalTarget } from './runtime-worktree-selection' +import { gateRemovalWhereArchiveHookCannotRun } from '../worktree-archive-hook-gate' export async function removeRuntimeRegisteredRemoteWorktree(args: { repo: Repo @@ -15,6 +16,10 @@ export async function removeRuntimeRegisteredRemoteWorktree(args: { provider: SshGitProvider /** From the resolved removal route; `repo.connectionId!` answered null for an `ssh:`-only row. */ connectionId: string + /** #19334: this path runs no archive hook, so the gate below decides what that means. */ + runHooks: boolean + /** Explicit waiver for that refusal; without it the block has no exit on this path. */ + allowFailedArchiveHook: boolean force: boolean allowUnverifiedPtyStop: boolean deleteBranch: boolean @@ -29,8 +34,17 @@ export async function removeRuntimeRegisteredRemoteWorktree(args: { fallbackHead: string | undefined ) => RemoveWorktreeResult finishRemoval: (result: RemoveWorktreeResult) => void -}): Promise { +}): Promise { const { repo, target, registeredWorktree, provider, connectionId } = args + // Precondition, before anything is stopped or deleted: no archive hook runs here, so a removal + // that asked for one refuses rather than deleting with the archive step silently skipped. + const hookGate = await gateRemovalWhereArchiveHookCannotRun({ + repo, + connectionId, + worktreePath: registeredWorktree.path, + runHooks: args.runHooks, + allowFailedArchiveHook: args.allowFailedArchiveHook + }) const removeOptions = !args.deleteBranch ? { deleteBranch: args.deleteBranch } : {} const gate = await args.acquireWatcherRemoval(registeredWorktree.path, connectionId) let rawResult: RemoveWorktreeResult | undefined @@ -54,5 +68,9 @@ export async function removeRuntimeRegisteredRemoteWorktree(args: { ) await args.deleteHistory() args.finishRemoval(result) - return result + return { + ...result, + ...(hookGate.override ? { archiveHookOverride: hookGate.override } : {}), + ...(hookGate.warning ? { warning: hookGate.warning } : {}) + } } 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/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') + }) +}) 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/runtime-worktree-selection.test.ts b/src/main/runtime/runtime-worktree-selection.test.ts index 0509d94a2b4..2fa602fe982 100644 --- a/src/main/runtime/runtime-worktree-selection.test.ts +++ b/src/main/runtime/runtime-worktree-selection.test.ts @@ -1,5 +1,39 @@ import { describe, expect, it } from 'vitest' -import { runtimeRepoMatchesExecutionHost } from './runtime-worktree-selection' +import { + getRuntimeWorktreeRemovalOptionsKey, + runtimeRepoMatchesExecutionHost +} from './runtime-worktree-selection' + +describe('getRuntimeWorktreeRemovalOptionsKey', () => { + it('separates a waived archive-hook retry from the attempt about to refuse on it (#19334)', () => { + const strict = getRuntimeWorktreeRemovalOptionsKey({ runHooks: true }) + expect( + getRuntimeWorktreeRemovalOptionsKey({ runHooks: true, allowFailedArchiveHook: true }) + ).not.toBe(strict) + }) + + it('keeps every waiver on its own axis, so none of them coalesce', () => { + const keys = [ + {}, + { force: true }, + { runHooks: true }, + { allowUnverifiedPtyStop: true }, + { allowFailedArchiveHook: true } + ].map(getRuntimeWorktreeRemovalOptionsKey) + expect(new Set(keys).size).toBe(keys.length) + }) + + it('treats an omitted option as its off value', () => { + expect(getRuntimeWorktreeRemovalOptionsKey({})).toBe( + getRuntimeWorktreeRemovalOptionsKey({ + force: false, + runHooks: false, + allowUnverifiedPtyStop: false, + allowFailedArchiveHook: false + }) + ) + }) +}) describe('runtimeRepoMatchesExecutionHost', () => { it('matches an unstamped SSH repo against its own host (#11163)', () => { diff --git a/src/main/runtime/runtime-worktree-selection.ts b/src/main/runtime/runtime-worktree-selection.ts index 7e3fc5be481..230f2952da8 100644 --- a/src/main/runtime/runtime-worktree-selection.ts +++ b/src/main/runtime/runtime-worktree-selection.ts @@ -26,15 +26,35 @@ export function gitStatusErrorMeansNotRepository(error: unknown): boolean { return /not a git repository/i.test(`${message}\n${stderr}`) } +/** + * Options for `removeManagedWorktree`. Named rather than positional on purpose: three of the + * four are interchangeable booleans that each waive a different safety check on a destructive + * delete, so a transposition would silently delete a checkout the caller meant to protect. + */ +export type RemoveManagedWorktreeOptions = { + force?: boolean + runHooks?: boolean + /** Waives proof that every PTY stopped (#11960). Set by explicit Force Delete only. */ + allowUnverifiedPtyStop?: boolean + /** Waives a FAILED archive hook (#19334). Never implied by `force`, never by `runHooks`. */ + allowFailedArchiveHook?: boolean + hostId?: string +} + export function getRuntimeWorktreeRemovalOptionsKey( - force: boolean, - runHooks: boolean, - allowUnverifiedPtyStop: boolean + options: Pick< + RemoveManagedWorktreeOptions, + 'force' | 'runHooks' | 'allowUnverifiedPtyStop' | 'allowFailedArchiveHook' + > ): string { // Why: a forced retry must not coalesce onto the in-flight attempt that just // failed the PTY gate — it would inherit that failure instead of retrying. - const ptyKey = allowUnverifiedPtyStop ? 'allow-unverified-pty' : 'require-pty-stop' - return `${force ? 'force' : 'normal'}:${runHooks ? 'run-hooks' : 'skip-hooks'}:${ptyKey}` + const ptyKey = options.allowUnverifiedPtyStop ? 'allow-unverified-pty' : 'require-pty-stop' + // Same reason for the archive waiver: a retry that waives the failed hook must not coalesce + // onto the in-flight attempt that is about to refuse on it. + const archiveKey = options.allowFailedArchiveHook ? 'allow-failed-archive' : 'require-archive' + const hooksKey = options.runHooks ? 'run-hooks' : 'skip-hooks' + return `${options.force ? 'force' : 'normal'}:${hooksKey}:${ptyKey}:${archiveKey}` } // Null executionHostId means host-unaware: path-only callers match any repo, and the first runtime 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/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-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 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/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/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/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/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/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/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/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/window/clipboard-ipc-handlers.ts b/src/main/window/clipboard-ipc-handlers.ts index 9b2bc509e38..f6a688ea3d3 100644 --- a/src/main/window/clipboard-ipc-handlers.ts +++ b/src/main/window/clipboard-ipc-handlers.ts @@ -179,7 +179,15 @@ export function registerClipboardHandlers(store: Store): void { ) ipcMain.handle('clipboard:writeText', async (event, text: string) => { assertTrustedClipboardTextSender(event) - return clipboard.writeText(await assertClipboardTextWriteWithinLimitWithYield(text)) + const safeText = await assertClipboardTextWriteWithinLimitWithYield(text) + try { + clipboard.writeText(safeText) + } catch (error) { + // Native failures can name paths or platform state, so they stay here; the renderer + // only renders a vetted reason (describeClipboardWriteFailure). + console.error('[clipboard] writeText failed', error) + throw error + } }) ipcMain.handle('clipboard:writeTerminalText', async (event, text: string) => { assertTrustedClipboardTextSender(event) 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-archive-hook-cannot-run.test.ts b/src/main/worktree-archive-hook-cannot-run.test.ts new file mode 100644 index 00000000000..9900f684c53 --- /dev/null +++ b/src/main/worktree-archive-hook-cannot-run.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from 'vitest' +import type { Repo } from '../shared/repo-types' +import { gateRemovalWhereArchiveHookCannotRun } from './worktree-archive-hook-gate' +import { + ARCHIVE_HOOK_FAILED_REMOVAL_CODE, + asArchiveHookRefusal +} from '../shared/worktree/archive-hook-removal-gate' + +// Mocked at the SSH-aware reader, because that is the whole point: on an SSH worktree the hook +// lives on the execution host, not on the runtime's local disk. +const { getArchiveHooksForRemovalMock } = vi.hoisted(() => ({ + getArchiveHooksForRemovalMock: vi.fn() +})) +vi.mock('./ipc/worktrees/removal/worktree-archive-hook', () => ({ + getArchiveHooksForRemoval: getArchiveHooksForRemovalMock +})) + +const REPO: Repo = { id: 'r', path: '/repo', displayName: 'r', badgeColor: '#000', addedAt: 0 } + +const withArchiveHook = (present: boolean): void => { + getArchiveHooksForRemovalMock.mockResolvedValue( + present ? { scripts: { archive: 'archive.sh' } } : null + ) +} + +const gate = (over: Partial[0]> = {}) => + gateRemovalWhereArchiveHookCannotRun({ + repo: REPO, + connectionId: undefined, + worktreePath: '/w/f', + runHooks: true, + allowFailedArchiveHook: false, + ...over + }) + +// Why (#19334 / S1): the runtime's SSH path runs no archive hook. Silently deleting there would +// reproduce the reported bug in the one place `worktree.archive-failure-blocking.v1` promises it +// cannot happen, so the capability would be advertising a guarantee it does not keep. +describe('gateRemovalWhereArchiveHookCannotRun', () => { + it('lets a repo with no archive hook through untouched', async () => { + withArchiveHook(false) + await expect(gate()).resolves.toEqual({}) + }) + + it('warns rather than refuses when hooks were not requested', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + withArchiveHook(true) + await expect(gate({ runHooks: false })).resolves.toMatchObject({ + warning: expect.stringContaining('pass --run-hooks to run it') + }) + }) + + // Why (#19334): reading locally would miss the committed hook on an SSH host entirely. + it('asks the execution host whether a hook exists, not the local disk', async () => { + withArchiveHook(false) + await gate({ connectionId: 'ssh-target' }) + expect(getArchiveHooksForRemovalMock).toHaveBeenCalledWith(REPO, 'ssh-target') + }) + + it('refuses a hooks-requested removal it cannot honour, as unverifiable', async () => { + withArchiveHook(true) + const refusal = asArchiveHookRefusal(await gate().catch((error: unknown) => error)) + + expect(refusal.code).toBe(ARCHIVE_HOOK_FAILED_REMOVAL_CODE) + // Never `exited`: nothing ran, so nothing reported an exit to read. + expect(refusal.data).toMatchObject({ worktreePath: '/w/f', outcome: 'unverifiable' }) + expect(refusal.data.exitCode).toBeUndefined() + }) + + // Why this matters: without it the refusal is a dead loop. The desktop's "Delete Anyway" and the + // CLI's --allow-failed-archive-hook both land here, and a block with no reachable exit on the + // surface where it happens is the failure mode this PR fixed on the desktop path. + it('deletes anyway when the refusal is explicitly waived, and records it', async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + withArchiveHook(true) + + const result = await gate({ allowFailedArchiveHook: true }) + + expect(result.warning).toBeUndefined() + expect(result.override).toMatchObject({ + worktreePath: '/w/f', + outcome: 'unverifiable', + overridden: true + }) + }) +}) diff --git a/src/main/worktree-archive-hook-gate.ts b/src/main/worktree-archive-hook-gate.ts new file mode 100644 index 00000000000..1c50635372b --- /dev/null +++ b/src/main/worktree-archive-hook-gate.ts @@ -0,0 +1,86 @@ +import type { Repo } from '../shared/repo-types' +import { getArchiveHooksForRemoval } from './ipc/worktrees/removal/worktree-archive-hook' +import { + WorktreeArchiveHookFailedError, + formatArchiveHookOverride, + type ArchiveHookFailure, + classifyArchiveHookFailure, + formatArchiveHookFailure, + type ArchiveHookOverride, + type ArchiveHookRunResult +} from '../shared/worktree/archive-hook-removal-gate' + +/** + * The archive-hook precondition for a destructive worktree removal (#19334). Call it while the + * checkout, its registration, its agents and its ownership evidence are all still intact: on a + * failure it throws, and no caller may stop a PTY, deregister, or delete before it has returned. + * + * Returns the override record when the failure was explicitly waived, `undefined` on success. + */ +export function gateWorktreeRemovalOnArchiveHook(args: { + worktreePath: string + result: ArchiveHookRunResult + allowFailure: boolean +}): ArchiveHookOverride | undefined { + if (args.result.success) { + return undefined + } + const failure = classifyArchiveHookFailure(args.worktreePath, args.result) + if (!args.allowFailure) { + console.error(`[hooks] ${formatArchiveHookFailure(failure)}`) + throw new WorktreeArchiveHookFailedError(failure) + } + console.warn( + `[hooks] archive hook failure overridden for ${args.worktreePath}; deleting anyway:`, + args.result.output + ) + return { ...failure, overridden: true } +} + +/** + * The runtime's SSH removal path cannot run an archive hook at all (see #18563, which adds it). + * Until it can, a removal that asked for hooks has to refuse rather than delete: deleting would + * repeat exactly the bug this gate exists to stop, and reporting success would make + * `worktree.archive-failure-blocking.v1` a lie in the one case the reporter asked it to cover. + * + * Modelled as `unverifiable` because that is what it is — the hook's outcome was never observed — + * so it reuses the same typed error, the same `--allow-failed-archive-hook` waiver, and the same + * desktop "Delete Anyway" affordance as any other unobserved hook. Waiving it records the same + * `archiveHookOverride` the other paths return, so a caller is told what it accepted. + * + * Returns the skipped-hook warning when hooks were not requested, matching the local path. + * + * Hooks are read through `getArchiveHooksForRemoval` rather than `getEffectiveHooks`: on an + * SSH-hosted worktree `repo.path` names a path on the EXECUTION host, so a local read would miss + * the committed `orca.yaml` this gate exists for, and could refuse on a coincidental local one. + */ +export async function gateRemovalWhereArchiveHookCannotRun(args: { + repo: Repo + /** The removal route's owner; `repo.connectionId` is null for an `ssh:`-only row. */ + connectionId: string | undefined + worktreePath: string + runHooks: boolean + /** Explicit waiver. Without it the refusal below has no exit on this path. */ + allowFailedArchiveHook: boolean +}): Promise<{ warning?: string; override?: ArchiveHookOverride }> { + const hooks = await getArchiveHooksForRemoval(args.repo, args.connectionId) + if (!hooks?.scripts.archive) { + return {} + } + if (!args.runHooks) { + const warning = `orca.yaml archive hook skipped for ${args.worktreePath}; pass --run-hooks to run it.` + console.warn(`[hooks] ${warning}`) + return { warning } + } + const failure: ArchiveHookFailure = { + worktreePath: args.worktreePath, + outcome: 'unverifiable', + output: + 'This host cannot run an archive hook for an SSH-hosted worktree, so the hook never ran. Remove it from the desktop app, which does run it, or delete anyway to accept that nothing was archived.' + } + if (!args.allowFailedArchiveHook) { + throw new WorktreeArchiveHookFailedError(failure) + } + console.warn(`[hooks] ${formatArchiveHookOverride({ ...failure, overridden: true })}`) + return { override: { ...failure, overridden: true } } +} 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/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/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/preload/api/worktree-api.ts b/src/preload/api/worktree-api.ts index d665d278580..115ee66d9a2 100644 --- a/src/preload/api/worktree-api.ts +++ b/src/preload/api/worktree-api.ts @@ -98,6 +98,9 @@ export type WorktreeApi = { // may waive the proof that every PTY stopped. allowUnverifiedPtyStop?: boolean skipArchive?: boolean + // Why (#19334): distinct from `skipArchive` (never runs the hook) and never implied by + // `force` — this waives a hook that ran and FAILED. + allowFailedArchiveHook?: boolean snapshotPruneBatchId?: string }) => Promise // Forget a workspace from Orca only (no remote Git/FS work) — for workspaces pinned to a removed/disconnected SSH host. 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/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/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/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/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/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. } 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/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/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/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/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/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/IpynbCellEditor.tsx b/src/renderer/src/components/editor/IpynbCellEditor.tsx index 77f319f034c..3ec18d46701 100644 --- a/src/renderer/src/components/editor/IpynbCellEditor.tsx +++ b/src/renderer/src/components/editor/IpynbCellEditor.tsx @@ -1,9 +1,10 @@ -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react' +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import Editor, { type OnMount } from '@monaco-editor/react' import Markdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' import remarkGfm from 'remark-gfm' +import { cn } from '@/lib/utils' import { monaco } from '@/lib/monaco-setup' import { computeEditorFontSize, resolveEditorFontFamily } from '@/lib/editor-font-zoom' import { resolveDocumentTheme } from '@/lib/document-theme' @@ -14,11 +15,27 @@ import type { IpynbCell } from './ipynb-parse' import MonacoCodeExcerpt from './MonacoCodeExcerpt' export function IpynbMarkdownCell({ source }: { source: string }): React.JSX.Element { + const settings = useAppStore((s) => s.settings) + const theme = settings?.theme ?? 'system' + const [systemDark, setSystemDark] = useState(() => resolveDocumentTheme('system')) + useEffect(() => { + if (theme !== 'system' || typeof window.matchMedia !== 'function') { + return + } + const media = window.matchMedia('(prefers-color-scheme: dark)') + const onChange = () => setSystemDark(media.matches) + onChange() + media.addEventListener('change', onChange) + return () => media.removeEventListener('change', onChange) + }, [theme]) + const isDark = theme === 'system' ? systemDark : resolveDocumentTheme(theme) return ( -
- - {source || '\u00a0'} - +
+
+ + {source || '\u00a0'} + +
) } diff --git a/src/renderer/src/components/editor/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/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-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/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/tiptap-marked-facade.ts b/src/renderer/src/components/editor/tiptap-marked-facade.ts index e34f6755566..80cadb343f9 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': @@ -73,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/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/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/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index 108a188c4ab..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( () => ({ @@ -256,10 +309,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 @@ -305,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-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/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/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) +} 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/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..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 @@ -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) => { @@ -163,6 +160,11 @@ export function useNativeChatTranscriptWindow({ } } }) + // 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) && + (instance.scrollDirection !== 'backward' || !instance.itemSizeCache.has(item.key)) const finishReaderTakeover = useCallback(() => { if (readerTakeoverFrameRef.current !== null) { 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/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/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/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/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/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx new file mode 100644 index 00000000000..d3490443815 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx @@ -0,0 +1,104 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR } from '../../../../../../shared/clipboard-text' +import type { DiffComment } from '../../../../../../shared/diff-comment-types' + +const mocks = vi.hoisted(() => ({ + toastError: vi.fn<(title: string, options: { description?: string }) => void>(), + writeClipboardText: vi.fn() +})) + +vi.mock('sonner', () => ({ toast: { error: mocks.toastError, message: vi.fn() } })) +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: Record) => unknown) => selector({}) +})) +vi.mock('@/store/worktree-diff-comments-selector', () => ({ + selectWorktreeDiffCommentsOrEmpty: () => [ + { + id: 'c1', + worktreeId: 'wt-1', + filePath: 'src/app.ts', + lineNumber: 1, + body: 'rename this', + createdAt: 1, + side: 'modified' + } satisfies DiffComment + ] +})) + +import { useSourceControlDiffCommentNotes } from './use-diff-comment-notes' + +function renderNotes() { + return renderHook(() => + useSourceControlDiffCommentNotes({ + activeWorktreeId: 'wt-1', + clearDiffComments: async () => true, + clearDiffCommentsForFile: async () => true + }) + ) +} + +describe('diff-comment notes copy failures', () => { + beforeEach(() => { + vi.clearAllMocks() + Object.assign(window, { api: { ui: { writeClipboardText: mocks.writeClipboardText } } }) + }) + + function readErrorToast(): [string, { description?: string }] { + expect(mocks.toastError).toHaveBeenCalledTimes(1) + const firstCall = mocks.toastError.mock.calls[0] + if (!firstCall) { + throw new Error('Expected an error toast') + } + return firstCall + } + + it('never shows "Copied" for a clipboard write that rejected', async () => { + mocks.writeClipboardText.mockRejectedValue( + new Error( + "Error invoking remote method 'ui:writeClipboardText': Error: NSPasteboard failed at /Users/someone/Library/Caches/orca" + ) + ) + const { result } = renderNotes() + + await act(async () => { + await result.current.handleCopyDiffComments() + }) + + expect(result.current.diffCommentsCopied).toBe(false) + const [title, options] = readErrorToast() + expect(title).toBe('Failed to copy notes') + // An unrecognized native failure must not reach the toast (CWE-209). + expect(options.description).toBeUndefined() + }) + + it('describes only the recognized size failure', async () => { + mocks.writeClipboardText.mockRejectedValue( + new Error( + `Error invoking remote method 'ui:writeClipboardText': Error: ${CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR}` + ) + ) + const { result } = renderNotes() + + await act(async () => { + await result.current.handleCopyDiffComments() + }) + + expect(result.current.diffCommentsCopied).toBe(false) + expect(readErrorToast()[1].description).toBe('The text is too large to copy.') + }) + + it('stays silent when the write resolves', async () => { + mocks.writeClipboardText.mockResolvedValue(undefined) + const { result } = renderNotes() + + await act(async () => { + await result.current.handleCopyDiffComments() + }) + + expect(result.current.diffCommentsCopied).toBe(true) + expect(mocks.toastError).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts index 53c8d58bdc1..d002080c83a 100644 --- a/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts +++ b/src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.ts @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { formatDiffComments } from '@/lib/diff-comments-format' +import { describeClipboardWriteFailure } from '@/lib/clipboard-write-failure' import { useAppStore } from '@/store' import { selectWorktreeDiffCommentsOrEmpty } from '@/store/worktree-diff-comments-selector' import { @@ -61,8 +62,16 @@ export function useSourceControlDiffCommentNotes({ try { await window.api.ui.writeClipboardText(diffCommentsPrompt) showDiffCommentsCopied(true) - } catch { - // Why: swallow — clipboard write can fail when unfocused; best-effort copy needs no error surface. + } catch (error) { + // Why report: the write can reject (untrusted sender, 16MiB size guard) and silence here + // reads as a successful copy — the user finds out on paste. + toast.error( + translate( + 'auto.components.right.sidebar.SourceControl.diffCommentNotesCopyFailed', + 'Failed to copy notes' + ), + { description: describeClipboardWriteFailure(error) } + ) } }, [diffCommentsForActive, diffCommentsPrompt, showDiffCommentsCopied]) diff --git a/src/renderer/src/components/settings/DevToolsPane.tsx b/src/renderer/src/components/settings/DevToolsPane.tsx index 5084dbfb4de..7042e67758a 100644 --- a/src/renderer/src/components/settings/DevToolsPane.tsx +++ b/src/renderer/src/components/settings/DevToolsPane.tsx @@ -102,6 +102,13 @@ function showDeleteFailureToast(): void { ), canForceDelete: true, forceDeleteReason: 'dirty', + onDeleteAnyway: () => + toast.error( + translate( + 'auto.components.settings.DevToolsPane.deleteAnywayClicked', + 'Delete Anyway clicked' + ) + ), onViewChanges: () => toast.message( translate( 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/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/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..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,12 +306,16 @@ 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') { 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/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/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/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/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/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts index e9d60cabace..9579ea2ba2c 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts @@ -290,7 +290,25 @@ describe('agent completion coordinator', () => { expect(dispatchCompletion).toHaveBeenCalledExactlyOnceWith('done') }) - it('resets exit confirmation across an unavailable inspection', async () => { + it.each([ + { + label: 'client-only uncertainty', + result: { + foregroundProcess: null, + hasChildProcesses: false, + verdict: 'unverifiable', + reason: 'transport_loss' + } satisfies RuntimeTerminalProcessInspection + }, + { + label: 'host child-process uncertainty', + result: { + foregroundProcess: '/bin/zsh', + hasChildProcesses: false, + childProcessEvidence: 'unverifiable' + } satisfies RuntimeTerminalProcessInspection + } + ])('resets exit confirmation across $label', async ({ result: unavailableResult }) => { let result: RuntimeTerminalProcessInspection = processResult('codex') const dispatchCompletion = vi.fn() const coordinator = createAgentCompletionCoordinator({ @@ -306,12 +324,7 @@ describe('agent completion coordinator', () => { await vi.advanceTimersByTimeAsync(2_000) result = processResult(null, false) await vi.advanceTimersByTimeAsync(750) - result = { - foregroundProcess: null, - hasChildProcesses: false, - verdict: 'unverifiable', - reason: 'transport_loss' - } + result = unavailableResult await vi.advanceTimersByTimeAsync(750) result = processResult(null, false) await vi.advanceTimersByTimeAsync(1_500) diff --git a/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts b/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts index f27960361bb..b1a74972311 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-inspection-result.ts @@ -53,13 +53,16 @@ export function handleAgentCompletionInspectionResult(args: { dispatchCompletion, remoteInspection } = args - if (isClientOnlyUnverifiableInspection(result)) { + const remote = options.isRemotePtyId?.(options.getPtyId() ?? '') === true + if ( + isClientOnlyUnverifiableInspection(result) || + (!remote && result.childProcessEvidence === 'unverifiable') + ) { state.pendingProcessExitAgent = null state.consecutiveInspectionErrors += 1 scheduleNextPoll() return false } - const remote = options.isRemotePtyId?.(options.getPtyId() ?? '') === true if (remote) { const evidence = result.foregroundProcessEvidence // Remote identity is host-authoritative. Compatibility names and unverifiable observations diff --git a/src/renderer/src/components/terminal-pane/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/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-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/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/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-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/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/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/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 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/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/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/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/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/locales/en.json b/src/renderer/src/i18n/locales/en.json index ebca3398176..2a0fe5755d1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -977,6 +977,9 @@ "preview": { "pairedOutsideWorktree": "Files outside the workspace can't be previewed on a paired server yet." } + }, + "clipboardWriteFailure": { + "tooLarge": "The text is too large to copy." } }, "hooks": { @@ -5799,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" + } } } }, @@ -10950,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" @@ -11622,7 +11631,12 @@ "label": "Microphone", "description": "Input device used for voice dictation. System default follows the OS microphone setting.", "accessHint": "Allow microphone access to list input devices.", - "allowAccess": "Allow access" + "allowAccess": "Allow access", + "noMicrophoneFound": "No microphone was found. Connect one, then try again.", + "openFailedDetail": "Could not open the microphone. {{value0}}", + "openFailed": "Could not open the microphone.", + "openedSystemSettings": "Opened macOS Privacy & Security. Grant microphone access, then try again.", + "permissionDenied": "Microphone access is blocked. Grant it in your system settings, then try again." }, "TerminalTccAttributionNotice": { "body": "The terminal daemon was started by an Orca install that no longer exists, so macOS can’t attribute its commands to Orca — Accessibility and Automation grants are silently ignored (osascript fails with error -25211). Restarting the daemon fixes this; running terminal sessions will close.", @@ -12385,7 +12399,8 @@ "entryUnstageFailed": "Failed to unstage “{{value0}}”", "entryDiscardFailed": "Failed to discard “{{value0}}”", "entryDeleteFailed": "Failed to delete “{{value0}}”", - "entryFailedInWorkspace": "{{value0}} in {{value1}}" + "entryFailedInWorkspace": "{{value0}} in {{value1}}", + "diffCommentNotesCopyFailed": "Failed to copy notes" }, "SourceControlAgentActionDialog": { "8e856842d1": "Could not start the selected agent.", @@ -17242,6 +17257,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/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/clipboard-write-failure.ts b/src/renderer/src/lib/clipboard-write-failure.ts new file mode 100644 index 00000000000..5cd6451a33e --- /dev/null +++ b/src/renderer/src/lib/clipboard-write-failure.ts @@ -0,0 +1,14 @@ +import { isClipboardTextWriteTooLargeError } from '../../../shared/clipboard-text' +import { translate } from '@/i18n/i18n' + +/** + * Closed set of clipboard-write reasons safe to show. A rejected write can carry native + * clipboard or platform detail, so an unrecognized reason gets no description at all and + * the raw error stays in the main-process log. + */ +export function describeClipboardWriteFailure(error: unknown): string | undefined { + if (isClipboardTextWriteTooLargeError(error)) { + return translate('auto.lib.clipboardWriteFailure.tooLarge', 'The text is too large to copy.') + } + return undefined +} 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/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/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/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-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/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/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/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/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/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/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/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/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/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-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/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/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() 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/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/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/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/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/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-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/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}` } } 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/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/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/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/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/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/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/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/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/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') } 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/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/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/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/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 { 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 = { 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/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/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/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/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}`) + }) +}) 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/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/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') } 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++) {