Files
orca/tests/e2e/project-group-creation-visibility.spec.ts
Neil f107499e44 fix(lint): enable anti-slop/no-reflect-get (#20786)
`anti-slop/no-reflect-get` rejects every call to `Reflect.get`. The
reflective read bypasses ordinary property access and throws away the
type evidence the compiler would otherwise give you: the result is
`any`/`unknown` with no narrowing, so a typo in the key or a shape drift
in the source object is invisible until runtime. The rule's remedy is to
parse dynamic input into a named domain type (or narrow it with `in`)
and then read the field normally.

Baseline: 86 violations across 67 files. Now zero unsuppressed
violations under
`npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile`.

Fix pattern
-----------
44 of the 86 were rewritten. The dominant shape was an `unknown` value
read through `Reflect.get` right after a `typeof === 'object'` guard;
those became `in`-narrowed property access, which TypeScript checks:

  - Reflect.get(value, 'agents')
  + 'agents' in value ? value.agents : null

Two further shapes:
- `Reflect.get(Object(x), 'k')` on a possibly-primitive envelope became a
  small named reader that boxes once and indexes a
  `Record<string, unknown>` (`settingsField` in
  mobile/src/transport/settings-read-operations.ts).
- Tests reaching into private state moved to TypeScript's checked
  bracket-index escape hatch (`runtime['layoutQueues']`), or to a
  documented read-only accessor on the owning class
  (`SearchSubprocessLineAccumulator.retainedCapacityBytes()`,
  `CodexSubagentExecutions.retentionSizes()`).

No type assertion was added anywhere: the diff contains zero net-new
`as` casts, `as any`, `as unknown as`, `@ts-ignore`, or
`@ts-expect-error`, so nothing was laundered into the sibling
assertion rules.

Suppressions
------------
42x `// oxlint-disable-next-line anti-slop/no-reflect-get` across 38
files. Every one is the default-forward branch of a `Proxy` `get` trap:

    get(target, property, receiver) {
      ...
      return Reflect.get(target, property, receiver)
    }

`Reflect.get(target, property, receiver)` is the only construct that
forwards with correct `receiver` semantics; `target[property]` invokes
an accessor with the wrong `this` and silently breaks getters that read
sibling state. There is no typed alternative, so these are suppressed
rather than rewritten.

3x `// oxlint-disable-next-line typescript-eslint/consistent-type-definitions
-- declaration merging requires interface` in
tests/e2e/github-url-smart-input-transition.spec.ts,
tests/e2e/linear-url-workspace-entry.spec.ts, and
tests/e2e/worktree-active-delete-scroll-position.spec.ts. Replacing
`Reflect.get(window, 'x')` with typed `window.x` requires a
`declare global { interface Window }` block, and `interface` is
mandatory for declaration merging. Matches the existing convention at
tests/e2e/helpers/runtime-types.ts:63.

1x `// eslint-disable-next-line no-var -- main-process gate handle for
this spec` in tests/e2e/project-group-creation-visibility.spec.ts, for
the same reason a `var` global is needed to type the handle. Matches
tests/e2e/agent-session-log-tail-stability.spec.ts:24.

Also updates two source-text anchors in mobile's rpc-recording mutation
harness (mobile/src/test-support/rpc-recording/operation-mutations.ts
and recording-runner.test.ts), which pin the exact text of the rewritten
line in settings-read-operations.ts and would otherwise fail with
"Mutant anchor matched 0 sites, expected 1".
2026-09-15 01:24:30 -07:00

179 lines
6.4 KiB
TypeScript

import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { runProcess } from '../../src/shared/child-process/run-process'
test.use({ seedTestRepo: false })
declare global {
// Resolved by the main-process gate this spec installs around the group-create response.
var __releaseGroupCreateResponse: (() => void) | undefined
}
for (const delayCreateResponse of [false, true]) {
test(`created groups survive sidebar expansion (${delayCreateResponse ? 'refresh first' : 'ordinary timing'})`, async ({
orcaPage,
electronApp,
registerPostElectronShutdownCleanup
}, testInfo) => {
await waitForSessionReady(orcaPage)
const root = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'orca-group-visibility-')))
registerPostElectronShutdownCleanup(async () => {
rmSync(root, { recursive: true, force: true })
})
const paths = Array.from({ length: 30 }, (_, index) =>
path.join(root, `repo-${String(index).padStart(2, '0')}`)
)
for (const repoPath of paths) {
mkdirSync(repoPath)
writeFileSync(path.join(repoPath, 'seed.txt'), 'seed\n')
for (const args of [
['init'],
['add', '.'],
[
'-c',
'user.name=Test',
'-c',
'user.email=test@example.com',
'-c',
'commit.gpgsign=false',
'commit',
'-m',
'seed'
]
]) {
const result = await runProcess({ program: 'git', args, cwd: repoPath, timeoutMs: 10_000 })
expect(result.code, result.stderr).toBe(0)
}
}
const repoIds = await orcaPage.evaluate(async (paths) => {
const store = window.__store!
for (const repoPath of paths) {
await window.api.repos.add({ path: repoPath })
}
await store.getState().awaitLocalRepoCatalogSettlement()
const repos = store.getState().repos.filter((repo) => paths.includes(repo.path))
for (const repo of repos) {
await store.getState().fetchWorktrees(repo.id)
}
store.getState().setGroupBy('repo')
store.getState().setProjectOrderBy('manual')
return repos.map((repo) => repo.id)
}, paths)
expect(repoIds).toHaveLength(paths.length)
// Force the adverse ordering separately from the ordinary IPC path.
if (delayCreateResponse) {
await electronApp.evaluate(({ ipcMain }) => {
if (!('_invokeHandlers' in ipcMain) || !(ipcMain._invokeHandlers instanceof Map)) {
throw new Error('Electron invoke handlers unavailable')
}
const create = ipcMain._invokeHandlers.get('projectGroups:create')
if (typeof create !== 'function') {
throw new Error('Group create handler unavailable')
}
const gate = Promise.withResolvers<void>()
Reflect.set(globalThis, '__releaseGroupCreateResponse', gate.resolve)
ipcMain.removeHandler('projectGroups:create')
ipcMain.handle('projectGroups:create', async (...args) => {
ipcMain.removeHandler('projectGroups:create')
ipcMain.handle('projectGroups:create', create)
const group = await create(...args)
await gate.promise
return group
})
})
}
const creation = orcaPage.evaluate(() =>
window.__store!.getState().createProjectGroup('Crowded group')
)
if (delayCreateResponse) {
try {
await expect
.poll(() =>
orcaPage.evaluate(() =>
window
.__store!.getState()
.projectGroups.some((group) => group.name === 'Crowded group')
)
)
.toBe(true)
} finally {
await electronApp.evaluate(() => {
const release = globalThis.__releaseGroupCreateResponse
if (typeof release !== 'function') {
throw new Error('Group create response gate unavailable')
}
release()
Reflect.deleteProperty(globalThis, '__releaseGroupCreateResponse')
})
}
}
const createdGroup = await creation
if (!createdGroup) {
throw new Error('Group creation failed')
}
await orcaPage.evaluate(
async ({ repoIds, groupId }) => {
const store = window.__store!
for (const repoId of repoIds.slice(0, 2)) {
await store.getState().moveProjectToGroup(repoId, groupId)
}
const collapsedGroups = store
.getState()
.projectHostSetups.map((setup) => `project:${setup.projectId}`)
await window.api.ui.set({ groupBy: 'repo', collapsedGroups })
store.setState({ collapsedGroups: new Set(collapsedGroups) })
},
{ repoIds, groupId: createdGroup.id }
)
const scroller = orcaPage.locator('[data-worktree-sidebar]')
const group = scroller.locator(`[data-project-group-header-id="${createdGroup.id}"]`)
const groupedRepos = repoIds
.slice(0, 2)
.map((id) => scroller.locator(`[data-repo-header-id="${id}"]`))
for (const repo of groupedRepos) {
await expect(repo).toBeVisible()
}
await orcaPage.screenshot({ path: testInfo.outputPath('before-expansion.png') })
for (const repoId of repoIds.slice(2, 12)) {
const repo = scroller.locator(`[data-repo-header-id="${repoId}"]`)
await expect
.poll(async () => {
if (await repo.count()) {
return true
}
await scroller.evaluate((element) => {
element.scrollTop += element.clientHeight / 2
})
return false
})
.toBe(true)
await repo.scrollIntoViewIfNeeded()
await expect(repo).toHaveAttribute('aria-expanded', 'false')
await repo.click()
await scroller.evaluate((element) => {
element.scrollTop = 0
})
for (const groupedRepo of groupedRepos) {
await expect(groupedRepo).toBeVisible()
}
}
await expect(group).toHaveCount(1)
await orcaPage.evaluate(() => window.__store!.getState().fetchProjectGroups())
await expect(group).toHaveCount(1)
await group.click()
for (const repo of groupedRepos) {
await expect(repo).toHaveCount(0)
}
await group.click()
for (const repo of groupedRepos) {
await expect(repo).toBeVisible()
}
await orcaPage.screenshot({ path: testInfo.outputPath('after-expansion.png') })
})
}