Files
ccs/tests/unit/utils/claude-subcommand-detector.test.ts
T
Tam Nhu Tran c2b00b7ad6 feat(dispatcher): pass through Claude subcommands without interactive-session args
Closes #1218.

When the user invokes `ccs <profile> agents` (and other Claude subcommands
like `mcp`, `doctor`, `plugin`, ...), CCS was unconditionally injecting
session-only Claude flags (`--append-system-prompt`, `--disallowedTools`,
`--settings`, official-channels plugin specs) and forwarding the
`DISABLE_TELEMETRY=1` env var from profile settings. Each of those
either errored out the subcommand (`error: unknown option
'--append-system-prompt'`) or silently flipped Claude into
non-interactive list mode, so the new `claude agents` agent view never
opened under CCS.

Add a small `claude-subcommand-detector` that recognizes the documented
Claude subcommand set after skipping known value-taking flags, then have
the three steering-prompt injectors (websearch, image-analysis, browser),
the cliproxy and settings launchers, and the official-channels plan
short-circuit when a subcommand invocation is detected. Also strip
`DISABLE_TELEMETRY` from the spawned env only for subcommand
invocations — upstream Claude Code uses that var as a kill switch for
the subcommand TUIs, and the user's telemetry preference still applies
to every normal interactive session.

Verified end-to-end against `ccs glm agents` and `ccs ck agents`: the
agent view opens correctly, no `unknown option` error, and all 1938
existing tests pass.
2026-05-12 16:35:37 -04:00

81 lines
3.1 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { isClaudeSubcommandInvocation } from '../../../src/utils/claude-subcommand-detector';
import { appendThirdPartyWebSearchToolArgs } from '../../../src/utils/websearch/claude-tool-args';
import { appendThirdPartyImageAnalysisToolArgs } from '../../../src/utils/image-analysis/claude-tool-args';
import { appendBrowserToolArgs } from '../../../src/utils/browser/claude-tool-args';
describe('isClaudeSubcommandInvocation', () => {
it('returns false for an empty arg list', () => {
expect(isClaudeSubcommandInvocation([])).toBe(false);
});
it('returns false for a prompt-only invocation', () => {
expect(isClaudeSubcommandInvocation(['fix the failing test'])).toBe(false);
});
it('detects bare subcommand', () => {
for (const cmd of [
'agents',
'auth',
'doctor',
'mcp',
'plugin',
'plugins',
'project',
'setup-token',
'ultrareview',
'update',
'upgrade',
'auto-mode',
'install',
]) {
expect(isClaudeSubcommandInvocation([cmd])).toBe(true);
}
});
it('skips past value-taking flags before the positional', () => {
expect(isClaudeSubcommandInvocation(['--model', 'sonnet', 'agents'])).toBe(true);
expect(isClaudeSubcommandInvocation(['--settings', '/tmp/s.json', 'doctor'])).toBe(true);
});
it('does not treat a flag value matching a subcommand name as a subcommand', () => {
// `--name auth` sets the session display name to "auth" — still an interactive launch.
expect(isClaudeSubcommandInvocation(['--name', 'auth'])).toBe(false);
});
it('handles --flag=value forms', () => {
expect(isClaudeSubcommandInvocation(['--model=sonnet', 'agents'])).toBe(true);
});
it('stops scanning at the -- terminator', () => {
expect(isClaudeSubcommandInvocation(['--', 'agents'])).toBe(false);
});
it('ignores subcommand-named tokens that come AFTER the first positional prompt', () => {
// First positional is the prompt; "agents" later is just a word in the prompt context.
expect(isClaudeSubcommandInvocation(['talk to me about', 'agents'])).toBe(false);
});
});
describe('subcommand passthrough — injectors short-circuit', () => {
it('appendThirdPartyWebSearchToolArgs returns args unchanged for subcommand invocations', () => {
expect(appendThirdPartyWebSearchToolArgs(['agents'])).toEqual(['agents']);
expect(appendThirdPartyWebSearchToolArgs(['doctor'])).toEqual(['doctor']);
expect(appendThirdPartyWebSearchToolArgs(['mcp', 'list'])).toEqual(['mcp', 'list']);
});
it('appendThirdPartyImageAnalysisToolArgs returns args unchanged for subcommand invocations', () => {
expect(appendThirdPartyImageAnalysisToolArgs(['agents'])).toEqual(['agents']);
});
it('appendBrowserToolArgs returns args unchanged for subcommand invocations', () => {
expect(appendBrowserToolArgs(['agents'])).toEqual(['agents']);
});
it('injectors still inject for non-subcommand interactive launches', () => {
const out = appendThirdPartyWebSearchToolArgs(['fix the bug']);
expect(out).toContain('--append-system-prompt');
expect(out).toContain('--disallowedTools');
});
});