fix(lint): enable anti-slop/no-object-parameters (#20781)

The rule rejects the broad `object` type on any function input (declarations,
expressions, arrows, methods, call/construct signatures, function types), plus
local aliases and unions that resolve to `object`. `object` accepts every
non-primitive while exposing no properties, so it documents nothing and pushes
callers into assertions at the boundary.

Fixes all 185 violations across src, config, tests and mobile, and flips the
rule from "off" to "error" in config/oxlint-anti-slop.json.

Approach: replace each `object` input with the type its owner already has.
Most sites took an existing domain type or a type-only import (36 added);
40 new aliases name shapes that had none. Where a value is genuinely only
compared by reference, it gets a named identity token instead of a shape --
`Record<string, never>`, the built-in `WeakKey`, or a `unique symbol` brand,
matching the branding already used in src/shared. Same treatment for WeakMap
and Map key parameters. Two `as unknown as` casts became unnecessary once the
parameter carried a real type and were removed; no new casts were added.

Suppressions added: none. No `oxlint-disable` for this rule anywhere, and no
max-lines disable or per-file bump.

Three files sat exactly at their max-lines cap, so the added type imports were
made line-neutral rather than suppressed:
- src/main/ipc/browser.ts exports the existing guest-registration args type
  (renamed BrowserGuestArgs) so browser.test.ts reuses it on one line.
- pane-scroll.ts takes TerminalScrollIntentTarget through the existing
  pane-manager-types import via a type-only re-export.
- direct-rpc-client.ts drops the identity parameter entirely: the session
  check moved into the sendProbe callback that owns the token.

Verified: anti-slop config reports zero violations over src config tests
mobile; run-typecheck-projects-in-parallel exits 0; 144 affected test files
pass (1749 tests); oxlint and oxfmt clean on all changed files. Mobile has no
runnable test/typecheck target in this worktree (expo is not installed), so
its 6 files were typechecked against a standalone config and diffed against
the base branch -- error sets are byte-identical, including test files.
This commit is contained in:
Neil
2026-09-15 01:59:58 -07:00
committed by GitHub
parent e4a9d24e0c
commit bfdec26352
121 changed files with 593 additions and 298 deletions
+1 -1
View File
@@ -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",
@@ -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<string, unknown>,
...sources: readonly Record<string, unknown>[]
) => {
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<string, unknown>) => {
const result = nativeObjectValues(value)
freshnessEntryVisits += result.length
return result
@@ -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<unknown>()
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)
}
@@ -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<string, never>
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) {
@@ -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<Record<string, never>>
/** 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<string, number>()
private readonly absentSinceExhaustion = new Set<string>()
private readonly announcedExhaustion = new Set<string>()
private readonly retryGenerationByHandle = new Map<string, object>()
private readonly retryGenerationByHandle = new Map<string, RetryGenerationToken>()
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
}
+3 -3
View File
@@ -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({
@@ -1,4 +1,5 @@
export type HostClientAcquisition = object
/** Holder identity token: the registry only compares references, never reads fields. */
export type HostClientAcquisition = Record<string, never>
export class HostClientAcquisitionRegistry {
private readonly acquisitions = new Map<string, Set<HostClientAcquisition>>()
+17 -6
View File
@@ -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<RelayDialStageSource>
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<RelayDialStageSource>
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 {
@@ -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'
@@ -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)
}
@@ -207,7 +207,10 @@ class ArtifactFaultServer {
rejectNextDeleteCode: string | null = null
rejectNextUpdateStatus: number | null = null
private readonly artifacts = new Map<string, string>()
private readonly createsByKey = new Map<string, { body: string; response: object }>()
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<string | null> {
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<string, unknown>
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,
@@ -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[]) {
@@ -54,7 +54,13 @@ export type CookieClearSession = {
restoreClearIdentities: CookieClearStore['restoreClearIdentities']
}
const mutationLocks = new WeakMap<object, Promise<void>>()
/**
* 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<CookieMutationLockOwner, Promise<void>>()
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<void>((resolve) => {
@@ -99,7 +107,10 @@ export async function acquireCookieMutationLock(owner: object): Promise<() => vo
return release
}
export async function withCookieMutationLock<T>(owner: object, run: () => Promise<T>): Promise<T> {
export async function withCookieMutationLock<T>(
owner: CookieMutationLockOwner,
run: () => Promise<T>
): Promise<T> {
const release = await acquireCookieMutationLock(owner)
try {
return await run()
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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,
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
set?: (details: Record<string, unknown>) => Promise<void>
}
}) => ({
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.
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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<string, unknown>; url: string }[]) =>
items.map(({ cookie, url }) => ({ url, ...cookie })),
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
set?: (details: Record<string, unknown>) => Promise<void>
}
}) => ({
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.
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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<string, unknown>; url: string }[]) =>
items.map(({ cookie, url }) => ({ url, ...cookie })),
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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<string, unknown>; url: string }[]) =>
items.map(({ cookie, url }) => ({ url, ...cookie })),
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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<string, unknown>; url: string }[]) =>
items.map(({ cookie, url }) => ({ url, ...cookie })),
@@ -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<unknown>
get: (filter: CookiesGetFilter) => Promise<unknown>
remove: (url: string, name: string) => Promise<void>
}
}) => ({
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<string, unknown>; url: string }[]) =>
items.map(({ cookie, url }) => ({ url, ...cookie })),
@@ -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<CdpKeyEvent>) => {
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<CdpKeyEvent>) => {
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<CdpKeyEvent>) => {
expect(parseCdpKeyEvent(raw)).toMatchObject({ ...expected, location: 1, text: null })
}
)
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
publishDocPreviewFailure: vi.fn(),
boundGrantIdByGuest: new Map<object, string>(),
boundGrantIdByGuest: new Map<Electron.WebContents, string>(),
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) => {
@@ -263,6 +263,6 @@ function createFixture(): {
}
}
function writeAuth(home: string, auth: object): void {
function writeAuth(home: string, auth: Record<string, unknown>): void {
writeFileSync(join(home, 'auth.json'), JSON.stringify(auth), { mode: 0o600 })
}
@@ -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<Record<MigrationFailureCountKey, unknown>>
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<string, unknown>)[key] : undefined
function readPositiveResultCount(
result: MigrationResultFields,
key: MigrationFailureCountKey
): boolean {
const value = result[key]
return typeof value === 'number' && value > 0
}
@@ -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<string>,
opts?: { final?: boolean; teardown?: boolean }
): Promise<Set<string>>
runExclusiveCheckpoint(operation: () => Promise<void>, options?: object): Promise<void>
runExclusiveCheckpoint(
operation: () => Promise<void>,
options?: { rescheduleDirty?: boolean; callerDeadlineMs?: number }
): Promise<void>
}
const originalCheckpointSessions = internals.checkpointSessions.bind(historyAdapter)
// Call-through spy: entering the exclusive gate is the observable "queued behind the in-flight checkpoint" moment.
+6 -2
View File
@@ -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(
+3 -2
View File
@@ -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<string, GitCapabilityCache>()
// 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<object, GitCapabilityCache>()
let sshCapabilitiesByProvider = new WeakMap<SshGitProvider, GitCapabilityCache>()
function getLocalGitExecutionHostKey(target: LocalGitCapabilityTarget): string {
const wslDistro =
@@ -56,7 +57,7 @@ export function withLocalGitCapabilityCacheForExecution<T>(
)
}
export function getSshGitCapabilityCache(provider: object): GitCapabilityCache {
export function getSshGitCapabilityCache(provider: SshGitProvider): GitCapabilityCache {
let cache = sshCapabilitiesByProvider.get(provider)
if (!cache) {
cache = new GitCapabilityCache()
@@ -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<string, unknow
}
/** The viewport bridge is handed a resolver rather than the contents, so unwrap one call argument. */
function resolvesToGuest(argument: unknown, guest: object): boolean {
function resolvesToGuest(argument: unknown, guest: PreviewGuestContents): boolean {
return argument === guest || (typeof argument === 'function' && argument() === guest)
}
+3 -2
View File
@@ -68,7 +68,7 @@ vi.mock('../browser/browser-manager', () => ({
}
}))
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(
{
+4 -4
View File
@@ -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)
)
+6 -2
View File
@@ -207,12 +207,16 @@ export async function withPlatform<T>(
}
}
function collectMocks(moduleMock: object): IpcMock[] {
function isMockContainer(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function collectMocks(moduleMock: Record<string, unknown>): 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) : []
})
}
@@ -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<WatcherProcessSubscription> {
if (this.subscribeError) {
+5 -1
View File
@@ -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<GlobalSettings>) => ({
...before,
...args
}))
registerSettingsHandlers(store as never)
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
event: typeof settingsInvokeEvent,
@@ -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> = {}): GitWorktreeInfo {
return {
@@ -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<WorktreeMeta>) => ({
...metaById[id],
...updates
}))
+2 -1
View File
@@ -1,4 +1,5 @@
import { type Mock, vi } from 'vitest'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
export type HandlerMap = Record<string, (_event: unknown, args: unknown) => 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<WorktreeMeta>) => unknown>
export type TestMainWindow = {
isDestroyed: () => boolean
@@ -86,7 +86,7 @@ export function closeWslTranscriptFsProcess(handle: WslTranscriptFsProcessHandle
}
export function isWslTranscriptFsProcessHandle(
value: object
value: FileHandle | WslTranscriptFsProcessHandle
): value is WslTranscriptFsProcessHandle {
return 'wslTranscriptFsProcessHandle' in value
}
@@ -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<string, number> = {
'socks5:': 1080
}
let proxyCredentialsBySession = new WeakMap<object, ElectronProxyCredentials>()
let proxyCredentialsBySession = new WeakMap<ProxySession, ElectronProxyCredentials>()
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
@@ -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<string, unknown> }
type PluginEventHandler = (input: { event: PluginEvent }) => Promise<void>
type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise<void> }
@@ -83,7 +87,7 @@ describe('OpenCode plugin fail-open ownership', () => {
return loadHooksWithSession({ list })
}
async function loadHooksWithSession(session: object): Promise<PluginHooks> {
async function loadHooksWithSession(session: SessionClientFixture): Promise<PluginHooks> {
return loadHooksWithContext({ client: { session } })
}
@@ -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<string, unknown> }
type PluginEventHandler = (input: { event: PluginEvent }) => Promise<void>
type PluginHooks = { event: PluginEventHandler; dispose?: () => Promise<void> }
@@ -83,7 +89,7 @@ describe('OpenCode plugin lifecycle delivery', () => {
return loadHooksWithSession({ list })
}
async function loadHooksWithSession(session: object): Promise<PluginHooks> {
async function loadHooksWithSession(session: SessionClientFixture): Promise<PluginHooks> {
const pluginPath = join(tempDir, 'orca-opencode-status.mjs')
writeFileSync(pluginPath, _internals.getOpenCodePluginSource())
const module = (await import(pathToFileURL(pluginPath).href)) as {
@@ -243,7 +243,7 @@ export function getAutomationRunWorkspaceDisplayName(
}
export function installAutomationPersistenceContext(
target: object,
target: AutomationPersistence,
source: AutomationPersistence
): void {
Object.defineProperty(target, automationPersistenceContext, {
@@ -316,7 +316,7 @@ export function removeWorkspaceLineageForFolderParent(
}
export function installMetadataLineageOperationsContext(
target: object,
target: MetadataLineageOperations,
source: MetadataLineageOperations
): void {
Object.defineProperty(target, metadataLineageOperationsContext, {
@@ -33,7 +33,7 @@ export class MobileTabSelectionPersistence {
}
export function installMobileTabSelectionPersistenceContext(
target: object,
target: MobileTabSelectionPersistence,
source: MobileTabSelectionPersistence
): void {
Object.defineProperty(target, mobileTabSelectionPersistenceContext, {
@@ -280,7 +280,7 @@ export function writeToDiskSync(
}
export function installPrimaryStateWriteOperationsContext(
target: object,
target: PrimaryStateWriteOperations,
source: PrimaryStateWriteOperations
): void {
Object.defineProperty(target, primaryStateWriteOperationsContext, {
@@ -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]
})
@@ -226,7 +226,7 @@ export function getFolderWorkspaceOperations(
}
export function installProjectCollectionOperationsContext(
target: object,
target: ProjectCollectionOperations,
source: ProjectCollectionOperations
): void {
Object.defineProperty(target, projectCollectionOperationsContext, {
@@ -266,7 +266,7 @@ function applyPtyBinding(
}
export function installPtyBindingPersistenceOperationsContext(
target: object,
target: PtyBindingPersistenceOperations,
source: PtyBindingPersistenceOperations
): void {
Object.defineProperty(target, ptyBindingPersistenceOperationsContext, {
@@ -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, {
@@ -110,7 +110,7 @@ export function applyRetiredWorktreeNames(
}
export function installRetiredWorktreeNamePersistenceContext(
target: object,
target: RetiredWorktreeNamePersistence,
source: RetiredWorktreeNamePersistence
): void {
Object.defineProperty(target, retiredWorktreeNamePersistenceContext, {
@@ -210,7 +210,7 @@ export function setHostWorkspaceSession(
}
export function installSessionHostPartitionOperationsContext(
target: object,
target: SessionHostPartitionOperations,
source: SessionHostPartitionOperations
): void {
Object.defineProperty(target, sessionHostPartitionOperationsContext, {
@@ -93,7 +93,7 @@ export function getSessionSnapshotOperationsContext(owner: SessionSnapshotOperat
}
export function installSessionSnapshotOperationsContext(
target: object,
target: SessionSnapshotOperations,
source: SessionSnapshotOperations
): void {
Object.defineProperty(target, sessionSnapshotOperationsContext, {
@@ -46,7 +46,7 @@ export class SparsePresetPersistence {
}
export function installSparsePresetPersistenceContext(
target: object,
target: SparsePresetPersistence,
source: SparsePresetPersistence
): void {
Object.defineProperty(target, sparsePresetPersistenceContext, {
@@ -245,7 +245,7 @@ export function getSshPtyLeaseOperations(owner: SshLeaseRecoveryOperations): Ssh
}
export function installSshLeaseRecoveryOperationsContext(
target: object,
target: SshLeaseRecoveryOperations,
source: SshLeaseRecoveryOperations
): void {
Object.defineProperty(target, sshLeaseRecoveryOperationsContext, {
@@ -146,7 +146,7 @@ export function getSshTargetStateOperations(owner: SshProfileOperations): SshTar
}
export function installSshProfileOperationsContext(
target: object,
target: SshProfileOperations,
source: SshProfileOperations
): void {
Object.defineProperty(target, sshProfileOperationsContext, {
@@ -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)
@@ -269,7 +269,7 @@ export function writeGithubCacheSnapshotSync(owner: WriteFlushBarrierOperations)
}
export function installWriteFlushBarrierOperationsContext(
target: object,
target: WriteFlushBarrierOperations,
source: WriteFlushBarrierOperations
): void {
Object.defineProperty(target, writeFlushBarrierOperationsContext, {
@@ -64,7 +64,7 @@ export function scheduleSave(owner: WriteSchedulingOperations): void {
}
export function installWriteSchedulingOperationsContext(
target: object,
target: WriteSchedulingOperations,
source: WriteSchedulingOperations
): void {
Object.defineProperty(target, writeSchedulingOperationsContext, {
@@ -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<SshRemotePtyLease>) => ({
targetId: 'builder',
ptyId: `pty-${index}`,
worktreeId,
@@ -21,7 +21,11 @@ type RuntimeFileChannelHost = {
statRuntimeFile(worktree: string, relativePath: string): Promise<unknown>
}
const stores = new WeakMap<object, BrowserClientDownloadTransferStore>()
// 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<DownloadTransferRuntime, BrowserClientDownloadTransferStore>()
/**
* Drops every staged download a page still owns.
@@ -31,7 +35,7 @@ const stores = new WeakMap<object, BrowserClientDownloadTransferStore>()
* opened a file channel.
*/
export function releaseBrowserClientDownloadTransfersForPage(
runtime: object,
runtime: DownloadTransferRuntime,
browserPageId: string
): Promise<void> {
return stores.get(runtime)?.releasePage(browserPageId) ?? Promise.resolve()
@@ -18,7 +18,9 @@ function createRuntime() {
return { runtime, removed }
}
async function stageTransfer(runtime: object, browserPageId: string): Promise<void> {
type FakeRuntime = ReturnType<typeof createRuntime>['runtime']
async function stageTransfer(runtime: FakeRuntime, browserPageId: string): Promise<void> {
await getBrowserClientDownloadTransferStore(runtime as never).accept({
transferId: `transfer-${browserPageId}`,
browserPageId,
@@ -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<string, unknown>
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)
}
}
@@ -269,7 +269,7 @@ export class RelayControlClient {
this.clearConnectPromise()
}
private sendActive(payload: object): void {
private sendActive(payload: Record<string, unknown>): void {
if (!this.socket || (this.state !== 'active' && this.state !== 'draining')) {
throw new Error('relay_control_not_active')
}
@@ -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<string, PendingRequest>()
@@ -34,7 +51,7 @@ export class RelayControlRequests {
createInvite(
reqId: string,
relayDeviceId: string,
send: (payload: object) => void
send: SendRelayControlRequest
): Promise<RelayInviteCreatedMessage> {
return this.request(
reqId,
@@ -44,11 +61,7 @@ export class RelayControlRequests {
) as Promise<RelayInviteCreatedMessage>
}
revokeDevice(
reqId: string,
relayDeviceId: string,
send: (payload: object) => void
): Promise<void> {
revokeDevice(reqId: string, relayDeviceId: string, send: SendRelayControlRequest): Promise<void> {
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<RelayDeviceCredentialInstalledMessage> {
return this.request(
reqId,
@@ -78,7 +86,7 @@ export class RelayControlRequests {
credentialInstallStatus(
reqId: string,
relayDeviceId: string,
send: (payload: object) => void
send: SendRelayControlRequest
): Promise<RelayDeviceCredentialInstallStatusResultMessage> {
return this.request(
reqId,
@@ -91,7 +99,7 @@ export class RelayControlRequests {
confirmResume(
reqId: string,
basisConnId: string,
send: (payload: object) => void
send: SendRelayControlRequest
): Promise<RelayDeviceResumeConfirmedMessage> {
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<unknown> {
if (this.pending.has(reqId)) {
return Promise.reject(new Error('duplicate_relay_request_id'))
@@ -226,9 +226,11 @@ export class RuntimeBrowserPageRegistry {
}
}
const registries = new WeakMap<object, RuntimeBrowserPageRegistry>()
/** 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<WeakKey, RuntimeBrowserPageRegistry>()
export function getRuntimeBrowserPageRegistry(runtime: object): RuntimeBrowserPageRegistry {
export function getRuntimeBrowserPageRegistry(runtime: WeakKey): RuntimeBrowserPageRegistry {
let registry = registries.get(runtime)
if (!registry) {
registry = new RuntimeBrowserPageRegistry()
@@ -13,9 +13,12 @@ type LinearFacadeInstance = {
type LinearMethodBag = Record<string, (...values: unknown[]) => unknown>
const delegators = new WeakSet<object>()
const receiverByCommands = new WeakMap<object, object>()
const receiverByCommands = new WeakMap<LinearMethodBag, LinearMethodBag>()
function collectMethodNames(instancePrototype: object, stopAt: object | null): Set<string> {
function collectMethodNames(
instancePrototype: RuntimeLinearBrowseCommands,
stopAt: RuntimeLinearBrowseCommands | null
): Set<string> {
const names = new Set<string>()
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<string>
): 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
@@ -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<Record<keyof TeardownRuntime, unknown>>
function runtimeDouble(hooks: TeardownRuntimeStubs): TeardownRuntime {
return Object.assign(Object.create(null), hooks)
}
@@ -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'
@@ -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<HostedReviewInfo | null>
/** 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<HostedReviewInfo | null> {
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
@@ -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/)
@@ -20,7 +20,9 @@ type BackfillScan = {
outstanding: boolean
}
const scansByStore = new WeakMap<object, Map<string, BackfillScan>>()
/** 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<WeakKey, Map<string, BackfillScan>>()
/** 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<unknown>): Promise<void> {
* 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<RetirementScanResult>
): Promise<Set<string>> {
@@ -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', () => {
+6 -5
View File
@@ -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
@@ -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<WatcherProcessSubscription> {
const unsubscribe = vi.fn(async () => undefined)
@@ -16,8 +16,15 @@ const testState = vi.hoisted(() => ({
runtimeEnvironments: [] as { id: string; createdAt: number; pairingRevision?: number }[]
}))
type MockedAppStoreState = {
settings: GlobalSettings | null
updateSettings: (settings: Partial<GlobalSettings>) => void
runtimeEnvironments: { id: string; createdAt: number; pairingRevision?: number }[]
runtimeStatusByEnvironmentId: Map<string, unknown>
}
vi.mock('@/store', () => ({
useAppStore: (selector: (state: object) => unknown) =>
useAppStore: (selector: (state: MockedAppStoreState) => unknown) =>
selector({
settings: testState.settings,
updateSettings: testState.updateSettings,
@@ -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<string, unknown>) => {
entries += 1
return realEntries(target)
}) as typeof Object.entries
@@ -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 }) =>
@@ -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<object, readonly Range[]>()
const activeRangeByInstance = new Map<object, Range>()
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<MarkdownPreviewSearchInstance, readonly Range[]>()
const activeRangeByInstance = new Map<MarkdownPreviewSearchInstance, Range>()
// 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<ReturnType<typeof getHighlightApi
}
}
export function clearMarkdownPreviewSearchHighlights(instanceId: object): void {
export function clearMarkdownPreviewSearchHighlights(
instanceId: MarkdownPreviewSearchInstance
): void {
searchRangesByInstance.delete(instanceId)
activeRangeByInstance.delete(instanceId)
const api = getHighlightApi()
@@ -261,7 +270,7 @@ export function clearMarkdownPreviewSearchHighlights(instanceId: object): void {
}
export function applyMarkdownPreviewSearchHighlights(
instanceId: object,
instanceId: MarkdownPreviewSearchInstance,
root: HTMLElement,
query: string
): Range[] {
@@ -309,7 +318,7 @@ export function applyMarkdownPreviewSearchHighlights(
}
export function setActiveMarkdownPreviewSearchMatch(
instanceId: object,
instanceId: MarkdownPreviewSearchInstance,
matches: readonly Range[],
activeIndex: number
): void {
@@ -14,8 +14,11 @@ function createEditor(
} as unknown as Editor
}
/** Identity-only stand-in for `document.activeElement`: the policy compares it, never reads it. */
type StubbedActiveElement = Record<string, never>
function setupScheduledFocus(
activeElement: object | null,
activeElement: StubbedActiveElement | null,
force = false
): {
focus: ReturnType<typeof vi.fn>
@@ -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: [
@@ -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({
@@ -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<string, unknown> } }
}
const actual = await vi.importActual<{
Paragraph: {
extend: (config: Record<string, unknown>) => { config: Record<string, unknown> }
}
}>('@tiptap/extension-paragraph')
// Simulates a Tiptap upgrade that drops `parseMarkdown` from the upstream paragraph.
const Paragraph = actual.Paragraph.extend({})
Paragraph.config.parseMarkdown = undefined
@@ -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: [
@@ -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<Range[]>([])
const searchInstanceRef = useRef<object>({})
const searchInstanceRef = useRef<MarkdownPreviewSearchInstance>({})
const lastAppliedInitialAnchorRef = useRef<string | null>(null)
const pendingEditorRevealFrameIdsRef = useRef<number[]>([])
const [isSearchOpen, setIsSearchOpen] = useState(false)
@@ -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
@@ -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<React.SetStateAction<GitHubChecksTabState>>
setRefreshingOwner: React.Dispatch<
React.SetStateAction<{ contextOwner: object; requestId: number } | null>
React.SetStateAction<{ contextOwner: GitHubChecksContextOwner; requestId: number } | null>
>
setRerunningOwner: React.Dispatch<React.SetStateAction<object | null>>
setRerunningOwner: React.Dispatch<React.SetStateAction<GitHubChecksContextOwner | null>>
onChecksUpdated: (checks: PRCheckDetail[]) => void
}
export async function refreshGitHubChecksTab(
ctx: ChecksTabActionContext,
expectedContextOwner?: object
expectedContextOwner?: GitHubChecksContextOwner
): Promise<PRCheckDetail[] | null> {
if (!ctx.canUseChecksRepoContext) {
toast.error(
@@ -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<string, never>
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<PRCheckDetail[] | null> =>
async (expectedContextOwner?: ChecksContextOwner): Promise<PRCheckDetail[] | null> =>
refreshGitHubChecksTab(
{
canUseChecksRepoContext,
@@ -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<PRCheckDetail[] | null>
handleRefresh: (expectedContextOwner?: ChecksContextOwner) => Promise<PRCheckDetail[] | null>
}): Promise<void> {
if (!args.canUseChecksRepoContext || args.rerunning) {
return
@@ -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<number | null>(null)
const [refreshingOwner, setRefreshingOwner] = useState<{
contextOwner: object
contextOwner: GitHubChecksContextOwner
requestId: number
} | null>(null)
const refreshing = refreshingOwner?.contextOwner === resolvedChecksState.contextOwner
const [rerunningOwner, setRerunningOwner] = useState<object | null>(null)
const [rerunningOwner, setRerunningOwner] = useState<GitHubChecksContextOwner | null>(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,
@@ -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<void>,
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>,
options: {
defaultsSupported?: boolean
sourceDefaultsSupported?: boolean
@@ -64,7 +64,7 @@ afterEach(() => {
function render(
repo: Repo,
updateRepo: (repoId: string, updates: object) => void | Promise<boolean>,
updateRepo: React.ComponentProps<typeof RepositoryWorktreeDefaultsSection>['updateRepo'],
options: {
settings?: Pick<GlobalSettings, 'workspaceDir' | 'worktreeVisibilityDefaults'> | null
refreshRepo?: (repoId: string) => void | Promise<unknown>
@@ -306,7 +306,10 @@ describe('selectWorktreeAgentOrchestration', () => {
}
let liveReads = 0
let retainedReads = 0
const countReads = (target: object, onRead: () => void): object =>
const countReads = (
target: Record<string, unknown>,
onRead: () => void
): Record<string, unknown> =>
new Proxy(target, {
get(source, key, receiver) {
if (typeof key === 'string') {
@@ -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<string, never>
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<string, QuietRevalidateState>()
@@ -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) {
@@ -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<Terminal>
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<object, HiddenOutputRestoreEntry>()
const inactiveRestoreQueue = new Map<HiddenOutputRestoreTarget, HiddenOutputRestoreEntry>()
let inactiveRestoreTimer: ReturnType<typeof setTimeout> | 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()
@@ -5,7 +5,14 @@ const hiddenClaimCounts = new Map<string, number>()
type VisibilityClaim = { ptyId: string; visible: boolean }
const visibilityClaimsByOwner = new Map<object, VisibilityClaim>()
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<RendererPtyVisibilityClaimOwner, VisibilityClaim>()
const visibleClaimCounts = new Map<string, number>()
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
@@ -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) {
@@ -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<never, never>
it('keeps newer timer slots when canceled callbacks are forced', () => {
const { terminal, textarea } = openTerminal(TerminalType)
const callbacks: (() => void)[] = []
const cleared = new Set<object>()
const cleared = new Set<FakeTimerToken>()
// 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)
@@ -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 {
@@ -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<object>({})
const quietRevalidateOwnerRef = useRef<QuietRevalidateRunOwner>({})
const quietRevalidateScopeRef = useRef({
queryKey: githubWorkItemMutationQueryKey,
generation: 0
@@ -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<string, RuntimeEnvironmentStatus>
}
vi.mock('@/store', () => ({
useAppStore: (selector: (state: object) => unknown) =>
useAppStore: (selector: (state: MockedSettingsNavState) => unknown) =>
selector({
settings: testState.settings,
repos: [],

Some files were not shown because too many files have changed in this diff Show More