From a5b2543d857e6cb4fe7a88b4407d91b5b4e0e8f5 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 23 Oct 2026 00:01:39 -0700 Subject: [PATCH] test: retire copied oracles and implementation inventories (#32206) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../rpc/terminal-stream-byte-length.test.ts | 348 +++--------------- .../runtime/runtime-git-api-contract.test.ts | 60 --- .../editor-save-guarded-write-paths.test.ts | 72 ---- .../terminal-title-tracker-parity.test.ts | 187 ---------- src/shared/cross-platform-path-guards.test.ts | 205 +---------- ...ss-platform-path-unguarded.test-fixture.ts | 267 -------------- 6 files changed, 56 insertions(+), 1083 deletions(-) delete mode 100644 src/main/runtime/runtime-git-api-contract.test.ts delete mode 100644 src/renderer/src/components/editor/editor-save-guarded-write-paths.test.ts delete mode 100644 src/shared/cross-platform-path-unguarded.test-fixture.ts diff --git a/src/main/runtime/rpc/terminal-stream-byte-length.test.ts b/src/main/runtime/rpc/terminal-stream-byte-length.test.ts index 1c0f08cf33f..2aae5a40b7a 100644 --- a/src/main/runtime/rpc/terminal-stream-byte-length.test.ts +++ b/src/main/runtime/rpc/terminal-stream-byte-length.test.ts @@ -1,153 +1,10 @@ import { describe, expect, it } from 'vitest' import { - MIN_NATIVE_BYTE_LENGTH_CODE_UNITS, measureTerminalStreamByteLength, terminalStreamByteLength, terminalStreamByteLengthExceeds } from './terminal-stream-byte-length' -// Copy of the pre-change implementation (shared/clipboard-text.ts -// measureClipboardTextByteLength), kept here so equivalence is checked against the -// ACTUAL old code path rather than a paraphrase of it. The one deliberate deviation is -// `legacyCodePointAt`: raw `String.prototype.codePointAt` reads one code unit past the end -// of a sliced string once V8 optimizes its caller, so the naive copy is not a stable -// reference. See src/shared/utf8-byte-limits.ts (readUtf8CodePointAt). -function legacyCodePointAt(text: string, index: number): number { - const leadUnit = text.charCodeAt(index) - if (leadUnit < 0xd800 || leadUnit > 0xdbff || index + 1 >= text.length) { - return leadUnit - } - const trailUnit = text.charCodeAt(index + 1) - if (trailUnit < 0xdc00 || trailUnit > 0xdfff) { - return leadUnit - } - return (leadUnit - 0xd800) * 0x400 + (trailUnit - 0xdc00) + 0x10000 -} - -function legacyUtf8ByteLengthForCodePoint(codePoint: number): number { - if (codePoint <= 0x7f) { - return 1 - } - if (codePoint <= 0x7ff) { - return 2 - } - if (codePoint <= 0xffff) { - return 3 - } - return 4 -} - -function legacyMeasure( - text: string, - options: { stopAfterBytes?: number } = {} -): { byteLength: number; exceededLimit: boolean } { - const stopAfterBytes = options.stopAfterBytes - let byteLength = 0 - for (let index = 0; index < text.length; index += 1) { - const codePoint = legacyCodePointAt(text, index) - byteLength += legacyUtf8ByteLengthForCodePoint(codePoint) - if (Number.isFinite(stopAfterBytes) && byteLength > (stopAfterBytes ?? 0)) { - return { byteLength, exceededLimit: true } - } - if (codePoint > 0xffff) { - index += 1 - } - } - return { byteLength, exceededLimit: false } -} - -function legacyByteLength(data: string): number { - return legacyMeasure(data).byteLength -} - -function legacyExceeds(data: string, maxBytes: number): boolean { - return legacyMeasure(data, { stopAfterBytes: maxBytes }).exceededLimit -} - -function mulberry32(seed: number): () => number { - let state = seed >>> 0 - return () => { - state = (state + 0x6d2b79f5) >>> 0 - let t = state - t = Math.imul(t ^ (t >>> 15), t | 1) - t ^= t + Math.imul(t ^ (t >>> 7), t | 61) - return ((t ^ (t >>> 14)) >>> 0) / 4294967296 - } -} - -// Mixed generator: ASCII, 2-byte, 3-byte, astral pairs, and LONE surrogates, so the -// fixtures exercise every branch of the legacy code-point scan. -function randomUnit(random: () => number): string { - const roll = random() - if (roll < 0.4) { - return String.fromCharCode(Math.floor(random() * 0x80)) - } - if (roll < 0.55) { - return String.fromCharCode(0x80 + Math.floor(random() * 0x780)) - } - if (roll < 0.72) { - return String.fromCharCode(0x800 + Math.floor(random() * 0xd000)) - } - if (roll < 0.88) { - return String.fromCodePoint(0x10000 + Math.floor(random() * 0x100000)) - } - return String.fromCharCode(0xd800 + Math.floor(random() * 0x800)) -} - -function randomString(random: () => number, maxUnits: number): string { - const count = Math.floor(random() * maxUnits) - let text = '' - for (let index = 0; index < count; index += 1) { - text += randomUnit(random) - } - return text -} - -// Raw UTF-16 with no code-point discipline at all: catches anything that assumes -// well-formedness (unpaired high after high, low before high, etc). -function rawUtf16(random: () => number, maxUnits: number): string { - const count = Math.floor(random() * maxUnits) - let text = '' - for (let index = 0; index < count; index += 1) { - text += String.fromCharCode(Math.floor(random() * 0x11000)) - } - return text -} - -const EDGE_STRINGS = [ - '', - 'a', - '\u0000', - '\u007f', - '€', - '߿', - 'ࠀ', - '￿', - '�', - '\u{10000}', - '\u{10ffff}', - '\ud800', - '\udfff', - '\ud800\ud800', - '\udc00\ud800', - '😀', - '😀\ud800', - '\ud800😀', - 'a\ud800b', - '\r\n\u001b[0m', - 'é́', - '\u{1f469}‍\u{1f4bb}', - 'x'.repeat(1000), - '\u{1f600}'.repeat(300), - `${'é'.repeat(500)}\ud800` -] - -// Regression for the intermittent "measurement diverged at 13 units" failure: the fuzzers -// build a rope and cut it at a fixed code-unit count, which can split a surrogate pair and -// leave the low half in the parent just past the slice. Optimized `codePointAt` pairs across -// that boundary, so the scan measured one byte too many, but only after the enclosing function -// tiered up, which made the failure look load-dependent. 13 code units is V8's minimum length -// for a sliced string, which is why the divergence started exactly there. describe('measuring a prefix slice that cuts a surrogate pair in half', () => { // Kept first in the file so the scan is still specializing on this shape when it tiers up. it('measures the slice like the encoder does in every JIT tier', () => { @@ -175,165 +32,70 @@ describe('measuring a prefix slice that cuts a surrogate pair in half', () => { }) }) -describe('terminal stream byte length equivalence with the legacy code-point scan', () => { - it('matches the legacy total byte length on edge strings', () => { - for (const text of EDGE_STRINGS) { - expect(terminalStreamByteLength(text)).toBe(legacyByteLength(text)) - } - }) - - it('matches the legacy total byte length over every Unicode code point', () => { - for (let codePoint = 0; codePoint <= 0x10ffff; codePoint += 1) { - const text = String.fromCodePoint(codePoint) - if (terminalStreamByteLength(text) !== legacyByteLength(text)) { - throw new Error(`byte length diverged at code point U+${codePoint.toString(16)}`) +describe('terminal stream UTF-8 byte accounting', () => { + it('matches the encoder across Unicode widths and the native-call boundary', () => { + const samples = [ + '', + '\u0000\u007f', + '\u0080\u07ff', + '\u0800\uffff', + '\u{10000}\u{10ffff}', + '\ud800', + '\udfff', + '\ud800\ud800', + '\udc00\ud800', + '😀\ud800', + '\ud800😀', + 'e\u0301', + '👩‍💻', + '\r\n\u001b[0m' + ] + for (const units of [15, 16, 17, 32]) { + for (const unit of ['a', 'é', '走', '😀', '\ud800']) { + samples.push(unit.repeat(units).slice(0, units)) } } - expect(terminalStreamByteLength('\u{10ffff}')).toBe(4) - }) - - it('matches the legacy total byte length over every lone surrogate', () => { - for (let unit = 0xd800; unit <= 0xdfff; unit += 1) { - const text = `a${String.fromCharCode(unit)}b` - expect(terminalStreamByteLength(text)).toBe(legacyByteLength(text)) - } - }) - - it('matches the legacy total byte length over fuzzed mixed and raw UTF-16 input', () => { - const random = mulberry32(0x5eed01) - for (let iteration = 0; iteration < 20000; iteration += 1) { - const text = iteration % 2 === 0 ? randomString(random, 24) : rawUtf16(random, 24) - if (terminalStreamByteLength(text) !== legacyByteLength(text)) { - throw new Error(`byte length diverged for ${JSON.stringify(text)}`) - } - } - expect(true).toBe(true) - }) - - it('matches the legacy exceededLimit decision across a dense (string, limit) sweep', () => { - const random = mulberry32(0xc0ffee) - let compared = 0 - for (let iteration = 0; iteration < 10000; iteration += 1) { - const text = iteration % 2 === 0 ? randomString(random, 16) : rawUtf16(random, 16) - const total = legacyByteLength(text) - // Sweep every limit from below zero to past the true total so the boundary is hit exactly. - for (let limit = -2; limit <= total + 2; limit += 1) { - if (terminalStreamByteLengthExceeds(text, limit) !== legacyExceeds(text, limit)) { - throw new Error(`exceededLimit diverged for ${JSON.stringify(text)} at limit ${limit}`) - } - compared += 1 - } - } - expect(compared).toBeGreaterThan(100000) - }) - - it('matches the legacy exceededLimit decision for non-finite and fractional limits', () => { - const random = mulberry32(0xfeed42) - for (const limit of [ - Number.NaN, - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, - 0.5, - 2.5, - -0.5, - Number.MAX_SAFE_INTEGER - ]) { - for (let iteration = 0; iteration < 400; iteration += 1) { - const text = rawUtf16(random, 12) - expect(terminalStreamByteLengthExceeds(text, limit)).toBe(legacyExceeds(text, limit)) - } - for (const text of EDGE_STRINGS) { - expect(terminalStreamByteLengthExceeds(text, limit)).toBe(legacyExceeds(text, limit)) + for (const text of samples) { + const byteLength = Buffer.byteLength(text, 'utf8') + expect(terminalStreamByteLength(text), JSON.stringify(text)).toBe(byteLength) + expect(measureTerminalStreamByteLength(text)).toEqual({ + byteLength, + exceededLimit: false + }) + for (const limit of [-1, 0, 0.5, byteLength - 1, byteLength, byteLength + 1]) { + expect( + terminalStreamByteLengthExceeds(text, limit), + `${JSON.stringify(text)} at ${limit}` + ).toBe(text.length > 0 && byteLength > limit) } } }) - it('matches the legacy measurement pair, byteLength included, across a stopAfterBytes sweep', () => { - const random = mulberry32(0xa11ce) - for (let iteration = 0; iteration < 3000; iteration += 1) { - const text = iteration % 2 === 0 ? randomString(random, 20) : rawUtf16(random, 20) - const total = legacyByteLength(text) - for (let stopAfterBytes = -1; stopAfterBytes <= total + 2; stopAfterBytes += 1) { - const actual = measureTerminalStreamByteLength(text, { stopAfterBytes }) - const expected = legacyMeasure(text, { stopAfterBytes }) - if ( - actual.byteLength !== expected.byteLength || - actual.exceededLimit !== expected.exceededLimit - ) { - throw new Error( - `measurement diverged for ${JSON.stringify(text)} at stopAfterBytes ${stopAfterBytes}` - ) - } - } + it.each([ + { text: '', limit: -1, byteLength: 0, exceededLimit: false }, + { text: 'abc', limit: 0, byteLength: 1, exceededLimit: true }, + { text: 'éé', limit: 2, byteLength: 4, exceededLimit: true }, + { text: '😀😀😀', limit: 5, byteLength: 8, exceededLimit: true }, + { text: '\ud800z', limit: 2, byteLength: 3, exceededLimit: true }, + { text: '😀'.repeat(9), limit: 5, byteLength: 8, exceededLimit: true }, + { text: 'é'.repeat(16), limit: 32, byteLength: 32, exceededLimit: false }, + { text: '走'.repeat(16), limit: 48, byteLength: 48, exceededLimit: false } + ])( + 'measures only through the first code point over $limit bytes', + ({ text, limit, ...expected }) => { + expect(measureTerminalStreamByteLength(text, { stopAfterBytes: limit })).toEqual(expected) } - expect( - measureTerminalStreamByteLength('\u{1f600}\u{1f600}\u{1f600}', { stopAfterBytes: 5 }) - ).toEqual(legacyMeasure('\u{1f600}\u{1f600}\u{1f600}', { stopAfterBytes: 5 })) - }) + ) - it('keeps the partial byteLength the legacy scan returned when the limit is exceeded', () => { - // A drop-in Buffer.byteLength would report 12 here; the legacy scan stops at 8. - const measurement = measureTerminalStreamByteLength('\u{1f600}\u{1f600}\u{1f600}', { - stopAfterBytes: 5 - }) - expect(measurement).toEqual({ byteLength: 8, exceededLimit: true }) - }) - - it('matches the legacy measurement with no stopAfterBytes and with an undefined option bag', () => { - const random = mulberry32(0xb0b) - for (let iteration = 0; iteration < 2000; iteration += 1) { - const text = rawUtf16(random, 32) - expect(measureTerminalStreamByteLength(text)).toEqual(legacyMeasure(text)) - expect(measureTerminalStreamByteLength(text, {})).toEqual(legacyMeasure(text, {})) - expect(measureTerminalStreamByteLength(text, { stopAfterBytes: undefined })).toEqual( - legacyMeasure(text, { stopAfterBytes: undefined }) - ) - } - }) - - // The code-unit floor routes short inputs back through the scan. Both sides of that - // boundary must stay legacy-identical, so sweep it exhaustively rather than by sampling. - it('matches the legacy result on both sides of the native-call floor', () => { - const random = mulberry32(0xf100a) - for (let units = 0; units <= MIN_NATIVE_BYTE_LENGTH_CODE_UNITS * 2; units += 1) { - for (let iteration = 0; iteration < 60; iteration += 1) { - let text = '' - while (text.length < units) { - text += - iteration % 2 === 0 - ? randomUnit(random) - : String.fromCharCode(Math.floor(random() * 0x11000)) - } - text = text.slice(0, units) - const total = legacyByteLength(text) - expect(terminalStreamByteLength(text)).toBe(total) - for (let limit = -1; limit <= total + 2; limit += 1) { - if (terminalStreamByteLengthExceeds(text, limit) !== legacyExceeds(text, limit)) { - throw new Error(`exceeds diverged at ${units} units, limit ${limit}`) - } - const actual = measureTerminalStreamByteLength(text, { stopAfterBytes: limit }) - const expected = legacyMeasure(text, { stopAfterBytes: limit }) - if ( - actual.byteLength !== expected.byteLength || - actual.exceededLimit !== expected.exceededLimit - ) { - throw new Error(`measurement diverged at ${units} units, limit ${limit}`) - } - } + it('treats non-finite limits as unbounded for both scan and native inputs', () => { + for (const text of ['', '😀é走a', '😀é走a'.repeat(8)]) { + for (const limit of [Number.NaN, Infinity, -Infinity]) { + expect(terminalStreamByteLengthExceeds(text, limit)).toBe(false) + expect(measureTerminalStreamByteLength(text, { stopAfterBytes: limit })).toEqual({ + byteLength: Buffer.byteLength(text, 'utf8'), + exceededLimit: false + }) } } }) }) - -// trimPendingOutputCoveredBySnapshot re-measures a sliced chunk with terminalStreamByteLength. -// The RPC suites never reach that slice branch (a `data.length` mutant there survives on -// unmodified HEAD too), so pin the byte accounting the branch depends on here. -describe('resync trim byte accounting for a snapshot-sliced chunk', () => { - it('re-measures a sliced chunk in UTF-8 bytes, not UTF-16 code units', () => { - const data = '\u{1f600}é走a' - const sliced = data.slice(2) - // Guards the mutant: code-unit length would be 4 here, UTF-8 is 6. - expect(terminalStreamByteLength(sliced)).toBe(6) - expect(terminalStreamByteLength(sliced)).not.toBe(sliced.length) - }) -}) diff --git a/src/main/runtime/runtime-git-api-contract.test.ts b/src/main/runtime/runtime-git-api-contract.test.ts deleted file mode 100644 index 28e1fa7a885..00000000000 --- a/src/main/runtime/runtime-git-api-contract.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../shared/global-settings-types' -import { GIT_METHODS } from './rpc/methods/git' -import { RuntimeGitCommands } from './orca-runtime-git' - -const RPC_TO_RUNTIME_COMMAND = { - 'git.status': 'getRuntimeGitStatus', - 'git.checkIgnored': 'checkRuntimeGitIgnoredPaths', - 'git.submoduleStatus': 'getRuntimeGitSubmoduleStatus', - 'git.history': 'getRuntimeGitHistory', - 'git.conflictOperation': 'getRuntimeGitConflictOperation', - 'git.mutationInFlight': 'getRuntimeGitMutationInFlight', - 'git.abortMerge': 'abortRuntimeGitMerge', - 'git.abortRebase': 'abortRuntimeGitRebase', - 'git.checkout': 'checkoutRuntimeGitBranch', - 'git.localBranches': 'listRuntimeGitLocalBranches', - 'git.diff': 'getRuntimeGitDiff', - 'git.branchDiff': 'getRuntimeGitBranchDiff', - 'git.commitDiff': 'getRuntimeGitCommitDiff', - 'git.branchCompare': 'getRuntimeGitBranchCompare', - 'git.commitCompare': 'getRuntimeGitCommitCompare', - 'git.upstreamStatus': 'getRuntimeGitUpstreamStatus', - 'git.fetch': 'fetchRuntimeGit', - 'git.forkSync': 'syncRuntimeGitForkDefaultBranch', - 'git.pull': 'pullRuntimeGit', - 'git.fastForward': 'fastForwardRuntimeGit', - 'git.rebaseFromBase': 'rebaseRuntimeGitFromBase', - 'git.push': 'pushRuntimeGit', - 'git.commit': 'commitRuntimeGit', - 'git.generateCommitMessage': 'generateRuntimeCommitMessage', - 'git.discoverCommitMessageModels': 'discoverRuntimeCommitMessageModels', - 'git.cancelGenerateCommitMessage': 'cancelRuntimeGenerateCommitMessage', - 'git.generatePullRequestFields': 'generateRuntimePullRequestFields', - 'git.cancelGeneratePullRequestFields': 'cancelRuntimeGeneratePullRequestFields', - 'git.stage': 'stageRuntimeGitPath', - 'git.bulkStage': 'bulkStageRuntimeGitPaths', - 'git.unstage': 'unstageRuntimeGitPath', - 'git.bulkUnstage': 'bulkUnstageRuntimeGitPaths', - 'git.discard': 'discardRuntimeGitPath', - 'git.bulkDiscard': 'bulkDiscardRuntimeGitPaths', - 'git.remoteFileUrl': 'getRuntimeGitRemoteFileUrl', - 'git.remoteCommitUrl': 'getRuntimeGitRemoteCommitUrl' -} as const satisfies Record - -describe('runtime Git API contract', () => { - it('keeps every registered Git RPC paired with one public runtime command', () => { - const commands = new RuntimeGitCommands({ - resolveRuntimeGitTarget: async () => { - throw new Error('not called') - }, - getRuntimeSettings: () => ({}) as GlobalSettings - }) - const registeredMethods = GIT_METHODS.map((method) => method.name).sort() - - expect(registeredMethods).toEqual(Object.keys(RPC_TO_RUNTIME_COMMAND).sort()) - for (const commandName of Object.values(RPC_TO_RUNTIME_COMMAND)) { - expect(commands[commandName]).toBeTypeOf('function') - } - }) -}) diff --git a/src/renderer/src/components/editor/editor-save-guarded-write-paths.test.ts b/src/renderer/src/components/editor/editor-save-guarded-write-paths.test.ts deleted file mode 100644 index e235ec9276d..00000000000 --- a/src/renderer/src/components/editor/editor-save-guarded-write-paths.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { readdirSync, readFileSync, statSync } from 'node:fs' -import { join, relative } from 'node:path' -import { describe, expect, it } from 'vitest' - -const EDITOR_DIR = __dirname -const RAW_WRITE = /\b(?:writeRuntimeFile|fs\.writeFile)\s*\(/ -const EDITOR_WRITE = /\bwriteEditorSave\s*\(/ - -function sourceFiles(dir: string): string[] { - return readdirSync(dir).flatMap((name) => { - const path = join(dir, name) - if (statSync(path).isDirectory()) { - return sourceFiles(path) - } - return /\.tsx?$/.test(name) && !/\.test\.tsx?$|fixture/.test(name) ? [path] : [] - }) -} - -function filesMatching(dir: string, pattern: RegExp): string[] { - return sourceFiles(dir) - .filter((path) => pattern.test(readFileSync(path, 'utf8'))) - .map((path) => relative(EDITOR_DIR, path).split('\\').join('/')) - .sort() -} - -function functionBody(source: string, name: string): string { - const body = source.slice(source.indexOf(`export function ${name}`)) - return body.slice(0, body.indexOf('\n}\n')) -} - -describe('editor save paths', () => { - it('make the raw file write only inside writeEditorSave', () => { - expect(filesMatching(EDITOR_DIR, RAW_WRITE)).toEqual(['editor-save-disk-precondition.ts']) - const source = readFileSync(join(EDITOR_DIR, 'editor-save-disk-precondition.ts'), 'utf8') - expect(source.split('writeRuntimeFile(')).toHaveLength(2) - const writer = functionBody(source, 'writeEditorSave') - expect(writer).toContain('writeRuntimeFile(') - // Only the explicit Overwrite reaches the writer without a precondition. - expect(writer).toContain("write.kind === 'guarded'") - }) - - it('route every editor surface that saves a file through writeEditorSave', () => { - expect(filesMatching(EDITOR_DIR, EDITOR_WRITE)).toEqual([ - 'combined-diff/review-controls/use-combined-diff-section-actions.ts', - 'editor-save-disk-precondition.ts', - 'editor-save-queue.ts' - ]) - }) - - it('never turn a missing or disputed baseline into a write', () => { - const source = readFileSync(join(EDITOR_DIR, 'editor-save-disk-precondition.ts'), 'utf8') - const builder = functionBody(source, 'editorSavePrecondition') - expect(builder).not.toMatch(/return null|return undefined/) - expect(builder.match(/kind: 'explicit-overwrite'/g)).toHaveLength(1) - expect(builder).toMatch( - /if \(trigger === 'overwrite'\) \{\n\s*return \{ kind: 'explicit-overwrite' \}/ - ) - expect(builder).not.toContain('mode') - }) - - it('grant overwrite consent only from the conflict UI', () => { - const grants = filesMatching( - join(EDITOR_DIR, '..', '..'), - /overwrite: true|\? 'overwrite'|, 'overwrite'\)/ - ) - expect(grants).toEqual([ - 'DiffSectionItem.tsx', - 'ExternalFileChangeBanner.tsx', - 'editor-autosave-controller.ts' - ]) - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts b/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts index adc33b2410a..592b724365e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts @@ -9,11 +9,8 @@ import { readFileSync } from 'node:fs' // event sequences match. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createAgentStatusOscProcessor } from '../../../../shared/agent-status-osc' -import { createCommandCodeOutputStatusDetector } from '../../../../shared/command-code-output-status' -import { createTerminalGitHubPRLinkDetector } from '../../../../shared/terminal-github-pr-link-detector' import { createTerminalTitleTracker } from '../../../../shared/terminal-output-side-effects' import { createPtyOutputProcessor } from './pty-output-processor' -import { createTerminalCommandLifecycle } from './terminal-command-lifecycle' const ESC = '\x1b' const BEL = '\x07' @@ -336,187 +333,3 @@ describe('main title tracker parity with the renderer transport processor', () = expect(paths.main.events).toEqual([]) }) }) - -// Why: slice 3 moves the renderer's OSC 133;D and PR-link byte parsing into -// main's tracker for local/SSH PTYs. Both must derive identical fact -// sequences from the same chunk boundaries, or flipping the kill switch -// changes which commands/links are observed. -type LifecycleFactEvent = ['command-finished', number | null] | ['pr-link', string, number] - -type LifecycleFactPath = { - events: LifecycleFactEvent[] - feed: (chunk: string) => void -} - -function createRendererLifecyclePath(): LifecycleFactPath { - const events: LifecycleFactEvent[] = [] - // Why: mirrors pty-connection's dataCallback wiring — the transport - // processor strips OSC 9999 before the lifecycle/PR-link byte scans run. - const processAgentStatusChunk = createAgentStatusOscProcessor() - const lifecycle = createTerminalCommandLifecycle({ - onCommandFinished: (exitCode) => events.push(['command-finished', exitCode]) - }) - const detectPRLinks = createTerminalGitHubPRLinkDetector() - return { - events, - feed(chunk: string): void { - const clean = processAgentStatusChunk(chunk).cleanData - lifecycle.handlePtyData(clean) - for (const link of detectPRLinks(clean)) { - events.push(['pr-link', link.url, link.number]) - } - } - } -} - -function createMainLifecyclePath(): LifecycleFactPath { - const events: LifecycleFactEvent[] = [] - const processAgentStatusChunk = createAgentStatusOscProcessor() - const tracker = createTerminalTitleTracker({ - onCommandFinished: (exitCode) => events.push(['command-finished', exitCode]), - onPrLink: (link) => events.push(['pr-link', link.url, link.number]) - }) - return { - events, - feed(chunk: string): void { - tracker.handleChunk(processAgentStatusChunk(chunk).cleanData) - } - } -} - -describe('main tracker parity with renderer 133;D and PR-link byte parsers', () => { - let paths: { renderer: LifecycleFactPath; main: LifecycleFactPath } - - beforeEach(() => { - paths = { renderer: createRendererLifecyclePath(), main: createMainLifecyclePath() } - }) - - it('derives identical command-finished facts from split OSC 133;D chunks', () => { - feedBoth(paths, `output${ESC}]133`) - feedBoth(paths, ';D;13') - feedBoth(paths, `0${BEL}prompt $ `) - feedBoth(paths, `${ESC}]133;D;0${BEL}`) - feedBoth(paths, `${ESC}]133;D${ST}`) - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([ - ['command-finished', 130], - ['command-finished', 0], - ['command-finished', null] - ]) - }) - - it('derives identical pr-link facts from split and repeated URLs', () => { - feedBoth(paths, 'Created https://github.com/acme/orca/pull/4') - feedBoth(paths, '2\r\nAlso https://github.com/acme/orca/pull/43 merged\r\n') - feedBoth(paths, 'again https://github.com/acme/orca/pull/42\r\n') - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([ - ['pr-link', 'https://github.com/acme/orca/pull/42', 42], - ['pr-link', 'https://github.com/acme/orca/pull/43', 43] - ]) - }) - - it('ignores 133;D and PR URLs inside stripped OSC 9999 payloads in both paths', () => { - feedBoth( - paths, - `${ESC}]9999;{"state":"done","prompt":"https://github.com/acme/orca/pull/9"}${BEL}\r\n` - ) - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([]) - }) -}) - -// Why: slice 4 moves the Command Code output scrape into main for local/SSH -// PTYs. The renderer byte path observes raw transport data; main observes the -// OSC 9999-stripped cleanData. Both must derive identical working/done -// sequences from the same chunk boundaries, or flipping the kill switch -// changes Command Code status rows. -type CommandCodeFactEvent = ['working' | 'done', string] - -type CommandCodeFactPath = { - events: CommandCodeFactEvent[] - feed: (chunk: string) => void -} - -function createCommandCodePath(options: { stripStatusPayloads: boolean }): CommandCodeFactPath { - const events: CommandCodeFactEvent[] = [] - const processAgentStatusChunk = createAgentStatusOscProcessor() - const detector = createCommandCodeOutputStatusDetector({ - startupCommand: null, - onWorking: (prompt) => events.push(['working', prompt]), - onDone: (prompt) => events.push(['done', prompt]) - }) - return { - events, - feed(chunk: string): void { - detector.observe( - options.stripStatusPayloads ? processAgentStatusChunk(chunk).cleanData : chunk - ) - } - } -} - -describe('main Command Code scrape parity with the renderer byte detector', () => { - let paths: { renderer: CommandCodeFactPath; main: CommandCodeFactPath } - - beforeEach(() => { - paths = { - renderer: createCommandCodePath({ stripStatusPayloads: false }), - main: createCommandCodePath({ stripStatusPayloads: true }) - } - }) - - it('derives identical working facts after the banner arms across chunks', () => { - feedBoth(paths, '# Command') - feedBoth(paths, ' Code v0.27.3\r\n') - feedBoth(paths, '❯ Fix the spinner\r\n\x1b[35m✻ Thinking...\x1b[0m') - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([['working', 'Fix the spinner']]) - }) - - it('derives identical done facts for a no-tool turn in both paths', () => { - feedBoth(paths, '# Command Code v0.27.3\r\n') - feedBoth(paths, '❯ say hi\r\n✻ Thinking...') - feedBoth(paths, '\r\n:: Hi!\r\n❯ Ask your question...') - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([ - ['working', 'say hi'], - ['done', 'say hi'] - ]) - }) - - it('recovers prompt capture from interleaved OSC 9999 payloads (main improvement)', () => { - // Deliberate divergence, not drift: the renderer's raw byte path lets an - // OSC 9999 payload leak partial text into the scrape window (its ANSI - // strip consumes only the ESC] introducer), which breaks the prompt-echo - // line match. Main feeds the OSC 9999-stripped cleanData, so the prompt - // (and therefore the done settle hint) survives an adjacent payload. - const payloadThenPrompt = [ - '# Command Code v0.27.3\r\n', - `${ESC}]9999;{"state":"working","agentType":"command-code"}${BEL}`, - '❯ say hi\r\n✻ Thinking...', - '\r\n:: Hi!\r\n❯ Ask your question...' - ] - for (const chunk of payloadThenPrompt) { - feedBoth(paths, chunk) - } - - expect(paths.renderer.events).toEqual([['working', '']]) - expect(paths.main.events).toEqual([ - ['working', 'say hi'], - ['done', 'say hi'] - ]) - }) - - it('stays silent without the Command Code banner in both paths', () => { - feedBoth(paths, '❯ Fix the spinner\r\nThinking about unrelated shell output...') - - expect(paths.main.events).toEqual(paths.renderer.events) - expect(paths.main.events).toEqual([]) - }) -}) diff --git a/src/shared/cross-platform-path-guards.test.ts b/src/shared/cross-platform-path-guards.test.ts index aec52a1f676..e718f02ffd5 100644 --- a/src/shared/cross-platform-path-guards.test.ts +++ b/src/shared/cross-platform-path-guards.test.ts @@ -1,210 +1,7 @@ -/** - * Proves the substring/char-code guards added to `cross-platform-path.ts` and `parseWslUncPath` - * are pure fast paths: a seeded differential fuzz against the pre-guard copy in - * `cross-platform-path-unguarded.test-fixture.ts`, plus counters that fail if the guards regress. - */ -import { describe, expect, it, afterEach } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' import * as guarded from './cross-platform-path' -import * as unguarded from './cross-platform-path-unguarded.test-fixture' import { parseWslUncPath } from './wsl-paths' -// ─── Deterministic path generator ──────────────────────────────────── - -function createRandom(seed: number): () => number { - let state = seed >>> 0 - return () => { - state = (state + 0x6d2b79f5) >>> 0 - let t = Math.imul(state ^ (state >>> 15), 1 | state) - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t - return ((t ^ (t >>> 14)) >>> 0) / 4294967296 - } -} - -const PREFIXES = [ - '', - '/', - '//', - '///', - '.', - './', - '../', - 'C:/', - 'c:\\', - 'Z:', - '\\\\', - '\\\\wsl.localhost\\Ubuntu', - '//wsl.localhost/Ubuntu-22.04', - '//WSL$/Debian', - '\\\\wsl$\\ubuntu', - '//server/share', - '/mnt/c', - '\\\\wsl.localhost\\Ubuntu\\mnt\\c' -] - -// NFD + KELVIN SIGN are the folds `normalizeRuntimePathForComparison` is built around. -const SEGMENTS = [ - 'home', - 'user', - 'orca', - 'workspaces', - '..', - '.', - '', - 'a', - 'B', - 'wsl$', - 'wsl.localhost', - 'mnt', - 'c', - 'C', - 'répertoire', - 're\u0301pertoire', - '\u212Aelvin', - 'Kelvin', - 'back\\slash', - 'sp ace', - 'Ubuntu' -] - -const JOINERS = ['/', '/', '/', '//', '///', '\\', '\\\\'] -const SUFFIXES = ['', '', '', '/', '//', '\\', '/.', '/..'] - -function generatePath(random: () => number): string { - const pick = (items: readonly T[]): T => items[Math.floor(random() * items.length)] - let path = pick(PREFIXES) - const segmentCount = Math.floor(random() * 5) - for (let index = 0; index < segmentCount; index++) { - path += (path === '' ? '' : pick(JOINERS)) + pick(SEGMENTS) - } - return path + pick(SUFFIXES) -} - -/** Roots that actually contain the candidate, so the matching branches get exercised too. */ -function generateRoot(random: () => number, candidate: string): string { - const roll = random() - if (roll < 0.35) { - const cut = Math.floor(random() * (candidate.length + 1)) - return candidate.slice(0, cut) - } - if (roll < 0.45) { - return candidate - } - return generatePath(random) -} - -// ─── Differential fuzz ─────────────────────────────────────────────── - -const FUZZ_ITERATIONS = 20_000 - -describe('guarded path normalization matches the pre-guard implementation', () => { - it(`agrees on every export across ${FUZZ_ITERATIONS} seeded paths`, () => { - const random = createRandom(0x5eed) - const mismatches: string[] = [] - const record = (label: string, path: string, root: string): void => { - if (mismatches.length < 5) { - mismatches.push(`${label}: candidate=${JSON.stringify(path)} root=${JSON.stringify(root)}`) - } - } - - for (let iteration = 0; iteration < FUZZ_ITERATIONS; iteration++) { - const path = generatePath(random) - const root = generateRoot(random, path) - const distro = Math.floor(random() * 2) === 0 ? 'Ubuntu' : 'debian' - - const singles: [string, (value: string) => unknown, (value: string) => unknown][] = [ - [ - 'isWindowsAbsolutePathLike', - guarded.isWindowsAbsolutePathLike, - unguarded.isWindowsAbsolutePathLike - ], - [ - 'isCaseInsensitiveRuntimeRoot', - guarded.isCaseInsensitiveRuntimeRoot, - unguarded.isCaseInsensitiveRuntimeRoot - ], - [ - 'normalizeRuntimePathSeparators', - guarded.normalizeRuntimePathSeparators, - unguarded.normalizeRuntimePathSeparators - ], - [ - 'normalizeRuntimePathForComparison', - guarded.normalizeRuntimePathForComparison, - unguarded.normalizeRuntimePathForComparison - ], - ['isRuntimePathAbsolute', guarded.isRuntimePathAbsolute, unguarded.isRuntimePathAbsolute], - ['getRuntimePathBasename', guarded.getRuntimePathBasename, unguarded.getRuntimePathBasename] - ] - for (const [label, left, right] of singles) { - if (left(path) !== right(path)) { - record(label, path, root) - } - } - - const identity = guarded.getLocalWindowsWslPathIdentity(path) - const expectedIdentity = unguarded.getLocalWindowsWslPathIdentity(path) - if ( - identity.normalizedPath !== expectedIdentity.normalizedPath || - identity.aliasComparisonPath !== expectedIdentity.aliasComparisonPath || - identity.isWslUnc !== expectedIdentity.isWslUnc - ) { - record('getLocalWindowsWslPathIdentity', path, root) - } - const wslUnc = parseWslUncPath(path) - const expectedWslUnc = unguarded.parseWslUncPath(path) - if ( - wslUnc?.distro !== expectedWslUnc?.distro || - wslUnc?.linuxPath !== expectedWslUnc?.linuxPath - ) { - record('parseWslUncPath', path, root) - } - if ( - guarded.areLocalWindowsWslPathAliases(root, path) !== - unguarded.areLocalWindowsWslPathAliases(root, path) - ) { - record('areLocalWindowsWslPathAliases', path, root) - } - if ( - guarded.isWslUncPathForCallerLinuxPath(root, path, distro) !== - unguarded.isWslUncPathForCallerLinuxPath(root, path, distro) - ) { - record('isWslUncPathForCallerLinuxPath', path, root) - } - if ( - guarded.isWslUncPathForLinuxMountedPath(root, path) !== - unguarded.isWslUncPathForLinuxMountedPath(root, path) - ) { - record('isWslUncPathForLinuxMountedPath', path, root) - } - if (guarded.resolveRuntimePath(root, path) !== unguarded.resolveRuntimePath(root, path)) { - record('resolveRuntimePath', path, root) - } - if (guarded.isPathInsideOrEqual(root, path) !== unguarded.isPathInsideOrEqual(root, path)) { - record('isPathInsideOrEqual', path, root) - } - if ( - guarded.createNormalizedPathInsideOrEqualMatcher(root)( - guarded.normalizeRuntimePathForComparison(path) - ) !== - unguarded.createNormalizedPathInsideOrEqualMatcher(root)( - unguarded.normalizeRuntimePathForComparison(path) - ) - ) { - record('createNormalizedPathInsideOrEqualMatcher', path, root) - } - if ( - guarded.relativePathInsideRoot(root, path) !== unguarded.relativePathInsideRoot(root, path) - ) { - record('relativePathInsideRoot', path, root) - } - } - - expect(mismatches).toEqual([]) - }, 120_000) -}) - -// ─── The guards must not skip work that was actually needed ────────── - describe('guards still do the work when the fast path does not apply', () => { it('collapses doubled slashes', () => { expect(guarded.normalizeRuntimePathForComparison('/a//b///c')).toBe('/a/b/c') diff --git a/src/shared/cross-platform-path-unguarded.test-fixture.ts b/src/shared/cross-platform-path-unguarded.test-fixture.ts deleted file mode 100644 index c6be537eea3..00000000000 --- a/src/shared/cross-platform-path-unguarded.test-fixture.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Verbatim pre-guard copy of `cross-platform-path.ts` and `parseWslUncPath`, kept only so - * `cross-platform-path-guards.test.ts` can differentially fuzz the guarded versions - * against what shipped. Comments were stripped; the code is otherwise unchanged. Update this file - * only when the guarded originals are meant to change behaviour. - */ -import { toWindowsWslPath } from './wsl-paths' - -type WslUncPathInfo = { distro: string; linuxPath: string } - -export function parseWslUncPath(path: string): WslUncPathInfo | null { - const normalized = path.replace(/\\/g, '/') - const match = normalized.match(/^\/\/(wsl\.localhost|wsl\$)\/([^/]+)(\/.*)?$/i) - if (!match) { - return null - } - return { distro: match[2], linuxPath: match[3] || '/' } -} - -function isWslUncPath(path: string): boolean { - return parseWslUncPath(path) !== null -} - -const SLASH_CHAR_CODE = '/'.charCodeAt(0) - -export function isWindowsAbsolutePathLike(value: string): boolean { - return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\') || value.startsWith('//') -} - -export function isCaseInsensitiveRuntimeRoot(rootPath: string): boolean { - return isWindowsAbsolutePathLike(rootPath) && !isWslUncPath(rootPath) -} - -export function normalizeRuntimePathSeparators(value: string): string { - const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/') - if (value.startsWith('\\\\') || value.startsWith('//')) { - return `//${normalized.replace(/^\/+/, '')}` - } - return normalized -} - -export function normalizeRuntimePathForComparison(rawValue: string): string { - const value = rawValue.normalize('NFC') - const isWindowsPath = isWindowsAbsolutePathLike(value) - const normalized = trimRuntimePathTrailingSlash( - isWindowsPath ? normalizeRuntimePathSeparators(value) : value.replace(/\/+/g, '/') - ) - const wslUnc = normalized.match(/^\/\/(?:wsl\.localhost|wsl\$)\/([^/]+)(\/[\s\S]*)?$/i) - if (wslUnc) { - return `//wsl/${wslUnc[1].toLowerCase()}${wslUnc[2] ?? ''}` - } - return isWindowsPath ? normalized.toLowerCase() : normalized -} - -export function isWslUncPathForCallerLinuxPath( - uncPath: string, - linuxPath: string, - callerDistro: string -): boolean { - const parsed = parseWslUncPath(uncPath) - if (!parsed) { - return false - } - return ( - parsed.distro.toLowerCase() === callerDistro.toLowerCase() && - normalizeRuntimePathForComparison(parsed.linuxPath) === - normalizeRuntimePathForComparison(linuxPath) - ) -} - -export function isWslUncPathForLinuxMountedPath(uncPath: string, linuxPath: string): boolean { - const parsed = parseWslUncPath(uncPath) - if (!parsed || !/^\/mnt\/[A-Za-z](?:\/|$)/.test(parsed.linuxPath)) { - return false - } - if (!/^\/mnt\/[A-Za-z](?:\/|$)/.test(linuxPath)) { - return false - } - return ( - normalizeRuntimePathForComparison(toWindowsWslPath(parsed.linuxPath, parsed.distro)) === - normalizeRuntimePathForComparison(toWindowsWslPath(linuxPath, parsed.distro)) - ) -} - -export function areLocalWindowsWslPathAliases(left: string, right: string): boolean { - const leftIdentity = getLocalWindowsWslPathIdentity(left) - const rightIdentity = getLocalWindowsWslPathIdentity(right) - return ( - (leftIdentity.isWslUnc || rightIdentity.isWslUnc) && - leftIdentity.aliasComparisonPath === rightIdentity.aliasComparisonPath - ) -} - -export type LocalWindowsWslPathIdentity = { - normalizedPath: string - aliasComparisonPath: string - isWslUnc: boolean -} - -export function getLocalWindowsWslPathIdentity(value: string): LocalWindowsWslPathIdentity { - const wslPath = parseWslUncPath(value) - const normalizedPath = normalizeRuntimePathForComparison(value) - return { - normalizedPath, - aliasComparisonPath: wslPath - ? normalizeRuntimePathForComparison(toWindowsWslPath(wslPath.linuxPath, wslPath.distro)) - : normalizedPath, - isWslUnc: wslPath !== null - } -} - -export function isRuntimePathAbsolute( - value: string, - pathFlavor: 'posix' | 'windows' = isWindowsPathFlavor(value) ? 'windows' : 'posix' -): boolean { - if (pathFlavor === 'windows') { - return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\') || value.startsWith('/') - } - return value.startsWith('/') -} - -export function resolveRuntimePath(basePath: string, targetPath: string): string { - const pathFlavor = - isWindowsPathFlavor(basePath) || isWindowsPathFlavor(targetPath) ? 'windows' : 'posix' - if (isRuntimePathAbsolute(targetPath, pathFlavor)) { - return normalizeRuntimePathDots(targetPath, pathFlavor) - } - return normalizeRuntimePathDots( - `${trimRuntimePathTrailingSlash(normalizeRuntimePathSeparators(basePath))}/${targetPath}`, - pathFlavor - ) -} - -export function getRuntimePathBasename(value: string): string { - const trimmed = value.replace(/[\\/]+$/g, '') - if (!trimmed) { - return '' - } - return trimmed.split(/[\\/]/).findLast(Boolean) ?? '' -} - -export function createNormalizedPathInsideOrEqualMatcher( - rootPath: string -): (normalizedCandidate: string) => boolean { - const root = normalizeRuntimePathForComparison(rootPath) - const rootWithBoundary = - root === '/' || /^[a-z]:\/$/i.test(root) ? root : `${root.replace(/\/+$/, '')}/` - return (normalizedCandidate) => - normalizedCandidate === root || normalizedCandidate.startsWith(rootWithBoundary) -} - -export function isPathInsideOrEqual(rootPath: string, candidatePath: string): boolean { - return createNormalizedPathInsideOrEqualMatcher(rootPath)( - normalizeRuntimePathForComparison(candidatePath) - ) -} - -export function relativePathInsideRoot(rootPath: string, candidatePath: string): string | null { - const normalizedCandidate = trimRuntimePathTrailingSlash( - isWindowsAbsolutePathLike(candidatePath.normalize('NFC')) - ? normalizeRuntimePathSeparators(candidatePath) - : candidatePath.replace(/\/+/g, '/') - ) - const comparisonRoot = normalizeRuntimePathForComparison(rootPath) - const comparisonCandidate = normalizeRuntimePathForComparison(candidatePath) - - if (comparisonCandidate === comparisonRoot) { - return '' - } - const isRoot = comparisonRoot === '/' || /^[a-z]:\/$/i.test(comparisonRoot) - const comparisonPrefix = isRoot ? comparisonRoot : `${comparisonRoot}/` - if (!comparisonCandidate.startsWith(comparisonPrefix)) { - return null - } - return sliceCandidatePastRootSegments(comparisonRoot, normalizedCandidate) -} - -function sliceCandidatePastRootSegments(root: string, candidate: string): string { - let remainingRootSegments = 0 - let inRootSegment = false - for (let index = 0; index < root.length; index++) { - if (root.charCodeAt(index) === SLASH_CHAR_CODE) { - inRootSegment = false - } else if (!inRootSegment) { - inRootSegment = true - remainingRootSegments++ - } - } - - let inSegment = false - for (let index = 0; index < candidate.length; index++) { - if (candidate.charCodeAt(index) === SLASH_CHAR_CODE) { - inSegment = false - continue - } - if (!inSegment) { - inSegment = true - if (remainingRootSegments-- === 0) { - return candidate.slice(index) - } - } - } - return '' -} - -function trimRuntimePathTrailingSlash(value: string): string { - if (value === '/' || /^[A-Za-z]:\/$/.test(value)) { - return value - } - return value.replace(/\/+$/, '') -} - -function isWindowsPathFlavor(value: string): boolean { - return /^[A-Za-z]:[\\/]/.test(value) || value.includes('\\') || value.startsWith('//') -} - -function normalizeRuntimePathDots(value: string, pathFlavor: 'posix' | 'windows'): string { - const normalized = normalizeRuntimePathSeparators(value) - const { root, rest } = splitRuntimePathRoot(normalized, pathFlavor) - const segments: string[] = [] - for (const segment of rest.split('/')) { - if (!segment || segment === '.') { - continue - } - if (segment === '..') { - if (segments.length > 0 && segments.at(-1) !== '..') { - segments.pop() - } else if (!root) { - segments.push(segment) - } - continue - } - segments.push(segment) - } - const suffix = segments.join('/') - if (!root) { - return suffix || '.' - } - return suffix ? `${root}${suffix}` : trimRuntimePathTrailingSlash(root) -} - -function splitRuntimePathRoot( - value: string, - pathFlavor: 'posix' | 'windows' -): { root: string; rest: string } { - if (pathFlavor === 'windows') { - const drive = value.match(/^([A-Za-z]:)(?:\/|$)/) - if (drive) { - return { root: `${drive[1]}/`, rest: value.slice(drive[0].length) } - } - if (value.startsWith('//')) { - const parts = value.slice(2).split('/') - if (parts.length >= 2 && parts[0] && parts[1]) { - const root = `//${parts[0]}/${parts[1]}/` - return { root, rest: parts.slice(2).join('/') } - } - return { root: '//', rest: value.slice(2) } - } - if (value.startsWith('/')) { - return { root: '/', rest: value.slice(1) } - } - } - if (value.startsWith('/')) { - return { root: '/', rest: value.slice(1) } - } - return { root: '', rest: value } -}