mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
chore: version packages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview Tests for Claude CLI startup permission modes, focused on the
|
||||
* 'auto' mode (`--permission-mode auto`, Anthropic's recommended low-prompt mode)
|
||||
* added alongside the default `--dangerously-skip-permissions`.
|
||||
*
|
||||
* Covers BOTH spawn paths, which build the permission flags independently:
|
||||
* - session-cli-builder.buildInteractiveArgs (direct PTY, non-mux fallback)
|
||||
* - tmux-manager.buildSpawnCommand (tmux pane command string)
|
||||
* The default must stay 'dangerously-skip-permissions' when the setting is unset.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildInteractiveArgs } from '../src/session-cli-builder.js';
|
||||
import { buildSpawnCommand } from '../src/tmux-manager.js';
|
||||
|
||||
describe('buildInteractiveArgs permission modes (direct PTY path)', () => {
|
||||
it('keeps --dangerously-skip-permissions as the skip-mode flag', () => {
|
||||
const args = buildInteractiveArgs('sid-1', 'dangerously-skip-permissions');
|
||||
expect(args).toContain('--dangerously-skip-permissions');
|
||||
expect(args).not.toContain('--permission-mode');
|
||||
});
|
||||
|
||||
it('auto mode emits --permission-mode auto and never the skip flag', () => {
|
||||
const args = buildInteractiveArgs('sid-1', 'auto');
|
||||
const idx = args.indexOf('--permission-mode');
|
||||
expect(idx).toBeGreaterThanOrEqual(0);
|
||||
expect(args[idx + 1]).toBe('auto');
|
||||
expect(args).not.toContain('--dangerously-skip-permissions');
|
||||
});
|
||||
|
||||
it('normal mode emits no permission flag at all', () => {
|
||||
const args = buildInteractiveArgs('sid-1', 'normal');
|
||||
expect(args).not.toContain('--dangerously-skip-permissions');
|
||||
expect(args).not.toContain('--permission-mode');
|
||||
});
|
||||
|
||||
it('allowedTools mode is unchanged by the auto addition', () => {
|
||||
const args = buildInteractiveArgs('sid-1', 'allowedTools', undefined, 'Read,Grep');
|
||||
expect(args).toEqual(expect.arrayContaining(['--allowedTools', 'Read,Grep']));
|
||||
expect(args).not.toContain('--permission-mode');
|
||||
});
|
||||
|
||||
it('auto mode composes with model and effort flags', () => {
|
||||
const args = buildInteractiveArgs('sid-1', 'auto', 'opus', undefined, 'high');
|
||||
expect(args).toEqual(expect.arrayContaining(['--permission-mode', 'auto', '--model', 'opus', '--effort', 'high']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSpawnCommand permission modes (tmux path)', () => {
|
||||
it('unset claudeMode defaults to --dangerously-skip-permissions', () => {
|
||||
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1' });
|
||||
expect(cmd).toContain('claude --dangerously-skip-permissions --session-id "sid-1"');
|
||||
expect(cmd).not.toContain('--permission-mode');
|
||||
});
|
||||
|
||||
it('auto mode emits --permission-mode auto and never the skip flag', () => {
|
||||
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', claudeMode: 'auto' });
|
||||
expect(cmd).toContain('claude --permission-mode auto --session-id "sid-1"');
|
||||
expect(cmd).not.toContain('--dangerously-skip-permissions');
|
||||
});
|
||||
|
||||
it('auto mode carries into BOTH legs of the resume fallback command', () => {
|
||||
const cmd = buildSpawnCommand({
|
||||
mode: 'claude',
|
||||
sessionId: 'sid-1',
|
||||
claudeMode: 'auto',
|
||||
resumeSessionId: 'abc-123',
|
||||
});
|
||||
const [resumeLeg, fallbackLeg] = cmd.split('||');
|
||||
expect(resumeLeg).toContain('--permission-mode auto');
|
||||
expect(resumeLeg).toContain('--resume "abc-123"');
|
||||
expect(fallbackLeg).toContain('--permission-mode auto');
|
||||
expect(fallbackLeg).toContain('--session-id "sid-1"');
|
||||
expect(cmd).not.toContain('--dangerously-skip-permissions');
|
||||
});
|
||||
|
||||
it('normal mode emits no permission flag', () => {
|
||||
const cmd = buildSpawnCommand({ mode: 'claude', sessionId: 'sid-1', claudeMode: 'normal' });
|
||||
expect(cmd).toContain('claude --session-id "sid-1"');
|
||||
expect(cmd).not.toContain('--permission-mode');
|
||||
expect(cmd).not.toContain('--dangerously-skip-permissions');
|
||||
});
|
||||
});
|
||||
@@ -88,11 +88,25 @@ describe('buildDockerLaunchCommand', () => {
|
||||
expect(cmd).toContain("sh -lc '");
|
||||
});
|
||||
|
||||
it('injects the resume flag ONLY when a resume id is passed', () => {
|
||||
it('pins a deterministic conversation id with a reboot-surviving fallback (fresh launch)', () => {
|
||||
const cmd = buildDockerLaunchCommand(launchOpts());
|
||||
// --session-id first (fresh start), || --resume so a container stop/reboot
|
||||
// relaunch of the SAME session resumes instead of dead-paning on
|
||||
// "Session ID already in use".
|
||||
expect(cmd).toContain(
|
||||
'claude --dangerously-skip-permissions --session-id 1a2b3c4d5e6f || ' +
|
||||
'claude --dangerously-skip-permissions --resume 1a2b3c4d5e6f'
|
||||
);
|
||||
// exec is stripped from the claude pane command — an exec'd first branch could never fall back.
|
||||
expect(cmd).not.toContain('exec claude');
|
||||
});
|
||||
|
||||
it('resumes an explicit id with a --session-id fallback (stale id never dead-panes)', () => {
|
||||
const withResume = buildDockerLaunchCommand(launchOpts({ resumeSessionId: 'abc-123-def' }));
|
||||
expect(withResume).toContain('exec claude --dangerously-skip-permissions --resume abc-123-def');
|
||||
const without = buildDockerLaunchCommand(launchOpts());
|
||||
expect(without).not.toContain('--resume');
|
||||
expect(withResume).toContain(
|
||||
'claude --dangerously-skip-permissions --resume abc-123-def || ' +
|
||||
'claude --dangerously-skip-permissions --session-id 1a2b3c4d5e6f'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses codex resume syntax and drops an unsafe resume id', () => {
|
||||
@@ -101,8 +115,10 @@ describe('buildDockerLaunchCommand', () => {
|
||||
);
|
||||
expect(codex).toContain('exec codex resume 01H-codex-id');
|
||||
const unsafe = buildDockerLaunchCommand(launchOpts({ resumeSessionId: 'x; rm -rf /' }));
|
||||
expect(unsafe).not.toContain('--resume');
|
||||
expect(unsafe).not.toContain('x; rm'); // unsafe id dropped entirely
|
||||
expect(unsafe).not.toContain('rm -rf');
|
||||
// falls back to the deterministic fresh-launch chain on the session's own id
|
||||
expect(unsafe).toContain('--session-id 1a2b3c4d5e6f');
|
||||
});
|
||||
|
||||
it('forwards codex/gemini keys NAME-ONLY (no value in argv)', () => {
|
||||
@@ -128,10 +144,13 @@ describe('buildDockerLaunchCommand', () => {
|
||||
expect(cmd).toContain('/home/arkon/my cases/proj');
|
||||
});
|
||||
|
||||
it('honors a per-host command override', () => {
|
||||
it('honors a per-host command override (exec stripped for the session-id chain)', () => {
|
||||
const docker = { ...toSessionDocker(HOST, CASE), commands: { claude: 'exec claude --model opus' } };
|
||||
const cmd = buildDockerLaunchCommand(launchOpts({ docker }));
|
||||
expect(cmd).toContain('exec claude --model opus');
|
||||
expect(cmd).toContain('claude --model opus --session-id 1a2b3c4d5e6f');
|
||||
const shellOverride = { ...toSessionDocker(HOST, CASE), commands: { shell: 'exec zsh -l' } };
|
||||
const shellCmd = buildDockerLaunchCommand(launchOpts({ mode: 'shell' as SessionMode, docker: shellOverride }));
|
||||
expect(shellCmd).toContain('exec zsh -l'); // non-claude overrides keep their exec
|
||||
});
|
||||
|
||||
it('seeds writable config (guarded copies, mkdir -p parent) from the read-only seed mounts', () => {
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
isSafeTarMember,
|
||||
parseLoadedImageRef,
|
||||
exportDockerCase,
|
||||
validateImportManifest,
|
||||
DOCKER_EXPORT_SCHEMA,
|
||||
type DockerExportManifest,
|
||||
} from '../src/docker-export.js';
|
||||
import { toSessionDocker } from '../src/docker-hosts.js';
|
||||
import type { DockerCase, DockerHost } from '../src/types.js';
|
||||
@@ -63,6 +65,43 @@ describe('isSafeTarMember (import traversal guard)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateImportManifest (untrusted cross-machine input)', () => {
|
||||
const good = (): DockerExportManifest => ({
|
||||
schemaVersion: DOCKER_EXPORT_SCHEMA,
|
||||
caseName: 'myproj',
|
||||
mode: 'full',
|
||||
engine: 'docker',
|
||||
image: 'codeman/agent:base',
|
||||
containerWorkdir: '/home/arkon/cases/myproj',
|
||||
network: 'bridge',
|
||||
createdAt: 1,
|
||||
codemanVersion: '1.4.1',
|
||||
mountCredentials: true,
|
||||
secretFree: true,
|
||||
checksums: {},
|
||||
});
|
||||
|
||||
it('accepts a well-formed manifest', () => {
|
||||
expect(() => validateImportManifest(good())).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects a hostile engine (would select the probe/launch binary)', () => {
|
||||
expect(() => validateImportManifest({ ...good(), engine: 'rm' as never })).toThrow(/engine/);
|
||||
});
|
||||
|
||||
it('rejects shell metacharacters in containerWorkdir', () => {
|
||||
expect(() => validateImportManifest({ ...good(), containerWorkdir: '/w; rm -rf ~' })).toThrow(/containerWorkdir/);
|
||||
expect(() => validateImportManifest({ ...good(), containerWorkdir: 'relative/path' })).toThrow(/containerWorkdir/);
|
||||
});
|
||||
|
||||
it('rejects bad image refs, case names, networks, and schema versions', () => {
|
||||
expect(() => validateImportManifest({ ...good(), image: '-bad$(x)' })).toThrow(/image/);
|
||||
expect(() => validateImportManifest({ ...good(), caseName: '../evil' })).toThrow(/caseName/);
|
||||
expect(() => validateImportManifest({ ...good(), network: 'host' })).toThrow(/network/);
|
||||
expect(() => validateImportManifest({ ...good(), schemaVersion: 99 })).toThrow(/schema version/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseLoadedImageRef', () => {
|
||||
it('parses "Loaded image ID: sha256:..."', () => {
|
||||
expect(parseLoadedImageRef('Loaded image ID: sha256:abc123def')).toBe('sha256:abc123def');
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
defaultDockerCommandForMode,
|
||||
ensureAgentBaseImage,
|
||||
hostGatewayAlias,
|
||||
persistDockerCaseClaudeSessionId,
|
||||
probeDockerCliVersion,
|
||||
readDockerCases,
|
||||
readDockerHosts,
|
||||
@@ -67,6 +68,17 @@ describe('docker-hosts storage', () => {
|
||||
expect(await readDockerHosts(dir)).toEqual([]);
|
||||
expect(await readDockerCases(dir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('persists the last Claude conversation id keyed by container name', async () => {
|
||||
await writeDockerCases(dir, [CASE, { ...CASE, name: 'other', container: 'custom-name' }]);
|
||||
await persistDockerCaseClaudeSessionId(dir, dockerContainerName(CASE.name), 'conv-1');
|
||||
await persistDockerCaseClaudeSessionId(dir, 'custom-name', 'conv-2');
|
||||
await persistDockerCaseClaudeSessionId(dir, 'no-such-container', 'conv-3'); // no-op
|
||||
const cases = await readDockerCases(dir);
|
||||
expect(cases.find((c) => c.name === 'myproj')?.lastClaudeSessionId).toBe('conv-1');
|
||||
expect(cases.find((c) => c.name === 'other')?.lastClaudeSessionId).toBe('conv-2');
|
||||
expect(cases.some((c) => c.lastClaudeSessionId === 'conv-3')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('naming / display / defaults', () => {
|
||||
@@ -427,7 +439,7 @@ describe('agentImageBuildArgs', () => {
|
||||
|
||||
describe('ensureAgentBaseImage (no-op under VITEST)', () => {
|
||||
it('reports the image as already present without spawning a build', async () => {
|
||||
const r = await ensureAgentBaseImage('docker', DEFAULT_AGENT_IMAGE);
|
||||
const r = await ensureAgentBaseImage({ engine: 'docker' }, DEFAULT_AGENT_IMAGE);
|
||||
expect(r).toEqual({ ok: true, built: false, alreadyPresent: true });
|
||||
});
|
||||
});
|
||||
@@ -442,7 +454,7 @@ describe('daemon probes (no-op under VITEST)', () => {
|
||||
|
||||
it('checkDockerTmuxAvailable + image present are canned-true', async () => {
|
||||
expect((await checkDockerTmuxAvailable({ engine: 'docker', image: DEFAULT_AGENT_IMAGE })).ok).toBe(true);
|
||||
expect(await checkDockerImagePresent('docker', DEFAULT_AGENT_IMAGE)).toBe(true);
|
||||
expect(await checkDockerImagePresent({ engine: 'docker' }, DEFAULT_AGENT_IMAGE)).toBe(true);
|
||||
});
|
||||
|
||||
it('probeDockerCliVersion is undefined under test', async () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ const MOBILE_VISIBLE_ALLOWLIST = new Set<string>([]);
|
||||
// that removes a hide rule fails loudly (not silently). The attachments button is
|
||||
// NOT here: it's opt-in (default-hidden everywhere via its own --hidden marker), so
|
||||
// it's excluded from the default-visible enumeration rather than mobile-hidden.
|
||||
const KNOWN_PHONE_HIDDEN = ['btn-settings', 'btn-lifecycle-log', 'btn-session-manager'];
|
||||
const KNOWN_PHONE_HIDDEN = ['btn-settings', 'btn-lifecycle-log', 'btn-session-manager', 'btn-file-viewer'];
|
||||
|
||||
function attrOf(openTag: string, name: string): string {
|
||||
const m = openTag.match(new RegExp(`${name}="([^"]*)"`));
|
||||
|
||||
Reference in New Issue
Block a user