Files
orca/src/shared/agent-resume-launch-command.test.ts
T
Neil 231e805b1e fix(lint): enable anti-slop/no-shape-in-symbol-names (#20785)
Flip `anti-slop/no-shape-in-symbol-names` from "off" to "error" and clear
every violation under src, config, tests and mobile.

What the rule bans
------------------
The case-insensitive substring "shape" in any JS/TS identifier: variables,
functions, parameters, types, type parameters, class members, private names,
object-literal keys and JSX identifiers. The one exemption is a statically
accessed member read owned by another value (`zodObject.shape` is fine), so
third-party APIs stay readable without a suppression.

"Shape" names a value's structure rather than its domain role. `UserShape`,
`validateArgShape` and `errorShape` all tell you the symbol is "an object
with some fields" -- which is already what a type says -- while saying
nothing about what the value is for or who owns it. The rule forces the
name to carry the domain instead.

Violations fixed
----------------
689 violations across 109 files at baseline (verified by re-running the
audit against the pre-change tree with the rule set to "error").

Fix pattern
-----------
Rename for the domain role, not the structure:

  -type FieldShape = 'list' | 'map' | 'whole'
  -const FIELD_SHAPES = { ... } satisfies Record<keyof Observation, FieldShape>
  +type FieldEncoding = 'list' | 'map' | 'whole'
  +const FIELD_ENCODINGS = { ... } satisfies Record<keyof Observation, FieldEncoding>

  -function assertGitPushTargetShape(target: unknown): void
  +function assertValidGitPushTarget(target: unknown): void

  -function describeReadDirPathShape(p: string): ReadDirPathKind
  +function classifyReadDirPath(p: string): ReadDirPathKind

Predicates became statements about the value (`isDeltaShapedProviderFrameKind`
-> `isDeltaProviderFrameKind`, `isDeleteShapedDiscardEntry` ->
`discardDeletesEntryFile`, `isSkillsCliAgentKeyShaped` ->
`isUsableSkillsCliAgentKey`). Type aliases dropped the suffix where the
remaining name was already unambiguous (`GhGraphqlErrorShape` ->
`GhGraphqlError`).

No wire-visible name was renamed: no IPC or RPC channel, stream opcode,
request/response param, persisted field, or i18n key. The `--shape=symlink|copy`
CLI flag read by .github/workflows/skill-update-roundtrip.yml is unchanged --
only the local variable holding it was renamed.

Exemptions
----------
They are file-scoped entries in config/oxlint-anti-slop.json, not inline
`oxlint-disable` comments. An inline directive naming an anti-slop rule reads
back as an UNUSED directive under the root lint scan, which does not load this
plugin -- the changed-code quality gate counts that warning, so the comment form
cannot be used for a rule that lives only in this config.

* src/renderer/src/components/browser-pane/annotate/**:
  in the screenshot annotator a "shape" is the drawn geometry -- pen, arrow,
  rect, ellipse, highlight. That is a genuine domain noun, and it pervades
  every symbol in the module.
* repo-icon.tsx, repo-header-project-actions.tsx, mobile MobileRepoIcon.tsx:
  lucide exports the icon component as `Shapes`. The name is theirs, and the
  matching REPO_LUCIDE_ICONS key is the persisted icon name shared with the
  desktop picker -- renaming it would orphan saved repo icons.
* src/shared/onboarding-state-types.ts, src/shared/constants.ts:
  `shapedSidebar` is a persisted onboarding-checklist field and a telemetry
  enum member; renaming it would orphan saved state.
* src/shared/rpc-contract/rpc-send-params.ts: matching zod's own literal `shape`
  property is what selects the ZodObject branch of the conditional type.

No exemption was added merely to avoid a rename. Eight symbols initially
suppressed as "a cross-module refactor outside this change" were proven to have
zero non-TypeScript references repo-wide and renamed instead.

Zod's `ZodRawShape` needed no exemption at all: `Readonly<Record<string,
z.ZodType>>` is its definition, so repo-update-params.ts and
ui-update-value-tolerance-params.ts spell it out instead. Likewise
telemetry-event-classification.ts now reads `.shape` through an `in` narrowing,
which also retires two pre-existing type assertions; three more assertions the
rename had dragged onto changed lines (two `JSON.parse` sites, one node:sqlite
row read) became annotations and an explicit row mapping.

Verified
--------
* Audit reports zero violations; confirmed the rule genuinely fires by
  planting a probe violation.
* node config/scripts/run-typecheck-projects-in-parallel.mjs exits 0.
* Vitest over src/shared, src/main/github/project-view, the annotate module,
  the repo-icon components and the Chromium SameSite electron spec: all green.
* All 66 removed "shape" identifiers grepped repo-wide across every file type;
  none survive.
* node config/scripts/generate-rpc-params-catalog.mjs --check exits 0.
* node --check on every changed .mjs; oxfmt clean on all changed files.
* `pnpm run check:code-quality:changed` reports 0 findings.

Not machine-verified: the 3 mobile/ files (its Vitest run cannot resolve
`expo/tsconfig.base.json` in this worktree), and the WSL- and Playwright-gated
specs. All are rename- or comment-only hunks, read in full.
2026-09-15 02:00:27 -07:00

581 lines
22 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildClaudeResumeLaunchCommand } from './agent-resume-launch-command'
import { buildAgentResumeStartupPlan, buildAgentStartupPlan } from './tui-agent-startup'
import { tokenizeStartupCommand, type AgentStartupShell } from './tui-agent-startup-shell'
const SESSION_ID = 'claude-session-1'
const RESUME = ['--resume', SESSION_ID] as const
const providerSession = { key: 'session_id', id: SESSION_ID } as const
const SHELLS: { platform: NodeJS.Platform; shell: AgentStartupShell }[] = [
{ platform: 'linux', shell: 'posix' },
{ platform: 'darwin', shell: 'posix' },
{ platform: 'win32', shell: 'powershell' },
{ platform: 'win32', shell: 'cmd' }
]
/** Independent selector oracle — deliberately NOT the implementation's own
* predicate, so a regression that shrinks the stripped set cannot also blind
* this assertion. */
function isResumeSelectorToken(token: string): boolean {
return (
['--resume', '--continue', '-r', '-c'].includes(token) ||
['--resume=', '--continue=', '-r=', '-c='].some((prefix) => token.startsWith(prefix))
)
}
/** Tokenizes a launch command and asserts exactly one identity-bearing resume. */
function expectSingleAuthoritativeResume(command: string, shell: AgentStartupShell): void {
const tokenized = tokenizeStartupCommand(command, shell)
expect(tokenized.ok).toBe(true)
if (!tokenized.ok) {
return
}
const selectors = tokenized.tokens.filter(isResumeSelectorToken)
expect(selectors).toEqual(['--resume'])
const index = tokenized.tokens.indexOf('--resume')
expect(tokenized.tokens[index + 1]).toBe(SESSION_ID)
}
describe('buildClaudeResumeLaunchCommand', () => {
it.each(SHELLS)('appends the authoritative selector to a plain base ($shell)', ({ shell }) => {
const command = buildClaudeResumeLaunchCommand('claude', RESUME, shell)
expectSingleAuthoritativeResume(command, shell)
expect(command.startsWith('claude ')).toBe(true)
})
it.each(SHELLS)('strips a bare persisted --resume picker default ($shell)', ({ shell }) => {
const base = shell === 'cmd' ? 'claude "--resume"' : "claude '--resume'"
const command = buildClaudeResumeLaunchCommand(base, RESUME, shell)
expectSingleAuthoritativeResume(command, shell)
})
it.each([
'claude --resume stale-session',
'claude --resume=stale-session',
'claude -r stale-session',
'claude -r=stale-session',
'claude --resume= --model sonnet',
'claude --continue',
'claude -c',
'claude --continue=1',
'claude -c=1',
'claude --resume stale -r older --continue -c'
])('replaces stale selectors in %s', (base) => {
const command = buildClaudeResumeLaunchCommand(base, RESUME, 'posix')
expectSingleAuthoritativeResume(command, 'posix')
})
it('keeps surviving options when stripping selectors', () => {
expect(
buildClaudeResumeLaunchCommand('claude --resume stale --model sonnet', RESUME, 'posix')
).toBe(`claude --model sonnet '--resume' '${SESSION_ID}'`)
})
it('never mistakes a dash-leading option value for a selector', () => {
expect(buildClaudeResumeLaunchCommand('claude --model -recent', RESUME, 'posix')).toBe(
`claude --model -recent '--resume' '${SESSION_ID}'`
)
expect(
buildClaudeResumeLaunchCommand('claude --append-system-prompt -rules-here', RESUME, 'posix')
).toBe(`claude --append-system-prompt -rules-here '--resume' '${SESSION_ID}'`)
expect(buildClaudeResumeLaunchCommand('claude --agent -reviewer', RESUME, 'posix')).toBe(
`claude --agent -reviewer '--resume' '${SESSION_ID}'`
)
expect(buildClaudeResumeLaunchCommand('claude --plugin-url -remote.zip', RESUME, 'posix')).toBe(
`claude --plugin-url -remote.zip '--resume' '${SESSION_ID}'`
)
})
it('leaves the ambiguous joined -r<id> form alone (degrades to pre-guard behavior)', () => {
expect(buildClaudeResumeLaunchCommand('claude -rstale-session', RESUME, 'posix')).toBe(
`claude -rstale-session '--resume' '${SESSION_ID}'`
)
})
it('leaves wrapper commands untouched and appends at the end', () => {
expect(buildClaudeResumeLaunchCommand('bash -c claude', RESUME, 'posix')).toBe(
`bash -c claude '--resume' '${SESSION_ID}'`
)
expect(buildClaudeResumeLaunchCommand('mise exec -- claude', RESUME, 'posix')).toBe(
`mise exec -- claude '--resume' '${SESSION_ID}'`
)
expect(buildClaudeResumeLaunchCommand('sudo -u dev -- claude', RESUME, 'posix')).toBe(
`sudo -u dev -- claude '--resume' '${SESSION_ID}'`
)
})
it('strips selectors that follow claude after a wrapper terminator', () => {
expect(
buildClaudeResumeLaunchCommand("mise exec -- claude '--resume' stale", RESUME, 'posix')
).toBe(`mise exec -- claude '--resume' '${SESSION_ID}'`)
})
it('fails open for claude outside command position (wrapper without --)', () => {
// Why: npx/bunx-style passthrough cannot be told apart from an argument
// that merely names claude, so the guard defers to append-only behavior.
expect(buildClaudeResumeLaunchCommand("npx claude '--resume'", RESUME, 'posix')).toBe(
`npx claude '--resume' '--resume' '${SESSION_ID}'`
)
})
it('never mistakes a claude-suffixed argument for the executable', () => {
expect(
buildClaudeResumeLaunchCommand(
'ssh -i ~/.ssh/claude devbox -- claude --resume OLD',
RESUME,
'posix'
)
).toBe(`ssh -i ~/.ssh/claude devbox -- claude '--resume' '${SESSION_ID}'`)
expect(
buildClaudeResumeLaunchCommand(
'mise exec --cd /Users/me/src/claude -- claude --resume OLD',
RESUME,
'posix'
)
).toBe(`mise exec --cd /Users/me/src/claude -- claude '--resume' '${SESSION_ID}'`)
// No claude in command position at all: wrapper flags stay untouched.
expect(
buildClaudeResumeLaunchCommand(
'nix develop /Users/me/src/claude -c claude --resume OLD',
RESUME,
'posix'
)
).toBe(`nix develop /Users/me/src/claude -c claude --resume OLD '--resume' '${SESSION_ID}'`)
})
it.each(['claude "\\--resume" old', 'claude "\\-r" old', 'claude --model "\\--resume"'])(
'fails open on a double-quoted backslash the shell keeps literal: %s',
(base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
}
)
it('still strips when a double-quoted backslash is shell-consumed', () => {
expect(
buildClaudeResumeLaunchCommand('claude --model "a\\"b" --resume old', RESUME, 'posix')
).toBe(`claude --model "a\\"b" '--resume' '${SESSION_ID}'`)
})
it.each([
'claude "--resu\\\nme" --resume stale',
"claude $'-c' --resume stale",
"claude $'--resu\\x6de' --resume stale"
])('fails open when an escape hides a selector from the tokenizer: %s', (base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it.each([
'claude --resume old --append-system-prompt "spend under 5$"',
'claude --resume old --note x$',
'claude --resume old --note "a$"'
])('still strips when a $ is not an expansion opener: %s', (base) => {
const command = buildClaudeResumeLaunchCommand(base, RESUME, 'posix')
expect(command).not.toContain('--resume old')
expectSingleAuthoritativeResume(command, 'posix')
})
it.each(['claude a^" --resume ^"b', 'claude ^"x --resume^" --resume old'])(
'fails open when a cmd caret escapes a quote: %s',
(base) => {
// cmd strips the caret and the child's parser reads a bare quote
// delimiter, so the tokenizer's word boundaries stop matching argv.
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe(
`${base} "--resume" "${SESSION_ID}"`
)
}
)
it.each(['claude --resume (Get-Content id.txt)', 'claude --hook { npm -c run } --resume old'])(
'fails open on bare powershell evaluation syntax: %s',
(base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
}
)
it.each([
'claude "-`r" foo --model x',
'claude "-`u{72}" oldid --resume x',
'claude -`r foo --model x',
'claude --resum`e stale --model x'
])('fails open on powershell escape sequences, quoted or bare: %s', (base) => {
// PowerShell expands these in bare arguments too, so the tokenizer's
// token value is not what argv receives.
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on a token-leading powershell backtick before whitespace', () => {
// PowerShell drops the backtick AND the whitespace, emitting no token,
// so the tokenizer's extra token would shift the locator.
const base = 'claude --resume ` \t"q"'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on the powershell stop-parsing token', () => {
// After a bare --%, PowerShell hands the rest of the line to the child
// literally, so an appended quoted selector would arrive as literal bytes.
const base = 'claude --% --resume stale --model sonnet'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('still strips when --% is quoted or on cmd, where it is an ordinary token', () => {
expect(
buildClaudeResumeLaunchCommand('claude "--%" --resume stale', RESUME, 'powershell')
).toBe(`claude "--%" '--resume' '${SESSION_ID}'`)
expect(buildClaudeResumeLaunchCommand('claude --% --resume stale', RESUME, 'cmd')).toBe(
`claude --% "--resume" "${SESSION_ID}"`
)
})
it('still strips with parens safely inside quotes on powershell', () => {
expect(
buildClaudeResumeLaunchCommand(
'claude --allowedTools "Bash(git:*)" --resume old',
RESUME,
'powershell'
)
).toBe(`claude --allowedTools "Bash(git:*)" '--resume' '${SESSION_ID}'`)
})
it.each([
'claude "--add-dir" "C:\\a\\" "--" "--out" "D:\\x\\"',
'claude --add-dir "C:\\repo\\" --resume old'
])('fails open when a cmd backslash run makes a quote literal: %s', (base) => {
// An odd run of backslashes makes the quote a literal byte to the child's
// CommandLineToArgvW parser, so tokenizer boundaries stop matching argv.
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe(
`${base} "--resume" "${SESSION_ID}"`
)
})
it.each([
'claude --add-dir "C:\\Users\\me\\repo" --resume old',
'claude --add-dir "C:\\Program Files (x86)\\x" --resume old'
])('still strips for ordinary quoted windows paths: %s', (base) => {
const command = buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')
expect(command).not.toContain('--resume old')
expectSingleAuthoritativeResume(command, 'cmd')
})
it('fails open when a cmd caret escapes a real argument separator', () => {
// cmd strips ^ before the child re-splits on the bare space, so the
// tokenizer's merged token would drop the user's second argument.
const base = 'claude --resume a^ b --model x'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'cmd')).toBe(
`${base} "--resume" "${SESSION_ID}"`
)
})
it.each([
['claude --resume old \\', 'posix' as const],
['claude --resume old `', 'powershell' as const],
['claude --resume old ^', 'cmd' as const]
])('fails open on a trailing unpaired escape: %s', (base, shell) => {
// A dangling escape would swallow the separator before the appended
// selector, so claude would receive no exact --resume at all.
const quoted = shell === 'cmd' ? `"--resume" "${SESSION_ID}"` : `'--resume' '${SESSION_ID}'`
expect(buildClaudeResumeLaunchCommand(base, RESUME, shell)).toBe(`${base} ${quoted}`)
})
it('fails open on a windows escaped line continuation', () => {
const powershellBase = 'claude --resume stale `\n--resume hidden'
expect(buildClaudeResumeLaunchCommand(powershellBase, RESUME, 'powershell')).toBe(
`${powershellBase} '--resume' '${SESSION_ID}'`
)
const cmdBase = 'claude --resume stale ^\n--resume hidden'
expect(buildClaudeResumeLaunchCommand(cmdBase, RESUME, 'cmd')).toBe(
`${cmdBase} "--resume" "${SESSION_ID}"`
)
})
it('fails open on a posix line continuation hiding a selector', () => {
const base = 'claude --model x \\\n--resume old'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on escape characters the windows shells keep literal', () => {
// cmd keeps ^ literal inside double quotes; PowerShell keeps ` literal
// inside single quotes, so these tokens are not really selectors.
expect(buildClaudeResumeLaunchCommand('claude "-^-resume" old', RESUME, 'cmd')).toBe(
`claude "-^-resume" old "--resume" "${SESSION_ID}"`
)
expect(buildClaudeResumeLaunchCommand("claude '-`-resume' old", RESUME, 'powershell')).toBe(
`claude '-\`-resume' old '--resume' '${SESSION_ID}'`
)
})
it('fails open on an unclosed expansion opened before claude', () => {
const base = '$(x; npx -- claude --resume stale)'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open when the executable token itself is unmodelable', () => {
// cmd has no single-quote syntax, so 'claude' is not the claude executable.
expect(buildClaudeResumeLaunchCommand("'claude' --resume old", RESUME, 'cmd')).toBe(
`'claude' --resume old "--resume" "${SESSION_ID}"`
)
})
it.each(['cmd', 'powershell'] as const)(
'fails open on a posix-style assignment prefix under %s',
(shell) => {
// NAME=value prefixes are posix-only syntax; on Windows shells that
// token is a bogus executable, so nothing may be spliced.
const base = "FOO='bar' claude --resume old"
const quoted = shell === 'cmd' ? `"--resume" "${SESSION_ID}"` : `'--resume' '${SESSION_ID}'`
expect(buildClaudeResumeLaunchCommand(base, RESUME, shell)).toBe(`${base} ${quoted}`)
}
)
it('preserves an env-assignment or path prefix byte for byte', () => {
expect(
buildClaudeResumeLaunchCommand('FOO="$HOME/x" ~/bin/claude \'--resume\'', RESUME, 'posix')
).toBe(`FOO="$HOME/x" ~/bin/claude '--resume' '${SESSION_ID}'`)
})
it('recognizes Windows claude executable spellings', () => {
expect(buildClaudeResumeLaunchCommand('C:\\tools\\claude.CMD "--resume"', RESUME, 'cmd')).toBe(
`C:\\tools\\claude.CMD "--resume" "${SESSION_ID}"`
)
})
it("inserts the selector before claude's own -- terminator", () => {
expect(
buildClaudeResumeLaunchCommand('claude --resume stale -- positional', RESUME, 'posix')
).toBe(`claude '--resume' '${SESSION_ID}' -- positional`)
})
it('fails open when the base cannot be tokenized', () => {
expect(buildClaudeResumeLaunchCommand('claude "unterminated', RESUME, 'posix')).toBe(
`claude "unterminated '--resume' '${SESSION_ID}'`
)
})
it('fails open when no claude executable token exists', () => {
expect(buildClaudeResumeLaunchCommand('my-agent-wrapper --resume', RESUME, 'posix')).toBe(
`my-agent-wrapper --resume '--resume' '${SESSION_ID}'`
)
})
it.each([
'claude -c && echo done',
'claude --resume stale; echo hi',
'claude --resume stale;echo hi',
'claude -c | tee /tmp/log',
'claude --resume stale 2>/tmp/x.log',
'claude --resume stale\n--verbose',
'claude --resume stale # note'
])('fails open when the base chains shell syntax after claude: %s', (base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it.each([
'claude --resume old "y"; echo hi',
"claude --resume old 'y'; echo hi",
'claude --resume old --model "sonnet"&&echo hi',
'claude --resume old "y"| tee /tmp/x',
'claude --resume old \\"; echo hi'
])('fails open when shell syntax hides behind partial quoting: %s', (base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on escape-adjacent operators in windows shells', () => {
const powershellBase = 'claude --resume old `x; echo hi'
expect(buildClaudeResumeLaunchCommand(powershellBase, RESUME, 'powershell')).toBe(
`${powershellBase} '--resume' '${SESSION_ID}'`
)
const cmdBase = 'claude --resume old ^x& echo hi'
expect(buildClaudeResumeLaunchCommand(cmdBase, RESUME, 'cmd')).toBe(
`${cmdBase} "--resume" "${SESSION_ID}"`
)
})
it.each([
'claude --resume $(cat sid.txt)',
'claude --resume=$(cat sid.txt)',
'claude --resume `cat sid.txt`',
'claude --resume ${SID:-a b}'
])('fails open on unquoted multi-token shell expansions: %s', (base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it.each([
'claude --resume "`cat "a b"`"',
'claude --model "$(pick "x -c y")"',
'claude --model "$(f "x --resume y")"',
'claude --resume "$(cat "$HOME/My Sessions/id")"'
])('fails open on expansions nested inside double quotes: %s', (base) => {
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'posix')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on a powershell double-quoted subexpression', () => {
const base = 'claude --model "$(pick "x -c y")"'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('fails open on a powershell subexpression locator', () => {
const base = 'claude --resume $(Get-Content sid.txt)'
expect(buildClaudeResumeLaunchCommand(base, RESUME, 'powershell')).toBe(
`${base} '--resume' '${SESSION_ID}'`
)
})
it('never walks the separator backoff into an escaped-space token', () => {
expect(
buildClaudeResumeLaunchCommand(
'claude --append-system-prompt Be\\ nice\\ --resume OLD',
RESUME,
'posix'
)
).toBe(`claude --append-system-prompt Be\\ nice\\ '--resume' '${SESSION_ID}'`)
})
it('fails open when cmd operators hide in single quotes', () => {
// cmd.exe has no single-quote syntax, so '&' is a live command separator.
expect(buildClaudeResumeLaunchCommand("claude '&' --resume=old-session", RESUME, 'cmd')).toBe(
`claude '&' --resume=old-session "--resume" "${SESSION_ID}"`
)
})
it('fails open on cmd single-quoted literals that merely look like selectors', () => {
// cmd passes the quotes through, so these are junk positionals for
// claude, not real selectors or terminators.
expect(buildClaudeResumeLaunchCommand("claude '--resume' old", RESUME, 'cmd')).toBe(
`claude '--resume' old "--resume" "${SESSION_ID}"`
)
expect(buildClaudeResumeLaunchCommand("claude '--' --resume old", RESUME, 'cmd')).toBe(
`claude '--' --resume old "--resume" "${SESSION_ID}"`
)
})
it('still strips next to a caret-escaped literal ampersand on cmd', () => {
// ^& is an inactive, literal & in cmd, so the guard may keep working.
expect(buildClaudeResumeLaunchCommand('claude --resume old ^&', RESUME, 'cmd')).toBe(
`claude ^& "--resume" "${SESSION_ID}"`
)
})
it('still strips when operators are safely quoted', () => {
expect(
buildClaudeResumeLaunchCommand(
"claude --append-system-prompt 'use && wisely' --resume stale",
RESUME,
'posix'
)
).toBe(`claude --append-system-prompt 'use && wisely' '--resume' '${SESSION_ID}'`)
})
it('recognizes claude behind the PowerShell call operator', () => {
expect(buildClaudeResumeLaunchCommand("& claude '--resume'", RESUME, 'powershell')).toBe(
`& claude '--resume' '${SESSION_ID}'`
)
expect(
buildClaudeResumeLaunchCommand(
"& 'C:\\Program Files\\claude\\claude.exe' --resume old",
RESUME,
'powershell'
)
).toBe(`& 'C:\\Program Files\\claude\\claude.exe' '--resume' '${SESSION_ID}'`)
})
})
describe('buildAgentResumeStartupPlan claude selector guard', () => {
it.each(SHELLS)(
'emits one identity-bearing resume for a persisted bare selector ($platform/$shell)',
({ platform, shell }) => {
const initial = buildAgentStartupPlan({
agent: 'claude',
prompt: '',
cmdOverrides: {},
agentArgs: '--resume',
platform,
shell,
allowEmptyPromptLaunch: true
})
expect(initial).not.toBeNull()
const restored = buildAgentResumeStartupPlan({
agent: 'claude',
providerSession,
cmdOverrides: {},
agentArgs: initial?.launchConfig.agentArgs,
agentCommand: initial?.launchConfig.agentCommand,
platform,
shell
})
expect(restored).not.toBeNull()
expectSingleAuthoritativeResume(restored?.launchCommand ?? '', shell)
}
)
it('emits one identity-bearing resume when only default args carry a stale id', () => {
const restored = buildAgentResumeStartupPlan({
agent: 'claude',
providerSession,
cmdOverrides: {},
agentArgs: '--resume stale-session --model sonnet',
platform: 'linux'
})
expect(restored?.launchCommand).toBe(`claude '--model' 'sonnet' '--resume' '${SESSION_ID}'`)
})
it('still launches exotic custom commands that the tokenizer rejects', () => {
const restored = buildAgentResumeStartupPlan({
agent: 'claude',
providerSession,
cmdOverrides: {},
agentCommand: 'claude --model $(cat ~/.claude-model) "unterminated',
platform: 'darwin'
})
expect(restored).not.toBeNull()
expect(restored?.launchCommand.endsWith(`'--resume' '${SESSION_ID}'`)).toBe(true)
})
it('does not change other agents', () => {
const restored = buildAgentResumeStartupPlan({
agent: 'gemini',
providerSession,
cmdOverrides: {},
agentArgs: '--resume',
platform: 'linux'
})
expect(restored?.launchCommand).toBe(`gemini '--resume' '--resume' '${SESSION_ID}'`)
})
it('persists the original base command unchanged', () => {
const restored = buildAgentResumeStartupPlan({
agent: 'claude',
providerSession,
cmdOverrides: {},
agentCommand: "claude '--resume'",
platform: 'linux'
})
expect(restored?.launchConfig.agentCommand).toBe("claude '--resume'")
})
})