mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
`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".
179 lines
6.4 KiB
TypeScript
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') })
|
|
})
|
|
}
|