mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(docker): add Compose deployment support
This commit is contained in:
@@ -80,6 +80,26 @@ describe('buildDockerLaunchCommand', () => {
|
||||
expect(cmd).toContain("docker start 'codeman-case-myproj'");
|
||||
});
|
||||
|
||||
it('avoids eager create expansion, tolerates a concurrent creator, and preserves real failures in compatibility mode', () => {
|
||||
const opts = launchOpts();
|
||||
opts.createContext.disableSwapLimit = true;
|
||||
const cmd = buildDockerLaunchCommand(opts);
|
||||
// No command substitution or shell variables: either could expand eagerly
|
||||
// before the inspect side of || short-circuits in a nested launch shell.
|
||||
expect(cmd).not.toContain('$(');
|
||||
expect(cmd).not.toContain('codeman_create_output');
|
||||
expect(cmd).toContain('if docker create');
|
||||
expect(cmd).toContain("'/tmp/codeman-create-1a2b3c4d5e6f.log'");
|
||||
// If another session created the case between inspect and create, re-inspect
|
||||
// succeeds and the losing creator continues without printing the conflict.
|
||||
expect(cmd).toContain("elif docker inspect 'codeman-case-myproj' >/dev/null 2>&1; then rm -f");
|
||||
expect(cmd).toContain('Your kernel does not support swap limit capabilities');
|
||||
expect(cmd).toContain('else sed');
|
||||
expect(cmd).toContain('>&2; rm -f');
|
||||
expect(cmd).toContain('; false; fi;');
|
||||
expect(cmd).not.toContain('--memory-swap');
|
||||
});
|
||||
|
||||
it('execs a TTY into the durable in-container tmux', () => {
|
||||
const cmd = buildDockerLaunchCommand(launchOpts());
|
||||
expect(cmd).toContain("exec docker exec -it --workdir '/home/arkon/cases/myproj'");
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
resolveClaudeJsonSeedMount,
|
||||
resolveDockerClaudeArtifacts,
|
||||
resolveDockerCredentialArtifacts,
|
||||
resolveDockerDaemonMountSource,
|
||||
toSessionDocker,
|
||||
writeDockerCases,
|
||||
writeDockerHosts,
|
||||
@@ -267,6 +268,32 @@ describe('buildDockerCreateArgs', () => {
|
||||
expect(s).not.toContain('--storage-opt');
|
||||
expect(buildDockerCreateArgs(ctx()).join(' ')).not.toContain('--gpus');
|
||||
});
|
||||
|
||||
it('omits the unsupported swap limit while retaining the memory limit when disabled', () => {
|
||||
const s = buildDockerCreateArgs(ctx({ disableSwapLimit: true })).join(' ');
|
||||
expect(s).toContain('--memory 4g');
|
||||
expect(s).not.toContain('--memory-swap');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDockerDaemonMountSource', () => {
|
||||
const runtimeHome = join(tmpdir(), 'codeman-runtime-home');
|
||||
const daemonHome = join(tmpdir(), 'codeman-daemon-home');
|
||||
|
||||
it('maps paths beneath the runtime HOME into the daemon-visible HOME', () => {
|
||||
const source = join(runtimeHome, '.codeman', 'docker-seeds', 'codeman-case-test1.json');
|
||||
expect(resolveDockerDaemonMountSource(source, runtimeHome, daemonHome)).toBe(
|
||||
join(daemonHome, '.codeman', 'docker-seeds', 'codeman-case-test1.json')
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves direct-host and non-HOME sources', () => {
|
||||
const source = join(runtimeHome, '.claude', 'settings.json');
|
||||
expect(resolveDockerDaemonMountSource(source, runtimeHome)).toBe(source);
|
||||
|
||||
const outsideHome = join(tmpdir(), 'codeman-cases', 'test1');
|
||||
expect(resolveDockerDaemonMountSource(outsideHome, runtimeHome, daemonHome)).toBe(outsideHome);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDockerCredentialArtifacts (isolated codex/gemini/gcloud/opencode)', () => {
|
||||
|
||||
@@ -19,19 +19,20 @@
|
||||
* including the sweep's deleted-workspace guard.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import { mkdtemp, rm, readFile, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createMockRouteContext } from '../mocks/index.js';
|
||||
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
||||
import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||
import { registerSessionRoutes } from '../../src/web/routes/session-routes.js';
|
||||
import { generateHooksConfig, applyWorkspaceHooks } from '../../src/hooks-config.js';
|
||||
import { getDataDir } from '../../src/config/instance.js';
|
||||
import { CASES_DIR } from '../../src/web/route-helpers.js';
|
||||
import { Session } from '../../src/session.js';
|
||||
|
||||
interface HooksFile {
|
||||
hooks?: Record<string, Array<{ matcher?: string; hooks?: Array<{ command?: string }> }>>;
|
||||
@@ -223,6 +224,7 @@ describe('POST /api/sessions workspace hooks', () => {
|
||||
|
||||
describe('POST /api/quick-start workspace hooks', () => {
|
||||
let app: FastifyInstance;
|
||||
let ctx: MockRouteContext;
|
||||
|
||||
const quickStart = (payload: Record<string, unknown>) =>
|
||||
app.inject({ method: 'POST', url: '/api/quick-start', payload });
|
||||
@@ -230,15 +232,19 @@ describe('POST /api/quick-start workspace hooks', () => {
|
||||
const hooksFileIn = (dir: string) => join(dir, '.claude', 'settings.local.json');
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(Session.prototype, 'startInteractive').mockResolvedValue(undefined);
|
||||
vi.spyOn(Session.prototype, 'startShell').mockResolvedValue(undefined);
|
||||
app = Fastify({ logger: false });
|
||||
await app.register(fastifyCookie);
|
||||
registerSessionRoutes(app, createMockRouteContext());
|
||||
ctx = createMockRouteContext();
|
||||
registerSessionRoutes(app, ctx);
|
||||
installRouteErrorHandler(app);
|
||||
await app.ready();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
vi.restoreAllMocks();
|
||||
// Docker fixtures + case dirs must not leak into the next test.
|
||||
await rm(join(getDataDir(), 'docker-hosts.json'), { force: true });
|
||||
await rm(join(getDataDir(), 'docker-cases.json'), { force: true });
|
||||
@@ -260,7 +266,7 @@ describe('POST /api/quick-start workspace hooks', () => {
|
||||
});
|
||||
|
||||
/** Minimal docker host + case fixtures (docker IO is no-op'd under vitest). */
|
||||
const writeDockerFixtures = async (caseName: string, hostWorkspacePath: string) => {
|
||||
const writeDockerFixtures = async (caseName: string, hostWorkspacePath: string, lastClaudeSessionId?: string) => {
|
||||
await mkdir(getDataDir(), { recursive: true });
|
||||
await writeFile(
|
||||
join(getDataDir(), 'docker-hosts.json'),
|
||||
@@ -268,7 +274,7 @@ describe('POST /api/quick-start workspace hooks', () => {
|
||||
);
|
||||
await writeFile(
|
||||
join(getDataDir(), 'docker-cases.json'),
|
||||
JSON.stringify([{ name: caseName, type: 'docker', hostId: 'd1', hostWorkspacePath }])
|
||||
JSON.stringify([{ name: caseName, type: 'docker', hostId: 'd1', hostWorkspacePath, lastClaudeSessionId }])
|
||||
);
|
||||
};
|
||||
|
||||
@@ -300,6 +306,35 @@ describe('POST /api/quick-start workspace hooks', () => {
|
||||
await rm(ws, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each(['codex', 'gemini'] as const)('does not pass a saved Claude conversation id to Docker %s', async (mode) => {
|
||||
const ws = await mkdtemp(join(tmpdir(), `codeman-docker-${mode}-`));
|
||||
try {
|
||||
await writeDockerFixtures('dockexternal', ws, 'e83a9063-3cb4-44d2-a9a0-df153b81721f');
|
||||
|
||||
const res = await quickStart({ caseName: 'dockexternal', mode });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const session = ctx.sessions.get(JSON.parse(res.body).sessionId);
|
||||
expect(session?.toState().resumeSessionId).toBeUndefined();
|
||||
} finally {
|
||||
await rm(ws, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('passes a saved Claude conversation id only to Docker Claude', async () => {
|
||||
const ws = await mkdtemp(join(tmpdir(), 'codeman-docker-resume-'));
|
||||
const resumeId = 'e83a9063-3cb4-44d2-a9a0-df153b81721f';
|
||||
try {
|
||||
await writeDockerFixtures('dockresume', ws, resumeId);
|
||||
|
||||
const res = await quickStart({ caseName: 'dockresume', mode: 'claude' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const session = ctx.sessions.get(JSON.parse(res.body).sessionId);
|
||||
expect(session?.toState().resumeSessionId).toBe(resumeId);
|
||||
} finally {
|
||||
await rm(ws, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyWorkspaceHooks (the shared decision core in hooks-config)', () => {
|
||||
|
||||
Reference in New Issue
Block a user