mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 03:27:44 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
471 lines
17 KiB
TypeScript
471 lines
17 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
buildCommitPrompt,
|
|
cleanGeneratedCommitMessage,
|
|
excerptAgentFailureOutput,
|
|
planCustomCommand,
|
|
STAGED_DIFF_BYTE_BUDGET,
|
|
stripPrefilledReasoningPreamble,
|
|
tokenizeCustomCommandTemplate,
|
|
truncateDiffForPrompt
|
|
} from './commit-message-prompt'
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('buildCommitPrompt', () => {
|
|
it('embeds the diff into the base prompt', () => {
|
|
const prompt = buildCommitPrompt('diff --git a/foo b/foo\n+hello', '')
|
|
expect(prompt).toContain('diff --git a/foo b/foo')
|
|
expect(prompt).toContain('+hello')
|
|
expect(prompt).toContain('First line: imperative mood')
|
|
})
|
|
|
|
it('appends a custom suffix when non-empty', () => {
|
|
const prompt = buildCommitPrompt('diff', 'Use Conventional Commits.')
|
|
expect(prompt).toContain('Additional user prompt:')
|
|
expect(prompt.endsWith('Use Conventional Commits.')).toBe(true)
|
|
})
|
|
|
|
it('does not append the suffix block for whitespace-only suffixes', () => {
|
|
const prompt = buildCommitPrompt('diff', ' \n ')
|
|
expect(prompt).not.toContain('Additional user prompt:')
|
|
})
|
|
})
|
|
|
|
describe('truncateDiffForPrompt', () => {
|
|
it('returns the diff unchanged when within budget', () => {
|
|
const diff = 'line\n'.repeat(10)
|
|
expect(truncateDiffForPrompt(diff)).toBe(diff)
|
|
})
|
|
|
|
it('truncates and appends a marker when over budget', () => {
|
|
const oversized = `${'line\n'.repeat(STAGED_DIFF_BYTE_BUDGET / 5 + 100)}`
|
|
const result = truncateDiffForPrompt(oversized)
|
|
expect(result.length).toBeLessThan(oversized.length)
|
|
expect(result).toMatch(/diff truncated, \d+ bytes omitted/)
|
|
})
|
|
|
|
it('clips on a line boundary so the diff is never cut mid-line', () => {
|
|
const diff = `${'keep this line\n'.repeat(40)}`
|
|
const result = truncateDiffForPrompt(diff, 95)
|
|
const body = result.split('\n...(diff truncated')[0]
|
|
for (const line of body.split('\n').filter(Boolean)) {
|
|
expect(line).toBe('keep this line')
|
|
}
|
|
})
|
|
|
|
it('keeps clipped output within a tight custom budget', () => {
|
|
const files = Array.from(
|
|
{ length: 20 },
|
|
(_, i) => `diff --git a/file-${i}.txt b/file-${i}.txt\n${'+x\n'.repeat(200)}`
|
|
).join('')
|
|
const result = truncateDiffForPrompt(files, 120)
|
|
|
|
expect(result.length).toBeLessThanOrEqual(120)
|
|
})
|
|
|
|
it('shares the budget fairly so a huge file does not starve a small one', () => {
|
|
const hugeFile = `diff --git a/data.jsonl b/data.jsonl\n${'+x\n'.repeat(5000)}`
|
|
const smallFile = 'diff --git a/src/app.ts b/src/app.ts\n+const meaningful = true\n'
|
|
const result = truncateDiffForPrompt(`${hugeFile}${smallFile}`, 1_000)
|
|
|
|
expect(result).toContain('a/src/app.ts')
|
|
expect(result).toContain('const meaningful = true')
|
|
expect(result).toMatch(/diff truncated, \d+ bytes omitted/)
|
|
})
|
|
})
|
|
|
|
describe('cleanGeneratedCommitMessage', () => {
|
|
it('trims whitespace', () => {
|
|
expect(cleanGeneratedCommitMessage(' feat: hello \n')).toBe('feat: hello')
|
|
})
|
|
|
|
it('strips a single enclosing fenced code block', () => {
|
|
const raw = '```\nfeat: hello\n```'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('feat: hello')
|
|
})
|
|
|
|
it('strips a fenced block with a language tag', () => {
|
|
const raw = '```text\nfix: bug\n```'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('fix: bug')
|
|
})
|
|
|
|
it('drops a leading "Generating…" preamble line', () => {
|
|
const raw = 'Generating…\nfeat: hello world'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('feat: hello world')
|
|
})
|
|
|
|
it('strips the trailing jcode [Tokens] usage line', () => {
|
|
const raw = 'feat: hello\n[Tokens] upload: 14926 download: 17 cache_read: 14720 cache_write: 0'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('feat: hello')
|
|
})
|
|
|
|
it('extracts the answer from a jcode --json envelope', () => {
|
|
const raw = JSON.stringify({ session_id: 's1', model: 'm', text: 'fix-login-crash', usage: {} })
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('fix-login-crash')
|
|
})
|
|
|
|
it('normalizes CRLF line endings', () => {
|
|
expect(cleanGeneratedCommitMessage('feat: a\r\nbody line\r\n')).toBe('feat: a\nbody line')
|
|
})
|
|
|
|
it('cleans large fenced CRLF output without regex-wide normalization', () => {
|
|
const replaceSpy = vi.spyOn(String.prototype, 'replace')
|
|
const matchSpy = vi.spyOn(String.prototype, 'match')
|
|
const fence = '```'
|
|
const raw = `\r\n${fence}text\r\nfeat: large output\r\n${'body line\r\n'.repeat(10_000)}${fence}\r\n`
|
|
|
|
const result = cleanGeneratedCommitMessage(raw)
|
|
|
|
expect(result.startsWith('feat: large output\nbody line')).toBe(true)
|
|
expect(result.endsWith('body line')).toBe(true)
|
|
expect(result).not.toContain('\r\n')
|
|
const usedCrlfReplace = replaceSpy.mock.calls.some(
|
|
([pattern]) => pattern instanceof RegExp && pattern.source === '\\r\\n'
|
|
)
|
|
const usedFenceMatch = matchSpy.mock.calls.some(
|
|
([pattern]) => pattern instanceof RegExp && pattern.source.includes('[\\s\\S]')
|
|
)
|
|
expect(usedCrlfReplace).toBe(false)
|
|
expect(usedFenceMatch).toBe(false)
|
|
})
|
|
|
|
it('strips a leading list marker from the commit subject', () => {
|
|
expect(cleanGeneratedCommitMessage('● Add Copilot entry to agent results')).toBe(
|
|
'Add Copilot entry to agent results'
|
|
)
|
|
expect(cleanGeneratedCommitMessage('1. Add numbered entry')).toBe('Add numbered entry')
|
|
})
|
|
|
|
it('returns empty string when input is whitespace', () => {
|
|
expect(cleanGeneratedCommitMessage(' \n\t')).toBe('')
|
|
})
|
|
|
|
it('drops a leading <think> reasoning block and unwraps the answer', () => {
|
|
expect(
|
|
cleanGeneratedCommitMessage('<think>\nThe diff fixes a typo.\n</think>\n\nFix typo\n\n- Why')
|
|
).toBe('Fix typo\n\n- Why')
|
|
expect(cleanGeneratedCommitMessage('<think>short</think>\n```\nfeat: add parser\n```')).toBe(
|
|
'feat: add parser'
|
|
)
|
|
})
|
|
|
|
it('drops a leading Kimi-VL ◁think▷ reasoning block', () => {
|
|
expect(cleanGeneratedCommitMessage('◁think▷Typo in README.◁/think▷Fix typo in README')).toBe(
|
|
'Fix typo in README'
|
|
)
|
|
})
|
|
|
|
it('keeps think tags that are not a leading reasoning block', () => {
|
|
const quoted = 'Strip <think>…</think> blocks from generated messages'
|
|
expect(cleanGeneratedCommitMessage(quoted)).toBe(quoted)
|
|
expect(cleanGeneratedCommitMessage('fix: handle stray </think> in stream')).toBe(
|
|
'fix: handle stray </think> in stream'
|
|
)
|
|
expect(cleanGeneratedCommitMessage('<think>still reasoning')).toBe('<think>still reasoning')
|
|
})
|
|
})
|
|
|
|
describe('stripPrefilledReasoningPreamble', () => {
|
|
it('drops reasoning that ends in a closing tag whose opening tag was prefilled', () => {
|
|
expect(
|
|
stripPrefilledReasoningPreamble("We need the message only.\nLet's final.</think>Fix typo")
|
|
).toBe('Fix typo')
|
|
expect(stripPrefilledReasoningPreamble('Reasoning.◁/think▷\nFix typo')).toBe('Fix typo')
|
|
})
|
|
|
|
it('keeps a closing tag that follows its own opening tag', () => {
|
|
const quoted = 'Strip <think>…</think> blocks'
|
|
expect(stripPrefilledReasoningPreamble(quoted)).toBe(quoted)
|
|
})
|
|
})
|
|
|
|
describe('excerptAgentFailureOutput', () => {
|
|
// Real Codex failure shape: config preamble first, operative ERROR line last.
|
|
const codexErrorLine =
|
|
'ERROR: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The \'gpt-5.3-codex-spark\' model is not supported when using Codex with a ChatGPT account."}}'
|
|
const codexStderr = [
|
|
'--------',
|
|
'workdir: C:\\Storage\\Projects\\bagplanner',
|
|
'model: gpt-5.3-codex-spark',
|
|
'reasoning effort: medium',
|
|
'--------',
|
|
'user',
|
|
'You are generating a single git commit message...',
|
|
'hook: SessionStart',
|
|
'hook: SessionStart Completed',
|
|
codexErrorLine
|
|
].join('\n')
|
|
|
|
it('excerpts both ends so a tail-anchored Codex error stays visible', () => {
|
|
expect(excerptAgentFailureOutput('', codexStderr)).toBe(
|
|
`-------- workdir: C:\\Storage\\Projects\\bagplanner … ${codexErrorLine.slice(0, 130).trimEnd()}…`
|
|
)
|
|
})
|
|
|
|
// Real pi 0.80.6 auth failure: primary line and remedy first, doc paths last.
|
|
const piAuthStderr = [
|
|
'No API key found for github-copilot.',
|
|
'',
|
|
'Use /login to log into a provider via OAuth or API key. See:',
|
|
' /private/tmp/pi-exit1-repro/node_modules/@earendil-works/pi-coding-agent/docs/providers.md',
|
|
' /private/tmp/pi-exit1-repro/node_modules/@earendil-works/pi-coding-agent/docs/models.md'
|
|
].join('\n')
|
|
|
|
it('keeps a head-anchored pi auth failure visible', () => {
|
|
expect(excerptAgentFailureOutput('', piAuthStderr)).toBe(
|
|
'No API key found for github-copilot. Use /login to log into a provider via OAuth or API key. See: … /private/tmp/pi-exit1-repro/node_modules/@earendil-works/pi-coding-agent/docs/models.md'
|
|
)
|
|
})
|
|
|
|
it('prefers stderr and never excerpts an echoed prompt from stdout', () => {
|
|
expect(
|
|
excerptAgentFailureOutput(
|
|
'You are generating a single git commit message for /secret/repo',
|
|
'No API key found for openai.'
|
|
)
|
|
).toBe('No API key found for openai.')
|
|
})
|
|
|
|
it('falls back to stdout when stderr is blank', () => {
|
|
expect(excerptAgentFailureOutput('Not logged in · Please run /login', ' \n')).toBe(
|
|
'Not logged in · Please run /login'
|
|
)
|
|
})
|
|
|
|
it('returns null when both streams are blank', () => {
|
|
expect(excerptAgentFailureOutput(' \n\t', '')).toBeNull()
|
|
})
|
|
|
|
it('joins up to three lines without an ellipsis', () => {
|
|
expect(excerptAgentFailureOutput('', 'one\ntwo\nthree\n')).toBe('one two three')
|
|
})
|
|
|
|
it('does not parse or unwrap JSON payloads', () => {
|
|
expect(excerptAgentFailureOutput('', '401: {"message":"Invalid API key provided"}')).toBe(
|
|
'401: {"message":"Invalid API key provided"}'
|
|
)
|
|
})
|
|
|
|
it('strips ANSI colors and OSC titles', () => {
|
|
const esc = String.fromCharCode(27)
|
|
const bel = String.fromCharCode(7)
|
|
expect(
|
|
excerptAgentFailureOutput(
|
|
'',
|
|
`${esc}]0;pi${bel}${esc}[91mError: no payment method${esc}[0m\n`
|
|
)
|
|
).toBe('Error: no payment method')
|
|
})
|
|
|
|
it('treats bare `\\r` progress frames as line boundaries', () => {
|
|
expect(excerptAgentFailureOutput('', 'Fetching 50%\rFetching 100%\rConnection error.')).toBe(
|
|
'Fetching 50% Fetching 100% Connection error.'
|
|
)
|
|
})
|
|
|
|
it('handles CRLF output', () => {
|
|
expect(excerptAgentFailureOutput('', 'one\r\ntwo\r\n')).toBe('one two')
|
|
})
|
|
|
|
it('collapses repeated retry lines instead of echoing them twice', () => {
|
|
expect(excerptAgentFailureOutput('', 'Retrying request…\n'.repeat(10))).toBe(
|
|
'Retrying request… Retrying request…'
|
|
)
|
|
})
|
|
|
|
it('truncates an overlong single line to the persistence budget', () => {
|
|
const line = `Error: ${'m'.repeat(300)}`
|
|
expect(excerptAgentFailureOutput('', line)).toBe(`Error: ${'m'.repeat(233)}…`)
|
|
})
|
|
|
|
it('reads the head and tail windows of oversized multi-line output', () => {
|
|
const stderr = `first line\n${'filler line\n'.repeat(3000)}last: operative error`
|
|
expect(excerptAgentFailureOutput('', stderr)).toBe(
|
|
'first line filler line … last: operative error'
|
|
)
|
|
})
|
|
|
|
it('bounds the excerpt for a giant single-line stream', () => {
|
|
expect(excerptAgentFailureOutput('', 'x'.repeat(20_000))).toBe(`${'x'.repeat(100)}…`)
|
|
})
|
|
})
|
|
|
|
describe('tokenizeCustomCommandTemplate', () => {
|
|
it('splits on whitespace', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude -p')
|
|
expect(r).toEqual({ ok: true, tokens: ['claude', '-p'], spans: expect.any(Array) })
|
|
})
|
|
|
|
it('groups double-quoted segments with spaces', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "hello world"')
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
tokens: ['claude', '--msg', 'hello world'],
|
|
spans: expect.any(Array)
|
|
})
|
|
})
|
|
|
|
it('groups single-quoted segments verbatim', () => {
|
|
const r = tokenizeCustomCommandTemplate(`agent --json '{"k":"v"}'`)
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
tokens: ['agent', '--json', '{"k":"v"}'],
|
|
spans: expect.any(Array)
|
|
})
|
|
})
|
|
|
|
it('honors backslash escapes inside double quotes', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "she said \\"hi\\""')
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
tokens: ['claude', '--msg', 'she said "hi"'],
|
|
spans: expect.any(Array)
|
|
})
|
|
})
|
|
|
|
it('keeps adjacent quoted/unquoted regions in one token (a"b"c → abc)', () => {
|
|
const r = tokenizeCustomCommandTemplate('foo a"b"c')
|
|
expect(r).toEqual({ ok: true, tokens: ['foo', 'abc'], spans: expect.any(Array) })
|
|
})
|
|
|
|
it('always reports one span per token', () => {
|
|
for (const source of ['claude -p', 'claude --msg "hello world"', 'foo a"b"c', ' \t ']) {
|
|
const r = tokenizeCustomCommandTemplate(source)
|
|
expect(r.ok && r.spans.length).toBe(r.ok && r.tokens.length)
|
|
}
|
|
})
|
|
|
|
it('reports source spans covering each raw token including quotes', () => {
|
|
const source = 'claude --msg "hello world"'
|
|
const r = tokenizeCustomCommandTemplate(source)
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
tokens: ['claude', '--msg', 'hello world'],
|
|
spans: [
|
|
{ start: 0, end: 6, divergesFromShell: false },
|
|
{ start: 7, end: 12, divergesFromShell: false },
|
|
{ start: 13, end: 26, divergesFromShell: false }
|
|
]
|
|
})
|
|
if (r.ok) {
|
|
expect(r.spans.map(({ start, end }) => source.slice(start, end))).toEqual([
|
|
'claude',
|
|
'--msg',
|
|
'"hello world"'
|
|
])
|
|
}
|
|
})
|
|
|
|
it('returns an error for an unclosed quote', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "no end')
|
|
expect(r.ok).toBe(false)
|
|
if (!r.ok) {
|
|
expect(r.error).toMatch(/unclosed/i)
|
|
}
|
|
})
|
|
|
|
it('returns an empty token list for whitespace-only input', () => {
|
|
const r = tokenizeCustomCommandTemplate(' \t ')
|
|
expect(r).toEqual({ ok: true, tokens: [], spans: [] })
|
|
})
|
|
})
|
|
|
|
describe('planCustomCommand', () => {
|
|
it('routes prompt via stdin when {prompt} is absent', () => {
|
|
const r = planCustomCommand('claude -p', 'COMMIT MSG')
|
|
expect(r).toEqual({ ok: true, binary: 'claude', args: ['-p'], stdinPayload: 'COMMIT MSG' })
|
|
})
|
|
|
|
it('substitutes {prompt} as a whole token via argv', () => {
|
|
const r = planCustomCommand('codex exec {prompt}', 'PROMPT')
|
|
expect(r).toEqual({ ok: true, binary: 'codex', args: ['exec', 'PROMPT'], stdinPayload: null })
|
|
})
|
|
|
|
it('treats "{prompt}" identically to bare {prompt} (no shell, no double-quoting)', () => {
|
|
const a = planCustomCommand('codex exec {prompt}', 'PROMPT')
|
|
const b = planCustomCommand('codex exec "{prompt}"', 'PROMPT')
|
|
expect(a).toEqual(b)
|
|
})
|
|
|
|
it('substitutes {prompt} embedded inside a token', () => {
|
|
const r = planCustomCommand('agent --msg={prompt}', 'PROMPT')
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
binary: 'agent',
|
|
args: ['--msg=PROMPT'],
|
|
stdinPayload: null
|
|
})
|
|
})
|
|
|
|
it('errors on empty templates', () => {
|
|
const r = planCustomCommand(' ', 'PROMPT')
|
|
expect(r.ok).toBe(false)
|
|
})
|
|
|
|
it('propagates tokenizer errors', () => {
|
|
const r = planCustomCommand('agent "unclosed', 'PROMPT')
|
|
expect(r.ok).toBe(false)
|
|
if (!r.ok) {
|
|
expect(r.error).toMatch(/unclosed/i)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Windows command overrides keep native path separators (#11375)', () => {
|
|
const WINDOWS_PATH = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
|
|
|
it('eats backslashes under the POSIX default, which is what broke Windows paths', () => {
|
|
// Pinned as the reason 'literal' exists, not as desired behavior.
|
|
const posix = tokenizeCustomCommandTemplate(WINDOWS_PATH)
|
|
|
|
expect(posix.ok && posix.tokens).toEqual([
|
|
'C:WindowsSystem32WindowsPowerShellv1.0powershell.exe'
|
|
])
|
|
})
|
|
|
|
it('keeps a native absolute path intact in literal mode', () => {
|
|
const literal = tokenizeCustomCommandTemplate(WINDOWS_PATH, 'literal')
|
|
|
|
expect(literal.ok && literal.tokens).toEqual([WINDOWS_PATH])
|
|
})
|
|
|
|
it('still splits on whitespace and honours quotes in literal mode', () => {
|
|
const quoted = tokenizeCustomCommandTemplate(
|
|
'"C:\\Program Files\\Git\\bin\\bash.exe" --login -i',
|
|
'literal'
|
|
)
|
|
|
|
expect(quoted.ok && quoted.tokens).toEqual([
|
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
'--login',
|
|
'-i'
|
|
])
|
|
})
|
|
|
|
it('leaves a trailing backslash alone instead of swallowing the delimiter', () => {
|
|
const trailing = tokenizeCustomCommandTemplate('C:\\tools\\ --flag', 'literal')
|
|
|
|
expect(trailing.ok && trailing.tokens).toEqual(['C:\\tools\\', '--flag'])
|
|
})
|
|
|
|
it('keeps POSIX escaping the default so `foo\\ bar` stays one token', () => {
|
|
const posix = tokenizeCustomCommandTemplate('/usr/local/my\\ agent/bin --flag')
|
|
|
|
expect(posix.ok && posix.tokens).toEqual(['/usr/local/my agent/bin', '--flag'])
|
|
})
|
|
|
|
it('substitutes {prompt} as one argument with a Windows binary path', () => {
|
|
const plan = planCustomCommand(
|
|
`${WINDOWS_PATH} -Command {prompt}`,
|
|
'write a commit message',
|
|
'literal'
|
|
)
|
|
|
|
expect(plan.ok && plan.binary).toBe(WINDOWS_PATH)
|
|
expect(plan.ok && plan.args).toEqual(['-Command', 'write a commit message'])
|
|
})
|
|
})
|