mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 08:02:31 +00:00
* 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>
248 lines
9.1 KiB
TypeScript
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)
|
|
})
|
|
})
|