mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
test: retire relay, preload and shared cases that re-prove an owned contract (#24007)
Audit sweep over `src/relay`, `src/preload` and `src/shared` (1,087 test files reviewed). 101 case declarations removed across 40 files, 6 test files deleted outright, 1,143 lines gone. Executed-case count falls further, since several removals were `it.each` tables. Dominant patterns, by frequency: - Self-comparisons that cannot fail: `expect(f(x)).toBe(f(x))`, `JSON.parse(JSON.stringify(literal))` deep-equalling the literal for a type with no codec, and `normalizeKeyToken(t) === normalizeKeyToken(t)` presented as proof of memoization. - Object literals asserting their own fields back, where the guarantee comes from the type annotation and the runtime assertion cannot fail. - Copied inventories: constants compared to their own initializers, and a function returning a copy of an exported constant checked against that constant's literal contents. - Duplicate invocations of a contract owned at a stronger boundary, including provider-local replays of a shared helper. - Table rows varying a field production never reads, so every row runs one path. - Names promising more than the input exercises: a "Windows launch" case in a module with no platform input, and a case whose named branch is never entered. Two production symbols go with them, each a test-only export whose sole caller was a deleted case: - `getGitHubProjectRefInputByteLength` — a one-line forward to `getClipboardTextByteLength`. The real bound (`GITHUB_PROJECT_REF_INPUT_MAX_BYTES`) and its guard stay. - `GRAB_STYLE_PROPERTIES` — an intended shared source of truth that nothing ever consulted; the property set is hand-enumerated at three independent sites. One case was deliberately restored and strengthened rather than dropped. The relay integration suite is the only place the real `SshChannelMultiplexer` is wired to `RelayDispatcher`, so it reaches transport behavior the handler suites cannot (they use `createMockDispatcher`). Its `fs.writeFile` roundtrip is the one case producing a void result, and `JSON.stringify` drops an absent `result` member — a shape no other surviving case exercises. Restored with an assertion pinning what the client actually observes: `null`, not `undefined`. That assertion failed on first run, so the fact was previously unasserted anywhere. One deletion was reverted mid-audit. A case asserting that optional fields stay invisible to "old attach and ready decoders" builds those decoders from `z.object` schemas declared in the test file, so it demonstrates zod's unknown-key stripping rather than anything shipped. It is nonetheless the only forward-compatibility coverage these envelopes have, and `reliability-gates.jsonc:6232` names it as evidence verbatim, so it stays. Note that `check-reliability-gates.mjs` passed both with and without it: the script resolves manifest paths and commands, and does not check that a named assertion still corresponds to a live case. Kept deliberately: everything a reliability gate cites as evidence; the three `registers all expected handlers` RPC manifests (a dropped registration is a silent wire break no type checker catches, and one carries the STA-4571 `pty.ackData` ratchet); the `child-process` direct-import ratchet; and prototype-spy cases paired with a `.repeat(10_000)` input, which assert a real memory bound rather than merely forbidding a technique. Verified: `pnpm test src/shared src/relay src/preload` (1073 files, 11996 passed, 1 pre-existing `it.fails`, 131 skipped), `pnpm tc` after clearing `.tsbuildinfo`, `check-reliability-gates.mjs` (140 gates), `check:code-quality:changed` (0 new findings).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readRendererProcessMemory } from './renderer-process-memory-reader'
|
||||
|
||||
// Why the partial type: Electron types `residentSet` as required, but Chromium
|
||||
@@ -38,10 +38,4 @@ describe('readRendererProcessMemory', () => {
|
||||
readRendererProcessMemory(source(async () => ({ private: Number.NaN, shared: 0 })))
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('does not call the API more than once per read', async () => {
|
||||
const getProcessMemoryInfo = vi.fn(async () => ({ private: 2048, shared: 0 }))
|
||||
await readRendererProcessMemory(source(getProcessMemoryInfo))
|
||||
expect(getProcessMemoryInfo).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,11 +81,4 @@ describe('native preload linux package recovery methods', () => {
|
||||
|
||||
expect(invoke.mock.calls).toEqual([['updater:showLinuxPackage']])
|
||||
})
|
||||
|
||||
it('surfaces a main-process validation rejection to the caller', async () => {
|
||||
const api = await loadApi()
|
||||
invoke.mockRejectedValueOnce(new Error('hash mismatch'))
|
||||
|
||||
await expect(api.updater.getLinuxPackageInstallInstructions()).rejects.toThrow('hash mismatch')
|
||||
})
|
||||
})
|
||||
|
||||
+16
-218
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { rm, readFile, stat } from 'node:fs/promises'
|
||||
import { rm, readFile } from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
@@ -119,6 +119,21 @@ describe('Integration: Client Mux ↔ Relay Dispatcher', () => {
|
||||
expect(result.isBinary).toBe(false)
|
||||
})
|
||||
|
||||
// Why this one stays: writeRelayFile resolves to undefined, so the response frame
|
||||
// carries neither `result` nor `error` — the one payload shape the other cases here
|
||||
// do not produce, and one a mock dispatcher never has to settle. The client sees
|
||||
// `null`, not `undefined`: the member is absent from the frame, not carried as void.
|
||||
it('writeFile creates/overwrites file content', async () => {
|
||||
const filePath = path.join(tmpDir, 'output.txt')
|
||||
|
||||
await expect(
|
||||
mux.request('fs.writeFile', { filePath, content: 'written via relay' })
|
||||
).resolves.toBeNull()
|
||||
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
expect(content).toBe('written via relay')
|
||||
})
|
||||
|
||||
it('readFileStream round-trip preserves a 12 MB binary file', async () => {
|
||||
const filePath = path.join(tmpDir, 'big.png')
|
||||
const original = randomBytes(12 * 1024 * 1024)
|
||||
@@ -127,78 +142,6 @@ describe('Integration: Client Mux ↔ Relay Dispatcher', () => {
|
||||
const { content } = await readFileViaStream(mux, filePath)
|
||||
expect(Buffer.from(content, 'base64').equals(original)).toBe(true)
|
||||
}, 30_000)
|
||||
|
||||
it('writeFile creates/overwrites file content', async () => {
|
||||
const filePath = path.join(tmpDir, 'output.txt')
|
||||
|
||||
await mux.request('fs.writeFile', { filePath, content: 'written via relay' })
|
||||
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
expect(content).toBe('written via relay')
|
||||
})
|
||||
|
||||
it('stat returns file metadata', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'sized.txt'), 'abcdef')
|
||||
const result = (await mux.request('fs.stat', {
|
||||
filePath: path.join(tmpDir, 'sized.txt')
|
||||
})) as { size: number; type: string; mtime: number }
|
||||
expect(result).toMatchObject({ type: 'file', size: 6 })
|
||||
expect(typeof result.mtime).toBe('number')
|
||||
})
|
||||
|
||||
it('createFile + deletePath roundtrip', async () => {
|
||||
const filePath = path.join(tmpDir, 'nested', 'deep', 'new.txt')
|
||||
|
||||
await mux.request('fs.createFile', { filePath })
|
||||
const s = await stat(filePath)
|
||||
expect(s.isFile()).toBe(true)
|
||||
|
||||
await mux.request('fs.deletePath', { targetPath: filePath })
|
||||
await expect(stat(filePath)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('createDir creates directories recursively', async () => {
|
||||
const dirPath = path.join(tmpDir, 'a', 'b', 'c')
|
||||
|
||||
await mux.request('fs.createDir', { dirPath })
|
||||
const s = await stat(dirPath)
|
||||
expect(s.isDirectory()).toBe(true)
|
||||
})
|
||||
|
||||
it('rename moves files', async () => {
|
||||
const oldPath = path.join(tmpDir, 'before.txt')
|
||||
const newPath = path.join(tmpDir, 'after.txt')
|
||||
writeFileSync(oldPath, 'moving')
|
||||
|
||||
await mux.request('fs.rename', { oldPath, newPath })
|
||||
|
||||
await expect(stat(oldPath)).rejects.toThrow()
|
||||
const content = await readFile(newPath, 'utf-8')
|
||||
expect(content).toBe('moving')
|
||||
})
|
||||
|
||||
it('copy duplicates files', async () => {
|
||||
const src = path.join(tmpDir, 'src.txt')
|
||||
const dst = path.join(tmpDir, 'dst.txt')
|
||||
writeFileSync(src, 'original')
|
||||
|
||||
await mux.request('fs.copy', { source: src, destination: dst })
|
||||
|
||||
const content = await readFile(dst, 'utf-8')
|
||||
expect(content).toBe('original')
|
||||
})
|
||||
|
||||
it('readFile returns error for non-existent file', async () => {
|
||||
await expect(
|
||||
mux.request('fs.readFile', { filePath: path.join(tmpDir, 'nope.txt') })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('errors propagate correctly through the protocol', async () => {
|
||||
await expect(
|
||||
mux.request('fs.stat', { filePath: '/nonexistent/path/that/does/not/exist' })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Git ────────────────────────────────────────────────────────
|
||||
@@ -210,66 +153,6 @@ describe('Integration: Client Mux ↔ Relay Dispatcher', () => {
|
||||
gitCommit(tmpDir, 'initial commit')
|
||||
})
|
||||
|
||||
it('git.status returns clean status for committed repo', async () => {
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: unknown[]; conflictOperation: string }
|
||||
|
||||
expect(result.entries).toEqual([])
|
||||
expect(result.conflictOperation).toBe('unknown')
|
||||
})
|
||||
|
||||
it('git.status detects modifications', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'modified')
|
||||
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: { path: string; status: string; area: string }[] }
|
||||
|
||||
const entry = result.entries.find((e) => e.path === 'file.txt')
|
||||
expect(entry).toBeDefined()
|
||||
expect(entry!.status).toBe('modified')
|
||||
expect(entry!.area).toBe('unstaged')
|
||||
})
|
||||
|
||||
it('git.status detects untracked files', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'new.txt'), 'new')
|
||||
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: { path: string; status: string; area: string }[] }
|
||||
|
||||
const entry = result.entries.find((e) => e.path === 'new.txt')
|
||||
expect(entry).toBeDefined()
|
||||
expect(entry!.status).toBe('untracked')
|
||||
})
|
||||
|
||||
it('git.stage + git.status shows staged entry', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'changed')
|
||||
|
||||
await mux.request('git.stage', { worktreePath: tmpDir, filePath: 'file.txt' })
|
||||
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: { path: string; area: string }[] }
|
||||
|
||||
const staged = result.entries.find((e) => e.area === 'staged')
|
||||
expect(staged).toBeDefined()
|
||||
})
|
||||
|
||||
it('git.unstage reverses staging', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'changed')
|
||||
await mux.request('git.stage', { worktreePath: tmpDir, filePath: 'file.txt' })
|
||||
await mux.request('git.unstage', { worktreePath: tmpDir, filePath: 'file.txt' })
|
||||
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: { area: string }[] }
|
||||
|
||||
const staged = result.entries.filter((e) => e.area === 'staged')
|
||||
expect(staged.length).toBe(0)
|
||||
})
|
||||
|
||||
it('git.diff returns original and modified content', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'updated content')
|
||||
|
||||
@@ -283,91 +166,6 @@ describe('Integration: Client Mux ↔ Relay Dispatcher', () => {
|
||||
expect(result.originalContent).toBe('initial')
|
||||
expect(result.modifiedContent).toBe('updated content')
|
||||
})
|
||||
|
||||
it('git.diff returns staged diff', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'staged version')
|
||||
execFileSync('git', ['add', 'file.txt'], { cwd: tmpDir, stdio: 'pipe' })
|
||||
|
||||
const result = (await mux.request('git.diff', {
|
||||
worktreePath: tmpDir,
|
||||
filePath: 'file.txt',
|
||||
staged: true
|
||||
})) as { originalContent: string; modifiedContent: string }
|
||||
|
||||
expect(result.originalContent).toBe('initial')
|
||||
expect(result.modifiedContent).toBe('staged version')
|
||||
})
|
||||
|
||||
it('git.discard restores tracked file to HEAD', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'file.txt'), 'dirty')
|
||||
|
||||
await mux.request('git.discard', { worktreePath: tmpDir, filePath: 'file.txt' })
|
||||
|
||||
const content = await readFile(path.join(tmpDir, 'file.txt'), 'utf-8')
|
||||
expect(content).toBe('initial')
|
||||
})
|
||||
|
||||
it('git.discard removes untracked file', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'temp.txt'), 'throwaway')
|
||||
|
||||
await mux.request('git.discard', { worktreePath: tmpDir, filePath: 'temp.txt' })
|
||||
|
||||
await expect(stat(path.join(tmpDir, 'temp.txt'))).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('git.conflictOperation returns unknown for normal repo', async () => {
|
||||
const result = await mux.request('git.conflictOperation', {
|
||||
worktreePath: tmpDir
|
||||
})
|
||||
expect(result).toBe('unknown')
|
||||
})
|
||||
|
||||
it('git.listWorktrees returns the main worktree', async () => {
|
||||
const result = (await mux.request('git.listWorktrees', {
|
||||
repoPath: tmpDir
|
||||
})) as { path: string; isMainWorktree: boolean }[]
|
||||
|
||||
expect(result.length).toBeGreaterThanOrEqual(1)
|
||||
expect(result[0].isMainWorktree).toBe(true)
|
||||
})
|
||||
|
||||
it('git.branchCompare works across branches', async () => {
|
||||
// Get current branch name (might be "main" or "master" depending on config)
|
||||
const defaultBranch = execFileSync('git', ['branch', '--show-current'], {
|
||||
cwd: tmpDir,
|
||||
encoding: 'utf-8'
|
||||
}).trim()
|
||||
|
||||
execFileSync('git', ['checkout', '-b', 'feature'], { cwd: tmpDir, stdio: 'pipe' })
|
||||
writeFileSync(path.join(tmpDir, 'feature.txt'), 'feature work')
|
||||
gitCommit(tmpDir, 'feature commit')
|
||||
|
||||
const result = (await mux.request('git.branchCompare', {
|
||||
worktreePath: tmpDir,
|
||||
baseRef: defaultBranch
|
||||
})) as { summary: { status: string; commitsAhead: number }; entries: unknown[] }
|
||||
|
||||
expect(result.summary.status).toBe('ready')
|
||||
expect(result.summary.commitsAhead).toBe(1)
|
||||
expect(result.entries.length).toBe(1)
|
||||
})
|
||||
|
||||
it('git.bulkStage stages multiple files at once', async () => {
|
||||
writeFileSync(path.join(tmpDir, 'a.txt'), 'a')
|
||||
writeFileSync(path.join(tmpDir, 'b.txt'), 'b')
|
||||
|
||||
await mux.request('git.bulkStage', {
|
||||
worktreePath: tmpDir,
|
||||
filePaths: ['a.txt', 'b.txt']
|
||||
})
|
||||
|
||||
const result = (await mux.request('git.status', {
|
||||
worktreePath: tmpDir
|
||||
})) as { entries: { path: string; area: string }[] }
|
||||
|
||||
const staged = result.entries.filter((e) => e.area === 'staged')
|
||||
expect(staged.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── Error propagation ──────────────────────────────────────────
|
||||
|
||||
@@ -315,16 +315,6 @@ describe('parseHexAddress', () => {
|
||||
expect(result).toEqual({ host: '0.0.0.0', port: 8080 })
|
||||
})
|
||||
|
||||
it('parses port 22 correctly', () => {
|
||||
const result = parseHexAddress('00000000:0016')
|
||||
expect(result).toEqual({ host: '0.0.0.0', port: 22 })
|
||||
})
|
||||
|
||||
it('parses port 443 correctly', () => {
|
||||
const result = parseHexAddress('0100007F:01BB')
|
||||
expect(result).toEqual({ host: '127.0.0.1', port: 443 })
|
||||
})
|
||||
|
||||
it('parses a non-localhost IPv4 address', () => {
|
||||
// 192.168.1.100 in little-endian: 6401A8C0
|
||||
const result = parseHexAddress('6401A8C0:1388')
|
||||
@@ -351,22 +341,6 @@ describe('parseHexAddress', () => {
|
||||
expect(parseHexAddress('')).toBeNull()
|
||||
expect(parseHexAddress('::::')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses high ports correctly', () => {
|
||||
// Port 65535 = FFFF
|
||||
const result = parseHexAddress('0100007F:FFFF')
|
||||
expect(result).toEqual({ host: '127.0.0.1', port: 65535 })
|
||||
})
|
||||
|
||||
it('parses port 5432 (postgres)', () => {
|
||||
const result = parseHexAddress('0100007F:1538')
|
||||
expect(result).toEqual({ host: '127.0.0.1', port: 5432 })
|
||||
})
|
||||
|
||||
it('parses port 3306 (mysql)', () => {
|
||||
const result = parseHexAddress('00000000:0CEA')
|
||||
expect(result).toEqual({ host: '0.0.0.0', port: 3306 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseWindowsPowerShellPortRows', () => {
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RecentPtyOutputBuffer } from '../main/runtime/recent-pty-output-buffer'
|
||||
import { REPLAY_BUFFER_MAX } from './pty-handler'
|
||||
|
||||
// Reference: the pre-change replay buffer, a rolling string sliced per append.
|
||||
function appendStringTail(previous: string, data: string): string {
|
||||
if (data.length === 0) {
|
||||
return previous
|
||||
}
|
||||
const next = previous + data
|
||||
return next.length > REPLAY_BUFFER_MAX ? next.slice(-REPLAY_BUFFER_MAX) : next
|
||||
}
|
||||
|
||||
function makeBuffer(): RecentPtyOutputBuffer {
|
||||
return new RecentPtyOutputBuffer({ preserveChunkBoundaries: false, limit: REPLAY_BUFFER_MAX })
|
||||
}
|
||||
|
||||
function replayEquals(chunks: string[]): { deque: string; reference: string } {
|
||||
const buffer = makeBuffer()
|
||||
let reference = ''
|
||||
for (const chunk of chunks) {
|
||||
buffer.append(chunk)
|
||||
reference = appendStringTail(reference, chunk)
|
||||
}
|
||||
return { deque: buffer.read(), reference }
|
||||
}
|
||||
|
||||
// Deterministic PRNG so a failure is reproducible.
|
||||
function makeRandom(seed: number): () => number {
|
||||
let state = seed >>> 0
|
||||
return () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0
|
||||
return state / 0x100000000
|
||||
}
|
||||
}
|
||||
|
||||
describe('relay replay buffer equivalence', () => {
|
||||
it('matches the rolling-string tail below the cap', () => {
|
||||
const { deque, reference } = replayEquals(['hello ', 'world', '\r\n$ '])
|
||||
expect(deque).toBe(reference)
|
||||
expect(deque).toBe('hello world\r\n$ ')
|
||||
})
|
||||
|
||||
it('matches once the window saturates', () => {
|
||||
const chunks = Array.from({ length: 40 }, (_value, index) => `chunk-${index}-`.repeat(400))
|
||||
const { deque, reference } = replayEquals(chunks)
|
||||
expect(deque.length).toBe(REPLAY_BUFFER_MAX)
|
||||
expect(deque).toBe(reference)
|
||||
})
|
||||
|
||||
it('matches when one append alone exceeds the cap', () => {
|
||||
const { deque, reference } = replayEquals(['x'.repeat(REPLAY_BUFFER_MAX * 2 + 7)])
|
||||
expect(deque.length).toBe(REPLAY_BUFFER_MAX)
|
||||
expect(deque).toBe(reference)
|
||||
})
|
||||
|
||||
it('matches when an oversized append follows existing content', () => {
|
||||
const { deque, reference } = replayEquals(['prefix', 'y'.repeat(REPLAY_BUFFER_MAX + 3)])
|
||||
expect(deque).toBe(reference)
|
||||
expect(deque.startsWith('prefix')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches at exactly the cap boundary', () => {
|
||||
for (const total of [REPLAY_BUFFER_MAX - 1, REPLAY_BUFFER_MAX, REPLAY_BUFFER_MAX + 1]) {
|
||||
const { deque, reference } = replayEquals(['a'.repeat(total)])
|
||||
expect(deque).toBe(reference)
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores empty appends exactly as the string form did', () => {
|
||||
const { deque, reference } = replayEquals(['a', '', 'b', '', 'c'])
|
||||
expect(deque).toBe(reference)
|
||||
expect(deque).toBe('abc')
|
||||
})
|
||||
|
||||
it('is stable across repeated reads', () => {
|
||||
const buffer = makeBuffer()
|
||||
for (let index = 0; index < 200; index += 1) {
|
||||
buffer.append(`line ${index}\r\n`.repeat(30))
|
||||
}
|
||||
expect(buffer.read()).toBe(buffer.read())
|
||||
})
|
||||
|
||||
it('keeps appending correctly after a read collapses the deque', () => {
|
||||
const buffer = makeBuffer()
|
||||
let reference = ''
|
||||
for (let index = 0; index < 60; index += 1) {
|
||||
const chunk = `mid-${index}-`.repeat(300)
|
||||
buffer.append(chunk)
|
||||
reference = appendStringTail(reference, chunk)
|
||||
// Interleave reads: attach/adopt/revive can land at any point in the stream.
|
||||
if (index % 7 === 0) {
|
||||
expect(buffer.read()).toBe(reference)
|
||||
}
|
||||
}
|
||||
expect(buffer.read()).toBe(reference)
|
||||
})
|
||||
|
||||
// Why fuzz: the deque trims across chunk boundaries with a deferred head offset,
|
||||
// so the risky cases are irregular chunk sizes straddling the cap repeatedly.
|
||||
it('matches the rolling string across randomized chunk streams', () => {
|
||||
for (let seed = 1; seed <= 40; seed += 1) {
|
||||
const random = makeRandom(seed)
|
||||
const buffer = makeBuffer()
|
||||
let reference = ''
|
||||
for (let step = 0; step < 120; step += 1) {
|
||||
const size = Math.floor(random() * (REPLAY_BUFFER_MAX / 8))
|
||||
const chunk = String.fromCharCode(97 + (step % 26)).repeat(size)
|
||||
buffer.append(chunk)
|
||||
reference = appendStringTail(reference, chunk)
|
||||
}
|
||||
expect(buffer.read(), `seed ${seed}`).toBe(reference)
|
||||
}
|
||||
})
|
||||
|
||||
// Why surrogates: trimming by code unit can split a pair, and the string form did
|
||||
// exactly the same thing. The deque must not "fix" it into a different tail.
|
||||
it('splits surrogate pairs identically to the rolling string', () => {
|
||||
// Why the trailing unit: the cap is even and a pair is 2 units, so an emoji run
|
||||
// alone always cuts on a pair boundary. One odd unit shifts the cut mid-pair.
|
||||
const { deque, reference } = replayEquals([`${'\u{1F600}'.repeat(REPLAY_BUFFER_MAX)}z`])
|
||||
expect(deque).toBe(reference)
|
||||
expect(deque.length).toBe(REPLAY_BUFFER_MAX)
|
||||
const leadUnit = deque.charCodeAt(0)
|
||||
expect(leadUnit).toBeGreaterThanOrEqual(0xdc00)
|
||||
expect(leadUnit).toBeLessThanOrEqual(0xdfff)
|
||||
})
|
||||
|
||||
it('keeps a pair-aligned tail intact when the cut lands on a boundary', () => {
|
||||
const { deque, reference } = replayEquals(['\u{1F600}'.repeat(REPLAY_BUFFER_MAX)])
|
||||
expect(deque).toBe(reference)
|
||||
const leadUnit = deque.charCodeAt(0)
|
||||
expect(leadUnit).toBeGreaterThanOrEqual(0xd800)
|
||||
expect(leadUnit).toBeLessThanOrEqual(0xdbff)
|
||||
})
|
||||
})
|
||||
@@ -205,21 +205,6 @@ describe('relay oversized notification survival', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it("publishAgentHookEnvelope does not mutate the caller's envelope", () => {
|
||||
const sink: Buffer[] = []
|
||||
const dispatcher = createDispatcher(NODE21_HWM, () => {}, sink)
|
||||
const envelope = buildMaximalHookEnvelope()
|
||||
const before = structuredClone(envelope)
|
||||
|
||||
try {
|
||||
publishAgentHookEnvelope(dispatcher, envelope as unknown as AgentHookRelayEnvelope)
|
||||
// The hook server replays this exact object after --connect, so shedding must never touch it.
|
||||
expect(envelope).toEqual(before)
|
||||
} finally {
|
||||
dispatcher.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('reattach + replay does not re-kill', () => {
|
||||
const sink: Buffer[] = []
|
||||
let killCloses = 0
|
||||
|
||||
@@ -234,38 +234,6 @@ describe('Subprocess: Relay entry point', () => {
|
||||
expect(repaired.result).toMatchObject({ id: expect.stringMatching(/^pty2:[^:]+:2$/) })
|
||||
}, 10_000)
|
||||
|
||||
it('responds to fs.stat over stdin/stdout', async () => {
|
||||
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-sub-'))
|
||||
writeFileSync(path.join(tmpDir, 'test.txt'), 'hello')
|
||||
|
||||
relay = spawn()
|
||||
await relay.sentinelReceived
|
||||
|
||||
const id = relay.send('fs.stat', { filePath: path.join(tmpDir, 'test.txt') })
|
||||
const resp = await relay.waitForResponse(id)
|
||||
|
||||
expect(resp.result).toBeDefined()
|
||||
const result = resp.result as { size: number; type: string }
|
||||
expect(result.type).toBe('file')
|
||||
expect(result.size).toBe(5)
|
||||
}, 10_000)
|
||||
|
||||
it('responds to fs.readDir', async () => {
|
||||
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-sub-'))
|
||||
writeFileSync(path.join(tmpDir, 'a.txt'), 'a')
|
||||
writeFileSync(path.join(tmpDir, 'b.txt'), 'b')
|
||||
|
||||
relay = spawn()
|
||||
await relay.sentinelReceived
|
||||
|
||||
const id = relay.send('fs.readDir', { dirPath: tmpDir })
|
||||
const resp = await relay.waitForResponse(id)
|
||||
|
||||
const entries = resp.result as { name: string }[]
|
||||
const names = entries.map((e) => e.name).sort()
|
||||
expect(names).toEqual(['a.txt', 'b.txt'])
|
||||
}, 10_000)
|
||||
|
||||
it('responds to fs.readFile and fs.writeFile', async () => {
|
||||
tmpDir = mkdtempSync(path.join(tmpdir(), 'relay-sub-'))
|
||||
|
||||
|
||||
@@ -108,15 +108,4 @@ describe('normalizeDshEvent', () => {
|
||||
).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores events the bridge cannot send', () => {
|
||||
// Notification and PermissionRequest are Claude-only; if one ever arrives it is not
|
||||
// from this bridge, and guessing a state from it would be unfounded.
|
||||
expect(
|
||||
normalizeAndAccept(state, 'dsh', event('Notification', { message: 'waiting' }))
|
||||
).toBeNull()
|
||||
expect(
|
||||
normalizeAndAccept(state, 'dsh', event('PermissionRequest', { tool_name: 'bash' }))
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,13 +166,4 @@ describe('normalizeZCodeEvent', () => {
|
||||
it('ignores lifecycle events it does not model', () => {
|
||||
expect(normalizeAndAccept(state, 'zcode', zcodeEvent('SomethingElse'))).toBeNull()
|
||||
})
|
||||
|
||||
it('attributes every event to zcode, never to claude, despite the compatible payload', () => {
|
||||
const event = normalizeAndAccept(
|
||||
state,
|
||||
'zcode',
|
||||
zcodeEvent('UserPromptSubmit', { prompt: 'hello' })
|
||||
)
|
||||
expect(event?.payload.agentType).toBe('zcode')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,38 +8,11 @@ import {
|
||||
createShedSubagentsField,
|
||||
isAgentHookSource,
|
||||
isRemoteAgentHooksEnabled,
|
||||
restoreShedStatusFields,
|
||||
type AgentHookRelayEnvelope
|
||||
restoreShedStatusFields
|
||||
} from './agent-hook-relay'
|
||||
import type { ParsedAgentStatusPayload } from './agent-status-types'
|
||||
|
||||
describe('agent-hook-relay wire shape', () => {
|
||||
it('encodes/decodes through JSON without losing fields', () => {
|
||||
const envelope: AgentHookRelayEnvelope = {
|
||||
source: 'claude',
|
||||
paneKey: 'tab-1:0',
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
connectionId: null,
|
||||
env: 'production',
|
||||
version: '1',
|
||||
providerPromptId: '11111111-1111-4111-8111-111111111111',
|
||||
compactTrigger: 'manual',
|
||||
payload: {
|
||||
state: 'working',
|
||||
workingMode: 'monitoring',
|
||||
prompt: 'roundtrip',
|
||||
agentType: 'claude'
|
||||
}
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(JSON.stringify(envelope)) as AgentHookRelayEnvelope
|
||||
expect(decoded).toEqual(envelope)
|
||||
expect(decoded.connectionId).toBeNull()
|
||||
expect(decoded.payload.prompt).toBe('roundtrip')
|
||||
expect(decoded.payload.workingMode).toBe('monitoring')
|
||||
})
|
||||
|
||||
it('exposes stable JSON-RPC method names', () => {
|
||||
expect(AGENT_HOOK_NOTIFICATION_METHOD).toBe('agent.hook')
|
||||
expect(AGENT_HOOK_REQUEST_REPLAY_METHOD).toBe('agent_hook.requestReplay')
|
||||
|
||||
@@ -12,26 +12,6 @@ describe('agent session resume metadata', () => {
|
||||
expect(isResumableTuiAgent('devin')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats omp as a resumable TUI agent', () => {
|
||||
expect(isResumableTuiAgent('omp')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats Prime Agent as a resumable TUI agent', () => {
|
||||
expect(isResumableTuiAgent('prime-agent')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats copilot as a resumable TUI agent', () => {
|
||||
expect(isResumableTuiAgent('copilot')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats Kimi Code as a resumable TUI agent', () => {
|
||||
expect(isResumableTuiAgent('kimi')).toBe(true)
|
||||
})
|
||||
|
||||
it('treats CodeBuddy as a resumable TUI agent', () => {
|
||||
expect(isResumableTuiAgent('codebuddy')).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['claude', { session_id: 'claude-session' }, { key: 'session_id', id: 'claude-session' }],
|
||||
['codex', { session_id: 'codex-session' }, { key: 'session_id', id: 'codex-session' }],
|
||||
|
||||
@@ -38,38 +38,4 @@ describe('OSC 9999 split-frame scan budget', () => {
|
||||
// Re-scanning the accumulation would quadruple the budget when the feed doubles.
|
||||
expect(large.searchedChars).toBeLessThan(small.searchedChars * 3)
|
||||
})
|
||||
|
||||
it.each(['\x07', '\x1b\\'])(
|
||||
'matches whole-string parsing when split at every offset with terminator %j',
|
||||
(terminator) => {
|
||||
const stream = `head\x1b]9999;{"state":"working","prompt":"p"}${terminator}tail`
|
||||
const whole = createAgentStatusOscProcessor()(stream)
|
||||
|
||||
for (let split = 1; split < stream.length; split += 1) {
|
||||
const process = createAgentStatusOscProcessor()
|
||||
const first = process(stream.slice(0, split))
|
||||
const second = process(stream.slice(split))
|
||||
expect({
|
||||
cleanData: first.cleanData + second.cleanData,
|
||||
payloads: [...first.payloads, ...second.payloads]
|
||||
}).toEqual({ cleanData: whole.cleanData, payloads: whole.payloads })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('finds a string terminator straddling the resume boundary', () => {
|
||||
const process = createAgentStatusOscProcessor()
|
||||
// The ESC lands as the last character of the carried frame; the backslash arrives next.
|
||||
expect(process('\x1b]9999;{"state":"working"}\x1b').payloads).toEqual([])
|
||||
expect(process('\\rest').payloads).toMatchObject([{ state: 'working' }])
|
||||
})
|
||||
|
||||
it('still parses a payload that completes many chunks later', () => {
|
||||
const process = createAgentStatusOscProcessor()
|
||||
process('\x1b]9999;{"state":"wor')
|
||||
for (const chunk of ['k', 'i', 'n', 'g']) {
|
||||
expect(process(chunk).payloads).toEqual([])
|
||||
}
|
||||
expect(process('"}\x07done').payloads).toMatchObject([{ state: 'working' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
GRAB_BUDGET,
|
||||
GRAB_SAFE_ATTRIBUTE_NAMES,
|
||||
GRAB_SECRET_PATTERNS,
|
||||
GRAB_STYLE_PROPERTIES,
|
||||
isAriaAttribute
|
||||
} from './browser-grab-types'
|
||||
|
||||
describe('browser-grab-types', () => {
|
||||
describe('GRAB_BUDGET', () => {
|
||||
it('defines all required budget fields', () => {
|
||||
expect(GRAB_BUDGET.textSnippetMaxLength).toBe(200)
|
||||
expect(GRAB_BUDGET.nearbyTextEntryMaxLength).toBe(200)
|
||||
expect(GRAB_BUDGET.nearbyTextMaxEntries).toBe(10)
|
||||
expect(GRAB_BUDGET.htmlSnippetMaxLength).toBe(4096)
|
||||
expect(GRAB_BUDGET.ancestorPathMaxEntries).toBe(10)
|
||||
expect(GRAB_BUDGET.nearbyElementMaxLength).toBe(160)
|
||||
expect(GRAB_BUDGET.cssClassesMaxLength).toBe(500)
|
||||
expect(GRAB_BUDGET.selectedTextMaxLength).toBe(500)
|
||||
expect(GRAB_BUDGET.annotationsMaxPerPage).toBe(20)
|
||||
expect(GRAB_BUDGET.screenshotMaxBytes).toBe(2 * 1024 * 1024)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAriaAttribute', () => {
|
||||
it('returns true for aria- prefixed attributes', () => {
|
||||
expect(isAriaAttribute('aria-label')).toBe(true)
|
||||
@@ -71,17 +54,4 @@ describe('browser-grab-types', () => {
|
||||
expect(GRAB_SECRET_PATTERNS).not.toContain('token')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GRAB_STYLE_PROPERTIES', () => {
|
||||
it('includes the curated subset of computed styles', () => {
|
||||
expect(GRAB_STYLE_PROPERTIES).toContain('display')
|
||||
expect(GRAB_STYLE_PROPERTIES).toContain('fontSize')
|
||||
expect(GRAB_STYLE_PROPERTIES).toContain('backgroundColor')
|
||||
expect(GRAB_STYLE_PROPERTIES).toContain('zIndex')
|
||||
})
|
||||
|
||||
it('has exactly 16 properties matching the type', () => {
|
||||
expect(GRAB_STYLE_PROPERTIES).toHaveLength(16)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -241,23 +241,3 @@ export const GRAB_SECRET_PATTERNS = [
|
||||
'password',
|
||||
'passwd'
|
||||
]
|
||||
|
||||
/** Computed style properties to extract — matches BrowserGrabComputedStyles keys. */
|
||||
export const GRAB_STYLE_PROPERTIES: readonly (keyof BrowserGrabComputedStyles)[] = [
|
||||
'display',
|
||||
'position',
|
||||
'width',
|
||||
'height',
|
||||
'margin',
|
||||
'padding',
|
||||
'color',
|
||||
'backgroundColor',
|
||||
'border',
|
||||
'borderRadius',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'lineHeight',
|
||||
'textAlign',
|
||||
'zIndex'
|
||||
]
|
||||
|
||||
@@ -115,8 +115,6 @@ describe('getDefaultSettings', () => {
|
||||
expect(getDefaultSettings('/tmp').experimentalAgentDashboardShowIdle).toBeUndefined()
|
||||
})
|
||||
|
||||
it('routes fresh Codex profiles through the real-home rollout by default', () => {})
|
||||
|
||||
it('defaults local Windows projects to the host runtime', () => {
|
||||
expect(getDefaultSettings('/tmp').localWindowsRuntimeDefault).toEqual({
|
||||
kind: 'windows-host'
|
||||
|
||||
@@ -2,31 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
|
||||
describe('Freebuff startup', () => {
|
||||
it.each([
|
||||
['darwin', 'posix', false],
|
||||
['linux', 'posix', false],
|
||||
['win32', 'powershell', false],
|
||||
['win32', 'cmd', false],
|
||||
['linux', 'posix', true]
|
||||
] as const)('delivers prompts after launch on %s/%s (remote=%s)', (platform, shell, isRemote) => {
|
||||
const prompt = 'Review "quoted paths"; $(echo task)\nThen explain the changes.'
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'freebuff',
|
||||
prompt,
|
||||
cmdOverrides: { codebuff: 'codebuff --debug' },
|
||||
platform,
|
||||
shell,
|
||||
isRemote
|
||||
})
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
agent: 'freebuff',
|
||||
launchCommand: 'freebuff',
|
||||
expectedProcess: 'freebuff',
|
||||
followupPrompt: prompt
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps Freebuff command overrides separate from Codebuff', () => {
|
||||
const cmdOverrides = { freebuff: 'freebuff --debug' }
|
||||
const freebuff = buildAgentStartupPlan({
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
parseGitRevListAheadBehindCounts,
|
||||
parseGitRevListFirstParentOid
|
||||
} from './git-rev-list-output'
|
||||
|
||||
describe('parseGitRevListAheadBehindCounts', () => {
|
||||
it('parses rev-list counts without whitespace-regex splitting', () => {
|
||||
const splitSpy = vi.spyOn(String.prototype, 'split')
|
||||
try {
|
||||
expect(parseGitRevListAheadBehindCounts(' 12\t3\r\n')).toEqual({
|
||||
status: 'ok',
|
||||
ahead: 12,
|
||||
behind: 3
|
||||
})
|
||||
const usedWhitespaceSplit = splitSpy.mock.calls.some(
|
||||
([separator]) => separator instanceof RegExp && separator.source === '\\s+'
|
||||
)
|
||||
expect(usedWhitespaceSplit).toBe(false)
|
||||
} finally {
|
||||
splitSpy.mockRestore()
|
||||
}
|
||||
it('parses counts surrounded by mixed whitespace', () => {
|
||||
expect(parseGitRevListAheadBehindCounts(' 12\t3\r\n')).toEqual({
|
||||
status: 'ok',
|
||||
ahead: 12,
|
||||
behind: 3
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects missing or extra fields', () => {
|
||||
|
||||
@@ -2,38 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
GITHUB_PROJECT_REF_INPUT_MAX_BYTES,
|
||||
getGitHubProjectRefInputByteLength,
|
||||
hasBoundedGitHubProjectRefInputText,
|
||||
isGitHubProjectRefInputTooLarge
|
||||
} from './project-ref-input'
|
||||
|
||||
describe('GitHub project reference input limits', () => {
|
||||
it('allows normal project references below the byte budget', () => {
|
||||
expect(
|
||||
isGitHubProjectRefInputTooLarge('https://github.com/orgs/acme/projects/42/views/3')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('measures UTF-8 bytes instead of JavaScript string length', () => {
|
||||
expect(getGitHubProjectRefInputByteLength('\u00e9')).toBe(2)
|
||||
})
|
||||
|
||||
it('rejects oversized pasted project references', () => {
|
||||
expect(isGitHubProjectRefInputTooLarge('x'.repeat(GITHUB_PROJECT_REF_INPUT_MAX_BYTES))).toBe(
|
||||
false
|
||||
)
|
||||
expect(
|
||||
isGitHubProjectRefInputTooLarge('x'.repeat(GITHUB_PROJECT_REF_INPUT_MAX_BYTES + 1))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects multibyte project references whose character count is below the limit', () => {
|
||||
const reference = '😀'.repeat(Math.floor(GITHUB_PROJECT_REF_INPUT_MAX_BYTES / 4) + 1)
|
||||
|
||||
expect(reference.length).toBeLessThan(GITHUB_PROJECT_REF_INPUT_MAX_BYTES)
|
||||
expect(isGitHubProjectRefInputTooLarge(reference)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects oversized whitespace before submit checks trim the reference', () => {
|
||||
const oversizedWhitespace = ' '.repeat(GITHUB_PROJECT_REF_INPUT_MAX_BYTES + 1)
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { getClipboardTextByteLength, isClipboardTextByteLengthOverLimit } from '../clipboard-text'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../clipboard-text'
|
||||
|
||||
export const GITHUB_PROJECT_REF_INPUT_MAX_BYTES = 2 * 1024
|
||||
export const GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR = 'Project reference is too large to resolve.'
|
||||
|
||||
export function getGitHubProjectRefInputByteLength(input: string): number {
|
||||
return getClipboardTextByteLength(input)
|
||||
}
|
||||
|
||||
export function isGitHubProjectRefInputTooLarge(
|
||||
input: string,
|
||||
maxBytes = GITHUB_PROJECT_REF_INPUT_MAX_BYTES
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
GITHUB_WORK_ITEMS_QUERY_MAX_BYTES,
|
||||
isGitHubWorkItemsQueryTooLarge
|
||||
} from './work-items-query-bounds'
|
||||
|
||||
describe('shared GitHub work item query bounds', () => {
|
||||
it('allows normal GitHub search syntax', () => {
|
||||
expect(isGitHubWorkItemsQueryTooLarge('is:issue is:open label:bug')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects oversized pasted work item queries by byte length', () => {
|
||||
expect(isGitHubWorkItemsQueryTooLarge('x'.repeat(GITHUB_WORK_ITEMS_QUERY_MAX_BYTES))).toBe(
|
||||
false
|
||||
)
|
||||
expect(isGitHubWorkItemsQueryTooLarge('x'.repeat(GITHUB_WORK_ITEMS_QUERY_MAX_BYTES + 1))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects multibyte pasted queries whose character count is below the limit', () => {
|
||||
const query = '😀'.repeat(Math.floor(GITHUB_WORK_ITEMS_QUERY_MAX_BYTES / 4) + 1)
|
||||
|
||||
expect(query.length).toBeLessThan(GITHUB_WORK_ITEMS_QUERY_MAX_BYTES)
|
||||
expect(isGitHubWorkItemsQueryTooLarge(query)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY,
|
||||
GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY,
|
||||
RUNTIME_CAPABILITIES
|
||||
} from './protocol-version'
|
||||
|
||||
describe('hosted review Ready capabilities', () => {
|
||||
it('advertises both provider mutation contracts', () => {
|
||||
expect(RUNTIME_CAPABILITIES).toContain(GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY)
|
||||
expect(RUNTIME_CAPABILITIES).toContain(GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { KEYBINDING_DEFINITIONS } from './keybindings/definitions'
|
||||
import { getDefaultBindings } from './keybindings/effective'
|
||||
import { formatKeybindingList } from './keybindings/formatting'
|
||||
import { normalizeKeyToken, parseKeybinding } from './keybindings/parser'
|
||||
|
||||
describe('parseKeybinding memoization', () => {
|
||||
@@ -12,11 +9,6 @@ describe('parseKeybinding memoization', () => {
|
||||
expect(second).toBe(first)
|
||||
})
|
||||
|
||||
it('caches rejections without re-parsing', () => {
|
||||
expect(parseKeybinding('K+J')).toBeNull()
|
||||
expect(parseKeybinding('K+J')).toBeNull()
|
||||
})
|
||||
|
||||
it('never hands out an entry a caller can corrupt', () => {
|
||||
const parsed = parseKeybinding('Mod+P')
|
||||
expect(Object.isFrozen(parsed)).toBe(true)
|
||||
@@ -32,44 +24,7 @@ describe('parseKeybinding memoization', () => {
|
||||
expect(parseKeybinding('DoubleTap+Shift')?.doubleTapModifier).toBe('Shift')
|
||||
})
|
||||
|
||||
it('parses every distinct token form identically across repeat calls', () => {
|
||||
const tokens = [
|
||||
' ',
|
||||
'a',
|
||||
'7',
|
||||
'f7',
|
||||
'[',
|
||||
'}',
|
||||
'-',
|
||||
'_',
|
||||
'=',
|
||||
'+',
|
||||
',',
|
||||
'.',
|
||||
'/',
|
||||
'\\',
|
||||
';',
|
||||
"'",
|
||||
'`',
|
||||
'return',
|
||||
'esc',
|
||||
'spacebar',
|
||||
'pgup',
|
||||
'pgdn',
|
||||
'arrowleft',
|
||||
'left',
|
||||
'down',
|
||||
'backspace',
|
||||
'del',
|
||||
'ins',
|
||||
'numpadadd',
|
||||
'subtract',
|
||||
'nonsense',
|
||||
''
|
||||
]
|
||||
for (const token of tokens) {
|
||||
expect(normalizeKeyToken(token)).toBe(normalizeKeyToken(token))
|
||||
}
|
||||
it('maps token spellings through the table and blocks prototype keys', () => {
|
||||
expect(normalizeKeyToken(' ')).toBe('Space')
|
||||
expect(normalizeKeyToken('pgdn')).toBe('PageDown')
|
||||
expect(normalizeKeyToken('subtract')).toBe('NumpadSubtract')
|
||||
@@ -80,16 +35,3 @@ describe('parseKeybinding memoization', () => {
|
||||
expect(normalizeKeyToken('__proto__')).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shortcut label output', () => {
|
||||
it('formats every default binding identically on repeat calls, on both glyph platforms', () => {
|
||||
for (const platform of ['darwin', 'win32'] as const) {
|
||||
for (const definition of KEYBINDING_DEFINITIONS) {
|
||||
const bindings = getDefaultBindings(definition, platform)
|
||||
const label = formatKeybindingList(bindings, platform)
|
||||
expect(formatKeybindingList(bindings, platform)).toBe(label)
|
||||
expect(label.length).toBeGreaterThan(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -151,19 +151,6 @@ describe('buildLinearIssueLinkUpdates', () => {
|
||||
expectNoUndefinedValues(result as Record<string, unknown>)
|
||||
})
|
||||
|
||||
it('links digit-prefixed Linear team keys from bare identifiers and URLs', () => {
|
||||
expect(buildLinearIssueLinkUpdates('4K-12')).toEqual({
|
||||
linkedLinearIssue: '4K-12',
|
||||
linkedLinearIssueWorkspaceId: null,
|
||||
linkedLinearIssueOrganizationUrlKey: null
|
||||
})
|
||||
expect(buildLinearIssueLinkUpdates('https://linear.app/acme/issue/4K-12')).toEqual({
|
||||
linkedLinearIssue: '4K-12',
|
||||
linkedLinearIssueWorkspaceId: null,
|
||||
linkedLinearIssueOrganizationUrlKey: 'acme'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for unparseable input', () => {
|
||||
expect(buildLinearIssueLinkUpdates('not an issue')).toBeNull()
|
||||
expect(buildLinearIssueLinkUpdates('https://github.com/o/r/issues/12')).toBeNull()
|
||||
|
||||
@@ -9,32 +9,6 @@ describe('parseServerShareAddress', () => {
|
||||
expect(parseServerShareAddress('192.168.1.50').ok).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts host:port', () => {
|
||||
expect(parseServerShareAddress('192.168.1.50:6768')).toEqual({
|
||||
ok: true,
|
||||
value: '192.168.1.50:6768'
|
||||
})
|
||||
expect(parseServerShareAddress('my-host:443').ok).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts IPv6 literals with an optional port', () => {
|
||||
expect(parseServerShareAddress('fd7a:115c:a1e0::1').ok).toBe(true)
|
||||
expect(parseServerShareAddress('[fd7a:115c:a1e0::1]:6768').ok).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts ws:// and wss:// URLs', () => {
|
||||
expect(parseServerShareAddress('wss://my-host/path').ok).toBe(true)
|
||||
expect(parseServerShareAddress('ws://192.168.1.50:6768').ok).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects WebSocket URLs with fragments', () => {
|
||||
expect(parseServerShareAddress('wss://my-host/path#fragment').ok).toBe(false)
|
||||
})
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(parseServerShareAddress(' my-host:8080 ')).toEqual({ ok: true, value: 'my-host:8080' })
|
||||
})
|
||||
|
||||
it('rejects empty, whitespace-containing, and malformed input', () => {
|
||||
for (const bad of [
|
||||
'',
|
||||
@@ -53,8 +27,4 @@ describe('parseServerShareAddress', () => {
|
||||
}
|
||||
expect(parseServerShareAddress('a'.repeat(PAIRING_ENDPOINT_MAX_CHARACTERS + 1)).ok).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects an out-of-range port', () => {
|
||||
expect(parseServerShareAddress('my-host:70000').ok).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pluginManifestSchema } from './plugin-manifest'
|
||||
|
||||
describe('hello Orca plugin fixture', () => {
|
||||
it('uses an ESM entry that remains loadable outside a type-module package', async () => {
|
||||
const root = join(process.cwd(), 'examples', 'plugins', 'hello-orca')
|
||||
const manifest = pluginManifestSchema.parse(
|
||||
JSON.parse(await readFile(join(root, 'orca-plugin.json'), 'utf8'))
|
||||
)
|
||||
|
||||
expect(manifest.main).toBe('main.mjs')
|
||||
const workerModule = (await import(pathToFileURL(join(root, manifest.main!)).href)) as {
|
||||
default?: unknown
|
||||
}
|
||||
expect(workerModule.default).toBeTypeOf('function')
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { Script } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('hostile panel fixture', () => {
|
||||
it('is complete JavaScript and retains every containment probe', async () => {
|
||||
const html = await readFile(
|
||||
join(process.cwd(), 'examples', 'plugins', 'hostile-panel', 'panel.html'),
|
||||
'utf8'
|
||||
)
|
||||
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1]
|
||||
|
||||
expect(script).toBeTruthy()
|
||||
if (!script) {
|
||||
throw new Error('hostile fixture script is missing')
|
||||
}
|
||||
expect(() => new Script(script)).not.toThrow()
|
||||
expect(html).toContain('self-navigation')
|
||||
expect(html).toContain('meta-refresh-navigation')
|
||||
expect(html).toContain('Navigation probes are opt-in')
|
||||
expect(html).toContain("window.name = ''")
|
||||
expect(html).toContain('oversized-message')
|
||||
expect(html).toContain('message-flood')
|
||||
expect(html).toContain("data.errorCode === 'rate_limited'")
|
||||
expect(html).toContain('busy-loop')
|
||||
expect(html.trimEnd()).toMatch(/<\/html>$/)
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,9 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildPluginPanelShellHtml,
|
||||
PANEL_DESIGN_TOKEN_ALLOWLIST,
|
||||
PLUGIN_PANEL_CSP
|
||||
} from './plugin-panel-shell'
|
||||
import { buildPluginPanelShellHtml, PLUGIN_PANEL_CSP } from './plugin-panel-shell'
|
||||
|
||||
describe('buildPluginPanelShellHtml', () => {
|
||||
it('keeps destructive surface and foreground tokens paired', () => {
|
||||
expect(PANEL_DESIGN_TOKEN_ALLOWLIST).toEqual(
|
||||
expect.arrayContaining(['--destructive', '--destructive-foreground'])
|
||||
)
|
||||
})
|
||||
|
||||
it('places CSP and navigation guards before plugin content', () => {
|
||||
const html = buildPluginPanelShellHtml('<main id="plugin-content">Plugin</main>')
|
||||
const pluginOffset = html.indexOf('plugin-content')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getProcessOutputFields,
|
||||
iterateProcessOutputLines,
|
||||
@@ -23,18 +23,6 @@ describe('getProcessOutputFields', () => {
|
||||
).toEqual(['TCP', '127.0.0.1:3000', '0.0.0.0:0', 'LISTENING', '4242'])
|
||||
})
|
||||
|
||||
it('does not use regex whitespace splitting', () => {
|
||||
const splitSpy = vi.spyOn(String.prototype, 'split')
|
||||
|
||||
getProcessOutputFields('alpha beta gamma', 2)
|
||||
|
||||
const usedWhitespaceFieldSplit = splitSpy.mock.calls.some(
|
||||
([separator]) => separator instanceof RegExp && separator.source.includes('\\s+')
|
||||
)
|
||||
splitSpy.mockRestore()
|
||||
expect(usedWhitespaceFieldSplit).toBe(false)
|
||||
})
|
||||
|
||||
it('caps scan work for oversized rows', () => {
|
||||
const fields = getProcessOutputFields('x'.repeat(PROCESS_OUTPUT_FIELD_SCAN_MAX_CHARS + 100), 2)
|
||||
|
||||
|
||||
@@ -25,26 +25,6 @@ describe('evaluateCompat', () => {
|
||||
expect(verdict).toEqual({ kind: 'ok' })
|
||||
})
|
||||
|
||||
it('returns ok when desktop reports version equal to mobile', () => {
|
||||
const verdict = evaluateCompat({
|
||||
mobileProtocolVersion: MOBILE_V,
|
||||
minCompatibleDesktopVersion: 0,
|
||||
desktopProtocolVersion: MOBILE_V,
|
||||
desktopMinCompatibleMobileVersion: 0
|
||||
})
|
||||
expect(verdict).toEqual({ kind: 'ok' })
|
||||
})
|
||||
|
||||
it('returns ok when desktop reports a newer version (additive changes assumed safe)', () => {
|
||||
const verdict = evaluateCompat({
|
||||
mobileProtocolVersion: MOBILE_V,
|
||||
minCompatibleDesktopVersion: 0,
|
||||
desktopProtocolVersion: MOBILE_V + 5,
|
||||
desktopMinCompatibleMobileVersion: 0
|
||||
})
|
||||
expect(verdict).toEqual({ kind: 'ok' })
|
||||
})
|
||||
|
||||
it('allows desktop protocol 3 to roll out before mobile protocol 2 updates', () => {
|
||||
const verdict = evaluateCompat({
|
||||
mobileProtocolVersion: 2,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { detectLinuxInstallCommandFromOsRelease } from './quick-open-install-rg'
|
||||
|
||||
describe('detectLinuxInstallCommandFromOsRelease', () => {
|
||||
@@ -24,18 +24,4 @@ describe('detectLinuxInstallCommandFromOsRelease', () => {
|
||||
'install ripgrep via your package manager (e.g. apt/dnf/pacman)'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not use whitespace regex splitting for ID_LIKE parsing', () => {
|
||||
const splitSpy = vi.spyOn(String.prototype, 'split')
|
||||
|
||||
expect(detectLinuxInstallCommandFromOsRelease('ID=unknown\nID_LIKE="rhel fedora"')).toBe(
|
||||
'sudo dnf install ripgrep'
|
||||
)
|
||||
|
||||
const usedWhitespaceFieldSplit = splitSpy.mock.calls.some(
|
||||
([separator]) => separator instanceof RegExp && separator.source.includes('\\s+')
|
||||
)
|
||||
splitSpy.mockRestore()
|
||||
expect(usedWhitespaceFieldSplit).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { RateLimitState } from './rate-limit-types'
|
||||
|
||||
describe('RateLimitState', () => {
|
||||
it('documents provider surfaces used by settings and status-bar UI', () => {
|
||||
// Why: the AccountsPane and the status bar both read these fields
|
||||
// from RateLimitState. The shape must stay stable so that the
|
||||
// visibility check (status-bar-provider-visibility) keeps working
|
||||
// across refactors.
|
||||
const state: RateLimitState = {
|
||||
claude: null,
|
||||
codex: null,
|
||||
gemini: null,
|
||||
opencodeGo: null,
|
||||
kimi: null,
|
||||
antigravity: null,
|
||||
minimax: null,
|
||||
grok: null,
|
||||
cursor: null,
|
||||
zcode: null,
|
||||
minimaxCookieConfigured: false,
|
||||
minimaxApiKeyConfigured: false,
|
||||
opencodeGoApiKeyConfigured: false,
|
||||
grokAuthConfigured: false,
|
||||
cursorAuthConfigured: false,
|
||||
claudeTarget: { runtime: 'host', wslDistro: null },
|
||||
codexTarget: { runtime: 'host', wslDistro: null },
|
||||
inactiveClaudeAccounts: [],
|
||||
inactiveCodexAccounts: []
|
||||
}
|
||||
|
||||
expect(state.antigravity).toBeNull()
|
||||
expect(state.minimax).toBeNull()
|
||||
expect(state.minimaxCookieConfigured).toBe(false)
|
||||
expect(state.minimaxApiKeyConfigured).toBe(false)
|
||||
expect(state.cursor).toBeNull()
|
||||
expect(state.cursorAuthConfigured).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -126,15 +126,6 @@ describe('directoryEntryNeedsStat', () => {
|
||||
isSymbolicLink: () => kind === 'link'
|
||||
})
|
||||
|
||||
it('skips the stat for entries readdir already typed', () => {
|
||||
expect(directoryEntryNeedsStat(probe('file'))).toBe(false)
|
||||
expect(directoryEntryNeedsStat(probe('dir'))).toBe(false)
|
||||
})
|
||||
|
||||
it('stats links so they are followed', () => {
|
||||
expect(directoryEntryNeedsStat(probe('link'))).toBe(true)
|
||||
})
|
||||
|
||||
// Filesystems without d_type report DT_UNKNOWN: every predicate is false, and
|
||||
// without the stat a real directory's whole subtree is silently dropped.
|
||||
it('stats an entry whose type readdir could not report', () => {
|
||||
|
||||
@@ -1,38 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type {
|
||||
SshTarget,
|
||||
SshConnectionState,
|
||||
SshConnectionStatus,
|
||||
SshProviderEpoch
|
||||
} from './ssh-types'
|
||||
import type { SshConnectionState, SshProviderEpoch } from './ssh-types'
|
||||
|
||||
describe('SSH types', () => {
|
||||
it('SshTarget has required fields', () => {
|
||||
const target: SshTarget = {
|
||||
id: 'target-1',
|
||||
label: 'My Server',
|
||||
host: 'myserver.com',
|
||||
port: 22,
|
||||
username: 'deploy'
|
||||
}
|
||||
expect(target.id).toBe('target-1')
|
||||
expect(target.host).toBe('myserver.com')
|
||||
})
|
||||
|
||||
it('SshConnectionStatus covers all expected states', () => {
|
||||
const statuses: SshConnectionStatus[] = [
|
||||
'disconnected',
|
||||
'connecting',
|
||||
'auth-failed',
|
||||
'deploying-relay',
|
||||
'connected',
|
||||
'reconnecting',
|
||||
'reconnection-failed',
|
||||
'error'
|
||||
]
|
||||
expect(statuses).toHaveLength(8)
|
||||
})
|
||||
|
||||
it('SshConnectionState composes correctly', () => {
|
||||
const state: SshConnectionState = {
|
||||
targetId: 'target-1',
|
||||
@@ -47,25 +16,4 @@ describe('SSH types', () => {
|
||||
expect(state.providerEpoch).toBe('provider-a')
|
||||
expect(state.connectionGeneration).toBe(1)
|
||||
})
|
||||
|
||||
it('Repo.connectionId is optional for backward compatibility', () => {
|
||||
// Import the Repo type to verify connectionId is optional
|
||||
const repo = {
|
||||
id: 'repo-1',
|
||||
path: '/path/to/repo',
|
||||
displayName: 'My Repo',
|
||||
badgeColor: '#ff0000',
|
||||
addedAt: Date.now()
|
||||
}
|
||||
// Should compile without connectionId
|
||||
expect(repo.id).toBe('repo-1')
|
||||
expect('connectionId' in repo).toBe(false)
|
||||
|
||||
// Should also work with connectionId
|
||||
const remoteRepo = {
|
||||
...repo,
|
||||
connectionId: 'target-1'
|
||||
}
|
||||
expect(remoteRepo.connectionId).toBe('target-1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,15 +71,6 @@ describe('per-launch structured feasibility', () => {
|
||||
expect(support(overrides)).toEqual({ supported: false, blocker })
|
||||
})
|
||||
|
||||
// The client cannot see whether the host can read a provider child's start time, so neither
|
||||
// provider is refused here on platform; agentSession.createSupport answers that at create time.
|
||||
it.each(['claude', 'codex'] as const)(
|
||||
'leaves a Windows %s launch to the executing host',
|
||||
(agent) => {
|
||||
expect(support({ agent })).toEqual({ supported: true })
|
||||
}
|
||||
)
|
||||
|
||||
it('blocks a WSL or repair-required project runtime', () => {
|
||||
expect(
|
||||
support({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { eventSchemas, featureWallTileIdSchema } from './telemetry-events'
|
||||
import { eventSchemas } from './telemetry-events'
|
||||
|
||||
describe('feature wall schemas', () => {
|
||||
it('accepts the unconditional open and close payloads', () => {
|
||||
@@ -124,8 +124,4 @@ describe('feature wall schemas', () => {
|
||||
}).success
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('feature wall enum schemas accept known values', () => {
|
||||
expect(featureWallTileIdSchema.safeParse('tile-01').success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -280,14 +280,4 @@ describe('terminal title classification', () => {
|
||||
expect(resolveExplicitTerminalTitleAgentType(title)).toBe(explicitTitleAgent)
|
||||
}
|
||||
)
|
||||
|
||||
it('returns the same verdict on the second read of every title', () => {
|
||||
for (const title of TERMINAL_TITLE_CLASSIFICATION_CORPUS) {
|
||||
expect(detectAgentStatusFromTitle(title)).toBe(detectAgentStatusFromTitle(title))
|
||||
expect(getAgentLabel(title)).toBe(getAgentLabel(title))
|
||||
expect(resolveExplicitTerminalTitleAgentType(title)).toBe(
|
||||
resolveExplicitTerminalTitleAgentType(title)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { normalizeSupportedUiLocale, resolveUiLocale, resolveRendererUiLocale } from './ui-locale'
|
||||
import { normalizeSupportedUiLocale, resolveUiLocale } from './ui-locale'
|
||||
import {
|
||||
UI_LANGUAGE_CHINESE,
|
||||
UI_LANGUAGE_ENGLISH,
|
||||
@@ -19,28 +19,6 @@ describe('ui-locale', () => {
|
||||
expect(normalizeSupportedUiLocale('zh-SG')).toBe('zh')
|
||||
})
|
||||
|
||||
it('normalizes Korean locale prefixes', () => {
|
||||
expect(normalizeSupportedUiLocale('ko-KR')).toBe('ko')
|
||||
expect(normalizeSupportedUiLocale('ko')).toBe('ko')
|
||||
})
|
||||
|
||||
it('normalizes Japanese locale prefixes', () => {
|
||||
expect(normalizeSupportedUiLocale('ja-JP')).toBe('ja')
|
||||
expect(normalizeSupportedUiLocale('ja')).toBe('ja')
|
||||
})
|
||||
|
||||
it('normalizes Spanish locale prefixes', () => {
|
||||
expect(normalizeSupportedUiLocale('es-ES')).toBe('es')
|
||||
expect(normalizeSupportedUiLocale('es-MX')).toBe('es')
|
||||
expect(normalizeSupportedUiLocale('es')).toBe('es')
|
||||
})
|
||||
|
||||
it('normalizes French locale prefixes', () => {
|
||||
expect(normalizeSupportedUiLocale('fr-FR')).toBe('fr')
|
||||
expect(normalizeSupportedUiLocale('fr-CA')).toBe('fr')
|
||||
expect(normalizeSupportedUiLocale('fr')).toBe('fr')
|
||||
})
|
||||
|
||||
it('falls back unsupported locales to English', () => {
|
||||
expect(normalizeSupportedUiLocale('de-DE')).toBe('en')
|
||||
})
|
||||
@@ -89,13 +67,4 @@ describe('ui-locale', () => {
|
||||
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'es-MX')).toBe('es')
|
||||
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'fr-FR')).toBe('fr')
|
||||
})
|
||||
|
||||
it('uses renderer system locale only for the system setting', () => {
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_ENGLISH)).toBe('en')
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_CHINESE)).toBe('zh')
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_KOREAN)).toBe('ko')
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_JAPANESE)).toBe('ja')
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_SPANISH)).toBe('es')
|
||||
expect(resolveRendererUiLocale(UI_LANGUAGE_FRENCH)).toBe('fr')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,18 +10,6 @@ import {
|
||||
} from './workspace-statuses'
|
||||
|
||||
describe('workspace status visuals', () => {
|
||||
it.each([13, 20, 21, 64])('keeps all %i authored columns in order', (count) => {
|
||||
const authored = Array.from({ length: count }, (_, index) => ({
|
||||
id: `state-${index + 1}`,
|
||||
label: `State ${index + 1}`
|
||||
}))
|
||||
|
||||
const statuses = normalizeWorkspaceStatuses(authored)
|
||||
|
||||
expect(statuses).toHaveLength(count)
|
||||
expect(statuses.map((status) => status.id)).toEqual(authored.map((status) => status.id))
|
||||
})
|
||||
|
||||
it('normalizes every valid status without truncating the workflow', () => {
|
||||
const authored = Array.from({ length: 500 }, (_, index) => ({
|
||||
id: `state-${index}`,
|
||||
@@ -280,17 +268,6 @@ describe('workspace status visuals', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps intentional custom in-progress visuals', () => {
|
||||
const statuses = normalizeWorkspaceStatuses([
|
||||
{ id: 'in-progress', label: 'Doing', color: 'blue', icon: 'circle-dot' }
|
||||
])
|
||||
|
||||
expect(statuses[0]).toMatchObject({
|
||||
color: 'blue',
|
||||
icon: 'circle-dot'
|
||||
})
|
||||
})
|
||||
|
||||
it('clamps workspace board column widths to resizable bounds', () => {
|
||||
expect(clampWorkspaceBoardColumnWidth(undefined)).toBe(WORKSPACE_BOARD_COLUMN_WIDTH_DEFAULT)
|
||||
expect(clampWorkspaceBoardColumnWidth(100)).toBe(WORKSPACE_BOARD_COLUMN_WIDTH_MIN)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
COMPACT_WORKTREE_CARD_PROPERTIES,
|
||||
DEFAULT_WORKTREE_CARD_PROPERTIES,
|
||||
TASK_WORKTREE_CARD_PROPERTIES,
|
||||
getWorktreeCardModeProperties,
|
||||
getWorktreeCardModeUpdates,
|
||||
isDefaultedCompactWorktreeCardProperties,
|
||||
@@ -10,43 +7,6 @@ import {
|
||||
} from './card-properties'
|
||||
|
||||
describe('worktree card properties', () => {
|
||||
it('defines Default with inline agents and without branch', () => {
|
||||
const props = getWorktreeCardModeProperties('Default')
|
||||
|
||||
expect(props).toContain('inline-agents')
|
||||
expect(props).not.toContain('branch')
|
||||
expect(props).toContain('pr')
|
||||
expect(props).toContain('automation')
|
||||
expect(props).toEqual(DEFAULT_WORKTREE_CARD_PROPERTIES)
|
||||
})
|
||||
|
||||
it('defines Compact as the status-only quiet preset', () => {
|
||||
const props = getWorktreeCardModeProperties('Compact')
|
||||
|
||||
expect(props).toEqual(['status'])
|
||||
expect(props).toEqual(COMPACT_WORKTREE_CARD_PROPERTIES)
|
||||
expect(props).not.toContain('inline-agents')
|
||||
expect(props).not.toContain('issue')
|
||||
expect(props).not.toContain('linear-issue')
|
||||
expect(props).not.toContain('jira-issue')
|
||||
expect(props).not.toContain('comment')
|
||||
expect(props).not.toContain('ports')
|
||||
expect(props).not.toContain('branch')
|
||||
expect(props).not.toContain('pr')
|
||||
expect(props).not.toContain('automation')
|
||||
})
|
||||
|
||||
it('keeps status enabled in both presets', () => {
|
||||
expect(getWorktreeCardModeProperties('Default')).toEqual(expect.arrayContaining(['status']))
|
||||
expect(getWorktreeCardModeProperties('Compact')).toEqual(expect.arrayContaining(['status']))
|
||||
})
|
||||
|
||||
it('keeps provider-specific task metadata together in Default mode', () => {
|
||||
expect(getWorktreeCardModeProperties('Default')).toEqual(
|
||||
expect.arrayContaining(TASK_WORKTREE_CARD_PROPERTIES)
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes fixed and legacy properties while preserving selected properties', () => {
|
||||
expect(normalizeWorktreeCardProperties(['ci', 'branch', 'pr', 'automation', 'unread'])).toEqual(
|
||||
['status', 'unread', 'ci', 'branch', 'pr', 'automation']
|
||||
|
||||
@@ -21,8 +21,4 @@ describe('canonical worktree identity', () => {
|
||||
canonicalWorktreeIdentity({ ...local, worktreeId: 'repo-1::/workspace/renamed-feature' })
|
||||
).toBe(canonicalWorktreeIdentity(local))
|
||||
})
|
||||
|
||||
it('does not use the display name as identity input', () => {
|
||||
expect(canonicalWorktreeIdentity(local)).toBe(canonicalWorktreeIdentity({ ...local }))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user