From f107499e4423ff9d9a0bc203dad3ca56c41ce7f8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:24:30 -0700 Subject: [PATCH] fix(lint): enable anti-slop/no-reflect-get (#20786) `anti-slop/no-reflect-get` rejects every call to `Reflect.get`. The reflective read bypasses ordinary property access and throws away the type evidence the compiler would otherwise give you: the result is `any`/`unknown` with no narrowing, so a typo in the key or a shape drift in the source object is invisible until runtime. The rule's remedy is to parse dynamic input into a named domain type (or narrow it with `in`) and then read the field normally. Baseline: 86 violations across 67 files. Now zero unsuppressed violations under `npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile`. Fix pattern ----------- 44 of the 86 were rewritten. The dominant shape was an `unknown` value read through `Reflect.get` right after a `typeof === 'object'` guard; those became `in`-narrowed property access, which TypeScript checks: - Reflect.get(value, 'agents') + 'agents' in value ? value.agents : null Two further shapes: - `Reflect.get(Object(x), 'k')` on a possibly-primitive envelope became a small named reader that boxes once and indexes a `Record` (`settingsField` in mobile/src/transport/settings-read-operations.ts). - Tests reaching into private state moved to TypeScript's checked bracket-index escape hatch (`runtime['layoutQueues']`), or to a documented read-only accessor on the owning class (`SearchSubprocessLineAccumulator.retainedCapacityBytes()`, `CodexSubagentExecutions.retentionSizes()`). No type assertion was added anywhere: the diff contains zero net-new `as` casts, `as any`, `as unknown as`, `@ts-ignore`, or `@ts-expect-error`, so nothing was laundered into the sibling assertion rules. Suppressions ------------ 42x `// oxlint-disable-next-line anti-slop/no-reflect-get` across 38 files. Every one is the default-forward branch of a `Proxy` `get` trap: get(target, property, receiver) { ... return Reflect.get(target, property, receiver) } `Reflect.get(target, property, receiver)` is the only construct that forwards with correct `receiver` semantics; `target[property]` invokes an accessor with the wrong `this` and silently breaks getters that read sibling state. There is no typed alternative, so these are suppressed rather than rewritten. 3x `// oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface` in tests/e2e/github-url-smart-input-transition.spec.ts, tests/e2e/linear-url-workspace-entry.spec.ts, and tests/e2e/worktree-active-delete-scroll-position.spec.ts. Replacing `Reflect.get(window, 'x')` with typed `window.x` requires a `declare global { interface Window }` block, and `interface` is mandatory for declaration merging. Matches the existing convention at tests/e2e/helpers/runtime-types.ts:63. 1x `// eslint-disable-next-line no-var -- main-process gate handle for this spec` in tests/e2e/project-group-creation-visibility.spec.ts, for the same reason a `var` global is needed to type the handle. Matches tests/e2e/agent-session-log-tail-stability.spec.ts:24. Also updates two source-text anchors in mobile's rpc-recording mutation harness (mobile/src/test-support/rpc-recording/operation-mutations.ts and recording-runner.test.ts), which pin the exact text of the rewritten line in settings-read-operations.ts and would otherwise fail with "Mutant anchor matched 0 sites, expected 1". --- config/oxlint-anti-slop.json | 2 +- .../mutants/operation-mutations.ts | 4 +- .../rpc-recording/recording-runner.test.ts | 2 +- .../src/transport/settings-read-operations.ts | 10 ++++- .../managed-hook-detection-commands.ts | 8 ++-- ...session-document-stream-boundaries.test.ts | 2 +- src/main/codex/codex-prompt-registry.ts | 7 +--- .../codex/codex-subagent-executions.test.ts | 8 ++-- src/main/codex/codex-subagent-executions.ts | 5 +++ src/main/daemon/daemon-client-rpc-request.ts | 7 +++- ...ructured-agent-session-close-retry.test.ts | 1 + ...issing-worktree-terminal-reconciliation.ts | 1 + .../mobile-subscribe-integration.test.ts | 40 ++++++++++++------- .../terminal-listing.spec.ts | 3 +- .../mailbox-pointer-stage.test.ts | 6 ++- .../runtime/remote-desktop-driver.test.ts | 20 +++++----- .../runtime/runtime-linear-command-surface.ts | 1 + ...erminal-orphan-topology-validation.test.ts | 1 + .../structured-agent-session-runtime.test.ts | 4 +- .../skill-bundle-install-service.test.ts | 1 + .../skill-cloud-grant-installation.test.ts | 1 + ...pload-session-admission-regression.test.ts | 9 ++--- src/main/updater-test-harness.ts | 3 +- src/main/workspace-space-repo-scan.test.ts | 1 + src/main/worktree-name-retirement.ts | 7 +++- src/relay/managed-hook-installer.ts | 4 +- ...handler-inventory-process-evidence.test.ts | 1 + src/relay/pty-source-credit-ledger.test.ts | 1 + ...ard-snapshot-orchestration-routing.test.ts | 1 + .../use-agent-row-conversation-name.test.ts | 1 + .../components/editor/tiptap-marked-facade.ts | 1 + ...ssue-attribute-filter-primary-team.test.ts | 1 + .../ProjectCombobox.dialog-handoff.test.tsx | 3 +- .../active-checks-status.test.ts | 1 + ...rent-pr-checks-projection-selector.test.ts | 5 ++- .../parent-pr-checks-projection-selector.ts | 3 +- ...worktree-agent-orchestration-batch.test.ts | 3 ++ ...worktree-agent-orchestration-index.test.ts | 1 + .../terminal-tab-activity-status.test.ts | 1 + ...-watcher-synchronization.react185.test.tsx | 1 + ...minal-provider-snapshot-capability.test.ts | 1 + .../useIpcEvents-rate-limit-hydration.test.ts | 4 +- .../hooks/useIpcEvents-updater-status.test.ts | 7 ++-- .../hooks/useIpcEvents-zoom-routing.test.ts | 8 ++-- .../src/lib/codex-pane-selection-lane.test.ts | 3 +- .../pane-manager/terminal-ligatures-addon.ts | 1 + ...ession-write-subscriber-allocation.test.ts | 1 + .../store/project-host-setup-selector.test.ts | 4 +- src/renderer/src/store/selectors.test.ts | 1 + .../slices/tab-group-reference-repair.test.ts | 1 + ...ted-workspace-reconciliation-batch.test.ts | 1 + .../slices/terminal-tab-owner-index.test.ts | 1 + .../slices/terminal-tab-title-batch.test.ts | 1 + ...ace-cleanup-enrichment-performance.test.ts | 1 + .../src/web/preload-api/web-fallback-api.ts | 1 + .../web/web-preload-api-composition.test.ts | 3 +- .../web/web-preload-api-runtime-calls.test.ts | 2 +- src/shared/automation-list-scope.test.ts | 1 + .../host-balanced-listing-scaling.test.ts | 1 + src/shared/pr-bot-author-overrides.test.ts | 1 + src/shared/search-subprocess-lines.test.ts | 4 +- src/shared/search-subprocess-lines.ts | 5 +++ .../host-terminal-runtime-stub.ts | 1 + .../versioned-agent-session-wire.ts | 4 +- .../github-url-smart-input-transition.spec.ts | 36 +++++++++++++---- tests/e2e/linear-url-workspace-entry.spec.ts | 14 +++++-- .../project-group-creation-visibility.spec.ts | 7 +++- ...tree-active-delete-scroll-position.spec.ts | 10 ++++- 68 files changed, 211 insertions(+), 96 deletions(-) diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index 9e5eda11ae0..bf41f269552 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -33,7 +33,7 @@ "anti-slop/no-object-parameters": "off", "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", - "anti-slop/no-reflect-get": "off", + "anti-slop/no-reflect-get": "error", "anti-slop/no-runtime-typeof": "off", "anti-slop/no-shape-in-symbol-names": "off", "anti-slop/no-unknown-parameters": "off", diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index 457bae3c2a8..6a4c488b296 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -36,8 +36,8 @@ export const OPERATION_MUTATIONS = { // Reads the overrides one level above the settings envelope. 'bot-overrides-envelope': { file: 'settings-read-operations.ts', - before: "settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')", - after: "raw == null ? undefined : Reflect.get(Object(raw), 'prBotAuthorOverrides')" + before: "settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')", + after: "raw == null ? undefined : settingsField(raw, 'prBotAuthorOverrides')" }, // Publishes the settings envelope instead of the accepted operation value. 'workspace-context-envelope': { diff --git a/mobile/src/test-support/rpc-recording/recording-runner.test.ts b/mobile/src/test-support/rpc-recording/recording-runner.test.ts index 0a92941e602..7b523be49ff 100644 --- a/mobile/src/test-support/rpc-recording/recording-runner.test.ts +++ b/mobile/src/test-support/rpc-recording/recording-runner.test.ts @@ -445,7 +445,7 @@ describe('recording boundaries', () => { const root = mkdtempSync(join(tmpdir(), 'rpc-mutant-')) try { const anchor = - "const overrides = settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides')" + "const overrides = settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides')" mkdirSync(join(root, 'mod'), { recursive: true }) writeFileSync( join(root, 'mod/settings-read-operations.ts'), diff --git a/mobile/src/transport/settings-read-operations.ts b/mobile/src/transport/settings-read-operations.ts index 549b512c87c..d33e66458d2 100644 --- a/mobile/src/transport/settings-read-operations.ts +++ b/mobile/src/transport/settings-read-operations.ts @@ -7,6 +7,12 @@ function settingsMember(raw: unknown): unknown { return boxed!.settings } +// Box primitives so a non-object settings value reads as absent instead of throwing. +function settingsField(settings: unknown, key: string): unknown { + const boxed: Record = Object(settings) + return boxed[key] +} + // Settings remain opaque: callers historically retain fields without validating their shapes. const settingsReader: RpcCompatibleReader = (raw) => ({ compatible: true, @@ -27,7 +33,7 @@ const optionalSettingsReader: RpcCompatibleReader = (raw) => { const settings = raw == null ? undefined : settingsMember(raw) const overrides: unknown = - settings == null ? undefined : Reflect.get(Object(settings), 'prBotAuthorOverrides') + settings == null ? undefined : settingsField(settings, 'prBotAuthorOverrides') return { compatible: true, variant: 'bot-logins', @@ -85,7 +91,7 @@ export const newTabSettingsRead = bindDeferredRpcOperation( const copyTrimsGutterReader: RpcCompatibleReader = (raw) => { const settings = raw == null ? undefined : settingsMember(raw) const trims: unknown = - settings == null ? undefined : Reflect.get(Object(settings), 'terminalCopyTrimsGutter') + settings == null ? undefined : settingsField(settings, 'terminalCopyTrimsGutter') return { compatible: true, variant: 'copy-trims-gutter', diff --git a/src/main/agent-hooks/managed-hook-detection-commands.ts b/src/main/agent-hooks/managed-hook-detection-commands.ts index b5183af7ec0..f9507160e27 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.ts @@ -53,10 +53,12 @@ export function readManagedHookDetectionResult(value: unknown): { if (value === null || typeof value !== 'object') { return { agents: [], claudeVersion: null } } - const agents = detectedManagedHookAgents(Reflect.get(value, 'agents')) - const versions = Reflect.get(value, 'versions') + const agents = detectedManagedHookAgents('agents' in value ? value.agents : null) + const versions = 'versions' in value ? value.versions : null const rawClaudeVersion = - versions !== null && typeof versions === 'object' ? Reflect.get(versions, 'claude') : null + versions !== null && typeof versions === 'object' && 'claude' in versions + ? versions.claude + : null return { agents, claudeVersion: parseClaudeCliVersion( diff --git a/src/main/ai-vault/session-document-stream-boundaries.test.ts b/src/main/ai-vault/session-document-stream-boundaries.test.ts index 67901707cbd..f6dfe349000 100644 --- a/src/main/ai-vault/session-document-stream-boundaries.test.ts +++ b/src/main/ai-vault/session-document-stream-boundaries.test.ts @@ -88,7 +88,7 @@ describe('independent JSON boundary review', () => { expect(await parseHermesSessionDocument(file, bytes(content, 1), 'linux', options)).toEqual( await parseHermesSessionContent(file, content, 'linux', options) ) - expect(Reflect.get({}, 'polluted')).toBeUndefined() + expect('polluted' in {}).toBe(false) }) for (const content of [ '{"messages":[],}', diff --git a/src/main/codex/codex-prompt-registry.ts b/src/main/codex/codex-prompt-registry.ts index c6d0d7f4bef..f3ba3fa3601 100644 --- a/src/main/codex/codex-prompt-registry.ts +++ b/src/main/codex/codex-prompt-registry.ts @@ -9,6 +9,7 @@ import { readQuestionIds, readQuestionOptionAnswers } from './codex-prompt-registry-bounds' +import { readRecord, readString as readRecordString } from './codex-item-field-readers' export const CODEX_COMMAND_APPROVAL_METHOD = 'item/commandExecution/requestApproval' export const CODEX_FILE_CHANGE_APPROVAL_METHOD = 'item/fileChange/requestApproval' @@ -36,11 +37,7 @@ export type CodexPromptClaim = { } function readString(params: unknown, key: string): string | null { - if (typeof params !== 'object' || params === null) { - return null - } - const value = Reflect.get(params, key) - return typeof value === 'string' && value.length > 0 ? value : null + return readRecordString(readRecord(params), key) } export function isCodexPromptMethod(method: string): boolean { diff --git a/src/main/codex/codex-subagent-executions.test.ts b/src/main/codex/codex-subagent-executions.test.ts index aceff7ab465..c7f7955bb23 100644 --- a/src/main/codex/codex-subagent-executions.test.ts +++ b/src/main/codex/codex-subagent-executions.test.ts @@ -13,8 +13,9 @@ describe('CodexSubagentExecutions retention and identity', () => { executions.observeTurn(id, id, 'completed') } expect(executions.workingChildren().map((child) => child.agentThreadId)).toEqual(['long-lived']) - expect(Reflect.get(executions, 'children').size).toBeLessThanOrEqual(128) - expect(Reflect.get(executions, 'settledTurns').size).toBeLessThanOrEqual(256) + const { children, settledTurns } = executions.retentionSizes() + expect(children).toBeLessThanOrEqual(128) + expect(settledTurns).toBeLessThanOrEqual(256) }) it('retains early live owner events at capacity and makes room only after settlement', () => { @@ -45,7 +46,6 @@ describe('CodexSubagentExecutions retention and identity', () => { executions.observeTurn('child', 'turn', 'failed') expect(executions.workingChildren()[0]?.execution?.turnId).toBe('new-turn') executions.clear() - expect(Reflect.get(executions, 'children').size).toBe(0) - expect(Reflect.get(executions, 'settledTurns').size).toBe(0) + expect(executions.retentionSizes()).toEqual({ children: 0, settledTurns: 0 }) }) }) diff --git a/src/main/codex/codex-subagent-executions.ts b/src/main/codex/codex-subagent-executions.ts index cd33b4eb1d5..d5ac62bfa74 100644 --- a/src/main/codex/codex-subagent-executions.ts +++ b/src/main/codex/codex-subagent-executions.ts @@ -106,6 +106,11 @@ export class CodexSubagentExecutions { this.settledTurns.clear() } + /** Retention bounds are not observable through the child/turn API, so expose the two counts. */ + retentionSizes(): { children: number; settledTurns: number } { + return { children: this.children.size, settledTurns: this.settledTurns.size } + } + private child(agentThreadId: string): CodexExecutionChild | undefined { const existing = this.children.get(agentThreadId) if (existing) { diff --git a/src/main/daemon/daemon-client-rpc-request.ts b/src/main/daemon/daemon-client-rpc-request.ts index fb72538eaa0..2bd4f6741de 100644 --- a/src/main/daemon/daemon-client-rpc-request.ts +++ b/src/main/daemon/daemon-client-rpc-request.ts @@ -59,8 +59,11 @@ export function requestDaemonRpc(opts: DaemonRpcRequestOptions): Promise { const createTimeoutError = (): DaemonRequestTimeoutError => new DaemonRequestTimeoutError(`Request ${type} timed out after ${opts.timeoutMs}ms`) const createSessionId = - type === 'createOrAttach' && payload !== null && typeof payload === 'object' - ? Reflect.get(payload, 'sessionId') + type === 'createOrAttach' && + payload !== null && + typeof payload === 'object' && + 'sessionId' in payload + ? payload.sessionId : null const requestPayload = type === 'createOrAttach' && payload !== null && typeof payload === 'object' diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts index b79091c7f9f..1141f821174 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-close-retry.test.ts @@ -91,6 +91,7 @@ function flakyClose(journal: AgentSessionJournal, failures: number): AgentSessio return new Proxy(journal, { get(target, property, receiver) { if (property !== 'close') { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } return async () => { diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts index 11f888a5404..c8d5e06900c 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -24,6 +24,7 @@ function withSharedProcessSnapshot(provider: IPtyProvider): IPtyProvider { // receiver, a provider whose own method called `this.listProcesses()` // would silently read this sweep's cached snapshot instead of the live // host — the batching must not leak past the calls it was built for. + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const member: unknown = Reflect.get(target, property) return typeof member === 'function' ? member.bind(target) : member } diff --git a/src/main/runtime/mobile-subscribe-integration.test.ts b/src/main/runtime/mobile-subscribe-integration.test.ts index 61e064681c1..b3be748d5f0 100644 --- a/src/main/runtime/mobile-subscribe-integration.test.ts +++ b/src/main/runtime/mobile-subscribe-integration.test.ts @@ -87,8 +87,24 @@ const store = { } } +/** Reclaim clears protected retention maps that no public reader exposes. */ +class ObservableRuntime extends OrcaRuntimeService { + get restoreTimers(): typeof this.pendingRestoreTimers { + return this.pendingRestoreTimers + } + get softLeavers(): typeof this.pendingSoftLeavers { + return this.pendingSoftLeavers + } + get fitOverrides(): typeof this.terminalFitOverrides { + return this.terminalFitOverrides + } + get drivers(): typeof this.terminalDrivers { + return this.terminalDrivers + } +} + function createRuntime() { - const runtime = new OrcaRuntimeService(store) + const runtime = new ObservableRuntime(store) const ptySizes = new Map() ptySizes.set('pty-1', { cols: 150, rows: 40 }) ptySizes.set('pty-2', { cols: 120, rows: 35 }) @@ -865,9 +881,9 @@ describe('mobile subscribe integration', () => { runtime.handleMobileUnsubscribe('pty-1', 'client-a') await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 }) - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map + const pendingRestore = runtime.restoreTimers pendingRestore.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-b' }) - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingSoft = runtime.softLeavers expect(pendingSoft.has('pty-1')).toBe(true) await runtime.reclaimTerminalForDesktop('pty-1') expect(pendingRestore.has('pty-1')).toBe(false) @@ -878,10 +894,10 @@ describe('mobile subscribe integration', () => { const { runtime } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) runtime.handleMobileUnsubscribe('pty-1', 'client-a') - ;(Reflect.get(runtime, 'terminalFitOverrides') as Map).delete('pty-1') + runtime.fitOverrides.delete('pty-1') - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingRestore = runtime.restoreTimers + const pendingSoft = runtime.softLeavers await runtime.reclaimTerminalForDesktop('pty-1') expect(pendingRestore.has('pty-1')).toBe(false) expect(pendingSoft.has('pty-1')).toBe(false) @@ -891,15 +907,11 @@ describe('mobile subscribe integration', () => { const { runtime } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) runtime.handleMobileUnsubscribe('pty-1', 'client-a') - ;(Reflect.get(runtime, 'terminalFitOverrides') as Map).delete('pty-1') - ;( - Reflect.get(runtime, 'terminalDrivers') as { - set: (ptyId: string, driver: { kind: 'idle' }) => void - } - ).set('pty-1', { kind: 'idle' }) + runtime.fitOverrides.delete('pty-1') + runtime.drivers.set('pty-1', { kind: 'idle' }) - const pendingRestore = Reflect.get(runtime, 'pendingRestoreTimers') as Map - const pendingSoft = Reflect.get(runtime, 'pendingSoftLeavers') as Map + const pendingRestore = runtime.restoreTimers + const pendingSoft = runtime.softLeavers expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false) expect(pendingRestore.has('pty-1')).toBe(false) expect(pendingSoft.has('pty-1')).toBe(false) diff --git a/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts index 9d87b49f904..fae08b482fb 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-listing.spec.ts @@ -436,10 +436,11 @@ describe('OrcaRuntimeService', () => { throw new Error('onPtyData should use the PTY leaf index') } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, prop, target) return typeof value === 'function' ? value.bind(target) : value } - }) as Map + }) runtime.onPtyData(`pty-${targetIndex}`, 'hello indexed\n', 123) diff --git a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts index ed02d7e71ec..4f26d57555b 100644 --- a/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts +++ b/src/main/runtime/orchestration/mailbox-pointer-stage.test.ts @@ -99,10 +99,11 @@ describe('mailbox pointer staging watermark', () => { throw new Error('SQLITE_BUSY') } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } - }) as OrchestrationDb + }) const state = new OrchestrationMailboxPointerState() const args = stageArgs(db, state) @@ -178,10 +179,11 @@ describe('mailbox pointer staging watermark', () => { stealNextClaim = false return () => false } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(target, prop, receiver) return typeof value === 'function' ? value.bind(target) : value } - }) as OrchestrationDb + }) const writePty = vi.fn(() => WRITE_ACCEPTED) const delivery = new OrchestrationMailboxPointerDelivery({ diff --git a/src/main/runtime/remote-desktop-driver.test.ts b/src/main/runtime/remote-desktop-driver.test.ts index bf2370ee924..bb25c20a94a 100644 --- a/src/main/runtime/remote-desktop-driver.test.ts +++ b/src/main/runtime/remote-desktop-driver.test.ts @@ -340,17 +340,18 @@ describe('remote desktop viewer width driver', () => { const { runtime } = createRuntime() await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 100, 30) await runtime.updateRemoteDesktopViewer('pty-1', 'sub-B', 'viewer-B', 80, 24, false) - const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map< - string, - { running: Promise; pending: { target: { ownerSubscriptionKey?: string } }[] } - > - layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) + const layoutQueues = runtime['layoutQueues'] + layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) void runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 90, 28) void runtime.claimRemoteDesktopViewer('pty-1', 'sub-B') expect( - layoutQueues.get('pty-1')?.pending.map(({ target }) => target.ownerSubscriptionKey) + layoutQueues + .get('pty-1') + ?.pending.map(({ target }) => + 'ownerSubscriptionKey' in target ? target.ownerSubscriptionKey : undefined + ) ).toEqual(['sub-A', 'sub-B']) layoutQueues.delete('pty-1') }) @@ -358,11 +359,8 @@ describe('remote desktop viewer width driver', () => { it('makes a host claim join a pending disconnect reclaim', async () => { const { runtime } = createRuntime() await runtime.updateRemoteDesktopViewer('pty-1', 'sub-A', 'viewer-A', 80, 24) - const layoutQueues = Reflect.get(runtime, 'layoutQueues') as Map< - string, - { running: Promise; pending: { waiters: unknown[] }[] } - > - layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) + const layoutQueues = runtime['layoutQueues'] + layoutQueues.set('pty-1', { running: new Promise(() => {}), pending: [] }) void runtime.unregisterRemoteDesktopViewer('pty-1', 'sub-A') void runtime.claimRemoteDesktopHost('pty-1', 150, 40) diff --git a/src/main/runtime/runtime-linear-command-surface.ts b/src/main/runtime/runtime-linear-command-surface.ts index 0f234768203..cf9ad9e69df 100644 --- a/src/main/runtime/runtime-linear-command-surface.ts +++ b/src/main/runtime/runtime-linear-command-surface.ts @@ -47,6 +47,7 @@ function overrideAwareReceiver( return override.bind(facade) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. return Reflect.get(target, property, proxyReceiver) } }) diff --git a/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts b/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts index cb8f22e864b..9fff4da0edf 100644 --- a/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts +++ b/src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts @@ -35,6 +35,7 @@ it('validates large restored MRU lists with linear tab-order reads', () => { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, key, receiver) } }) diff --git a/src/main/runtime/structured-agent-session-runtime.test.ts b/src/main/runtime/structured-agent-session-runtime.test.ts index 2ce51b1c29b..29ee4740414 100644 --- a/src/main/runtime/structured-agent-session-runtime.test.ts +++ b/src/main/runtime/structured-agent-session-runtime.test.ts @@ -5,7 +5,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { agentSessionJournalCloseRetries } from '../native-chat/agent-session-journal/journal-close-retry' import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-store-test-open' -import type { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store' import type { AgentSessionClaimStatus, AgentSessionExecutionLocation, @@ -346,6 +345,7 @@ describe('a teardown that fails is retried by the next stop', () => { const flaky = new Proxy(real, { get(target, property, receiver) { if (property !== 'close') { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } return async () => { @@ -356,7 +356,7 @@ describe('a teardown that fails is retried by the next stop', () => { await target.close() } } - }) as AgentSessionJournal + }) await agentSessionJournalCloseRetries.closeOrRetain(flaky) // The host's teardown runs the registry retry, so this stop surfaces it. diff --git a/src/main/skills/skill-bundle-install-service.test.ts b/src/main/skills/skill-bundle-install-service.test.ts index 78c8eae0652..f5de4faf7eb 100644 --- a/src/main/skills/skill-bundle-install-service.test.ts +++ b/src/main/skills/skill-bundle-install-service.test.ts @@ -104,6 +104,7 @@ describe('skill bundle installation', () => { } } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, target) as unknown return typeof value === 'function' ? value.bind(target) : value } diff --git a/src/main/skills/skill-cloud-grant-installation.test.ts b/src/main/skills/skill-cloud-grant-installation.test.ts index 5355e6f4869..8534ce7a176 100644 --- a/src/main/skills/skill-cloud-grant-installation.test.ts +++ b/src/main/skills/skill-cloud-grant-installation.test.ts @@ -195,6 +195,7 @@ it.each(['skill-install-cancelled', 'skill-install-filesystem-failed'])( if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/main/skills/skill-upload-session-admission-regression.test.ts b/src/main/skills/skill-upload-session-admission-regression.test.ts index f0c7cd54405..9bbdb71ef68 100644 --- a/src/main/skills/skill-upload-session-admission-regression.test.ts +++ b/src/main/skills/skill-upload-session-admission-regression.test.ts @@ -4,6 +4,7 @@ import type * as NodeFsPromises from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' +import type { SkillUploadRetainedPaths } from './skill-upload-retained-paths' import { SkillUploadSessionService } from './skill-upload-session-service' const roots: string[] = [] @@ -30,10 +31,6 @@ vi.mock('node:fs/promises', async (importOriginal) => { } }) -type RetainedPathCleanup = { - removeFailedCleanup(path: string): Promise -} - afterEach(async () => { vi.useRealTimers() openGate.release = null @@ -51,8 +48,8 @@ function identity(bytes: Buffer) { } } -function retainedPathCleanup(service: SkillUploadSessionService): RetainedPathCleanup { - return Reflect.get(service, 'retainedPaths') as RetainedPathCleanup +function retainedPathCleanup(service: SkillUploadSessionService): SkillUploadRetainedPaths { + return service['retainedPaths'] } async function stagedArchiveCount(uploads: string): Promise { diff --git a/src/main/updater-test-harness.ts b/src/main/updater-test-harness.ts index 36687d0a79e..83379b7ac9b 100644 --- a/src/main/updater-test-harness.ts +++ b/src/main/updater-test-harness.ts @@ -146,6 +146,7 @@ export function createUpdaterMocks(): UpdaterMocks { const loadedGeneration = currentGeneration return new Proxy(autoUpdaterMock, { get(target, property) { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. const value = Reflect.get(target, property) if (loadedGeneration === currentGeneration || typeof value !== 'function') { return value @@ -155,7 +156,7 @@ export function createUpdaterMocks(): UpdaterMocks { set(target, property, value) { return loadedGeneration === currentGeneration ? Reflect.set(target, property, value) : true } - }) as AutoUpdaterMock + }) } const reset = () => { diff --git a/src/main/workspace-space-repo-scan.test.ts b/src/main/workspace-space-repo-scan.test.ts index fbf570f12c1..5447ca93550 100644 --- a/src/main/workspace-space-repo-scan.test.ts +++ b/src/main/workspace-space-repo-scan.test.ts @@ -15,6 +15,7 @@ describe('summarizeWorkspaceSpaceRows', () => { ) { reads[property] += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/main/worktree-name-retirement.ts b/src/main/worktree-name-retirement.ts index 58ffc99bbfd..99a6fdd559c 100644 --- a/src/main/worktree-name-retirement.ts +++ b/src/main/worktree-name-retirement.ts @@ -75,7 +75,12 @@ export function normalizeRetirableGeneratedName(name: string): string | null { /** A sparse create error carries this marker only when its rollback also failed, leaving the path * occupied even though creation rejected. */ export function failedWorktreeCreationNeedsRetirement(error: unknown): boolean { - return typeof error === 'object' && error !== null && Reflect.get(error, 'cleanupFailed') === true + return ( + typeof error === 'object' && + error !== null && + 'cleanupFailed' in error && + error.cleanupFailed === true + ) } async function getRetirementProbePath( diff --git a/src/relay/managed-hook-installer.ts b/src/relay/managed-hook-installer.ts index bdd65a789f6..3fa57dd5ed8 100644 --- a/src/relay/managed-hook-installer.ts +++ b/src/relay/managed-hook-installer.ts @@ -45,7 +45,9 @@ function readAgents(params: unknown): AgentHookTarget[] { function readClaudeVersion(params: unknown): string | undefined { const raw = - params !== null && typeof params === 'object' ? Reflect.get(params, 'claudeVersion') : null + params !== null && typeof params === 'object' && 'claudeVersion' in params + ? params.claudeVersion + : null return parseClaudeCliVersion(typeof raw === 'string' ? raw : null) ?? undefined } diff --git a/src/relay/pty-handler-inventory-process-evidence.test.ts b/src/relay/pty-handler-inventory-process-evidence.test.ts index c12e6da62b4..f0d5b068304 100644 --- a/src/relay/pty-handler-inventory-process-evidence.test.ts +++ b/src/relay/pty-handler-inventory-process-evidence.test.ts @@ -100,6 +100,7 @@ function countingRows(rows: ProcessTableRow[]): { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/relay/pty-source-credit-ledger.test.ts b/src/relay/pty-source-credit-ledger.test.ts index f4ff366c946..57c00d1adec 100644 --- a/src/relay/pty-source-credit-ledger.test.ts +++ b/src/relay/pty-source-credit-ledger.test.ts @@ -104,6 +104,7 @@ describe('RelayPtySourceCreditLedger', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { indexedReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts index 424c936ab0f..97697f5f8a4 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts @@ -109,6 +109,7 @@ describe('buildDashboardSnapshot orchestration routing', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts b/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts index dbf5cae455b..9065cc64e34 100644 --- a/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts +++ b/src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts @@ -113,6 +113,7 @@ describe('useAgentRowConversationName', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { tabReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/components/editor/tiptap-marked-facade.ts b/src/renderer/src/components/editor/tiptap-marked-facade.ts index 823217be6ef..80cadb343f9 100644 --- a/src/renderer/src/components/editor/tiptap-marked-facade.ts +++ b/src/renderer/src/components/editor/tiptap-marked-facade.ts @@ -74,6 +74,7 @@ export function createTiptapMarkedFacade(): typeof marked { return facade } default: + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts index 5a4bfe471a2..9adb06f6586 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts @@ -55,6 +55,7 @@ it('selects a primary team without pairwise membership checks or sorting all tea if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, key, receiver) } } diff --git a/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx b/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx index 339fd60840f..f77c1a34928 100644 --- a/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx +++ b/src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx @@ -68,7 +68,8 @@ beforeEach(() => { ? element.getAttribute('data-state') === 'closed' ? 'exit' : 'enter' - : Reflect.get(target, property) + : // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. + Reflect.get(target, property) }) } return style diff --git a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts index 5bf09276c72..84b65d83f5b 100644 --- a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts +++ b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts @@ -182,6 +182,7 @@ describe('getActiveChecksStatus caching', () => { { get(target, prop, receiver) { reads.add(prop) + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, prop, receiver) }, has(target, prop) { diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts index 0616bb510e9..7d7e1b1e4ad 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts @@ -61,9 +61,10 @@ describe('parent PR checks projection selector', () => { const observedCache = new Proxy( {}, { - get: (target, property, receiver) => { + get: (target, property) => { cacheRead(property) - return Reflect.get(target, property, receiver) + const entries: Record = target + return entries[property] } } ) diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts index 02677575e48..42ad55de0a8 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.ts @@ -25,6 +25,7 @@ function trackCacheReads( ): ReviewCacheState[K] { return new Proxy(state[cacheName], { get: (target, property, receiver) => { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, receiver) if (typeof property === 'string') { dependencies.push({ cacheName, key: property, value }) @@ -41,7 +42,7 @@ function dependenciesAreCurrent( ): boolean { return dependencies.every( ({ cacheName, key, value }) => - state[cacheName] === previousState[cacheName] || Reflect.get(state[cacheName], key) === value + state[cacheName] === previousState[cacheName] || state[cacheName][key] === value ) } diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts index 705a1e0c43e..3d5a5e14c8c 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts @@ -364,6 +364,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) @@ -456,6 +457,7 @@ describe('selectRuntimeAgentOrchestrationBatch', () => { if (typeof key === 'string' && Object.hasOwn(target, key)) { runtimeValueReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) @@ -636,6 +638,7 @@ describe('selectRuntimeAgentOrchestrationBatch live-map churn', () => { if (typeof key === 'string') { reads.push(key) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(source, key, receiver) } }) diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts index 2d9ba917520..d97a7d41906 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts @@ -312,6 +312,7 @@ describe('selectWorktreeAgentOrchestration', () => { if (typeof key === 'string') { onRead() } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(source, key, receiver) } }) diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts index 8511fff1913..fc626b99132 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts @@ -356,6 +356,7 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { ownKeys, get: (target, property, receiver) => { valueReads += 1 + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx index 1e385354c21..bb2f44320cd 100644 --- a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx @@ -218,6 +218,7 @@ describe('parked terminal watcher synchronization', () => { if (typeof property === 'string') { harness.reconciliationPtyReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts b/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts index 8825f0489c5..e1985aa2555 100644 --- a/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts +++ b/src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts @@ -79,6 +79,7 @@ describe('terminal provider snapshot capabilities', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { indexedReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts b/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts index a95d1095d5e..f1116ad3001 100644 --- a/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts @@ -118,8 +118,8 @@ describe('useIpcEvents rate-limit hydration', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } diff --git a/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts b/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts index 1ff5554f59c..002e071a19d 100644 --- a/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-updater-status.test.ts @@ -281,10 +281,11 @@ describe('useIpcEvents updater integration', () => { })) vi.doMock('@/lib/zoom-events', () => ({ dispatchZoomLevelChanged: vi.fn() })) - const makeEvents = (target: Record = {}): Record => + const makeEvents = ( + target: Record = {} + ): Record => new Proxy(target, { - get: (namespace, prop) => - prop in namespace ? Reflect.get(namespace, prop) : () => () => {} + get: (namespace, prop) => (prop in namespace ? namespace[prop] : () => () => {}) }) vi.stubGlobal('window', { diff --git a/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts b/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts index dc954e451a6..5e20aa0994b 100644 --- a/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-zoom-routing.test.ts @@ -180,8 +180,8 @@ describe('useIpcEvents zoom routing', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } @@ -326,8 +326,8 @@ describe('useIpcEvents zoom routing', () => { const makeEvents = (target: Record = {}): Record => new Proxy(target, { get: (namespace, prop) => { - if (prop in namespace) { - return Reflect.get(namespace, prop) + if (typeof prop === 'string' && prop in namespace) { + return namespace[prop] } return () => () => {} } diff --git a/src/renderer/src/lib/codex-pane-selection-lane.test.ts b/src/renderer/src/lib/codex-pane-selection-lane.test.ts index a4b1dcefc56..c6729d291ae 100644 --- a/src/renderer/src/lib/codex-pane-selection-lane.test.ts +++ b/src/renderer/src/lib/codex-pane-selection-lane.test.ts @@ -367,9 +367,10 @@ describe('resolveCodexPaneSelectionLane', () => { if (property === 'worktreesByRepo') { throw new Error('state read blew up') } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: raw string|symbol pass-through; the receiver stays the target on purpose. return Reflect.get(target, property) } - }) as LaneState + }) // Why: this call sits outside the scan's per-pane failure guard, so a throw // would lose the notice for every pane in the batch, not just this one. expect( diff --git a/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts b/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts index 256f38befdc..774eb7e8fb7 100644 --- a/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts +++ b/src/renderer/src/lib/pane-manager/terminal-ligatures-addon.ts @@ -111,6 +111,7 @@ export class TerminalLigaturesAddon extends LigaturesAddon { target.refresh(start, end) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. const value = Reflect.get(target, property, target) as unknown return typeof value === 'function' ? value.bind(target) : value } diff --git a/src/renderer/src/lib/session-write-subscriber-allocation.test.ts b/src/renderer/src/lib/session-write-subscriber-allocation.test.ts index 74c9c6db968..3f2c5ce284c 100644 --- a/src/renderer/src/lib/session-write-subscriber-allocation.test.ts +++ b/src/renderer/src/lib/session-write-subscriber-allocation.test.ts @@ -210,6 +210,7 @@ describe('session write subscriber allocation', () => { if (typeof property === 'string') { read.add(property) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/project-host-setup-selector.test.ts b/src/renderer/src/store/project-host-setup-selector.test.ts index 610e44c1feb..c6de8f65820 100644 --- a/src/renderer/src/store/project-host-setup-selector.test.ts +++ b/src/renderer/src/store/project-host-setup-selector.test.ts @@ -20,8 +20,7 @@ function countCollectionReads(items: readonly T[]): { get(array, property) { if (property === 'map' || property === 'flatMap') { counters[property] += 1 - const method = Reflect.get(array, property) as (...args: unknown[]) => unknown - return method.bind(array) + return array[property].bind(array) } if (property === Symbol.iterator) { counters.iterator += 1 @@ -30,6 +29,7 @@ function countCollectionReads(items: readonly T[]): { if (property === 'length') { counters.length += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(array, property) } }) diff --git a/src/renderer/src/store/selectors.test.ts b/src/renderer/src/store/selectors.test.ts index 45278caf40d..43491683ce3 100644 --- a/src/renderer/src/store/selectors.test.ts +++ b/src/renderer/src/store/selectors.test.ts @@ -695,6 +695,7 @@ describe('selectFloatingWorkspaceHasUnread', () => { if (typeof property === 'string') { terminalUnreadReads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } } diff --git a/src/renderer/src/store/slices/tab-group-reference-repair.test.ts b/src/renderer/src/store/slices/tab-group-reference-repair.test.ts index a7cb3dca125..e2f265ba6a6 100644 --- a/src/renderer/src/store/slices/tab-group-reference-repair.test.ts +++ b/src/renderer/src/store/slices/tab-group-reference-repair.test.ts @@ -35,6 +35,7 @@ describe('appendOwnedTabIdsToGroups', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { reads++ } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts b/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts index e6a7bfe7002..ebe87a28ab5 100644 --- a/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts +++ b/src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts @@ -136,6 +136,7 @@ describe('whole-session workspace tab-model reconciliation', () => { if (property === 'filter') { scans += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts b/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts index 0600296d1d4..09ecdf4ccb9 100644 --- a/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-owner-index.test.ts @@ -73,6 +73,7 @@ describe('terminal tab owner index', () => { if (typeof property === 'string' && property.startsWith('wt-')) { bucketVisits += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts b/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts index e269ffaae86..5fa1700357e 100644 --- a/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-title-batch.test.ts @@ -140,6 +140,7 @@ describe('terminal tab title batches', () => { if (typeof property === 'string' && property.startsWith('wt-')) { bucketVisits += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts b/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts index ed710d1af7c..5478cdd973a 100644 --- a/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts +++ b/src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts @@ -42,6 +42,7 @@ function countOpenFileScans( return target.filter(predicate) } } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/renderer/src/web/preload-api/web-fallback-api.ts b/src/renderer/src/web/preload-api/web-fallback-api.ts index 3cfef000ef1..2f59cd72615 100644 --- a/src/renderer/src/web/preload-api/web-fallback-api.ts +++ b/src/renderer/src/web/preload-api/web-fallback-api.ts @@ -4,6 +4,7 @@ export function withFallback(target: T, path: string[]): T { return new Proxy(target, { get(current, property, receiver) { if (property in current) { + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. const value = Reflect.get(current, property, receiver) as unknown if (value && typeof value === 'object' && !Array.isArray(value)) { return withFallback(value as object, [...path, String(property)]) diff --git a/src/renderer/src/web/web-preload-api-composition.test.ts b/src/renderer/src/web/web-preload-api-composition.test.ts index f7091cac9bc..8bac20f5097 100644 --- a/src/renderer/src/web/web-preload-api-composition.test.ts +++ b/src/renderer/src/web/web-preload-api-composition.test.ts @@ -78,7 +78,8 @@ describe('web preload API composition', () => { 'telemetryAcknowledgeBanner' ]) expect(Object.keys(globals.window.api.projects)).toEqual([]) - expect(Reflect.get(globals.window.api.projects, 'then')).toBeUndefined() + const projects: Record = globals.window.api.projects + expect(projects.then).toBeUndefined() }) it('snapshots E2E config before runtime storage initialization', async () => { diff --git a/src/renderer/src/web/web-preload-api-runtime-calls.test.ts b/src/renderer/src/web/web-preload-api-runtime-calls.test.ts index fcf24437595..48d366d1a72 100644 --- a/src/renderer/src/web/web-preload-api-runtime-calls.test.ts +++ b/src/renderer/src/web/web-preload-api-runtime-calls.test.ts @@ -102,7 +102,7 @@ describe('web preload runtime calls', () => { if (!(rejection instanceof Error)) { throw new Error('Expected a domain Error rejection') } - expect(Reflect.get(rejection, 'code')).toBe('repo_unavailable') + expect('code' in rejection ? rejection.code : undefined).toBe('repo_unavailable') expect( JSON.parse(globals.storage.getItem('orca.web.runtimeEnvironment.v1') ?? '{}') ).toMatchObject({ runtimeId: 'runtime-domain-failure' }) diff --git a/src/shared/automation-list-scope.test.ts b/src/shared/automation-list-scope.test.ts index d9ff87eb99c..90915b4da06 100644 --- a/src/shared/automation-list-scope.test.ts +++ b/src/shared/automation-list-scope.test.ts @@ -210,6 +210,7 @@ describe('projectAutomationList', () => { if (property === 'map' || property === 'filter') { collectionMethodReads.push(String(property)) } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/shared/host-balanced-listing-scaling.test.ts b/src/shared/host-balanced-listing-scaling.test.ts index aa7e40c2eb6..0a0d2a093bf 100644 --- a/src/shared/host-balanced-listing-scaling.test.ts +++ b/src/shared/host-balanced-listing-scaling.test.ts @@ -22,6 +22,7 @@ it('retires exhausted host buckets from subsequent listing rounds', () => { if (typeof key === 'string' && /^\d+$/.test(key)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy `get` trap: only Reflect.get forwards a raw string|symbol key with the proxy receiver. return Reflect.get(target, key, receiver) } }) diff --git a/src/shared/pr-bot-author-overrides.test.ts b/src/shared/pr-bot-author-overrides.test.ts index 46476f0c5d1..e9e8d8a4e49 100644 --- a/src/shared/pr-bot-author-overrides.test.ts +++ b/src/shared/pr-bot-author-overrides.test.ts @@ -18,6 +18,7 @@ describe('PR bot author override normalization', () => { if (typeof property === 'string' && /^\d+$/.test(property)) { reads += 1 } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/src/shared/search-subprocess-lines.test.ts b/src/shared/search-subprocess-lines.test.ts index 34425801105..1a2c72de5fc 100644 --- a/src/shared/search-subprocess-lines.test.ts +++ b/src/shared/search-subprocess-lines.test.ts @@ -69,9 +69,9 @@ describe('SearchSubprocessLineAccumulator', () => { } expect(accepted).toBe(true) - expect(Reflect.get(parser, 'buffer')).toBeInstanceOf(Buffer) + expect(parser.retainedCapacityBytes()).toBeGreaterThanOrEqual(200_000) expect(parser.finish()).toBe('x'.repeat(200_000)) - expect(Reflect.get(parser, 'buffer')).toBeNull() + expect(parser.retainedCapacityBytes()).toBeNull() }) it('rejects invalid byte limits', () => { diff --git a/src/shared/search-subprocess-lines.ts b/src/shared/search-subprocess-lines.ts index 26f98b4d348..54e1defa5c0 100644 --- a/src/shared/search-subprocess-lines.ts +++ b/src/shared/search-subprocess-lines.ts @@ -65,6 +65,11 @@ export class SearchSubprocessLineAccumulator { this.bytes = 0 } + /** Capacity of the retained growable buffer, or null once it has been released. */ + retainedCapacityBytes(): number | null { + return this.buffer?.length ?? null + } + private append(segment: Buffer): void { const requiredBytes = this.bytes + segment.length if (!this.buffer || this.buffer.length < requiredBytes) { diff --git a/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts b/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts index f353ec3a097..db7bddcf2cf 100644 --- a/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts +++ b/tests/e2e/cross-version-wire/host-terminal-runtime-stub.ts @@ -198,6 +198,7 @@ export function createHostTerminalRuntimeStub( } return () => undefined } + // oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward. return Reflect.get(target, property, receiver) } }) diff --git a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts index 4d9e2de68ec..78efd420f83 100644 --- a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts +++ b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts @@ -69,10 +69,10 @@ type DispatcherModule = { function registeredMethodNames(methods: readonly unknown[]): string[] { return methods .flatMap((method) => { - if (!method || typeof method !== 'object') { + if (!method || typeof method !== 'object' || !('name' in method)) { return [] } - const name = Reflect.get(method, 'name') + const { name } = method return typeof name === 'string' ? [name] : [] }) .sort() diff --git a/tests/e2e/github-url-smart-input-transition.spec.ts b/tests/e2e/github-url-smart-input-transition.spec.ts index 3e2b0198812..f7e988a9c29 100644 --- a/tests/e2e/github-url-smart-input-transition.spec.ts +++ b/tests/e2e/github-url-smart-input-transition.spec.ts @@ -66,13 +66,24 @@ type TransitionFrame = { targetSelected: boolean } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Per-provider capture buffers written by startTransitionCapture below. + __githubUrlTransitionFrames?: TransitionFrame[] + __gitlabUrlTransitionFrames?: TransitionFrame[] + } +} + +type TransitionFrameKey = '__githubUrlTransitionFrames' | '__gitlabUrlTransitionFrames' + function pasteChord(): string { return process.platform === 'darwin' ? 'Meta+V' : 'Control+V' } async function startTransitionCapture( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, wrongTitle: string, targetTitle: string ): Promise { @@ -95,20 +106,29 @@ async function startTransitionCapture( requestAnimationFrame(capture) } } - Reflect.set(window, frameKey, frames) + window[frameKey] = frames capture() }, { frameKey, frameLimit: TRANSITION_FRAME_LIMIT, wrongTitle, targetTitle } ) } -async function readTransitionFrames(page: Page, frameKey: string): Promise { - return page.evaluate((key) => Reflect.get(window, key) as TransitionFrame[], frameKey) +async function readTransitionFrames( + page: Page, + frameKey: TransitionFrameKey +): Promise { + return page.evaluate((key) => { + const frames = window[key] + if (!frames) { + throw new Error(`Transition capture ${key} was never installed`) + } + return frames + }, frameKey) } async function expectLookupHeldWithoutStaleRow( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, targetUrl: string, wrongOption: Locator, targetOption: Locator @@ -125,7 +145,7 @@ async function expectLookupHeldWithoutStaleRow( async function expectExactTargetAfterLookup( page: Page, - frameKey: string, + frameKey: TransitionFrameKey, targetUrl: string, targetOption: Locator ): Promise { @@ -268,7 +288,7 @@ test('a pasted GitHub URL never selects a stale cached issue', async ({ }) await expect(wrongOption).toBeVisible() - const frameKey = '__githubUrlTransitionFrames' + const frameKey: TransitionFrameKey = '__githubUrlTransitionFrames' await startTransitionCapture(orcaPage, frameKey, WRONG_TITLE, TARGET_TITLE) await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), TARGET_URL) @@ -317,7 +337,7 @@ test('a pasted GitLab URL never selects a stale cached merge request', async ({ }) await expect(wrongOption).toBeVisible() - const frameKey = '__gitlabUrlTransitionFrames' + const frameKey: TransitionFrameKey = '__gitlabUrlTransitionFrames' await startTransitionCapture(orcaPage, frameKey, GITLAB_WRONG_TITLE, GITLAB_TARGET_TITLE) await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), GITLAB_TARGET_URL) diff --git a/tests/e2e/linear-url-workspace-entry.spec.ts b/tests/e2e/linear-url-workspace-entry.spec.ts index d1cb2677257..16491a785db 100644 --- a/tests/e2e/linear-url-workspace-entry.spec.ts +++ b/tests/e2e/linear-url-workspace-entry.spec.ts @@ -25,6 +25,14 @@ const LINEAR_ISSUE: LinearIssue = { updatedAt: '2026-08-12T00:00:00.000Z' } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Set by the fixture below while a Linear lookup is deliberately held open. + __orcaTestReleaseLinearLookup?: () => void + } +} + function pasteChord(): string { return process.platform === 'darwin' ? 'Meta+V' : 'Control+V' } @@ -86,7 +94,7 @@ async function installLinearFixture( async function releaseHeldLinearLookup(page: Page): Promise { await page.evaluate(() => { - const release = Reflect.get(window, '__orcaTestReleaseLinearLookup') + const release = window.__orcaTestReleaseLinearLookup if (typeof release !== 'function') { throw new Error('Linear lookup is not held') } @@ -131,9 +139,7 @@ test.describe('Linear URL workspace entry', () => { await pasteLinearUrl(orcaPage, input) await expect .poll(() => - orcaPage.evaluate( - () => typeof Reflect.get(window, '__orcaTestReleaseLinearLookup') === 'function' - ) + orcaPage.evaluate(() => typeof window.__orcaTestReleaseLinearLookup === 'function') ) .toBe(true) await input.press('Enter') diff --git a/tests/e2e/project-group-creation-visibility.spec.ts b/tests/e2e/project-group-creation-visibility.spec.ts index d659734570a..90deffecb1f 100644 --- a/tests/e2e/project-group-creation-visibility.spec.ts +++ b/tests/e2e/project-group-creation-visibility.spec.ts @@ -7,6 +7,11 @@ import { runProcess } from '../../src/shared/child-process/run-process' test.use({ seedTestRepo: false }) +declare global { + // Resolved by the main-process gate this spec installs around the group-create response. + var __releaseGroupCreateResponse: (() => void) | undefined +} + for (const delayCreateResponse of [false, true]) { test(`created groups survive sidebar expansion (${delayCreateResponse ? 'refresh first' : 'ordinary timing'})`, async ({ orcaPage, @@ -97,7 +102,7 @@ for (const delayCreateResponse of [false, true]) { .toBe(true) } finally { await electronApp.evaluate(() => { - const release = Reflect.get(globalThis, '__releaseGroupCreateResponse') + const release = globalThis.__releaseGroupCreateResponse if (typeof release !== 'function') { throw new Error('Group create response gate unavailable') } diff --git a/tests/e2e/worktree-active-delete-scroll-position.spec.ts b/tests/e2e/worktree-active-delete-scroll-position.spec.ts index 5e2f15f5b75..15cf6c97914 100644 --- a/tests/e2e/worktree-active-delete-scroll-position.spec.ts +++ b/tests/e2e/worktree-active-delete-scroll-position.spec.ts @@ -17,6 +17,14 @@ type RowRemovalFrame = { targetExists: boolean } +declare global { + // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface + interface Window { + // Frame sampling started in the page and awaited once the removal animation settles. + __activeDeleteRowRemovalFrames?: Promise + } +} + async function pauseForVisualProof(page: Page): Promise { if (process.env.ORCA_E2E_RECORD_VIDEO === '1') { await page.waitForTimeout(VISUAL_PROOF_PAUSE_MS) @@ -215,7 +223,7 @@ async function startRowRemovalSampling( async function finishRowRemovalSampling(page: Page): Promise { return page.evaluate(async () => { - const pending = Reflect.get(window, '__activeDeleteRowRemovalFrames') + const pending = window.__activeDeleteRowRemovalFrames if (!(pending instanceof Promise)) { throw new Error('Row removal sampling was not started') }