Files
orca/src/relay/git-handler-status-ops.test.ts
T
NeilandOrca 46646d7ff1 chore(lint): upgrade oxlint to 1.71 + enable 7 new rules (autofixed backlog) (#6841)
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules

Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.

Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):

error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol        (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse            (19: copy-then-reverse -> toReversed)

warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type  (aliasing footgun guard)
- typescript/no-unsafe-function-type         (bans bare Function type)
- unicorn/prefer-array-flat-map              (map().flat() -> flatMap())
- unicorn/prefer-regexp-test                 (.match() in bool ctx -> .test())

mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.

Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.

* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse

mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).

Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-06-29 22:38:29 -07:00

248 lines
9.1 KiB
TypeScript

import { mkdtempSync } from 'node:fs'
import * as fs from 'node:fs/promises'
import { tmpdir } from 'node:os'
import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitExec } from './git-handler-ops'
import { getStatusOp } from './git-handler-status-ops'
import { clearNoEffectiveUpstreamStatusCache } from './git-status-upstream-negative-cache'
const LARGE_STATUS_ENTRY_COUNT = 150_000
function buildLargeStatusOutput(count: number): string {
const lines: string[] = []
for (let index = 0; index < count; index += 1) {
lines.push(`1 A. N... 100644 100644 100644 000000 111111 generated-${index}.txt`)
}
return lines.join('\n')
}
function buildBranchStatusOutput(head: string, branch: string): string {
return [`# branch.oid ${head}`, `# branch.head ${branch}`].join('\n')
}
describe('getStatusOp', () => {
let tmpDir: string
beforeEach(() => {
clearNoEffectiveUpstreamStatusCache()
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-git-status-'))
})
afterEach(async () => {
vi.useRealTimers()
clearNoEffectiveUpstreamStatusCache()
await fs.rm(tmpDir, { recursive: true, force: true })
})
it('truncates huge status lists at the limit and flags didHitLimit', async () => {
const statusOutput = buildLargeStatusOutput(LARGE_STATUS_ENTRY_COUNT)
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: statusOutput, stderr: '' }
}
if (args.includes('diff')) {
return { stdout: '', stderr: '' }
}
throw new Error(`Unexpected git command: ${args.join(' ')}`)
})
const result = await getStatusOp(git, { worktreePath: tmpDir, limit: 10_000 })
expect(result.didHitLimit).toBe(true)
expect(result.statusLength).toBe(LARGE_STATUS_ENTRY_COUNT)
expect(result.entries).toHaveLength(10_000)
expect(result.entries[0]).toEqual({
path: 'generated-0.txt',
status: 'added',
area: 'staged'
})
// numstat (diff) must be skipped when the limit was hit.
expect(git.mock.calls.some(([args]) => args.includes('diff'))).toBe(false)
})
it('returns the full list and no limit flag when under the limit', async () => {
const statusOutput = buildLargeStatusOutput(5)
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: statusOutput, stderr: '' }
}
if (args.includes('diff')) {
return { stdout: '', stderr: '' }
}
throw new Error(`Unexpected git command: ${args.join(' ')}`)
})
const result = await getStatusOp(git, { worktreePath: tmpDir, limit: 10_000 })
expect(result.didHitLimit).toBeUndefined()
expect(result.entries).toHaveLength(5)
})
it('caches no-effective-upstream probes across status polls for the same head', async () => {
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: buildBranchStatusOutput('abc123', 'feature'), stderr: '' }
}
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const first = await getStatusOp(git, { worktreePath: tmpDir })
const firstCallCount = git.mock.calls.length
const second = await getStatusOp(git, { worktreePath: tmpDir })
expect(first.upstreamStatus).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
expect(second.upstreamStatus).toEqual(first.upstreamStatus)
expect(git.mock.calls).toHaveLength(firstCallCount + 1)
expect(
git.mock.calls.filter(([args]) => args[0] === 'rev-parse' && args.includes('HEAD@{u}'))
).toHaveLength(1)
expect(
git.mock.calls.filter(
([args]) => args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')
)
).toHaveLength(1)
})
it('keeps no-effective-upstream probes cached beyond thirty seconds', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: buildBranchStatusOutput('abc123', 'feature'), stderr: '' }
}
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
await getStatusOp(git, { worktreePath: tmpDir })
vi.setSystemTime(31_000)
await getStatusOp(git, { worktreePath: tmpDir })
expect(
git.mock.calls.filter(([args]) => args[0] === 'rev-parse' && args.includes('HEAD@{u}'))
).toHaveLength(1)
})
it('coalesces concurrent no-effective-upstream probes', async () => {
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: buildBranchStatusOutput('abc123', 'feature'), stderr: '' }
}
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
await Promise.resolve()
throw new Error('fatal: no upstream configured for branch feature')
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
await Promise.resolve()
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
await Promise.all([
getStatusOp(git, { worktreePath: tmpDir }),
getStatusOp(git, { worktreePath: tmpDir }),
getStatusOp(git, { worktreePath: tmpDir })
])
expect(
git.mock.calls.filter(([args]) => args[0] === 'rev-parse' && args.includes('HEAD@{u}'))
).toHaveLength(1)
expect(
git.mock.calls.filter(
([args]) => args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')
)
).toHaveLength(1)
})
it('invalidates cached no-effective-upstream probes when the branch changes', async () => {
let branch = 'feature'
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: buildBranchStatusOutput('abc123', branch), stderr: '' }
}
if (args[0] === 'symbolic-ref') {
return { stdout: `${branch}\n`, stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error(`fatal: no upstream configured for branch ${branch}`)
}
if (args[0] === 'rev-parse' && args.some((arg) => arg.startsWith('refs/remotes/origin/'))) {
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
await getStatusOp(git, { worktreePath: tmpDir })
branch = 'other-feature'
await getStatusOp(git, { worktreePath: tmpDir })
expect(
git.mock.calls
.filter(
([args]) =>
args[0] === 'rev-parse' && args.some((arg) => arg.startsWith('refs/remotes/origin/'))
)
.map(([args]) => args.at(-1))
).toEqual(['refs/remotes/origin/feature', 'refs/remotes/origin/other-feature'])
})
it('does not cache a configured push target signal', async () => {
const git = vi.fn<GitExec>(async (args) => {
if (args.includes('status')) {
return { stdout: buildBranchStatusOutput('abc123', 'feature/fix'), stderr: '' }
}
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature/fix\n', stderr: '' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature/fix')
}
if (args[0] === 'config' && args.includes('branch.feature/fix.pushRemote')) {
return { stdout: 'fork\n', stderr: '' }
}
if (args[0] === 'config' && args.includes('remote.pushDefault')) {
throw new Error('missing push default')
}
if (args[0] === 'config' && args.includes('branch.feature/fix.remote')) {
return { stdout: 'fork\n', stderr: '' }
}
if (args[0] === 'config' && args.includes('branch.feature/fix.merge')) {
return { stdout: 'refs/heads/feature/fix\n', stderr: '' }
}
if (args[0] === 'config' && args.includes('branch.feature/fix.base')) {
throw new Error('missing branch base')
}
if (args[0] === 'rev-parse' && args.some((arg) => arg.startsWith('refs/remotes/'))) {
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
await getStatusOp(git, { worktreePath: tmpDir })
await getStatusOp(git, { worktreePath: tmpDir })
expect(
git.mock.calls.filter(([args]) => args[0] === 'rev-parse' && args.includes('HEAD@{u}'))
).toHaveLength(2)
})
})