mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 16:02:42 +00:00
* test: remove mock echoes and duplicate contracts that only reading finds
Two veins in one wave, both requiring the production path to be read rather than
pattern-matched.
Mock echoes (36 strongest candidates reviewed, 2 real): the flagged shape —
literals shared between a mock factory and an expect matcher — is almost always
a test feeding an input and asserting a transform. The two genuine echoes are in
`pty-management.test.ts`, where the handler returns
`getDaemonFolderAccessMismatch(identity)` verbatim, so asserting the mock's own
`evidence('allowed')` object and its `null` proved only the mock. One case's own
comment conceded the handler makes no decision. The branch-exercising cases in
that file stay.
Semantic sweep of 80 files no detector flagged, 27 junk cases removed. What it
found has no mechanical signature:
- a handler that is literally `() => getComputerUsePermissionStatus()`, so
`resolves.toBe(result)` guarded nothing;
- "does not mutate a stale registration off Linux" whose refusal came from a
DIFFERENT guard — `cli.ts` has no platform check, and the test's own mock made
`resolveAppImageRuntimeIdentity` return null, which a sibling case already owns;
- "keeps probing a host that is still retained" asserting a no-op, because
`retainScopes` only cancels queued probes and stores no state;
- two cases comparing against `referenceAllowedRoots`, a verbatim copy of the
pre-change algorithm kept in the test file — expected values produced by the
thing under test. The third such case stays: it calls the old algorithm to
COUNT its work (100_000 containment checks vs 100), a real bound on the
authorization hot path;
- a remote-folder refusal that came from the fake provider's own rejection
message rather than any Orca guard;
- "advertises each capability once", where a duplicate entry is inert in
production because membership is `includes`;
- an Antigravity `scaffold self-check` built on a hand-typed five-line screen —
the exact fixture shape docs/reference/antigravity-readiness-evidence.md blames
for five failed detector attempts — whose banner had already drifted to
`Antigravity CLI 1.0.3` against a real captured `1.2.0`. The raw-capture
provenance guard and the transcript checklist ratchet in that file stay.
No production file is touched and no test file is deleted.
* test: retire duplicate daemon and filesystem cases the sweep found
Continues the semantic sweep into src/main/ipc filesystem handlers and
src/main/daemon. 16 cases removed across 13 files; no production file touched.
The recurring shape is a case that reaches the same branch as its sibling by a
different-looking route:
- `parseArgs` is a flag-scanning loop, so "handles flags in any order" asserts the
identical result object as the in-order case, and "throws with no args" lands on
the same `Usage:` throw the two missing-flag cases already reach;
- an ENOENT case with "no code at all" and its sibling with a numeric transport
code both fall through to the same `ENOENT_MESSAGE.test` branch;
- "establishes connection with hello handshake" and "receives stream events" are
strict subsets of cases that require two matching authenticated sockets and a
frame split inside a multibyte character.
Two were vacuous rather than duplicated: a fixture self-comparison asserting
`String.normalize` gives different NFC and NFD spellings, and a
`not.toThrow` batch case whose 200k events are truncated by
`MAX_BATCHED_WATCHER_EVENTS = 5_000` long before the argument spread it was
written to exercise.
One whole-file deletion was reversed: `freebuff-detection.test.ts` looked like a
table restatement, but the sibling detection test covers only dependency ordering
and platform gating, not freebuff/codebuff independence — and those two names
share a suffix, so it is the only guard against one shadowing the other.
200 lines
7.2 KiB
TypeScript
200 lines
7.2 KiB
TypeScript
import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
|
|
import type { Store } from '../persistence'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import { PATH_ACCESS_DENIED_MESSAGE, resolveAuthorizedPath } from './filesystem-auth'
|
|
import { isDescendantOrEqual } from './filesystem-path-containment'
|
|
|
|
/**
|
|
* Opening any file under a Korean-named workspace failed on macOS with
|
|
* "Access denied: path resolves outside allowed directories" (#21172).
|
|
*
|
|
* The file was inside the workspace the whole time. The two sides of the containment check reached
|
|
* it through different doors: the root was registered from the file picker or from git, which spell
|
|
* the name in NFC, while the path being read came back from the filesystem in NFD. Same name, same
|
|
* file on APFS, different bytes — so the guard reported an escape.
|
|
*
|
|
* "Same file" is the load-bearing half, so it is the half that gets proven: both spellings reaching
|
|
* one directory authorize the file, two distinct directories — which ext4 allows and APFS does not
|
|
* — leave the unregistered one denied. A symlink stands in for the APFS fold on a byte-exact host.
|
|
*/
|
|
|
|
const FOLDER = '테스트프로젝트'
|
|
const NFC_FOLDER = FOLDER.normalize('NFC')
|
|
const NFD_FOLDER = FOLDER.normalize('NFD')
|
|
|
|
const scratchDirs: string[] = []
|
|
|
|
async function makeScratchDir(): Promise<string> {
|
|
// realpath first: macOS fronts the temp dir with a /var symlink of its own.
|
|
const scratch = await mkdtemp(join(await realpath(tmpdir()), 'orca-unicode-path-'))
|
|
scratchDirs.push(scratch)
|
|
return scratch
|
|
}
|
|
|
|
function isEEXIST(error: unknown): boolean {
|
|
return error instanceof Error && 'code' in error && error.code === 'EEXIST'
|
|
}
|
|
|
|
/** One directory, both spellings. EEXIST means the filesystem already folded them itself. */
|
|
async function makeOneDirectoryTwoSpellings(
|
|
scratch: string
|
|
): Promise<{ onDisk: string; registered: string }> {
|
|
const onDisk = join(scratch, NFD_FOLDER)
|
|
const registered = join(scratch, NFC_FOLDER)
|
|
await mkdir(onDisk)
|
|
try {
|
|
await symlink(onDisk, registered, 'dir')
|
|
} catch (error) {
|
|
if (!isEEXIST(error)) {
|
|
throw error
|
|
}
|
|
}
|
|
return { onDisk, registered }
|
|
}
|
|
|
|
/** Two canonically equal names as two distinct directories; null where the filesystem folds them. */
|
|
async function makeDistinctSiblings(
|
|
scratch: string
|
|
): Promise<{ registered: string; sibling: string } | null> {
|
|
const registered = join(scratch, NFC_FOLDER)
|
|
const sibling = join(scratch, NFD_FOLDER)
|
|
await mkdir(registered)
|
|
try {
|
|
await mkdir(sibling)
|
|
} catch (error) {
|
|
if (isEEXIST(error)) {
|
|
return null
|
|
}
|
|
throw error
|
|
}
|
|
return { registered, sibling }
|
|
}
|
|
|
|
function makeStore(repoPath: string): Store {
|
|
const repo: Repo = {
|
|
id: 'repo-1',
|
|
path: repoPath,
|
|
displayName: 'workspace',
|
|
badgeColor: '#000000',
|
|
addedAt: 1,
|
|
kind: 'git'
|
|
}
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the guard reads only these four accessors; Store is a class, so a structural double cannot satisfy it without the cast.
|
|
return {
|
|
getRepos: () => [repo],
|
|
getProjectGroups: () => [],
|
|
getFolderWorkspaces: () => [],
|
|
getSettings: () => ({})
|
|
} as unknown as Store
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(scratchDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
|
|
})
|
|
|
|
describe('path containment across Unicode forms', () => {
|
|
it('accepts a child of a root the filesystem spells the other way', async () => {
|
|
const scratch = await makeScratchDir()
|
|
const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch)
|
|
|
|
expect(isDescendantOrEqual(join(onDisk, 'test.txt'), registered)).toBe(true)
|
|
})
|
|
|
|
it('accepts the root itself under its other spelling', async () => {
|
|
const scratch = await makeScratchDir()
|
|
const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch)
|
|
|
|
expect(isDescendantOrEqual(onDisk, registered)).toBe(true)
|
|
})
|
|
|
|
it('denies a canonically equal sibling that is a distinct directory', async () => {
|
|
const scratch = await makeScratchDir()
|
|
const siblings = await makeDistinctSiblings(scratch)
|
|
if (!siblings) {
|
|
// The filesystem folds the two names, so there is no second directory to reach.
|
|
return
|
|
}
|
|
|
|
expect(
|
|
isDescendantOrEqual(join(siblings.sibling, 'test.txt'), siblings.registered),
|
|
'the sibling is a different directory; the user opened only the registered one'
|
|
).toBe(false)
|
|
})
|
|
|
|
it('denies a fold it cannot check against the filesystem', () => {
|
|
// Neither spelling exists on disk, so identity is unproven — and unproven is denied.
|
|
expect(
|
|
isDescendantOrEqual(resolve(`/repos/${NFD_FOLDER}/test.txt`), resolve(`/repos/${NFC_FOLDER}`))
|
|
).toBe(false)
|
|
})
|
|
|
|
it('still rejects a sibling that only looks similar', () => {
|
|
// 테스트 is a prefix of 테스트프로젝트, not a canonical equivalent of it.
|
|
expect(
|
|
isDescendantOrEqual(resolve('/repos/테스트/test.txt'), resolve(`/repos/${NFC_FOLDER}`))
|
|
).toBe(false)
|
|
})
|
|
|
|
it('still rejects an escape out of a non-ASCII root', () => {
|
|
expect(
|
|
isDescendantOrEqual(
|
|
resolve(`/repos/${NFD_FOLDER}/../secrets`),
|
|
resolve(`/repos/${NFC_FOLDER}`)
|
|
)
|
|
).toBe(false)
|
|
})
|
|
|
|
it('leaves ASCII containment exactly as it was', () => {
|
|
expect(isDescendantOrEqual(resolve('/repos/app/src'), resolve('/repos/app'))).toBe(true)
|
|
expect(isDescendantOrEqual(resolve('/repos/apple'), resolve('/repos/app'))).toBe(false)
|
|
expect(isDescendantOrEqual(resolve('/repos/app'), resolve('/repos/app'))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('fs:readFile authorization for a Korean-named workspace', () => {
|
|
it('authorizes a file the filesystem spells the other way', async () => {
|
|
const scratch = await makeScratchDir()
|
|
const { onDisk, registered } = await makeOneDirectoryTwoSpellings(scratch)
|
|
const file = join(onDisk, 'test.txt')
|
|
await writeFile(file, 'hello')
|
|
|
|
const store = makeStore(registered)
|
|
// Resolved before the assertion so a rejection lands on expect(), not on an unawaited promise.
|
|
const expected = await realpath(file)
|
|
|
|
await expect(
|
|
resolveAuthorizedPath(file, store),
|
|
'the file is inside the opened workspace; only its spelling differs'
|
|
).resolves.toBe(expected)
|
|
})
|
|
|
|
it('denies a canonically equal sibling the user never opened', async () => {
|
|
const scratch = await makeScratchDir()
|
|
const siblings = await makeDistinctSiblings(scratch)
|
|
if (!siblings) {
|
|
return
|
|
}
|
|
const file = join(siblings.sibling, 'test.txt')
|
|
await writeFile(file, 'secret')
|
|
|
|
const store = makeStore(siblings.registered)
|
|
|
|
await expect(resolveAuthorizedPath(file, store)).rejects.toThrow(PATH_ACCESS_DENIED_MESSAGE)
|
|
})
|
|
|
|
it('still denies a file outside the workspace', async () => {
|
|
const scratch = await makeScratchDir()
|
|
await makeOneDirectoryTwoSpellings(scratch)
|
|
const outside = join(scratch, 'outside.txt')
|
|
await writeFile(outside, 'secret')
|
|
|
|
const store = makeStore(join(scratch, NFC_FOLDER))
|
|
|
|
await expect(resolveAuthorizedPath(outside, store)).rejects.toThrow(PATH_ACCESS_DENIED_MESSAGE)
|
|
})
|
|
})
|