diff --git a/config/oxlint-anti-slop.json b/config/oxlint-anti-slop.json index bf41f269552..3a115684ba9 100644 --- a/config/oxlint-anti-slop.json +++ b/config/oxlint-anti-slop.json @@ -30,7 +30,7 @@ "anti-slop/no-conditional-empty-object-spread": "off", "anti-slop/no-known-value-widening": "off", "anti-slop/no-module-mocking": "error", - "anti-slop/no-object-parameters": "off", + "anti-slop/no-object-parameters": "error", "anti-slop/no-reduce-accumulator-copy": "error", "anti-slop/no-reflect-apply": "error", "anti-slop/no-reflect-get": "error", diff --git a/config/scripts/agent-status-hot-path-benchmark.test.ts b/config/scripts/agent-status-hot-path-benchmark.test.ts index 6c30b82ebf7..ece89b89d50 100644 --- a/config/scripts/agent-status-hot-path-benchmark.test.ts +++ b/config/scripts/agent-status-hot-path-benchmark.test.ts @@ -244,7 +244,11 @@ describe('agent-status hot path benchmark', () => { let objectAssignCalls = 0 let objectAssignPropertyCopies = 0 let freshnessEntryVisits = 0 - Object.assign = ((target: object, ...sources: object[]) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `Object.assign` is an overload set no single arrow can satisfy; this wrapper only counts calls and forwards every argument to the captured native implementation. + Object.assign = (( + target: Record, + ...sources: readonly Record[] + ) => { objectAssignCalls += 1 for (const source of sources) { if (source && typeof source === 'object') { @@ -253,7 +257,8 @@ describe('agent-status hot path benchmark', () => { } return nativeObjectAssign(target, ...sources) }) as typeof Object.assign - Object.values = ((value: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: same overload-set limit as the `Object.assign` wrapper above; this one counts visited entries and returns the native result unchanged. + Object.values = ((value: Record) => { const result = nativeObjectValues(value) freshnessEntryVisits += result.length return result diff --git a/config/scripts/happy-dom-mutation-observer-retention.ts b/config/scripts/happy-dom-mutation-observer-retention.ts index a315b3d520c..c40a070bd58 100644 --- a/config/scripts/happy-dom-mutation-observer-retention.ts +++ b/config/scripts/happy-dom-mutation-observer-retention.ts @@ -48,12 +48,12 @@ export function installHappyDomMutationObserverRetention(): boolean { const disconnect = prototype.disconnect prototype.observe = function patchedObserve( - this: object, + this: PatchableMutationObserver, target: Node, options?: MutationObserverInit ): void { const existing = new Set(readMutationListeners(target)) - observe.call(this as unknown as PatchableMutationObserver, target, options) + observe.call(this, target, options) const pinned = retainedCallbacks.get(this) ?? new Set() for (const listener of readMutationListeners(target)) { if (existing.has(listener)) { @@ -69,8 +69,8 @@ export function installHappyDomMutationObserverRetention(): boolean { } } - prototype.disconnect = function patchedDisconnect(this: object): void { - disconnect.call(this as unknown as PatchableMutationObserver) + prototype.disconnect = function patchedDisconnect(this: PatchableMutationObserver): void { + disconnect.call(this) retainedCallbacks.delete(this) } diff --git a/mobile/src/diagnostics/connection-diagnostics-screen-data.ts b/mobile/src/diagnostics/connection-diagnostics-screen-data.ts index c8a2ed589c8..3ab974b46df 100644 --- a/mobile/src/diagnostics/connection-diagnostics-screen-data.ts +++ b/mobile/src/diagnostics/connection-diagnostics-screen-data.ts @@ -2,10 +2,13 @@ import type { ConnectionLogStore } from '../transport/connection-log-buffer' import type { ConnectionLogEntry, HostProfile } from '../transport/types' import type { RpcClientContextValue } from '../transport/rpc-client-context-contract' +/** Route identity token: compared by reference to detect navigating away and back, never read. */ +export type DiagnosticsRouteKey = Record + export type DiagnosticsHostSelection = { hostId: string requestedHostId: string | undefined - routeKey?: object + routeKey?: DiagnosticsRouteKey } export type DiagnosticsSubmissionState = 'sending' | 'sent' | 'failed' @@ -29,7 +32,7 @@ export function resolveDiagnosticsHostId( hosts: readonly HostProfile[], requestedHostId: string | undefined, manualSelection: DiagnosticsHostSelection | null, - routeKey?: object + routeKey?: DiagnosticsRouteKey ): string | null { const selected = manualSelection if (selected && selected.requestedHostId === requestedHostId && selected.routeKey === routeKey) { diff --git a/mobile/src/session/mobile-terminal-viewport-resubscribe.ts b/mobile/src/session/mobile-terminal-viewport-resubscribe.ts index 8ad3c99c13e..175eb9d92b8 100644 --- a/mobile/src/session/mobile-terminal-viewport-resubscribe.ts +++ b/mobile/src/session/mobile-terminal-viewport-resubscribe.ts @@ -79,6 +79,9 @@ export function shouldResubscribeAfterViewportMeasure(args: { return args.hostCols !== args.measured.cols || args.hostRows !== args.measured.rows } +/** Reference-identity token for a resubscribe attempt; carries no data, only compared by `===`. */ +type RetryGenerationToken = Readonly> + /** Per-handle resubscribe budget, mirroring the chat-side rearm bound: attempts * refill only when the handle actually left terminal.list and came back. A * still-listed non-converging handle re-funded on every list refresh would undo @@ -87,7 +90,7 @@ export class TerminalViewportResubscribeBudget { private readonly attemptsByHandle = new Map() private readonly absentSinceExhaustion = new Set() private readonly announcedExhaustion = new Set() - private readonly retryGenerationByHandle = new Map() + private readonly retryGenerationByHandle = new Map() attempts(handle: string): number { return this.attemptsByHandle.get(handle) ?? 0 @@ -97,17 +100,17 @@ export class TerminalViewportResubscribeBudget { this.attemptsByHandle.set(handle, this.attempts(handle) + 1) } - retryGeneration(handle: string): object { + retryGeneration(handle: string): RetryGenerationToken { const existing = this.retryGenerationByHandle.get(handle) if (existing) { return existing } - const generation = {} + const generation: RetryGenerationToken = {} this.retryGenerationByHandle.set(handle, generation) return generation } - isRetryGenerationCurrent(handle: string, generation: object): boolean { + isRetryGenerationCurrent(handle: string, generation: RetryGenerationToken): boolean { return this.retryGenerationByHandle.get(handle) === generation } diff --git a/mobile/src/transport/direct-rpc-client.ts b/mobile/src/transport/direct-rpc-client.ts index 0031de90c6c..b72c49bd1a7 100644 --- a/mobile/src/transport/direct-rpc-client.ts +++ b/mobile/src/transport/direct-rpc-client.ts @@ -72,7 +72,7 @@ export class DirectRpcClient implements RpcClient { }) this.liveness = new RpcSessionLivenessWatchdog({ transport: 'direct', - sendProbe: (identity) => this.sendLivenessProbe(identity), + sendProbe: (identity) => identity === this.livenessSession && this.sendLivenessProbe(), terminate: (identity) => { if (identity === this.livenessSession && this.socketSession === this.livenessSession) { this.socketClose.forceClose(this.livenessSession) @@ -297,8 +297,8 @@ export class DirectRpcClient implements RpcClient { return false } - private sendLivenessProbe(identity: object): boolean { - if (identity !== this.livenessSession || this.getState() !== 'connected') { + private sendLivenessProbe(): boolean { + if (this.getState() !== 'connected') { return false } return this.sendEncrypted({ diff --git a/mobile/src/transport/host-client-acquisition-registry.ts b/mobile/src/transport/host-client-acquisition-registry.ts index 4e09ebdd9dc..618a4be3c0a 100644 --- a/mobile/src/transport/host-client-acquisition-registry.ts +++ b/mobile/src/transport/host-client-acquisition-registry.ts @@ -1,4 +1,5 @@ -export type HostClientAcquisition = object +/** Holder identity token: the registry only compares references, never reads fields. */ +export type HostClientAcquisition = Record export class HostClientAcquisitionRegistry { private readonly acquisitions = new Map>() diff --git a/mobile/src/transport/relay-dial-stage.ts b/mobile/src/transport/relay-dial-stage.ts index c4a743f84f4..06c6e23477c 100644 --- a/mobile/src/transport/relay-dial-stage.ts +++ b/mobile/src/transport/relay-dial-stage.ts @@ -1,3 +1,5 @@ +import type { RpcClient } from './rpc-client' + // Where a relay dial is waiting, so a bound can tell "the cell never answered the // upgrade" from "the cell took the dial and is slow" — the two look identical from // ConnectionState, which stays 'connecting' until relay-hello arrives. @@ -17,12 +19,21 @@ export type RelayDialStageSource = { onDialStageChange(listener: (stage: RelayDialStage) => void): () => void } -export function relayDialStageSource(session: object): RelayDialStageSource | null { - const candidate = session as Partial - return typeof candidate.getDialStage === 'function' && - typeof candidate.onDialStageChange === 'function' - ? (candidate as RelayDialStageSource) - : null +/** An RPC client that may also report relay dial stages; only relay sessions do. */ +export type MaybeRelayDialStageSource = RpcClient & Partial + +function reportsDialStages( + session: MaybeRelayDialStageSource +): session is MaybeRelayDialStageSource & RelayDialStageSource { + return ( + typeof session.getDialStage === 'function' && typeof session.onDialStageChange === 'function' + ) +} + +export function relayDialStageSource( + session: MaybeRelayDialStageSource +): RelayDialStageSource | null { + return reportsDialStages(session) ? session : null } export class RelayDialStageTracker implements RelayDialStageSource { diff --git a/mobile/src/transport/rpc-session-liveness-watchdog.ts b/mobile/src/transport/rpc-session-liveness-watchdog.ts index 36525f60fb0..cbe891f810c 100644 --- a/mobile/src/transport/rpc-session-liveness-watchdog.ts +++ b/mobile/src/transport/rpc-session-liveness-watchdog.ts @@ -2,7 +2,10 @@ export const LIVENESS_IDLE_MS = 20_000 export const LIVENESS_PROBE_TIMEOUT_MS = 8_000 export const MISSED_PROBE_LIMIT = 3 -export type RpcSessionIdentity = object +declare const rpcSessionIdentityBrand: unique symbol + +/** Opaque per-session token; only ever compared by reference. */ +export type RpcSessionIdentity = object & { readonly [rpcSessionIdentityBrand]?: never } type WatchdogOptions = { transport: 'direct' | 'relay' diff --git a/src/main/ai-vault-search/session-search-clock.ts b/src/main/ai-vault-search/session-search-clock.ts index c9eaf609a34..3c973b273e6 100644 --- a/src/main/ai-vault-search/session-search-clock.ts +++ b/src/main/ai-vault-search/session-search-clock.ts @@ -2,8 +2,8 @@ // makes is "within one reconcile interval", and a guarantee stated in wall time // is only a claim until a test can advance the clock and watch it hold. -/** Opaque to the indexer; a fake clock hands back whatever it likes. */ -export type SessionSearchTimerHandle = object | number +/** Opaque to the indexer: the real clock hands back a timer, a fake clock an id. */ +export type SessionSearchTimerHandle = NodeJS.Timeout | number export type SessionSearchClock = { now(): number @@ -20,5 +20,5 @@ export const systemSessionSearchClock: SessionSearchClock = { timer.unref?.() return timer }, - clearTimeout: (handle) => clearTimeout(handle as NodeJS.Timeout) + clearTimeout: (handle) => clearTimeout(handle) } diff --git a/src/main/artifacts/artifact-cloud-recovery.test.ts b/src/main/artifacts/artifact-cloud-recovery.test.ts index fea3b73bda0..04eebbf25a8 100644 --- a/src/main/artifacts/artifact-cloud-recovery.test.ts +++ b/src/main/artifacts/artifact-cloud-recovery.test.ts @@ -207,7 +207,10 @@ class ArtifactFaultServer { rejectNextDeleteCode: string | null = null rejectNextUpdateStatus: number | null = null private readonly artifacts = new Map() - private readonly createsByKey = new Map() + private readonly createsByKey = new Map< + string, + { body: string; response: ArtifactResponseBody } + >() artifactSlugs(): string[] { return [...this.artifacts.keys()].sort() @@ -325,14 +328,17 @@ async function publishedLink(userDataPath: string): Promise { return result.status === 'ok' ? (result.value?.shareUrl ?? null) : null } -function jsonResponse(body: object, status: number): Response { +/** JSON payload the fake artifact API serialises for a response. */ +type ArtifactResponseBody = Record + +function jsonResponse(body: ArtifactResponseBody, status: number): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }) } -function createResponseBody(slug: string): object { +function createResponseBody(slug: string): ArtifactResponseBody { return { artifact: { version: 1, diff --git a/src/main/browser/agent-browser-bridge-test-harness.ts b/src/main/browser/agent-browser-bridge-test-harness.ts index 0e614600174..7aa38364a3c 100644 --- a/src/main/browser/agent-browser-bridge-test-harness.ts +++ b/src/main/browser/agent-browser-bridge-test-harness.ts @@ -1,4 +1,5 @@ import { vi, type Mock } from 'vitest' +import type { AgentBrowserBridge } from './agent-browser-bridge' import type { BrowserManager } from './browser-manager' export type ExecFileCallback = (error: unknown, stdout?: string, stderr?: string) => void @@ -95,15 +96,19 @@ export function mockWebContents( // Why: the bridge resolves webContents via dynamic require('electron').webContents.fromId // inside a try/catch. Override the private method to inject our mock. export function overrideBridgeWebContentsLookup( - bridgePrototype: object, + bridgePrototype: AgentBrowserBridge, webContentsFromIdMock: Mock ): void { - ;(bridgePrototype as { getWebContents: (id: number) => unknown }).getWebContents = function ( - id: number - ) { - const target = webContentsFromIdMock(id) as { isDestroyed: () => boolean } | null - return target && !target.isDestroyed() ? target : null - } + // Why defineProperty: getWebContents is protected, so a typed assignment is not expressible. + Object.defineProperty(bridgePrototype, 'getWebContents', { + configurable: true, + enumerable: true, + writable: true, + value: function (id: number) { + const target = webContentsFromIdMock(id) as { isDestroyed: () => boolean } | null + return target && !target.isDestroyed() ? target : null + } + }) } export function createSucceedWith(execFileMock: Mock, stdinWrites: string[]) { diff --git a/src/main/browser/browser-cookie-import-clear.ts b/src/main/browser/browser-cookie-import-clear.ts index af79c4249ed..b1b04a98ff9 100644 --- a/src/main/browser/browser-cookie-import-clear.ts +++ b/src/main/browser/browser-cookie-import-clear.ts @@ -54,7 +54,13 @@ export type CookieClearSession = { restoreClearIdentities: CookieClearStore['restoreClearIdentities'] } -const mutationLocks = new WeakMap>() +/** + * Reference identity of one live cookie jar — the partition's Electron Session on both import + * paths. Held weakly and compared by reference; the lock never reads a field off it. + */ +export type CookieMutationLockOwner = WeakKey + +const mutationLocks = new WeakMap>() function cookieClearKey(url: string, name: string): string { return JSON.stringify([url, name]) @@ -85,7 +91,9 @@ export function identitiesFromClearCookies( * remove cookies the newer import already reported as written. Callers that need the lock across a * try/finally take it directly; callers with a single callback use the wrapper below. */ -export async function acquireCookieMutationLock(owner: object): Promise<() => void> { +export async function acquireCookieMutationLock( + owner: CookieMutationLockOwner +): Promise<() => void> { const previous = mutationLocks.get(owner) ?? Promise.resolve() let release!: () => void const current = new Promise((resolve) => { @@ -99,7 +107,10 @@ export async function acquireCookieMutationLock(owner: object): Promise<() => vo return release } -export async function withCookieMutationLock(owner: object, run: () => Promise): Promise { +export async function withCookieMutationLock( + owner: CookieMutationLockOwner, + run: () => Promise +): Promise { const release = await acquireCookieMutationLock(owner) try { return await run() diff --git a/src/main/browser/browser-cookie-import-concurrency.test.ts b/src/main/browser/browser-cookie-import-concurrency.test.ts index 776ded2f7e7..2826cc1207c 100644 --- a/src/main/browser/browser-cookie-import-concurrency.test.ts +++ b/src/main/browser/browser-cookie-import-concurrency.test.ts @@ -2,6 +2,7 @@ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -33,11 +34,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: snapshotClearIdentitiesMock, restoreClearIdentities: async () => undefined, diff --git a/src/main/browser/browser-cookie-import-google-exclusion.test.ts b/src/main/browser/browser-cookie-import-google-exclusion.test.ts index 1cc13450cff..ee73dba1ed2 100644 --- a/src/main/browser/browser-cookie-import-google-exclusion.test.ts +++ b/src/main/browser/browser-cookie-import-google-exclusion.test.ts @@ -3,6 +3,7 @@ * path. Removing 'google.com' from NON_TRANSPLANTABLE_DOMAINS flips every test here red. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -33,12 +34,12 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise set?: (details: Record) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), // Why (STA-4300): the import writes go through CDP identities; route them to the same spy so // a missing method cannot silently reroute every write down the rejected-cookie path. diff --git a/src/main/browser/browser-cookie-import-partition-fidelity.test.ts b/src/main/browser/browser-cookie-import-partition-fidelity.test.ts index d7c6401eb42..f6fe9cedf6d 100644 --- a/src/main/browser/browser-cookie-import-partition-fidelity.test.ts +++ b/src/main/browser/browser-cookie-import-partition-fidelity.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -45,11 +46,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-replacement.test.ts b/src/main/browser/browser-cookie-import-replacement.test.ts index d645bf944ce..cf7c0795010 100644 --- a/src/main/browser/browser-cookie-import-replacement.test.ts +++ b/src/main/browser/browser-cookie-import-replacement.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -36,12 +37,12 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise set?: (details: Record) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), // Why (STA-4300): the import writes go through CDP identities; route them to the same spy so // a missing method cannot silently reroute every write down the rejected-cookie path. diff --git a/src/main/browser/browser-cookie-import-route-partition-staging.test.ts b/src/main/browser/browser-cookie-import-route-partition-staging.test.ts index 106b6742e80..b56b47ca7cc 100644 --- a/src/main/browser/browser-cookie-import-route-partition-staging.test.ts +++ b/src/main/browser/browser-cookie-import-route-partition-staging.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' import type * as NodeFs from 'node:fs' const { @@ -43,11 +44,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-scope.test.ts b/src/main/browser/browser-cookie-import-scope.test.ts index 33381f915d6..e64b33d94f1 100644 --- a/src/main/browser/browser-cookie-import-scope.test.ts +++ b/src/main/browser/browser-cookie-import-scope.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -34,11 +35,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import-undecryptable.test.ts b/src/main/browser/browser-cookie-import-undecryptable.test.ts index 95714b36d9e..c17615df22f 100644 --- a/src/main/browser/browser-cookie-import-undecryptable.test.ts +++ b/src/main/browser/browser-cookie-import-undecryptable.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeCrypto from 'node:crypto' import type * as NodeFs from 'node:fs' +import type { CookiesGetFilter } from 'electron' const { appGetPathMock, @@ -44,11 +45,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/browser-cookie-import.test.ts b/src/main/browser/browser-cookie-import.test.ts index 52662b2366d..32bd17d4018 100644 --- a/src/main/browser/browser-cookie-import.test.ts +++ b/src/main/browser/browser-cookie-import.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { CookiesGetFilter } from 'electron' import type * as NodeFs from 'node:fs' const { @@ -53,11 +54,11 @@ vi.mock('electron', () => ({ vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { - get: (filter: object) => Promise + get: (filter: CookiesGetFilter) => Promise remove: (url: string, name: string) => Promise } }) => ({ - get: (filter: object) => targetSession.cookies.get(filter), + get: (filter: CookiesGetFilter) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => items.map(({ cookie, url }) => ({ url, ...cookie })), diff --git a/src/main/browser/cdp-keyboard-us-layout.test.ts b/src/main/browser/cdp-keyboard-us-layout.test.ts index b9bcffa50ec..30cfd6264f8 100644 --- a/src/main/browser/cdp-keyboard-us-layout.test.ts +++ b/src/main/browser/cdp-keyboard-us-layout.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { imeFallbackKeyEvent, parseCdpKeyEvent } from './cdp-keyboard-us-layout' +import { imeFallbackKeyEvent, parseCdpKeyEvent, type CdpKeyEvent } from './cdp-keyboard-us-layout' describe('parseCdpKeyEvent', () => { it('maps every printable ASCII character to a key event that types that character', () => { @@ -39,7 +39,7 @@ describe('parseCdpKeyEvent', () => { ['Ctrl+Shift+K', { keyCode: 75, key: 'K', modifiers: 10, text: null }], ['Meta+r', { keyCode: 82, key: 'r', modifiers: 4, text: null }], ['Control+Shift+r', { keyCode: 82, key: 'R', modifiers: 10, text: null }] - ])('parses the shortcut %s', (raw: string, expected: object) => { + ])('parses the shortcut %s', (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject(expected) }) @@ -66,7 +66,7 @@ describe('parseCdpKeyEvent', () => { ['ContextMenu', { keyCode: 93, text: null }], ['F5', { keyCode: 116, key: 'F5', code: 'F5', text: null }], ['F12', { keyCode: 123, text: null }] - ])('parses the named key %s', (raw: string, expected: object) => { + ])('parses the named key %s', (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject(expected) }) @@ -77,7 +77,7 @@ describe('parseCdpKeyEvent', () => { ['Meta', { keyCode: 91, key: 'Meta', code: 'MetaLeft', modifiers: 4, selfModifier: 4 }] ])( 'reports the own modifier bit and left-side location for a bare %s press', - (raw: string, expected: object) => { + (raw: string, expected: Partial) => { expect(parseCdpKeyEvent(raw)).toMatchObject({ ...expected, location: 1, text: null }) } ) diff --git a/src/main/browser/doc-preview-download-block-notice.test.ts b/src/main/browser/doc-preview-download-block-notice.test.ts index c57998ea4e5..8cd45d436cc 100644 --- a/src/main/browser/doc-preview-download-block-notice.test.ts +++ b/src/main/browser/doc-preview-download-block-notice.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ publishDocPreviewFailure: vi.fn(), - boundGrantIdByGuest: new Map(), + boundGrantIdByGuest: new Map(), revocationListener: null as null | ((grant: { id: string }) => void) })) @@ -10,7 +10,8 @@ vi.mock('./doc-preview-failure-notice', () => ({ publishDocPreviewFailure: mocks.publishDocPreviewFailure })) vi.mock('./doc-preview-guest-policy', () => ({ - readDocPreviewGuestBoundGrantId: (guest: object) => mocks.boundGrantIdByGuest.get(guest) ?? null + readDocPreviewGuestBoundGrantId: (guest: Electron.WebContents) => + mocks.boundGrantIdByGuest.get(guest) ?? null })) vi.mock('./doc-preview-grant-registry', () => ({ onDocPreviewGrantRevoked: (listener: (grant: { id: string }) => void) => { diff --git a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts index d65a9344acc..e6354826366 100644 --- a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts +++ b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts @@ -263,6 +263,6 @@ function createFixture(): { } } -function writeAuth(home: string, auth: object): void { +function writeAuth(home: string, auth: Record): void { writeFileSync(join(home, 'auth.json'), JSON.stringify(auth), { mode: 0o600 }) } diff --git a/src/main/codex/codex-session-migration-scheduler.ts b/src/main/codex/codex-session-migration-scheduler.ts index d0f696031ae..879a63817db 100644 --- a/src/main/codex/codex-session-migration-scheduler.ts +++ b/src/main/codex/codex-session-migration-scheduler.ts @@ -253,12 +253,21 @@ export function createCodexSessionMigrationScheduler(args: { } } +type MigrationFailureCountKey = 'failedDirectories' | 'failedFiles' | 'failedHealAuditRecords' + +/** The run-result fields the scheduler consults; each runner returns its own summary shape. */ +type MigrationResultFields = Partial> + +function isMigrationResultFields(result: unknown): result is MigrationResultFields { + return typeof result === 'object' && result !== null +} + function isStoppedMigrationResult(result: unknown): boolean { return Boolean(result && typeof result === 'object' && 'stopped' in result && result.stopped) } function isIncompleteBackfillResult(result: unknown): boolean { - if (!result || typeof result !== 'object') { + if (!isMigrationResultFields(result)) { return true } return ( @@ -269,7 +278,10 @@ function isIncompleteBackfillResult(result: unknown): boolean { ) } -function readPositiveResultCount(result: object, key: string): boolean { - const value = key in result ? (result as Record)[key] : undefined +function readPositiveResultCount( + result: MigrationResultFields, + key: MigrationFailureCountKey +): boolean { + const value = result[key] return typeof value === 'number' && value > 0 } diff --git a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts index 820f318d6a8..4703f11300b 100644 --- a/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts +++ b/src/main/daemon/daemon-pty-adapter-history-recovery.test.ts @@ -235,12 +235,16 @@ describe('DaemonPtyAdapter history recovery', () => { ).id ) ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `checkpointSessions` and `runExclusiveCheckpoint` are `protected` on the checkpoint scheduler, so they are absent from the adapter's public type; the shape below mirrors their declarations and this suite only spies on them. const internals = historyAdapter as unknown as { checkpointSessions( sessionIds: Iterable, opts?: { final?: boolean; teardown?: boolean } ): Promise> - runExclusiveCheckpoint(operation: () => Promise, options?: object): Promise + runExclusiveCheckpoint( + operation: () => Promise, + options?: { rescheduleDirty?: boolean; callerDeadlineMs?: number } + ): Promise } const originalCheckpointSessions = internals.checkpointSessions.bind(historyAdapter) // Call-through spy: entering the exclusive gate is the observable "queued behind the in-flight checkpoint" moment. diff --git a/src/main/git/git-capability-state.test.ts b/src/main/git/git-capability-state.test.ts index b6e655efe62..845c2f47bf8 100644 --- a/src/main/git/git-capability-state.test.ts +++ b/src/main/git/git-capability-state.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshGitProvider } from '../providers/ssh-git-provider' import { clearGitCapabilityStateForTests, getLocalGitCapabilityCache, @@ -10,6 +11,9 @@ import { seedWslLinkedWorktreeGitRoutingForTests } from './wsl-linked-worktree-git-routing' +// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the cache keys providers by reference only and never calls a method on them. +const createProviderIdentity = (): SshGitProvider => ({}) as SshGitProvider + describe('Git capability execution-host state', () => { beforeEach(() => { clearGitCapabilityStateForTests() @@ -32,8 +36,8 @@ describe('Git capability execution-host state', () => { }) it('shares one SSH provider lifetime without leaking into a replacement provider', () => { - const provider = {} - const replacementProvider = {} + const provider = createProviderIdentity() + const replacementProvider = createProviderIdentity() expect(getSshGitCapabilityCache(provider)).toBe(getSshGitCapabilityCache(provider)) expect(getSshGitCapabilityCache(provider)).not.toBe( diff --git a/src/main/git/git-capability-state.ts b/src/main/git/git-capability-state.ts index 7721df722c8..d998c21ee75 100644 --- a/src/main/git/git-capability-state.ts +++ b/src/main/git/git-capability-state.ts @@ -1,4 +1,5 @@ import { GitCapabilityCache } from '../../shared/git-capability-cache' +import type { SshGitProvider } from '../providers/ssh-git-provider' import { parseWslUncPath } from '../../shared/wsl-paths' import { isWslLinkedWorktreeGitRoutingCandidate, @@ -14,7 +15,7 @@ type LocalGitCapabilityTarget = { const localCapabilitiesByExecutionHost = new Map() // Why: reconnecting creates a new provider, while concurrent IPC/runtime users // of one SSH connection must share the same remote Git capability results. -let sshCapabilitiesByProvider = new WeakMap() +let sshCapabilitiesByProvider = new WeakMap() function getLocalGitExecutionHostKey(target: LocalGitCapabilityTarget): string { const wslDistro = @@ -56,7 +57,7 @@ export function withLocalGitCapabilityCacheForExecution( ) } -export function getSshGitCapabilityCache(provider: object): GitCapabilityCache { +export function getSshGitCapabilityCache(provider: SshGitProvider): GitCapabilityCache { let cache = sshCapabilitiesByProvider.get(provider) if (!cache) { cache = new GitCapabilityCache() diff --git a/src/main/ipc/browser-preview-tool-authorization.test.ts b/src/main/ipc/browser-preview-tool-authorization.test.ts index 0b09f2e0ead..58d72ded647 100644 --- a/src/main/ipc/browser-preview-tool-authorization.test.ts +++ b/src/main/ipc/browser-preview-tool-authorization.test.ts @@ -179,6 +179,12 @@ function grantForNewDocPage(): { id: string; browserPageId: string } { return { id: grant.id, browserPageId } } +/** The fake WebContents a preview's policy installs onto; tools are matched against its identity. */ +type PreviewGuestContents = { + isDestroyed: () => boolean + getURL: () => string +} + /** A preview guest already showing its document, which is the only state a tool can act in. */ function renderPreviewForGrant( grant: { id: string; browserPageId: string }, @@ -186,7 +192,7 @@ function renderPreviewForGrant( ): { grantId: string browserPageId: string - contents: object + contents: PreviewGuestContents markContentsDestroyed: () => void } { const browserPageId = grant.browserPageId @@ -250,7 +256,7 @@ function toolArgs(channel: string, browserPageId: string): Record ({ } })) -import { registerBrowserHandlers, setAgentBrowserBridgeRef } from './browser' +import { registerBrowserHandlers, setAgentBrowserBridgeRef, type BrowserGuestArgs } from './browser' import { waitForAnyTabRegistration, waitForTabRegistration, @@ -136,9 +136,10 @@ describe('registerBrowserHandlers', () => { registerGuestMock.mockReturnValue(false) const settled = Promise.allSettled([waitForTabRegistration('page-1', 1000)]) registerBrowserHandlers() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: ipcMain.handle's mock records handlers as a loose tuple; this is the signature registerBrowserHandlers registered for this channel. const registerHandler = handleMock.mock.calls.find( ([channel]) => channel === 'browser:registerGuest' - )?.[1] as (event: { sender: Electron.WebContents }, args: object) => boolean + )?.[1] as (event: { sender: Electron.WebContents }, args: BrowserGuestArgs) => boolean const result = registerHandler( { diff --git a/src/main/ipc/browser.ts b/src/main/ipc/browser.ts index d9aa0409c08..d9841cf3bd1 100644 --- a/src/main/ipc/browser.ts +++ b/src/main/ipc/browser.ts @@ -24,7 +24,7 @@ import type { BrowserWebAuthnAccountResponse } from '../../shared/browser-webaut let agentBrowserBridgeRef: AgentBrowserBridge | null = null -type BrowserGuestRegistrationArgs = { +export type BrowserGuestArgs = { browserPageId: string workspaceId: string worktreeId: string @@ -48,7 +48,7 @@ export function registerBrowserHandlers(): void { const registerGuest = ( event: Electron.IpcMainInvokeEvent, - args: BrowserGuestRegistrationArgs, + args: BrowserGuestArgs, repairPolicies: boolean ): boolean => { if (!isTrustedBrowserRenderer(event.sender)) { @@ -96,7 +96,7 @@ export function registerBrowserHandlers(): void { return true } - ipcMain.handle('browser:registerGuest', (event, args: BrowserGuestRegistrationArgs) => + ipcMain.handle('browser:registerGuest', (event, args: BrowserGuestArgs) => registerGuest(event, args, false) ) @@ -136,7 +136,7 @@ export function registerBrowserHandlers(): void { } ) - ipcMain.handle('browser:repairGuestRegistration', (event, args: BrowserGuestRegistrationArgs) => + ipcMain.handle('browser:repairGuestRegistration', (event, args: BrowserGuestArgs) => registerGuest(event, args, true) ) diff --git a/src/main/ipc/filesystem-test-harness.ts b/src/main/ipc/filesystem-test-harness.ts index 47efa88d6cd..a3a06b95e85 100644 --- a/src/main/ipc/filesystem-test-harness.ts +++ b/src/main/ipc/filesystem-test-harness.ts @@ -207,12 +207,16 @@ export async function withPlatform( } } -function collectMocks(moduleMock: object): IpcMock[] { +function isMockContainer(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function collectMocks(moduleMock: Record): IpcMock[] { return Object.values(moduleMock).flatMap((value) => { if (vi.isMockFunction(value)) { return [value as IpcMock] } - return value && typeof value === 'object' ? collectMocks(value) : [] + return isMockContainer(value) ? collectMocks(value) : [] }) } diff --git a/src/main/ipc/runtime-watcher-process-pool.test.ts b/src/main/ipc/runtime-watcher-process-pool.test.ts index c722b5ccdd3..5b326191d03 100644 --- a/src/main/ipc/runtime-watcher-process-pool.test.ts +++ b/src/main/ipc/runtime-watcher-process-pool.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { WatcherProcessFailure } from './parcel-watcher-process-failure' +import type { WatcherProcessSubscribeOptions } from './parcel-watcher-process-protocol' import type { WatcherProcessCallback, WatcherProcessHooks, @@ -29,7 +30,7 @@ class FakeSupervisor { async subscribe( dir: string, _callback: WatcherProcessCallback, - _opts: object, + _opts: WatcherProcessSubscribeOptions, hooks: WatcherProcessHooks ): Promise { if (this.subscribeError) { diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index 31d587bd056..e2a27ad6aab 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' +import type { GlobalSettings } from '../../shared/global-settings-types' const { applyAppIconMock, @@ -840,7 +841,10 @@ describe('registerSettingsHandlers', () => { it('normalizes an agent-session-search write and hands the change to the index', async () => { const before = { aiVaultSearch: { enabled: false, historyDays: null } } store.getSettings.mockReturnValue(before) - store.updateSettings.mockImplementation((args: object) => ({ ...before, ...args })) + store.updateSettings.mockImplementation((args: Partial) => ({ + ...before, + ...args + })) registerSettingsHandlers(store as never) const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as ( event: typeof settingsInvokeEvent, diff --git a/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts index aa3fff56f77..1bd306c5c2d 100644 --- a/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts +++ b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts @@ -107,7 +107,7 @@ vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).pty const REPO_ID = 'repo-1' const REPO_PATH = '/workspace/repo' -const LOCAL_HOST_ID = 'local' +const LOCAL_HOST_ID = 'local' as const function worktree(path: string, overrides: Partial = {}): GitWorktreeInfo { return { diff --git a/src/main/ipc/worktrees-lineage-hydration.test.ts b/src/main/ipc/worktrees-lineage-hydration.test.ts index e109a13848f..79ece2cc369 100644 --- a/src/main/ipc/worktrees-lineage-hydration.test.ts +++ b/src/main/ipc/worktrees-lineage-hydration.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type { Worktree } from '../../shared/worktree/types' import { toSshExecutionHostId } from '../../shared/execution-host' import { LINEAGE_HYDRATION_TIMEOUT_MS } from './worktrees/metadata/host-lineage-listing' @@ -390,7 +391,7 @@ describe('registerWorktreeHandlers', () => { [childId]: { instanceId: 'child-instance' } } store.getWorktreeMeta.mockImplementation((id: string) => metaById[id]) - store.setWorktreeMeta.mockImplementation((id: string, updates: object) => ({ + store.setWorktreeMeta.mockImplementation((id: string, updates: Partial) => ({ ...metaById[id], ...updates })) diff --git a/src/main/ipc/worktrees-test-ipc-surface.ts b/src/main/ipc/worktrees-test-ipc-surface.ts index a858aacf6b8..7df4f6cda0a 100644 --- a/src/main/ipc/worktrees-test-ipc-surface.ts +++ b/src/main/ipc/worktrees-test-ipc-surface.ts @@ -1,4 +1,5 @@ import { type Mock, vi } from 'vitest' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' export type HandlerMap = Record unknown> @@ -7,7 +8,7 @@ type StoreMock = Mock<(...args: unknown[]) => unknown> /** Store lookups tests re-implement per id, so the first arg stays narrowed. */ type KeyedStoreMock = Mock<(id: string, ...rest: unknown[]) => unknown> /** Store writers tests re-implement by merging the patch they receive. */ -type KeyedStoreWriteMock = Mock<(id: string, patch: object) => unknown> +type KeyedStoreWriteMock = Mock<(id: string, patch: Partial) => unknown> export type TestMainWindow = { isDestroyed: () => boolean diff --git a/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts b/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts index fda412aa0fc..f601ebe0f11 100644 --- a/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts +++ b/src/main/native-chat/wsl-transcript-fs-process-dispatch.ts @@ -86,7 +86,7 @@ export function closeWslTranscriptFsProcess(handle: WslTranscriptFsProcessHandle } export function isWslTranscriptFsProcessHandle( - value: object + value: FileHandle | WslTranscriptFsProcessHandle ): value is WslTranscriptFsProcessHandle { return 'wslTranscriptFsProcessHandle' in value } diff --git a/src/main/network/electron-proxy-credentials.ts b/src/main/network/electron-proxy-credentials.ts index 43a93659474..d9ff26eb940 100644 --- a/src/main/network/electron-proxy-credentials.ts +++ b/src/main/network/electron-proxy-credentials.ts @@ -1,4 +1,5 @@ import { normalizeProxyUrl } from '../../shared/network-proxy' +import type { ProxySession } from './electron-default-proxy-session' export type ElectronProxyCredentials = { host: string @@ -20,7 +21,7 @@ const DEFAULT_PROXY_PORTS: Record = { 'socks5:': 1080 } -let proxyCredentialsBySession = new WeakMap() +let proxyCredentialsBySession = new WeakMap() function decodeProxyCredential(value: string): string { try { @@ -64,7 +65,7 @@ export function haveSameElectronProxyCredentials( } export function setElectronProxyCredentialsForSession( - proxySession: object, + proxySession: ProxySession, credentials: ElectronProxyCredentials | null ): void { if (credentials) { @@ -74,11 +75,11 @@ export function setElectronProxyCredentialsForSession( } } -export function clearElectronProxyCredentialsForSession(proxySession: object): void { +export function clearElectronProxyCredentialsForSession(proxySession: ProxySession): void { proxyCredentialsBySession.delete(proxySession) } -export function resetElectronProxyCredentialsForTests(proxySession?: object): void { +export function resetElectronProxyCredentialsForTests(proxySession?: ProxySession): void { if (proxySession) { clearElectronProxyCredentialsForSession(proxySession) } else { @@ -88,11 +89,11 @@ export function resetElectronProxyCredentialsForTests(proxySession?: object): vo export function handleElectronProxyLogin( event: { preventDefault(): void }, - webContents: { session: object } | null, + webContents: { session: ProxySession } | null, _authenticationResponseDetails: unknown, authInfo: { isProxy: boolean; host: string; port: number; scheme?: string; realm?: string }, callback: (username?: string, password?: string) => void, - defaultProxySession?: object + defaultProxySession?: ProxySession ): void { if (!authInfo.isProxy) { return diff --git a/src/main/opencode/hook-plugin-fail-open-ownership.test.ts b/src/main/opencode/hook-plugin-fail-open-ownership.test.ts index 9f48059604f..6262768da4d 100644 --- a/src/main/opencode/hook-plugin-fail-open-ownership.test.ts +++ b/src/main/opencode/hook-plugin-fail-open-ownership.test.ts @@ -19,6 +19,10 @@ vi.mock('electron', () => ({ import { _internals } from './hook-service' type SessionFixture = { id: string; parentID?: string } +/** The session half of the SDK client, as the plugin's ancestry lookup uses it. */ +type SessionClientFixture = { + list: (options?: { signal?: AbortSignal }) => Promise<{ data: SessionFixture[] }> +} type PluginEvent = { type: string; properties?: Record } type PluginEventHandler = (input: { event: PluginEvent }) => Promise type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise } @@ -83,7 +87,7 @@ describe('OpenCode plugin fail-open ownership', () => { return loadHooksWithSession({ list }) } - async function loadHooksWithSession(session: object): Promise { + async function loadHooksWithSession(session: SessionClientFixture): Promise { return loadHooksWithContext({ client: { session } }) } diff --git a/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts b/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts index 9fc69c8c78d..83a71533f1c 100644 --- a/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts +++ b/src/main/opencode/hook-plugin-lifecycle-delivery.test.ts @@ -19,6 +19,12 @@ vi.mock('electron', () => ({ import { _internals } from './hook-service' type SessionFixture = { id: string; parentID?: string } + +/** The plugin probes both SDK call conventions — current `(parameters, options)` and legacy + * single-options — so fixtures for one session-client method differ in arity. */ +type SessionClientCall = (...args: never[]) => Promise<{ data: SessionFixture[] }> + +type SessionClientFixture = { list: SessionClientCall; get?: SessionClientCall } type PluginEvent = { type: string; properties?: Record } type PluginEventHandler = (input: { event: PluginEvent }) => Promise type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise } @@ -83,7 +89,7 @@ describe('OpenCode plugin lifecycle delivery', () => { return loadHooksWithSession({ list }) } - async function loadHooksWithSession(session: object): Promise { + async function loadHooksWithSession(session: SessionClientFixture): Promise { const pluginPath = join(tempDir, 'orca-opencode-status.mjs') writeFileSync(pluginPath, _internals.getOpenCodePluginSource()) const module = (await import(pathToFileURL(pluginPath).href)) as { diff --git a/src/main/persistence/loading-store/automation-persistence.ts b/src/main/persistence/loading-store/automation-persistence.ts index 9f33f508fd9..2bed89ff09b 100644 --- a/src/main/persistence/loading-store/automation-persistence.ts +++ b/src/main/persistence/loading-store/automation-persistence.ts @@ -243,7 +243,7 @@ export function getAutomationRunWorkspaceDisplayName( } export function installAutomationPersistenceContext( - target: object, + target: AutomationPersistence, source: AutomationPersistence ): void { Object.defineProperty(target, automationPersistenceContext, { diff --git a/src/main/persistence/loading-store/metadata-lineage-operations.ts b/src/main/persistence/loading-store/metadata-lineage-operations.ts index 4b0a301fe26..2532ff3b2d3 100644 --- a/src/main/persistence/loading-store/metadata-lineage-operations.ts +++ b/src/main/persistence/loading-store/metadata-lineage-operations.ts @@ -316,7 +316,7 @@ export function removeWorkspaceLineageForFolderParent( } export function installMetadataLineageOperationsContext( - target: object, + target: MetadataLineageOperations, source: MetadataLineageOperations ): void { Object.defineProperty(target, metadataLineageOperationsContext, { diff --git a/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts b/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts index 321baad1aaf..8f0428c46bb 100644 --- a/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts +++ b/src/main/persistence/loading-store/mobile-tab-selection-persistence.ts @@ -33,7 +33,7 @@ export class MobileTabSelectionPersistence { } export function installMobileTabSelectionPersistenceContext( - target: object, + target: MobileTabSelectionPersistence, source: MobileTabSelectionPersistence ): void { Object.defineProperty(target, mobileTabSelectionPersistenceContext, { diff --git a/src/main/persistence/loading-store/primary-state-writes.ts b/src/main/persistence/loading-store/primary-state-writes.ts index f61f6c691bd..3820723fffb 100644 --- a/src/main/persistence/loading-store/primary-state-writes.ts +++ b/src/main/persistence/loading-store/primary-state-writes.ts @@ -280,7 +280,7 @@ export function writeToDiskSync( } export function installPrimaryStateWriteOperationsContext( - target: object, + target: PrimaryStateWriteOperations, source: PrimaryStateWriteOperations ): void { Object.defineProperty(target, primaryStateWriteOperationsContext, { diff --git a/src/main/persistence/loading-store/profile-preferences.ts b/src/main/persistence/loading-store/profile-preferences.ts index 0ec4e383c34..8e910ed235d 100644 --- a/src/main/persistence/loading-store/profile-preferences.ts +++ b/src/main/persistence/loading-store/profile-preferences.ts @@ -189,7 +189,10 @@ export function getFeatureInteractionOperations( } } -export function installProfilePreferencesContext(target: object, source: ProfilePreferences): void { +export function installProfilePreferencesContext( + target: ProfilePreferences, + source: ProfilePreferences +): void { Object.defineProperty(target, profilePreferencesContext, { value: source[profilePreferencesContext] }) diff --git a/src/main/persistence/loading-store/project-collection-operations.ts b/src/main/persistence/loading-store/project-collection-operations.ts index ecb1130072f..e8d22147769 100644 --- a/src/main/persistence/loading-store/project-collection-operations.ts +++ b/src/main/persistence/loading-store/project-collection-operations.ts @@ -226,7 +226,7 @@ export function getFolderWorkspaceOperations( } export function installProjectCollectionOperationsContext( - target: object, + target: ProjectCollectionOperations, source: ProjectCollectionOperations ): void { Object.defineProperty(target, projectCollectionOperationsContext, { diff --git a/src/main/persistence/loading-store/pty-binding-persistence.ts b/src/main/persistence/loading-store/pty-binding-persistence.ts index 27d62c8602b..9cdb51fffe7 100644 --- a/src/main/persistence/loading-store/pty-binding-persistence.ts +++ b/src/main/persistence/loading-store/pty-binding-persistence.ts @@ -266,7 +266,7 @@ function applyPtyBinding( } export function installPtyBindingPersistenceOperationsContext( - target: object, + target: PtyBindingPersistenceOperations, source: PtyBindingPersistenceOperations ): void { Object.defineProperty(target, ptyBindingPersistenceOperationsContext, { diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index ec1df02d714..2573c63305f 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -323,7 +323,7 @@ export function hydrateRepo(owner: RepoLifecycleOperations, repo: Repo): Repo { } export function installRepoLifecycleOperationsContext( - target: object, + target: RepoLifecycleOperations, source: RepoLifecycleOperations ): void { Object.defineProperty(target, repoLifecycleOperationsContext, { diff --git a/src/main/persistence/loading-store/retired-worktree-name-persistence.ts b/src/main/persistence/loading-store/retired-worktree-name-persistence.ts index c5260850522..bf0e7383667 100644 --- a/src/main/persistence/loading-store/retired-worktree-name-persistence.ts +++ b/src/main/persistence/loading-store/retired-worktree-name-persistence.ts @@ -110,7 +110,7 @@ export function applyRetiredWorktreeNames( } export function installRetiredWorktreeNamePersistenceContext( - target: object, + target: RetiredWorktreeNamePersistence, source: RetiredWorktreeNamePersistence ): void { Object.defineProperty(target, retiredWorktreeNamePersistenceContext, { diff --git a/src/main/persistence/loading-store/session-host-partitions.ts b/src/main/persistence/loading-store/session-host-partitions.ts index 353c89da4e5..d358f4c8f62 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -210,7 +210,7 @@ export function setHostWorkspaceSession( } export function installSessionHostPartitionOperationsContext( - target: object, + target: SessionHostPartitionOperations, source: SessionHostPartitionOperations ): void { Object.defineProperty(target, sessionHostPartitionOperationsContext, { diff --git a/src/main/persistence/loading-store/session-snapshot-operations.ts b/src/main/persistence/loading-store/session-snapshot-operations.ts index 2f5f1c64b86..37ffd9d366b 100644 --- a/src/main/persistence/loading-store/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/session-snapshot-operations.ts @@ -93,7 +93,7 @@ export function getSessionSnapshotOperationsContext(owner: SessionSnapshotOperat } export function installSessionSnapshotOperationsContext( - target: object, + target: SessionSnapshotOperations, source: SessionSnapshotOperations ): void { Object.defineProperty(target, sessionSnapshotOperationsContext, { diff --git a/src/main/persistence/loading-store/sparse-preset-persistence.ts b/src/main/persistence/loading-store/sparse-preset-persistence.ts index 2de83313ecd..20b78fcd9cc 100644 --- a/src/main/persistence/loading-store/sparse-preset-persistence.ts +++ b/src/main/persistence/loading-store/sparse-preset-persistence.ts @@ -46,7 +46,7 @@ export class SparsePresetPersistence { } export function installSparsePresetPersistenceContext( - target: object, + target: SparsePresetPersistence, source: SparsePresetPersistence ): void { Object.defineProperty(target, sparsePresetPersistenceContext, { diff --git a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts index 75aa19ad24b..7da5144898e 100644 --- a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts +++ b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts @@ -245,7 +245,7 @@ export function getSshPtyLeaseOperations(owner: SshLeaseRecoveryOperations): Ssh } export function installSshLeaseRecoveryOperationsContext( - target: object, + target: SshLeaseRecoveryOperations, source: SshLeaseRecoveryOperations ): void { Object.defineProperty(target, sshLeaseRecoveryOperationsContext, { diff --git a/src/main/persistence/loading-store/ssh-profile-operations.ts b/src/main/persistence/loading-store/ssh-profile-operations.ts index dce7a92890f..5fed1a3d021 100644 --- a/src/main/persistence/loading-store/ssh-profile-operations.ts +++ b/src/main/persistence/loading-store/ssh-profile-operations.ts @@ -146,7 +146,7 @@ export function getSshTargetStateOperations(owner: SshProfileOperations): SshTar } export function installSshProfileOperationsContext( - target: object, + target: SshProfileOperations, source: SshProfileOperations ): void { Object.defineProperty(target, sshProfileOperationsContext, { diff --git a/src/main/persistence/loading-store/store-domain-composition.ts b/src/main/persistence/loading-store/store-domain-composition.ts index 2bbe91a1a2e..c3f2059efe1 100644 --- a/src/main/persistence/loading-store/store-domain-composition.ts +++ b/src/main/persistence/loading-store/store-domain-composition.ts @@ -1,4 +1,5 @@ import type { StoreRuntimeState } from './store-runtime-state' +import type { Store } from './store' import { LoadedStateAdaptationOperations } from './loaded-state-adaptation' import { BackupRecoveryRotationOperations } from './backup-recovery-rotation' import { LoadedCohortMigrationOperations } from './loaded-cohort-migrations' @@ -108,7 +109,7 @@ export const STORE_DOMAIN_OPERATION_CLASSES = [ WriteFlushBarrierOperations ] as const -export function installStoreDomainContexts(target: object, domains: StoreDomains): void { +export function installStoreDomainContexts(target: Store, domains: StoreDomains): void { installWriteSchedulingOperationsContext(target, domains.scheduling) installPrimaryStateWriteOperationsContext(target, domains.writes) installProjectCollectionOperationsContext(target, domains.projects) diff --git a/src/main/persistence/loading-store/write-flush-barriers.ts b/src/main/persistence/loading-store/write-flush-barriers.ts index 1a80c77976f..c08d4367989 100644 --- a/src/main/persistence/loading-store/write-flush-barriers.ts +++ b/src/main/persistence/loading-store/write-flush-barriers.ts @@ -269,7 +269,7 @@ export function writeGithubCacheSnapshotSync(owner: WriteFlushBarrierOperations) } export function installWriteFlushBarrierOperationsContext( - target: object, + target: WriteFlushBarrierOperations, source: WriteFlushBarrierOperations ): void { Object.defineProperty(target, writeFlushBarrierOperationsContext, { diff --git a/src/main/persistence/loading-store/write-scheduling.ts b/src/main/persistence/loading-store/write-scheduling.ts index c78a5d0dfd4..0301da34a7e 100644 --- a/src/main/persistence/loading-store/write-scheduling.ts +++ b/src/main/persistence/loading-store/write-scheduling.ts @@ -64,7 +64,7 @@ export function scheduleSave(owner: WriteSchedulingOperations): void { } export function installWriteSchedulingOperationsContext( - target: object, + target: WriteSchedulingOperations, source: WriteSchedulingOperations ): void { Object.defineProperty(target, writeSchedulingOperationsContext, { diff --git a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts index 0588a2f6dae..2b744f6cdf0 100644 --- a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts +++ b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts @@ -3,6 +3,7 @@ import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../s import type { PersistedState } from '../../../shared/persisted-state-types' import type { Project } from '../../../shared/project-types' import type { Repo } from '../../../shared/repo-types' +import type { SshRemotePtyLease } from '../../../shared/ssh-types' import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' import type { WorktreeMeta } from '../../../shared/worktree/meta-types' import { @@ -299,7 +300,7 @@ describe('pruneSessionlessMissingLocalWorktreeMetadataForRepo', () => { for (const worktreeId of allIds) { state.worktreeMeta[worktreeId] = makeMeta(worktreeId) } - const lease = (worktreeId: string, index: number, extra: object) => ({ + const lease = (worktreeId: string, index: number, extra: Partial) => ({ targetId: 'builder', ptyId: `pty-${index}`, worktreeId, diff --git a/src/main/runtime/browser-client-download-transfer-store.ts b/src/main/runtime/browser-client-download-transfer-store.ts index 7da5042d7ea..c6c63b322fe 100644 --- a/src/main/runtime/browser-client-download-transfer-store.ts +++ b/src/main/runtime/browser-client-download-transfer-store.ts @@ -21,7 +21,11 @@ type RuntimeFileChannelHost = { statRuntimeFile(worktree: string, relativePath: string): Promise } -const stores = new WeakMap() +// Release runs from the lease registry, which only knows the runtime by id; the store itself is +// only ever created for a file-channel host. +type DownloadTransferRuntime = RuntimeFileChannelHost | { getRuntimeId(): string } + +const stores = new WeakMap() /** * Drops every staged download a page still owns. @@ -31,7 +35,7 @@ const stores = new WeakMap() * opened a file channel. */ export function releaseBrowserClientDownloadTransfersForPage( - runtime: object, + runtime: DownloadTransferRuntime, browserPageId: string ): Promise { return stores.get(runtime)?.releasePage(browserPageId) ?? Promise.resolve() diff --git a/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts b/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts index aaa78e6d45b..77b716218aa 100644 --- a/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts +++ b/src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts @@ -18,7 +18,9 @@ function createRuntime() { return { runtime, removed } } -async function stageTransfer(runtime: object, browserPageId: string): Promise { +type FakeRuntime = ReturnType['runtime'] + +async function stageTransfer(runtime: FakeRuntime, browserPageId: string): Promise { await getBrowserClientDownloadTransferStore(runtime as never).accept({ transferId: `transfer-${browserPageId}`, browserPageId, diff --git a/src/main/runtime/relay/relay-control-client.test.ts b/src/main/runtime/relay/relay-control-client.test.ts index 745a79ac84e..431d26574cf 100644 --- a/src/main/runtime/relay/relay-control-client.test.ts +++ b/src/main/runtime/relay/relay-control-client.test.ts @@ -8,6 +8,9 @@ import { MOBILE_RELAY_CLOSE_CODE } from '../../../shared/mobile-relay-close-code import { RelayControlClient } from './relay-control-client' const encoder = new TextEncoder() + +/** A JSON control frame, including the forward-compat frames the client must ignore. */ +type ControlFrame = { type: string } & Record const HOST_PROOF_DOMAIN = 'orca-relay-host-proof/v1' const CHALLENGE_DOMAIN = 'orca-relay-host-challenge/v1' @@ -410,7 +413,7 @@ class FakeControlSocket extends EventEmitter { this.close(1006) } - deliver(message: object): void { + deliver(message: ControlFrame): void { this.emit('message', JSON.stringify(message), false) } } diff --git a/src/main/runtime/relay/relay-control-client.ts b/src/main/runtime/relay/relay-control-client.ts index 0c863cce3ad..8321bf8caf2 100644 --- a/src/main/runtime/relay/relay-control-client.ts +++ b/src/main/runtime/relay/relay-control-client.ts @@ -269,7 +269,7 @@ export class RelayControlClient { this.clearConnectPromise() } - private sendActive(payload: object): void { + private sendActive(payload: Record): void { if (!this.socket || (this.state !== 'active' && this.state !== 'draining')) { throw new Error('relay_control_not_active') } diff --git a/src/main/runtime/relay/relay-control-requests.ts b/src/main/runtime/relay/relay-control-requests.ts index bbceb067a59..6151d634f0d 100644 --- a/src/main/runtime/relay/relay-control-requests.ts +++ b/src/main/runtime/relay/relay-control-requests.ts @@ -22,6 +22,23 @@ export type DeviceCredentialInstallAuthorization = | { mode: 'relay-basis'; basisConnId: string } | { mode: 'authenticated-direct'; directAuthId: string } +export type DeviceCredentialInstallInput = { + relayDeviceId: string + newResumeTokenHash: string + expectedCurrentHash?: string + authorization: DeviceCredentialInstallAuthorization +} + +/** Every control-plane request this class hands to `send`. */ +type RelayControlRequestPayload = + | { type: 'invite-create'; reqId: string; relayDeviceId: string } + | { type: 'device-revoke'; reqId: string; relayDeviceId: string } + | ({ type: 'device-credential-install'; v: 1; reqId: string } & DeviceCredentialInstallInput) + | { type: 'device-credential-install-status'; v: 1; reqId: string; relayDeviceId: string } + | { type: 'device-resume-confirm'; v: 1; reqId: string; basisConnId: string } + +type SendRelayControlRequest = (payload: RelayControlRequestPayload) => void + export class RelayControlRequests { private readonly pending = new Map() @@ -34,7 +51,7 @@ export class RelayControlRequests { createInvite( reqId: string, relayDeviceId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -44,11 +61,7 @@ export class RelayControlRequests { ) as Promise } - revokeDevice( - reqId: string, - relayDeviceId: string, - send: (payload: object) => void - ): Promise { + revokeDevice(reqId: string, relayDeviceId: string, send: SendRelayControlRequest): Promise { return this.request( reqId, 'revoke', @@ -59,13 +72,8 @@ export class RelayControlRequests { installCredential( reqId: string, - input: { - relayDeviceId: string - newResumeTokenHash: string - expectedCurrentHash?: string - authorization: DeviceCredentialInstallAuthorization - }, - send: (payload: object) => void + input: DeviceCredentialInstallInput, + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -78,7 +86,7 @@ export class RelayControlRequests { credentialInstallStatus( reqId: string, relayDeviceId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -91,7 +99,7 @@ export class RelayControlRequests { confirmResume( reqId: string, basisConnId: string, - send: (payload: object) => void + send: SendRelayControlRequest ): Promise { return this.request( reqId, @@ -156,8 +164,8 @@ export class RelayControlRequests { private request( reqId: string, kind: PendingRequest['kind'], - payload: object, - send: (payload: object) => void + payload: RelayControlRequestPayload, + send: SendRelayControlRequest ): Promise { if (this.pending.has(reqId)) { return Promise.reject(new Error('duplicate_relay_request_id')) diff --git a/src/main/runtime/runtime-browser-page-registry.ts b/src/main/runtime/runtime-browser-page-registry.ts index 9209e24e4af..f32f83dd105 100644 --- a/src/main/runtime/runtime-browser-page-registry.ts +++ b/src/main/runtime/runtime-browser-page-registry.ts @@ -226,9 +226,11 @@ export class RuntimeBrowserPageRegistry { } } -const registries = new WeakMap() +/** Keyed by runtime identity alone; this module never reads from the runtime, and the callers' + * declared host types share no member. */ +const registries = new WeakMap() -export function getRuntimeBrowserPageRegistry(runtime: object): RuntimeBrowserPageRegistry { +export function getRuntimeBrowserPageRegistry(runtime: WeakKey): RuntimeBrowserPageRegistry { let registry = registries.get(runtime) if (!registry) { registry = new RuntimeBrowserPageRegistry() diff --git a/src/main/runtime/runtime-linear-command-surface.ts b/src/main/runtime/runtime-linear-command-surface.ts index cf9ad9e69df..97c91e3af6d 100644 --- a/src/main/runtime/runtime-linear-command-surface.ts +++ b/src/main/runtime/runtime-linear-command-surface.ts @@ -13,9 +13,12 @@ type LinearFacadeInstance = { type LinearMethodBag = Record unknown> const delegators = new WeakSet() -const receiverByCommands = new WeakMap() +const receiverByCommands = new WeakMap() -function collectMethodNames(instancePrototype: object, stopAt: object | null): Set { +function collectMethodNames( + instancePrototype: RuntimeLinearBrowseCommands, + stopAt: RuntimeLinearBrowseCommands | null +): Set { const names = new Set() let prototype: object | null = instancePrototype while (prototype && prototype !== Object.prototype && prototype !== stopAt) { @@ -31,10 +34,10 @@ function collectMethodNames(instancePrototype: object, stopAt: object | null): S // Why: the chain used to live on the facade, so a facade override (test spy) has to win for re-entrant `this` calls too. function overrideAwareReceiver( - facade: object, - commands: object, + facade: LinearFacadeInstance, + commands: LinearMethodBag, surfaceNames: ReadonlySet -): object { +): LinearMethodBag { const cached = receiverByCommands.get(commands) if (cached) { return cached @@ -55,7 +58,7 @@ function overrideAwareReceiver( return receiver } -export function installRuntimeLinearCommandSurface(target: object): void { +export function installRuntimeLinearCommandSurface(target: LinearFacadeInstance): void { const names = collectMethodNames( RuntimeLinearCommands.prototype, RuntimeLinearCommandBase.prototype diff --git a/src/main/runtime/structured-session-worktree-teardown.test.ts b/src/main/runtime/structured-session-worktree-teardown.test.ts index 86258ad9dab..915fbd52986 100644 --- a/src/main/runtime/structured-session-worktree-teardown.test.ts +++ b/src/main/runtime/structured-session-worktree-teardown.test.ts @@ -144,7 +144,10 @@ function destructiveDeps(extra: { allowUnverifiedStop?: boolean; timeoutMs?: num } } -function runtimeDouble(hooks: object): TeardownRuntime { +/** Keys are pinned to the real runtime; each stub narrows its own args to what the case drives. */ +type TeardownRuntimeStubs = Partial> + +function runtimeDouble(hooks: TeardownRuntimeStubs): TeardownRuntime { return Object.assign(Object.create(null), hooks) } diff --git a/src/main/runtime/structured-worker-terminal-read.test.ts b/src/main/runtime/structured-worker-terminal-read.test.ts index 97859a988e0..8d7f4ef6b70 100644 --- a/src/main/runtime/structured-worker-terminal-read.test.ts +++ b/src/main/runtime/structured-worker-terminal-read.test.ts @@ -161,12 +161,17 @@ describe('reading a structured worker through the terminal-read path', () => { // could be perfect and a peer would still get `terminal_handle_stale` if nothing called it. const handle = registerWorker() installHost({ items: [message('i1', 'hello')] }) - const runtime = Object.assign(Object.create(OrcaRuntimeWithResolveTerminalPane.prototype), { + const runtime: { + readTerminal: ( + handle: string, + opts?: { cursor?: number; limit?: number; screen?: boolean } + ) => Promise<{ tail: string[] }> + } = Object.assign(Object.create(OrcaRuntimeWithResolveTerminalPane.prototype), { getOrchestrationDbIfAvailable: () => null, getLivePtyForHandle: () => { throw new Error('the PTY lookup must never be reached for a structured worker') } - }) as { readTerminal: (handle: string, opts?: object) => Promise<{ tail: string[] }> } + }) await expect(runtime.readTerminal(handle)).resolves.toMatchObject({ tail: ['[assistant] hello'], source: 'stream' diff --git a/src/main/source-control/hosted-review-branch-cache.ts b/src/main/source-control/hosted-review-branch-cache.ts index 7fafc855ba2..0e6f207ab9c 100644 --- a/src/main/source-control/hosted-review-branch-cache.ts +++ b/src/main/source-control/hosted-review-branch-cache.ts @@ -59,9 +59,14 @@ type CacheEntry = { startedAt: number } +declare const inflightTokenBrand: unique symbol + +/** Identity token for one lookup; only ever compared by reference. */ +type InflightToken = { readonly [inflightTokenBrand]?: never } + type InflightRecord = { /** Identity, so a detached lookup can only ever clear its own entry. */ - token: object + token: InflightToken startedAt: number promise: Promise /** Releases the callers and unpins the branch; idempotent. */ @@ -154,7 +159,7 @@ function storeEntry(key: string, entry: CacheEntry): void { } /** Clears the key's in-flight record only if it is still this lookup's. */ -function releaseInflight(key: string, token: object): boolean { +function releaseInflight(key: string, token: InflightToken): boolean { if (inflight.get(key)?.token !== token) { return false } @@ -271,7 +276,7 @@ function startLookup( ): Promise { const startedAt = Date.now() const generation = scopeGeneration(scope) - const token = {} + const token: InflightToken = {} /** The deadline released the callers; the lookup itself runs on, detached. */ let timedOut = false let completed = false diff --git a/src/main/worktree-retirement-backfill-scan.test.ts b/src/main/worktree-retirement-backfill-scan.test.ts index f90d4ccf405..0f02f11222f 100644 --- a/src/main/worktree-retirement-backfill-scan.test.ts +++ b/src/main/worktree-retirement-backfill-scan.test.ts @@ -32,7 +32,7 @@ function stallingScan(): { } /** Drive one namespace to the state where its listing is abandoned but still stuck in the kernel. */ -async function stallPastDeadline(store: object, scanKey: string) { +async function stallPastDeadline(store: WeakKey, scanKey: string) { const scan = stallingScan() const pending = runRetirementBackfillScan(store, scanKey, scan.run) const settled = expect(pending).rejects.toThrow(/exceeded/) diff --git a/src/main/worktree-retirement-backfill-scan.ts b/src/main/worktree-retirement-backfill-scan.ts index 8ca5b26ccd5..c0c111729a3 100644 --- a/src/main/worktree-retirement-backfill-scan.ts +++ b/src/main/worktree-retirement-backfill-scan.ts @@ -20,7 +20,9 @@ type BackfillScan = { outstanding: boolean } -const scansByStore = new WeakMap>() +/** Only the store's identity is the memo key — this module never reads from it, and cannot name the + * store's own type without importing its caller. */ +const scansByStore = new WeakMap>() /** Monotonic, like the WSL gate's own stuck timer: wall time misjudges a backoff across laptop * sleep or an NTP step, either pinning a namespace in its failure memo or ending it early. */ @@ -59,7 +61,7 @@ function withScanDeadline(scan: Promise): Promise { * the rule per namespace rather than process-wide is deliberate: a global budget lets one bad mount * spend it on its own retries and starve every healthy repo. */ export function runRetirementBackfillScan( - store: object, + store: WeakKey, scanKey: string, scan: () => Promise ): Promise> { diff --git a/src/relay/dispatcher-frame-guard-regressions.test.ts b/src/relay/dispatcher-frame-guard-regressions.test.ts index eb39e90bbdb..6574579d4ee 100644 --- a/src/relay/dispatcher-frame-guard-regressions.test.ts +++ b/src/relay/dispatcher-frame-guard-regressions.test.ts @@ -1,11 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import { RelayDispatcher } from './dispatcher' +import type { RelayClient } from './dispatcher-contract' import type { JsonRpcNotification } from './protocol' type DispatcherInternals = { - primaryClient: object + primaryClient: RelayClient estimateFrameBytes: (msg: JsonRpcNotification) => number - enqueueFrame: (client: object, msg: JsonRpcNotification, lane: string) => boolean + enqueueFrame: (client: RelayClient, msg: JsonRpcNotification, lane: string) => boolean } describe('RelayDispatcher frame guards', () => { diff --git a/src/relay/dispatcher.test.ts b/src/relay/dispatcher.test.ts index 280f9351200..f8ccc11c153 100644 --- a/src/relay/dispatcher.test.ts +++ b/src/relay/dispatcher.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { RelayDispatcher, type SinkWriteSettlement } from './dispatcher' +import type { PreparedRelayFrame, RelayClient } from './dispatcher-contract' import { relayWriterControlReserve } from './dispatcher-writer-admission' import { encodeJsonRpcFrame, @@ -723,18 +724,18 @@ describe('RelayDispatcher', () => { describe('legacy PTY chunk sizing', () => { type DispatcherInternals = { - primaryClient: object + primaryClient: RelayClient estimateFrameBytes: (msg: JsonRpcNotification) => number - prepareFrame: (msg: JsonRpcNotification) => object + prepareFrame: (msg: JsonRpcNotification) => PreparedRelayFrame enqueueFrame: ( - client: object, + client: RelayClient, msg: JsonRpcNotification, lane: string, onSettled?: (result: SinkWriteSettlement) => void ) => boolean enqueuePreparedFrame: ( - client: object, - frame: object, + client: RelayClient, + frame: PreparedRelayFrame, lane: string, onSettled?: (result: SinkWriteSettlement) => void ) => boolean diff --git a/src/relay/relay-filesystem-watch-registry.test.ts b/src/relay/relay-filesystem-watch-registry.test.ts index de924230f0f..dcea47e7d44 100644 --- a/src/relay/relay-filesystem-watch-registry.test.ts +++ b/src/relay/relay-filesystem-watch-registry.test.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { WatcherProcessFailure } from '../main/ipc/parcel-watcher-process-failure' import { WatcherProcessSupervisor } from '../main/ipc/parcel-watcher-process-supervisor' +import type { WatcherProcessSubscribeOptions } from '../main/ipc/parcel-watcher-process-protocol' import type { WatcherProcessCallback, WatcherProcessHooks, @@ -50,7 +51,7 @@ class FakeWatcherPool { async subscribe( rootPath: string, callback: WatcherProcessCallback, - _options: object, + _options: WatcherProcessSubscribeOptions, hooks: WatcherProcessHooks ): Promise { const unsubscribe = vi.fn(async () => undefined) diff --git a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx index ee45c71c30e..ab3ea61b1e2 100644 --- a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx +++ b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx @@ -16,8 +16,15 @@ const testState = vi.hoisted(() => ({ runtimeEnvironments: [] as { id: string; createdAt: number; pairingRevision?: number }[] })) +type MockedAppStoreState = { + settings: GlobalSettings | null + updateSettings: (settings: Partial) => void + runtimeEnvironments: { id: string; createdAt: number; pairingRevision?: number }[] + runtimeStatusByEnvironmentId: Map +} + vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => + useAppStore: (selector: (state: MockedAppStoreState) => unknown) => selector({ settings: testState.settings, updateSettings: testState.updateSettings, diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts b/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts index 618d04a9169..5f89b90f715 100644 --- a/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts +++ b/src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts @@ -65,7 +65,8 @@ function countAllocations(run: () => void): { entries: number; maps: number } { const RealMap = globalThis.Map let entries = 0 let maps = 0 - Object.entries = ((target: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `Object.entries` is an overload set no single arrow can satisfy; this wrapper only counts calls and returns the native result unchanged. + Object.entries = ((target: Record) => { entries += 1 return realEntries(target) }) as typeof Object.entries diff --git a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx index fb92076c081..b8ef6823236 100644 --- a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx +++ b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx @@ -19,6 +19,13 @@ afterEach(() => { vi.clearAllMocks() }) +/** No zones exist in this suite, so the hook never reaches these. */ +const viewZoneAccessor: MonacoEditor.IViewZoneChangeAccessor = { + addZone: () => '', + removeZone: () => undefined, + layoutZone: () => undefined +} + describe('useDiffCommentDecorator model lifecycle', () => { it('rebuilds model-scoped resources when a retained editor swaps models', () => { const editorDomNode = document.createElement('div') @@ -26,13 +33,15 @@ describe('useDiffCommentDecorator model lifecycle', () => { const disposeMouseMove = vi.fn() const disposeMouseLeave = vi.fn() const disposeScroll = vi.fn() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial stand-in for Monaco's ICodeEditor; useDiffCommentDecorator calls only the members defined here, and a real editor needs a laid-out DOM this suite does not build. const editor = { getDomNode: () => editorDomNode, getOption: () => 19, onMouseMove: () => ({ dispose: disposeMouseMove }), onMouseLeave: () => ({ dispose: disposeMouseLeave }), onDidScrollChange: () => ({ dispose: disposeScroll }), - changeViewZones: (callback: (accessor: object) => void) => callback({}) + changeViewZones: (callback: (accessor: MonacoEditor.IViewZoneChangeAccessor) => void) => + callback(viewZoneAccessor) } as unknown as MonacoEditor.ICodeEditor const hook = renderHook( ({ monacoModelIdentity }) => diff --git a/src/renderer/src/components/editor/markdown-preview-search.ts b/src/renderer/src/components/editor/markdown-preview-search.ts index 1b92f958fa8..3dc168ddd61 100644 --- a/src/renderer/src/components/editor/markdown-preview-search.ts +++ b/src/renderer/src/components/editor/markdown-preview-search.ts @@ -219,8 +219,15 @@ function getHighlightApi(): { // window). Track each instance's ranges by its own token and paint the UNION, // so a second preview's Find does not clobber the first's highlights. Ranges // live in each instance's own subtree, so the union paints every pane correctly. -const searchRangesByInstance = new Map() -const activeRangeByInstance = new Map() +declare const markdownPreviewSearchInstanceBrand: unique symbol + +/** Per-preview identity for the highlight maps; only compared by reference. */ +export type MarkdownPreviewSearchInstance = { + readonly [markdownPreviewSearchInstanceBrand]?: never +} + +const searchRangesByInstance = new Map() +const activeRangeByInstance = new Map() // Avoid array spread when collecting union ranges — a large doc can produce // 100k+ ranges and create()/registry writes must not build variadic arg lists. @@ -250,7 +257,9 @@ function paintActiveHighlight(api: NonNullable + function setupScheduledFocus( - activeElement: object | null, + activeElement: StubbedActiveElement | null, force = false ): { focus: ReturnType diff --git a/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts b/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts index 26847626853..e6893049633 100644 --- a/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-key-handler.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import { createIsolatedMarkdownExtensionForTests } from './isolated-markdown-extension-for-tests' import { createRichMarkdownKeyHandler, type KeyHandlerContext } from './rich-markdown-key-handler' @@ -14,7 +14,7 @@ vi.mock('@/lib/shortcut-platform', () => ({ const extensions = [StarterKit, createIsolatedMarkdownExtensionForTests()] -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { return new Editor({ element: null, extensions, @@ -133,7 +133,7 @@ function createContext(editor: Editor, typedMarker: boolean): KeyHandlerContext } } -function emptyTopLevelOrderedList(): object { +function emptyTopLevelOrderedList(): JSONContent { return { type: 'doc', content: [ diff --git a/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts b/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts index 31a3e06aa01..a184882a2e0 100644 --- a/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import { createIsolatedMarkdownExtensionForTests } from './isolated-markdown-extension-for-tests' import { @@ -10,7 +10,7 @@ import { isSingleEmptyTopLevelOrderedList } from './rich-markdown-list-continuation' -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { // Why: each Editor needs its own marked registry; sharing one module-scoped // extension accumulates tokenizer state across tests. return new Editor({ diff --git a/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts b/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts index d59cb0c4c04..1ec3fcc04df 100644 --- a/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-paragraph.test.ts @@ -2,9 +2,11 @@ import { describe, expect, it, vi } from 'vitest' import { RichMarkdownParagraph } from './rich-markdown-paragraph' vi.mock('@tiptap/extension-paragraph', async () => { - const actual = (await vi.importActual('@tiptap/extension-paragraph')) as { - Paragraph: { extend: (config: object) => { config: Record } } - } + const actual = await vi.importActual<{ + Paragraph: { + extend: (config: Record) => { config: Record } + } + }>('@tiptap/extension-paragraph') // Simulates a Tiptap upgrade that drops `parseMarkdown` from the upstream paragraph. const Paragraph = actual.Paragraph.extend({}) Paragraph.config.parseMarkdown = undefined diff --git a/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts b/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts index 1d597023b3e..a25a06141ec 100644 --- a/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { Editor } from '@tiptap/core' +import { Editor, type JSONContent } from '@tiptap/core' import StarterKit from '@tiptap/starter-kit' import TaskList from '@tiptap/extension-task-list' import TaskItem from '@tiptap/extension-task-item' @@ -8,7 +8,7 @@ import { createRichMarkdownExtensions } from './rich-markdown-extensions' import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport' import { createRichMarkdownKeyHandler, type KeyHandlerContext } from './rich-markdown-key-handler' -function createEditor(content: object): Editor { +function createEditor(content: JSONContent): Editor { // Why: each Editor needs its own marked registry; sharing one module-scoped // extension accumulates tokenizer state across tests. return new Editor({ @@ -43,7 +43,7 @@ function createMarkdownEditor(markdown: string): Editor { * editor has no plain-text markdown paste transform. The DOM-less test env * cannot parse HTML, so assert against the node shapes that paste produces. */ -function createNodeEditor(content: object): Editor { +function createNodeEditor(content: JSONContent): Editor { return new Editor({ element: null, extensions: createRichMarkdownExtensions({ @@ -53,18 +53,18 @@ function createNodeEditor(content: object): Editor { }) } -function para(text: string): object { +function para(text: string): JSONContent { return { type: 'paragraph', content: [{ type: 'text', text }] } } -function bullets(...items: object[][]): object { +function bullets(...items: JSONContent[][]): JSONContent { return { type: 'bulletList', content: items.map((content) => ({ type: 'listItem', content })) } } -function tasks(...items: object[][]): object { +function tasks(...items: JSONContent[][]): JSONContent { return { type: 'taskList', content: items.map((content) => ({ @@ -75,7 +75,7 @@ function tasks(...items: object[][]): object { } } -function doc(...content: object[]): object { +function doc(...content: JSONContent[]): JSONContent { return { type: 'doc', content } } @@ -175,7 +175,7 @@ function createContext(editor: Editor): KeyHandlerContext { } } -function bulletListDocument(): object { +function bulletListDocument(): JSONContent { return { type: 'doc', content: [ @@ -196,7 +196,7 @@ function bulletListDocument(): object { } } -function parentAndFixesDocument(): object { +function parentAndFixesDocument(): JSONContent { return { type: 'doc', content: [ @@ -226,7 +226,7 @@ function parentAndFixesDocument(): object { } } -function taskListDocument(): object { +function taskListDocument(): JSONContent { return { type: 'doc', content: [ diff --git a/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts b/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts index 1f32ed4f668..ca6f4c023f2 100644 --- a/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts +++ b/src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts @@ -6,6 +6,7 @@ import { isMarkdownComment } from '@/lib/diff-comment-compat' import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client' import { useAppStore } from '@/store' import { prewarmMarkdownPreviewLocalImages } from './markdown-preview-local-images' +import type { MarkdownPreviewSearchInstance } from './markdown-preview-search' import { deriveMarkdownPreviewSourceRoot, findMarkdownPreviewSourceOpenFile, @@ -40,7 +41,7 @@ export function useMarkdownPreviewSourceFoundation({ input.select() }, []) const matchesRef = useRef([]) - const searchInstanceRef = useRef({}) + const searchInstanceRef = useRef({}) const lastAppliedInitialAnchorRef = useRef(null) const pendingEditorRevealFrameIdsRef = useRef([]) const [isSearchOpen, setIsSearchOpen] = useState(false) diff --git a/src/renderer/src/components/github-checks-tab-state.ts b/src/renderer/src/components/github-checks-tab-state.ts index 18c12cf089b..6d60adaf71a 100644 --- a/src/renderer/src/components/github-checks-tab-state.ts +++ b/src/renderer/src/components/github-checks-tab-state.ts @@ -7,9 +7,14 @@ export type CheckDetailsLoadState = { error: string | null } +declare const checksContextOwnerBrand: unique symbol + +/** Identity minted per checks context; only its reference is ever compared. */ +export type GitHubChecksContextOwner = object & { readonly [checksContextOwnerBrand]?: never } + export type GitHubChecksTabState = { contextKey: string - contextOwner: object + contextOwner: GitHubChecksContextOwner sourceChecks: GitHubChecksSource localChecks: PRCheckDetail[] | null expandedCheckKey: string | null diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts index b4a46e24c33..21b10c2cd88 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab-actions.ts @@ -4,6 +4,7 @@ import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import { resetGitHubChecksTabForSource, updateGitHubChecksTabLocalChecks, + type GitHubChecksContextOwner, type GitHubChecksTabState } from '@/components/github-checks-tab-state' import { getGitHubRuntimeRepoId, type GitHubRuntimeHost } from '@/lib/github-source-runtime-context' @@ -28,21 +29,21 @@ export type ChecksTabActionContext = { headSha: string | undefined prRepo: GitHubOwnerRepo | null mountedRef: { current: boolean } - committedChecksContextOwnerRef: { current: object } + committedChecksContextOwnerRef: { current: GitHubChecksContextOwner } nextChecksRefreshRequestIdRef: { current: number } activeChecksRefreshRequestIdRef: { current: number | null } nextCheckDetailsRequestIdRef: { current: number } setChecksState: React.Dispatch> setRefreshingOwner: React.Dispatch< - React.SetStateAction<{ contextOwner: object; requestId: number } | null> + React.SetStateAction<{ contextOwner: GitHubChecksContextOwner; requestId: number } | null> > - setRerunningOwner: React.Dispatch> + setRerunningOwner: React.Dispatch> onChecksUpdated: (checks: PRCheckDetail[]) => void } export async function refreshGitHubChecksTab( ctx: ChecksTabActionContext, - expectedContextOwner?: object + expectedContextOwner?: GitHubChecksContextOwner ): Promise { if (!ctx.canUseChecksRepoContext) { toast.error( diff --git a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx index 3e9811d997c..9692d59fc18 100644 --- a/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx +++ b/src/renderer/src/components/github-item-dialog/inspect-pull-request/checks-tab.tsx @@ -40,6 +40,9 @@ import { import { requestGitHubCheckDetails } from './checks-tab-request-details' import { ChecksTabActions, ChecksTabCompactHeader } from './checks-tab-header' +/** Identity token for one checks context; compared by reference so a stale refresh is dropped. */ +type ChecksContextOwner = Record + export function ChecksTab({ item, repoPath, @@ -111,7 +114,7 @@ export function ChecksTab({ const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0) const handleRefresh = useCallback( - async (expectedContextOwner?: object): Promise => + async (expectedContextOwner?: ChecksContextOwner): Promise => refreshGitHubChecksTab( { canUseChecksRepoContext, diff --git a/src/renderer/src/components/pull-request-page/checks/rerun.ts b/src/renderer/src/components/pull-request-page/checks/rerun.ts index 40637c10993..6f8eaec76a7 100644 --- a/src/renderer/src/components/pull-request-page/checks/rerun.ts +++ b/src/renderer/src/components/pull-request-page/checks/rerun.ts @@ -6,12 +6,21 @@ import type { GitHubOwnerRepo } from '../../../../../shared/github/pull-request- import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import type { PRCheckDetail } from '../../../../../shared/github/check-types' import type { TaskSourceContext } from '../../../../../shared/task-source-context' +import type { GitHubChecksTabState } from '../../github-checks-tab-state' + +/** The checks tab mints one of these per context; only its reference identity is ever read. */ +type ChecksContextOwner = GitHubChecksTabState['contextOwner'] export async function rerunPullRequestChecks(args: { canUseChecksRepoContext: boolean rerunning: boolean - committedChecksContextOwnerRef: { current: object } - setRerunningOwner: (value: object | null | ((current: object | null) => object | null)) => void + committedChecksContextOwnerRef: { current: ChecksContextOwner } + setRerunningOwner: ( + value: + | ChecksContextOwner + | null + | ((current: ChecksContextOwner | null) => ChecksContextOwner | null) + ) => void runtimeHost: GitHubRuntimeHost | null sourceContext?: TaskSourceContext | null repoId: string | null @@ -21,7 +30,7 @@ export async function rerunPullRequestChecks(args: { prRepo: GitHubOwnerRepo | null failedOnly: boolean mountedRef: { current: boolean } - handleRefresh: (expectedContextOwner?: object) => Promise + handleRefresh: (expectedContextOwner?: ChecksContextOwner) => Promise }): Promise { if (!args.canUseChecksRepoContext || args.rerunning) { return diff --git a/src/renderer/src/components/pull-request-page/checks/tab.tsx b/src/renderer/src/components/pull-request-page/checks/tab.tsx index 9ccbc4d546e..0c12d91ae7e 100644 --- a/src/renderer/src/components/pull-request-page/checks/tab.tsx +++ b/src/renderer/src/components/pull-request-page/checks/tab.tsx @@ -6,7 +6,8 @@ import { CHECK_COLOR, CHECK_ICON } from '@/components/right-sidebar/checks-panel import { createGitHubChecksTabState, resolveGitHubChecksTabState, - toggleGitHubChecksTabExpandedKey + toggleGitHubChecksTabExpandedKey, + type GitHubChecksContextOwner } from '@/components/github-checks-tab-state' import { getCheckDetailsKey } from '@/components/github/pr-check-presentation' import { getCheckCounts, getChecksSummaryLabel } from '@/components/pr-check-counts' @@ -94,11 +95,11 @@ export function ChecksTab({ const nextChecksRefreshRequestIdRef = useRef(0) const activeChecksRefreshRequestIdRef = useRef(null) const [refreshingOwner, setRefreshingOwner] = useState<{ - contextOwner: object + contextOwner: GitHubChecksContextOwner requestId: number } | null>(null) const refreshing = refreshingOwner?.contextOwner === resolvedChecksState.contextOwner - const [rerunningOwner, setRerunningOwner] = useState(null) + const [rerunningOwner, setRerunningOwner] = useState(null) const rerunning = rerunningOwner === resolvedChecksState.contextOwner useLayoutEffect(() => { committedChecksContextOwnerRef.current = resolvedChecksState.contextOwner @@ -173,7 +174,7 @@ export function ChecksTab({ const canFixBrokenChecks = Boolean((repoId ?? item.repoId) && failedChecks.length > 0) const handleRefresh = useCallback( - async (expectedContextOwner?: object) => + async (expectedContextOwner?: GitHubChecksContextOwner) => refreshPullRequestChecks({ canUseChecksRepoContext, expectedContextOwner, diff --git a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx index a601584f60e..12aed316a9c 100644 --- a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx +++ b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import { GeneralWorkspaceSettingsSection } from './GeneralWorkspaceSettingsSection' import type { ReactNode } from 'react' +import type { GlobalSettings } from '../../../../shared/global-settings-types' vi.mock('./WorkspaceDirectorySetting', () => ({ WorkspaceDirectorySetting: () => null })) vi.mock('./OpenInMenuSetting', () => ({ OpenInMenuSetting: () => null })) @@ -30,7 +31,7 @@ afterEach(() => { }) function renderSection( - updateSettings: (updates: object) => void | Promise, + updateSettings: (updates: Partial) => void | Promise, options: { defaultsSupported?: boolean sourceDefaultsSupported?: boolean diff --git a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx index a01d7d99ffe..f764f6264b3 100644 --- a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx +++ b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx @@ -64,7 +64,7 @@ afterEach(() => { function render( repo: Repo, - updateRepo: (repoId: string, updates: object) => void | Promise, + updateRepo: React.ComponentProps['updateRepo'], options: { settings?: Pick | null refreshRepo?: (repoId: string) => void | Promise diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts index d97a7d41906..452cec1f813 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts @@ -306,7 +306,10 @@ describe('selectWorktreeAgentOrchestration', () => { } let liveReads = 0 let retainedReads = 0 - const countReads = (target: object, onRead: () => void): object => + const countReads = ( + target: Record, + onRead: () => void + ): Record => new Proxy(target, { get(source, key, receiver) { if (typeof key === 'string') { diff --git a/src/renderer/src/components/task-page-github-work-item-quiet-state.ts b/src/renderer/src/components/task-page-github-work-item-quiet-state.ts index e1862e51b56..2d9891020b5 100644 --- a/src/renderer/src/components/task-page-github-work-item-quiet-state.ts +++ b/src/renderer/src/components/task-page-github-work-item-quiet-state.ts @@ -1,5 +1,8 @@ import { taskPageGitHubFamilyDirtyKey } from './task-page-github-work-item-mutation-keys' +/** Identity token for the caller driving one quiet run; compared by reference, never read. */ +export type QuietRevalidateRunOwner = Record + export type QuietRevalidateState = { inFlight: boolean trailingQueued: boolean @@ -10,7 +13,7 @@ export type QuietRevalidateState = { networkFailureAttempts: number lastConfirmAt: number runGeneration: number - runOwner: object | null + runOwner: QuietRevalidateRunOwner | null } const quietByQueryKey = new Map() @@ -37,7 +40,7 @@ export function getOrCreateQuietRevalidateState(queryKey: string): QuietRevalida export function beginTaskPageQuietRevalidateRun( state: QuietRevalidateState, - owner: object + owner: QuietRevalidateRunOwner ): number | null { if (state.inFlight && state.runOwner === owner) { state.trailingQueued = true @@ -52,7 +55,7 @@ export function beginTaskPageQuietRevalidateRun( export function finishTaskPageQuietRevalidateRun( state: QuietRevalidateState, - owner: object, + owner: QuietRevalidateRunOwner, generation: number ): boolean { if (state.runOwner !== owner || state.runGeneration !== generation) { diff --git a/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts b/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts index df22453e59d..8d4ca1fb33e 100644 --- a/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts +++ b/src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.ts @@ -1,3 +1,9 @@ +import type { Terminal } from '@xterm/xterm' + +/** The pane's terminal, used only as the queue's identity key — no member is ever read, so a + * bare stand-in is a valid target. */ +type HiddenOutputRestoreTarget = Partial + type HiddenOutputRestorePriority = 'active' | 'inactive' /** Returns whether the pane actually started a replay; a guard-only return is free. */ @@ -11,7 +17,7 @@ type HiddenOutputRestoreEntry = { // on the active pane while still catching watched split panes up quickly. const INACTIVE_RESTORE_INTERVAL_MS = 16 -const inactiveRestoreQueue = new Map() +const inactiveRestoreQueue = new Map() let inactiveRestoreTimer: ReturnType | null = null function clearInactiveRestoreTimer(): void { @@ -51,7 +57,7 @@ function drainInactiveRestoreQueue(): void { } export function scheduleHiddenOutputRestore( - target: object, + target: HiddenOutputRestoreTarget, requestRestore: HiddenOutputRestoreRequest, priority: HiddenOutputRestorePriority ): void { @@ -64,7 +70,7 @@ export function scheduleHiddenOutputRestore( scheduleInactiveRestoreDrain() } -export function cancelScheduledHiddenOutputRestore(target: object): void { +export function cancelScheduledHiddenOutputRestore(target: HiddenOutputRestoreTarget): void { inactiveRestoreQueue.delete(target) if (inactiveRestoreQueue.size === 0) { clearInactiveRestoreTimer() diff --git a/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts b/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts index 57644da8a83..9296809816b 100644 --- a/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts +++ b/src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.ts @@ -5,7 +5,14 @@ const hiddenClaimCounts = new Map() type VisibilityClaim = { ptyId: string; visible: boolean } -const visibilityClaimsByOwner = new Map() +declare const visibilityClaimOwnerBrand: unique symbol + +/** The mounted transport holding a claim; only its reference is ever compared. */ +export type RendererPtyVisibilityClaimOwner = object & { + readonly [visibilityClaimOwnerBrand]?: never +} + +const visibilityClaimsByOwner = new Map() const visibleClaimCounts = new Map() function sendHiddenState(ptyId: string, hidden: boolean): void { @@ -72,7 +79,7 @@ function removeVisibleClaim(claim: VisibilityClaim): boolean { * a retiring pane from hiding a PTY after its replacement has already bound. */ export function setRendererPtyVisibilityClaim( - owner: object, + owner: RendererPtyVisibilityClaimOwner, ptyId: string, visible: boolean ): void { @@ -103,7 +110,7 @@ export function setRendererPtyVisibilityClaim( } } -export function releaseRendererPtyVisibilityClaim(owner: object): void { +export function releaseRendererPtyVisibilityClaim(owner: RendererPtyVisibilityClaimOwner): void { const previous = visibilityClaimsByOwner.get(owner) if (!previous) { return diff --git a/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts b/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts index 8f96ba80efb..7a2780a5d32 100644 --- a/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts +++ b/src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.ts @@ -1,3 +1,4 @@ +import type { IDisposable } from '@xterm/xterm' import type { PtyTransport } from './pty-transport' type CapturedTerminalInputDispatch = { @@ -38,8 +39,9 @@ export function sendCapturedTerminalInput({ return sent } +/** currentBinding arrives as the pane's raw xterm binding; only its identity is read. */ export function requestCapturedTerminalReconfirmation( - currentBinding: object | undefined, + currentBinding: IDisposable | TerminalCapturedInputBinding | undefined, capturedBinding: TerminalCapturedInputBinding | undefined ): void { if (currentBinding === capturedBinding) { diff --git a/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts b/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts index 94dbb221e43..394951d1a4e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts @@ -197,12 +197,16 @@ describe.each([ terminal.dispose() }) + /** The handle this suite's setTimeout stub hands back; only its identity is compared. */ + type FakeTimerToken = Record + it('keeps newer timer slots when canceled callbacks are forced', () => { const { terminal, textarea } = openTerminal(TerminalType) const callbacks: (() => void)[] = [] - const cleared = new Set() + const cleared = new Set() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub hands back an identity token instead of a real timer handle, which `typeof setTimeout` cannot express; only the clearTimeout stub below ever receives it. vi.spyOn(globalThis, 'setTimeout').mockImplementation(((callback: () => void) => { - const token = {} + const token: FakeTimerToken = {} callbacks.push(() => { if (!cleared.has(token)) { callback() @@ -210,7 +214,8 @@ describe.each([ }) return token }) as typeof setTimeout) - vi.spyOn(globalThis, 'clearTimeout').mockImplementation(((token: object) => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the matching stub for the setTimeout token above; `typeof clearTimeout` declares a real timer handle this suite never creates. + vi.spyOn(globalThis, 'clearTimeout').mockImplementation(((token: FakeTimerToken) => { cleared.add(token) }) as typeof clearTimeout) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts index d3d14e99204..d569e265098 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-lifecycle-primitives.ts @@ -1,5 +1,6 @@ import type { Terminal } from '@xterm/xterm' import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { PtyPaneStartup } from './pty-connection-types' import type { PtyTransport } from './pty-transport' import type { PaneCwdMap } from './resolve-split-cwd' import { writeTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' @@ -180,15 +181,15 @@ export function resolveTerminalHomePathFromEnv( } export function paneOwnsQueuedStartup( - paneStartup: object | null | undefined, - queuedStartup: object | null | undefined + paneStartup: PtyPaneStartup | null | undefined, + queuedStartup: PtyPaneStartup | null | undefined ): boolean { return queuedStartup != null && paneStartup === queuedStartup } export function createQueuedStartupConsumer( - paneStartup: object | null | undefined, - queuedStartup: object | null | undefined, + paneStartup: PtyPaneStartup | null | undefined, + queuedStartup: PtyPaneStartup | null | undefined, consume: () => void, isStillQueued: () => boolean ): (() => void) | undefined { diff --git a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts index 50267617a13..3359dd346f6 100644 --- a/src/renderer/src/components/use-task-page-github-quiet-refresh.ts +++ b/src/renderer/src/components/use-task-page-github-quiet-refresh.ts @@ -1,6 +1,7 @@ import type { TaskPageGitHubLandingRefreshModel } from './use-task-page-github-landing-refresh' import { useMountedRef } from '@/hooks/useMountedRef' import { useRef } from 'react' +import type { QuietRevalidateRunOwner } from '@/components/task-page-github-work-item-quiet-state' import { advanceTaskPageQuietRevalidateScope } from '@/components/task-page-github-work-item-mutations' import { useTaskPageGitHubQuietRefreshEffect } from './use-task-page-github-quiet-refresh-effect' export type TaskPageGitHubQuietRefreshPreludeModel = ReturnType< @@ -12,7 +13,7 @@ export function useTaskPageGitHubQuietRefreshPrelude(model: TaskPageGitHubLandin // shared quietState (inFlight/trailingQueued), so a nonce-triggered re-render // must NOT cancel the in-flight run's trailing bookkeeping. const quietRevalidateMountedRef = useMountedRef() - const quietRevalidateOwnerRef = useRef({}) + const quietRevalidateOwnerRef = useRef({}) const quietRevalidateScopeRef = useRef({ queryKey: githubWorkItemMutationQueryKey, generation: 0 diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx index fc0e4e04464..733716d2482 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx @@ -6,6 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { SettingsNavSection } from '@/lib/settings-navigation-types' +import type { RuntimeEnvironmentStatus } from '@/store/slices/runtime-status-types' +import type { Repo } from '../../../shared/repo-types' import { resetWindowsTerminalCapabilitiesForTests } from '@/lib/windows-terminal-capabilities' const testState = vi.hoisted(() => ({ @@ -14,8 +16,16 @@ const testState = vi.hoisted(() => ({ runtimeEnvironments: [] as { id: string; createdAt: number; pairingRevision?: number }[] })) +/** Only the store fields this screen's selectors read; the mock supplies nothing else. */ +type MockedSettingsNavState = { + settings: GlobalSettings | null + repos: Repo[] + runtimeEnvironments: typeof testState.runtimeEnvironments + runtimeStatusByEnvironmentId: Map +} + vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => + useAppStore: (selector: (state: MockedSettingsNavState) => unknown) => selector({ settings: testState.settings, repos: [], diff --git a/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx b/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx index 0b8a177016f..d3adc9bfe83 100644 --- a/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx +++ b/src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx @@ -15,7 +15,7 @@ const testState = vi.hoisted(() => ({ })) vi.mock('@/store', () => ({ - useAppStore: (selector: (state: object) => unknown) => selector(testState) + useAppStore: (selector: (state: typeof testState) => unknown) => selector(testState) })) vi.mock('@/lib/web-client-location', () => ({ diff --git a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts b/src/renderer/src/i18n/technical-literal-catalog-values.test.ts index 5cf9963175b..c8e24528c5f 100644 --- a/src/renderer/src/i18n/technical-literal-catalog-values.test.ts +++ b/src/renderer/src/i18n/technical-literal-catalog-values.test.ts @@ -53,7 +53,7 @@ const repairedEntries = [ const catalogs = { es, ko, zh } as const -function readValue(catalog: object, key: string): unknown { +function readValue(catalog: Record, key: string): unknown { return key.split('.').reduce((value, part) => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { return undefined diff --git a/src/renderer/src/lib/ime-composition-keyboard-event.ts b/src/renderer/src/lib/ime-composition-keyboard-event.ts index 280a26476b2..554a0dcd565 100644 --- a/src/renderer/src/lib/ime-composition-keyboard-event.ts +++ b/src/renderer/src/lib/ime-composition-keyboard-event.ts @@ -13,14 +13,16 @@ type ImeModifierGestureEvent = ImeKeyboardEvent & { shiftKey?: boolean } -/** True when the IME, rather than Orca, owns a keyboard event. */ -export function isImeOwnedKeyboardEvent(event: object): boolean { - const candidate = event as ImeKeyboardEvent +/** True when the IME, rather than Orca, owns a keyboard event. Generic so synthetic, native, and + * gesture events each pass their own richer shape. */ +export function isImeOwnedKeyboardEvent( + event: KeyEvent +): boolean { return ( - candidate.isComposing === true || - candidate.keyCode === 229 || - candidate.nativeEvent?.isComposing === true || - candidate.nativeEvent?.keyCode === 229 + event.isComposing === true || + event.keyCode === 229 || + event.nativeEvent?.isComposing === true || + event.nativeEvent?.keyCode === 229 ) } diff --git a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts index 66dc3fc872b..4d0696a2012 100644 --- a/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts @@ -87,7 +87,10 @@ function renderedText(pane: TestPane): string { } /** Reveal = the manager's resume pass, then the terminal regains real DOM focus. */ -async function reveal(panes: TestPane[], owner?: object): Promise { +async function reveal( + panes: TestPane[], + owner?: Parameters[1] +): Promise { resumePaneRendering(panes, owner) for (const pane of panes) { pane.terminal.focus() diff --git a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts index e84638c33ea..d613cac4a3c 100644 --- a/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { ITerminalAddon } from '@xterm/xterm' import { WebglAddon } from '@xterm/addon-webgl' import type { ManagedPaneInternal } from './pane-manager-types' import { @@ -508,6 +509,7 @@ describe('openTerminal — addon and provider wiring', () => { }) ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a hand-built stand-in for xterm's Terminal; openTerminal touches only the members defined here, and a real Terminal needs a rendering canvas this suite has no DOM for. const terminal = { element: fakeTerminalElement, textarea: null, @@ -516,7 +518,7 @@ describe('openTerminal — addon and provider wiring', () => { open: vi.fn(() => { events.push('open') }), - loadAddon: vi.fn((addon: object) => { + loadAddon: vi.fn((addon: ITerminalAddon) => { if (addon === fitAddon) { events.push('loadAddon:fit') } else if (addon === searchAddon) { diff --git a/src/renderer/src/lib/pane-manager/pane-manager-types.ts b/src/renderer/src/lib/pane-manager/pane-manager-types.ts index a26be1e3a9f..341e40a891b 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager-types.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager-types.ts @@ -10,6 +10,8 @@ import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TerminalLeafId } from '../../../../shared/stable-pane-id' import type { TerminalWebglAutoDecision } from './terminal-webgl-auto-policy' +export type { TerminalScrollIntentTarget } from './terminal-scroll-intent' + // --------------------------------------------------------------------------- // Public interfaces // --------------------------------------------------------------------------- diff --git a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts index 219f17f4be6..114a0b850ee 100644 --- a/src/renderer/src/lib/pane-manager/pane-rendering-control.ts +++ b/src/renderer/src/lib/pane-manager/pane-rendering-control.ts @@ -21,7 +21,8 @@ import { } from './pane-webgl-reattach' import { releaseHiddenWebglRetention, - tryRetainHiddenPanesWebgl + tryRetainHiddenPanesWebgl, + type HiddenWebglRetentionOwner } from './terminal-webgl-hidden-retention' export function setPaneGpuRenderingState( @@ -59,7 +60,10 @@ export function markPaneComplexScriptOutput( export function suspendPaneRendering( panes: Iterable, - retention?: { owner: object; livePanes: () => Iterable } + retention?: { + owner: HiddenWebglRetentionOwner + livePanes: () => Iterable + } ): void { const suspended = Array.from(panes) // Why: both branches must leave a suspended pane in the same state; only the retention @@ -89,7 +93,7 @@ export function suspendPaneRendering( export function resumePaneRendering( panes: Iterable, - retentionOwner?: object + retentionOwner?: HiddenWebglRetentionOwner ): void { if (retentionOwner) { releaseHiddenWebglRetention(retentionOwner) diff --git a/src/renderer/src/lib/pane-manager/pane-scroll.ts b/src/renderer/src/lib/pane-manager/pane-scroll.ts index bb79faa4556..5945e7257c2 100644 --- a/src/renderer/src/lib/pane-manager/pane-scroll.ts +++ b/src/renderer/src/lib/pane-manager/pane-scroll.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import type { ScrollState } from './pane-manager-types' +import type { ScrollState, TerminalScrollIntentTarget } from './pane-manager-types' import { captureLogicalLineAnchor, resolveLogicalCellOffsetLine @@ -8,7 +8,7 @@ import { forceTerminalViewportScrollbarSync } from './terminal-viewport-scrollba const terminalOutputEpochs = new WeakMap() const deferredScrollRestores = new WeakMap< - object, + TerminalScrollIntentTarget, { cancelled: boolean rafIds: number[] @@ -17,7 +17,7 @@ const deferredScrollRestores = new WeakMap< } >() const pendingFitScrollRestores = new WeakMap< - object, + TerminalScrollIntentTarget, { cancelled: boolean rafId: number | null @@ -38,7 +38,7 @@ export function getTerminalOutputEpoch(terminal: Terminal): number { return terminalOutputEpochs.get(terminal) ?? 0 } -export function cancelDeferredScrollRestore(terminal: object): void { +export function cancelDeferredScrollRestore(terminal: TerminalScrollIntentTarget): void { cancelPendingFitScrollRestore(terminal) const pending = deferredScrollRestores.get(terminal) if (!pending) { @@ -323,7 +323,7 @@ export function releaseScrollStateMarker(state: ScrollState): void { state.firstVisibleLineMarker = state.firstVisibleLogicalLineMarker = undefined } -function cancelPendingFitScrollRestore(terminal: object): void { +function cancelPendingFitScrollRestore(terminal: TerminalScrollIntentTarget): void { const pending = pendingFitScrollRestores.get(terminal) if (!pending) { return diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts b/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts index a245f2d82b9..7ba4c418f1f 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-output-ack-credit.ts @@ -1,4 +1,7 @@ -type TerminalOutputAckTarget = object +import type { ForegroundTerminalOutputTarget } from './pane-terminal-foreground-render-settle' + +/** The xterm instance the credits belong to; only its reference is used as a key. */ +type TerminalOutputAckTarget = ForegroundTerminalOutputTarget const inFlightAckCompletions = new WeakMap void>>() diff --git a/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts b/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts index 0b0185035c3..829075d11dd 100644 --- a/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts +++ b/src/renderer/src/lib/pane-manager/terminal-parsed-dirty-rows.ts @@ -15,6 +15,13 @@ export type ParsedDirtyRowSpan = { start: number; end: number } type RequestRefreshRowsEvent = { start: number; end: number } | undefined +/** + * A terminal instance, used as the span cache's identity. Not `ParsedDirtyRowSource`: callers hold + * their own partial structural views of the same xterm terminal, so the internals below stay a + * defensive probe rather than a requirement on the caller's type. + */ +export type ParsedDirtyRowTerminal = WeakKey + type ParsedDirtyRowSource = { _core?: { _inputHandler?: { @@ -35,9 +42,9 @@ type ParsedDirtyRowTracker = { // `null` marks a terminal whose parse spans cannot be observed, so callers keep // the full-grid behavior instead of narrowing on an absent signal. -const trackersByTerminal = new WeakMap() +const trackersByTerminal = new WeakMap() -function attachTracker(terminal: object): ParsedDirtyRowTracker | null { +function attachTracker(terminal: ParsedDirtyRowTerminal): ParsedDirtyRowTracker | null { const existing = trackersByTerminal.get(terminal) if (existing !== undefined) { return existing @@ -82,7 +89,7 @@ function attachTracker(terminal: object): ParsedDirtyRowTracker | null { } /** Start (or reset) parse-span observation for the write that is about to run. */ -export function resetParsedDirtyRows(terminal: object): void { +export function resetParsedDirtyRows(terminal: ParsedDirtyRowTerminal): void { const tracker = attachTracker(terminal) if (!tracker) { return @@ -98,7 +105,9 @@ export function resetParsedDirtyRows(terminal: object): void { * unknown (unobservable terminal, no parse seen, or an xterm full-refresh * request) and the caller must repaint the whole viewport. */ -export function readParsedDirtyRowSpan(terminal: object): ParsedDirtyRowSpan | null { +export function readParsedDirtyRowSpan( + terminal: ParsedDirtyRowTerminal +): ParsedDirtyRowSpan | null { const tracker = trackersByTerminal.get(terminal) if (!tracker || !tracker.observed || tracker.wholeViewport) { return null @@ -106,7 +115,7 @@ export function readParsedDirtyRowSpan(terminal: object): ParsedDirtyRowSpan | n return { start: tracker.start, end: tracker.end } } -export function disposeParsedDirtyRows(terminal: object): void { +export function disposeParsedDirtyRows(terminal: ParsedDirtyRowTerminal): void { const tracker = trackersByTerminal.get(terminal) if (tracker) { try { diff --git a/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts b/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts index 3de8e79a9f1..8ecc03617f5 100644 --- a/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts +++ b/src/renderer/src/lib/pane-manager/terminal-scroll-intent-rebuild.ts @@ -1,15 +1,17 @@ +import type { TerminalScrollIntentTarget } from './terminal-scroll-intent' + // Why: buffer rebuilds (snapshot replay clear + rewrite) parse asynchronously. // Until the rebuild's bytes have parsed, viewportY/baseY describe a transient // half-cleared buffer; any intent capture/enforce latched from it pins the // terminal at line 0. Callers bracket the rebuild and re-apply intent once // after parse (see terminal-scroll-intent.ts). -const terminalScrollIntentRebuilds = new WeakMap() +const terminalScrollIntentRebuilds = new WeakMap() const terminalScrollIntentRebuildCompletions = new WeakMap< - object, + TerminalScrollIntentTarget, Set<(completed: boolean) => void> >() const deferredTerminalGeometryMutations = new WeakMap< - object, + TerminalScrollIntentTarget, { mutations: Map void> } @@ -30,11 +32,11 @@ function notifyRebuildCompletions( } } -export function beginTerminalScrollIntentBufferRebuild(terminal: object): void { +export function beginTerminalScrollIntentBufferRebuild(terminal: TerminalScrollIntentTarget): void { terminalScrollIntentRebuilds.set(terminal, (terminalScrollIntentRebuilds.get(terminal) ?? 0) + 1) } -export function endTerminalScrollIntentBufferRebuild(terminal: object): void { +export function endTerminalScrollIntentBufferRebuild(terminal: TerminalScrollIntentTarget): void { const count = terminalScrollIntentRebuilds.get(terminal) ?? 0 if (count <= 1) { terminalScrollIntentRebuilds.delete(terminal) @@ -46,12 +48,14 @@ export function endTerminalScrollIntentBufferRebuild(terminal: object): void { terminalScrollIntentRebuilds.set(terminal, count - 1) } -export function isTerminalScrollIntentRebuildInFlight(terminal: object): boolean { +export function isTerminalScrollIntentRebuildInFlight( + terminal: TerminalScrollIntentTarget +): boolean { return (terminalScrollIntentRebuilds.get(terminal) ?? 0) > 0 } export function onTerminalScrollIntentBufferRebuildComplete( - terminal: object, + terminal: TerminalScrollIntentTarget, completion: (completed: boolean) => void ): () => void { if (!isTerminalScrollIntentRebuildInFlight(terminal)) { @@ -75,7 +79,7 @@ export function onTerminalScrollIntentBufferRebuildComplete( // Why: source-dimension replay must finish and restore its viewport before // unrelated fit/resize work is allowed to reflow the rebuilt buffer. export function deferTerminalGeometryMutationDuringRebuild( - terminal: object, + terminal: TerminalScrollIntentTarget, operationKey: string, mutation: () => void ): boolean { @@ -117,7 +121,9 @@ export function deferTerminalGeometryMutationDuringRebuild( return true } -export function cancelTerminalScrollIntentBufferRebuildCompletions(terminal: object): void { +export function cancelTerminalScrollIntentBufferRebuildCompletions( + terminal: TerminalScrollIntentTarget +): void { const completions = terminalScrollIntentRebuildCompletions.get(terminal) terminalScrollIntentRebuildCompletions.delete(terminal) notifyRebuildCompletions(completions, false) diff --git a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts index 708beed44f5..0c7cad8f89d 100644 --- a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts +++ b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts @@ -6,7 +6,8 @@ import { releaseHiddenWebglRetention, resetHiddenWebglRetentionForTest, retainedHiddenWebglOwnerCountForTest, - tryRetainHiddenPanesWebgl + tryRetainHiddenPanesWebgl, + type HiddenWebglRetentionOwner } from './terminal-webgl-hidden-retention' function createPane(withAddon = true): ManagedPaneInternal { @@ -24,7 +25,7 @@ function createPane(withAddon = true): ManagedPaneInternal { } as unknown as ManagedPaneInternal } -function retentionFor(owner: object, panes: ManagedPaneInternal[]) { +function retentionFor(owner: HiddenWebglRetentionOwner, panes: ManagedPaneInternal[]) { return { owner, livePanes: () => panes } } diff --git a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts index e4ca8fa63b0..bd467b6e71a 100644 --- a/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts +++ b/src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.ts @@ -6,8 +6,11 @@ import { disposeWebgl } from './pane-webgl-renderer' // letting hidden worktrees grow that cost with the mounted-pane population. const MAX_RETAINED_HIDDEN_WEBGL_CONTEXTS = 6 +/** Identity of the surface whose hidden panes are retained; compared by reference only. */ +export type HiddenWebglRetentionOwner = WeakKey + type RetainedHiddenEntry = { - owner: object + owner: HiddenWebglRetentionOwner livePanes: () => Iterable } @@ -30,7 +33,7 @@ function disposeEntryContexts(entry: RetainedHiddenEntry): void { } } -function removeEntry(owner: object): void { +function removeEntry(owner: HiddenWebglRetentionOwner): void { const index = retainedEntries.findIndex((entry) => entry.owner === owner) if (index !== -1) { retainedEntries.splice(index, 1) @@ -43,7 +46,7 @@ function removeEntry(owner: object): void { * least-recently-hidden owners to stay under the context cap. */ export function tryRetainHiddenPanesWebgl( - owner: object, + owner: HiddenWebglRetentionOwner, livePanes: () => Iterable ): boolean { removeEntry(owner) @@ -68,7 +71,7 @@ export function tryRetainHiddenPanesWebgl( } /** Drop retention bookkeeping on reveal/destroy; never disposes live addons. */ -export function releaseHiddenWebglRetention(owner: object): void { +export function releaseHiddenWebglRetention(owner: HiddenWebglRetentionOwner): void { removeEntry(owner) } diff --git a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts index 5c67e7138ea..cd0c0adb2be 100644 --- a/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts +++ b/src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.ts @@ -20,26 +20,26 @@ export type UndeliverableWriteReason = 'write-stalled' | 'replay-wedged' type UndeliverableWriteHandler = (reason: UndeliverableWriteReason) => void -const handlersByTerminal = new WeakMap() -const certifiedDeadTerminals = new WeakSet() +const handlersByTerminal = new WeakMap() +const certifiedDeadTerminals = new WeakSet() // Why: wedge verdicts must distinguish "dead" from "alive but behind". A // generation avoids same-millisecond misses and wall-clock adjustments while // keeping the completion hot path constant-time and terminal-scoped. -const parseProgressGenerationByTerminal = new WeakMap() +const parseProgressGenerationByTerminal = new WeakMap() /** Report one parsed write completion for this terminal. */ -export function recordTerminalParseProgress(terminal: object): void { +export function recordTerminalParseProgress(terminal: WriteTarget): void { const nextGeneration = (parseProgressGenerationByTerminal.get(terminal) ?? 0) + 1 parseProgressGenerationByTerminal.set(terminal, nextGeneration) } /** Capture the current parse-progress generation for a later quiet-window check. */ -export function captureTerminalParseProgressGeneration(terminal: object): number { +export function captureTerminalParseProgressGeneration(terminal: WriteTarget): number { return parseProgressGenerationByTerminal.get(terminal) ?? 0 } /** Whether a write completion parsed after `generation` was captured. */ -export function hasTerminalParseProgressSince(terminal: object, generation: number): boolean { +export function hasTerminalParseProgressSince(terminal: WriteTarget, generation: number): boolean { return captureTerminalParseProgressGeneration(terminal) !== generation } @@ -51,11 +51,11 @@ type StallWatch = { mode: StallWatchMode } -const stallWatchByTerminal = new WeakMap() +const stallWatchByTerminal = new WeakMap() export const WRITE_PIPELINE_STALL_CHECK_MS = 10_000 -function certifyTerminalWritePipelineDead(terminal: object, expectedWatch?: StallWatch): void { +function certifyTerminalWritePipelineDead(terminal: WriteTarget, expectedWatch?: StallWatch): void { const watch = stallWatchByTerminal.get(terminal) // Why: a real parse can settle and remove the watch before a stale probe // deadline runs. Only the watch that armed that deadline may certify. @@ -75,7 +75,7 @@ function certifyTerminalWritePipelineDead(terminal: object, expectedWatch?: Stal } export function registerUndeliverableWriteHandler( - terminal: object, + terminal: WriteTarget, handler: UndeliverableWriteHandler ): () => void { handlersByTerminal.set(terminal, handler) @@ -88,7 +88,10 @@ export function registerUndeliverableWriteHandler( /** One notification per terminal instance: recovery replaces the xterm, so a * second notification for the same object is always a duplicate. */ -export function notifyUndeliverableWrite(terminal: object, reason: UndeliverableWriteReason): void { +export function notifyUndeliverableWrite( + terminal: WriteTarget, + reason: UndeliverableWriteReason +): void { if (certifiedDeadTerminals.has(terminal)) { return } @@ -102,7 +105,7 @@ export function notifyUndeliverableWrite(terminal: object, reason: Undeliverable } } -export function isTerminalWritePipelineCertifiedDead(terminal: object): boolean { +export function isTerminalWritePipelineCertifiedDead(terminal: WriteTarget): boolean { return certifiedDeadTerminals.has(terminal) } @@ -196,7 +199,7 @@ export function requestTerminalWritePipelineProbe( } /** Cancel a pending watch without claiming that any bytes parsed. */ -export function cancelTerminalWriteStallWatch(terminal: object): void { +export function cancelTerminalWriteStallWatch(terminal: WriteTarget): void { const watch = stallWatchByTerminal.get(terminal) if (!watch) { return @@ -206,7 +209,7 @@ export function cancelTerminalWriteStallWatch(terminal: object): void { } /** Write completed normally — the pipeline is healthy; drop any pending watch. */ -export function settleTerminalWriteStallWatch(terminal: object): void { +export function settleTerminalWriteStallWatch(terminal: WriteTarget): void { recordTerminalParseProgress(terminal) if (stallWatchByTerminal.get(terminal)?.mode === 'fifo-probe') { return @@ -216,11 +219,11 @@ export function settleTerminalWriteStallWatch(terminal: object): void { /** A synchronous terminal.write failure proves the pipeline cannot accept the * issued bytes. Recover immediately without reporting fake parse progress. */ -export function failTerminalWriteStallWatch(terminal: object): void { +export function failTerminalWriteStallWatch(terminal: WriteTarget): void { certifyTerminalWritePipelineDead(terminal) } -export function _resetWritePipelineHealthForTests(terminal?: object): void { +export function _resetWritePipelineHealthForTests(terminal?: WriteTarget): void { if (terminal) { const watch = stallWatchByTerminal.get(terminal) if (watch) { diff --git a/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts b/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts index 17489399883..11a545ae2cf 100644 --- a/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts +++ b/src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts @@ -13,7 +13,7 @@ import { // Not an intersection with ErrorConstructor: both fields have to stay optional // so the absent-captureStackTrace platform can be simulated. type ErrorWithCapture = { - captureStackTrace?: (target: object, constructorOpt?: unknown) => void + captureStackTrace?: (target: { stack?: string }, constructorOpt?: unknown) => void stackTraceLimit?: number } const errorWithCapture = Error as unknown as ErrorWithCapture diff --git a/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts b/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts index ef8565255a8..6801a5c49b3 100644 --- a/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts +++ b/src/renderer/src/lib/react-commit-cascade-store-write-samples.ts @@ -33,6 +33,9 @@ export const MAX_REPORTED_CHANGED_KEYS = 12 type SampledWrite = { stack?: string } +/** The wrapping `set` function: V8 elides it and every frame above it, so only identity matters. */ +export type ReactCommitCascadeWriteBoundary = (...args: never[]) => unknown + let storeWrites = 0 let samples: SampledWrite[] = [] let changedKeys: Set | null = null @@ -58,10 +61,12 @@ export function resetReactCommitCascadeWriteSamples(): void { /** * Call only while armed. `boundary` is the wrapping `set` function, so V8 elides - * our own frames and the first captured frame is the real caller. Typed as - * `object` because zustand's `set` is an overload set, not a plain signature. + * our own frames and the first captured frame is the real caller. */ -export function noteReactCommitCascadeStoreWrite(boundary: object, partial: unknown): void { +export function noteReactCommitCascadeStoreWrite( + boundary: ReactCommitCascadeWriteBoundary, + partial: unknown +): void { storeWrites += 1 // Why the write count and not samples.length: samples only grows where // Error.captureStackTrace exists, so that cap would never engage without it and @@ -76,23 +81,20 @@ export function noteReactCommitCascadeStoreWrite(boundary: object, partial: unkn changedKeys.add(key) } } - const capture = Error as ErrorConstructor & { - captureStackTrace?: (target: object, constructorOpt?: unknown) => void - stackTraceLimit?: number - } - if (typeof capture.captureStackTrace !== 'function') { + // Only V8 has it; a non-V8 host gets no samples rather than a synthesized stack. + if (typeof Error.captureStackTrace !== 'function') { return } - const previousLimit = capture.stackTraceLimit + const previousLimit = Error.stackTraceLimit const sample: SampledWrite = {} try { - capture.stackTraceLimit = CAPTURE_STACK_FRAME_LIMIT - capture.captureStackTrace(sample, boundary) + Error.stackTraceLimit = CAPTURE_STACK_FRAME_LIMIT + Error.captureStackTrace(sample, boundary) samples.push(sample) } catch { // Best-effort crash evidence only. } finally { - capture.stackTraceLimit = previousLimit + Error.stackTraceLimit = previousLimit } } diff --git a/src/renderer/src/lib/simulator-launch-coordination.ts b/src/renderer/src/lib/simulator-launch-coordination.ts index db363683385..fee47276361 100644 --- a/src/renderer/src/lib/simulator-launch-coordination.ts +++ b/src/renderer/src/lib/simulator-launch-coordination.ts @@ -87,7 +87,10 @@ export function dispatchManualSimulatorLaunchFailed(worktreeId: string, message: }) } -function dispatchManualSimulatorLaunchEvent(type: string, detail: object): void { +function dispatchManualSimulatorLaunchEvent( + type: string, + detail: { worktreeId: string; message?: string } +): void { if (typeof window === 'undefined') { return } diff --git a/src/renderer/src/lib/state-collection-byte-estimate.ts b/src/renderer/src/lib/state-collection-byte-estimate.ts index 2491826420b..8f1d28dc16f 100644 --- a/src/renderer/src/lib/state-collection-byte-estimate.ts +++ b/src/renderer/src/lib/state-collection-byte-estimate.ts @@ -149,7 +149,9 @@ function estimateValueBytes(value: unknown, depth: number, ctx: EstimateContext) if (ArrayBuffer.isView(value)) { return BYTES_OBJECT_BASE + value.byteLength } - return BYTES_OBJECT_BASE + estimatePlainObjectEntries(value, depth, ctx) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the typeof switch and null check above leave only a non-null, non-collection object here. + const plainObject = value as Record + return BYTES_OBJECT_BASE + estimatePlainObjectEntries(plainObject, depth, ctx) } function estimateArrayElements(value: unknown[], depth: number, ctx: EstimateContext): number { @@ -208,7 +210,11 @@ function estimateIterableEntries( : Math.round((sampledBytes / sampledCount + BYTES_ENTRY_OVERHEAD) * size) } -function estimatePlainObjectEntries(value: object, depth: number, ctx: EstimateContext): number { +function estimatePlainObjectEntries( + value: Record, + depth: number, + ctx: EstimateContext +): number { let ownCount = 0 const sampledKeys: string[] = [] const entryFloor = depth === 0 ? ENTRY_DESCENT_RESERVE : 0 @@ -232,7 +238,7 @@ function estimatePlainObjectEntries(value: object, depth: number, ctx: EstimateC let sampledBytes = 0 for (const key of sampledKeys) { sampledBytes += BYTES_STRING_BASE + key.length * BYTES_PER_CHAR - sampledBytes += estimateValueBytes((value as Record)[key], depth + 1, ctx) + sampledBytes += estimateValueBytes(value[key], depth + 1, ctx) } return sampledKeys.length === 0 ? 0 diff --git a/src/renderer/src/store/react-commit-cascade-write-probe.test.ts b/src/renderer/src/store/react-commit-cascade-write-probe.test.ts index 0c18706e976..34d9db8fb6e 100644 --- a/src/renderer/src/store/react-commit-cascade-write-probe.test.ts +++ b/src/renderer/src/store/react-commit-cascade-write-probe.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create, type StoreApi, type UseBoundStore } from 'zustand' import { withReactCommitCascadeWriteProbe } from './react-commit-cascade-write-probe' +import type { ReactCommitCascadeWriteBoundary } from '@/lib/react-commit-cascade-store-write-samples' const { probe, noteWrite } = vi.hoisted(() => ({ probe: { armed: false }, @@ -8,7 +9,7 @@ const { probe, noteWrite } = vi.hoisted(() => ({ })) vi.mock('@/lib/react-commit-cascade-store-write-samples', () => ({ reactCommitCascadeWriteProbe: probe, - noteReactCommitCascadeStoreWrite: (boundary: object, partial: unknown) => + noteReactCommitCascadeStoreWrite: (boundary: ReactCommitCascadeWriteBoundary, partial: unknown) => noteWrite(boundary, partial) })) diff --git a/src/shared/browser-client-host-reconciliation-protocol.test.ts b/src/shared/browser-client-host-reconciliation-protocol.test.ts index 3d3fc526cc6..9ce314ee9b2 100644 --- a/src/shared/browser-client-host-reconciliation-protocol.test.ts +++ b/src/shared/browser-client-host-reconciliation-protocol.test.ts @@ -19,7 +19,10 @@ const inventoryPage = { state: 'active' as const } -const command = (reconciliationCommand: object) => ({ +/** Raw command input for the parser under test, including deliberately malformed shapes. */ +type RawReconciliationCommand = { type: string } & Record + +const command = (reconciliationCommand: RawReconciliationCommand) => ({ type: 'command' as const, authorityRuntimeId: 'runtime-a', authorityEpoch: 'epoch-new', diff --git a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts index 2f108571dcf..72a460a7146 100644 --- a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts +++ b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts @@ -4,7 +4,7 @@ import type { GitLabPipelineJob } from './gitlab-types' // Repro for #7732: the Checks side panel can only ask for a GitLab job trace if the mapped // check row still carries the numeric GitLab job id (gitlab:jobTrace takes { jobId }). -function numericHandles(value: object): number[] { +function numericHandles(value: Record): number[] { return Object.values(value).filter((v): v is number => typeof v === 'number') }