mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
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.
581 lines
22 KiB
TypeScript
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'")
|
|
})
|
|
})
|