mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
9486d56b01
- Move all v1 files (index, router, container-runner, db, ipc, types, logger, channels/registry, and all utilities) to src/v1/ as a fully self-contained archive with no shared dependencies - Rename v2 files to remove -v2 suffix (index-v2.ts → index.ts, etc.) - Update all imports across v2 source, tests, and setup files - Migrate shared utilities (config, env, container-runtime, mount-security, timezone, group-folder) from pino logger to v2 log module - Migrate setup/ files from logger to log with argument order swap - Container agent-runner: move v1 entry to v1/, rename v2 to index.ts - Update setup skill to offer all 13 v2 channels - Install all Chat SDK adapter packages - dist/index.js now runs v2; dist/v1/index.js runs v1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock logger
|
|
vi.mock('./logger.js', () => ({
|
|
logger: {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock child_process — store the mock fn so tests can configure it
|
|
const mockExecSync = vi.fn();
|
|
vi.mock('child_process', () => ({
|
|
execSync: (...args: unknown[]) => mockExecSync(...args),
|
|
}));
|
|
|
|
import {
|
|
CONTAINER_RUNTIME_BIN,
|
|
readonlyMountArgs,
|
|
stopContainer,
|
|
ensureContainerRuntimeRunning,
|
|
cleanupOrphans,
|
|
} from './container-runtime.js';
|
|
import { logger } from './logger.js';
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
// --- Pure functions ---
|
|
|
|
describe('readonlyMountArgs', () => {
|
|
it('returns -v flag with :ro suffix', () => {
|
|
const args = readonlyMountArgs('/host/path', '/container/path');
|
|
expect(args).toEqual(['-v', '/host/path:/container/path:ro']);
|
|
});
|
|
});
|
|
|
|
describe('stopContainer', () => {
|
|
it('calls docker stop for valid container names', () => {
|
|
stopContainer('nanoclaw-test-123');
|
|
expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-test-123`, {
|
|
stdio: 'pipe',
|
|
});
|
|
});
|
|
|
|
it('rejects names with shell metacharacters', () => {
|
|
expect(() => stopContainer('foo; rm -rf /')).toThrow('Invalid container name');
|
|
expect(() => stopContainer('foo$(whoami)')).toThrow('Invalid container name');
|
|
expect(() => stopContainer('foo`id`')).toThrow('Invalid container name');
|
|
expect(mockExecSync).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// --- ensureContainerRuntimeRunning ---
|
|
|
|
describe('ensureContainerRuntimeRunning', () => {
|
|
it('does nothing when runtime is already running', () => {
|
|
mockExecSync.mockReturnValueOnce('');
|
|
|
|
ensureContainerRuntimeRunning();
|
|
|
|
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
expect(mockExecSync).toHaveBeenCalledWith(`${CONTAINER_RUNTIME_BIN} info`, {
|
|
stdio: 'pipe',
|
|
timeout: 10000,
|
|
});
|
|
expect(logger.debug).toHaveBeenCalledWith('Container runtime already running');
|
|
});
|
|
|
|
it('throws when docker info fails', () => {
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw new Error('Cannot connect to the Docker daemon');
|
|
});
|
|
|
|
expect(() => ensureContainerRuntimeRunning()).toThrow('Container runtime is required but failed to start');
|
|
expect(logger.error).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// --- cleanupOrphans ---
|
|
|
|
describe('cleanupOrphans', () => {
|
|
it('stops orphaned nanoclaw containers', () => {
|
|
// docker ps returns container names, one per line
|
|
mockExecSync.mockReturnValueOnce('nanoclaw-group1-111\nnanoclaw-group2-222\n');
|
|
// stop calls succeed
|
|
mockExecSync.mockReturnValue('');
|
|
|
|
cleanupOrphans();
|
|
|
|
// ps + 2 stop calls
|
|
expect(mockExecSync).toHaveBeenCalledTimes(3);
|
|
expect(mockExecSync).toHaveBeenNthCalledWith(2, `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group1-111`, {
|
|
stdio: 'pipe',
|
|
});
|
|
expect(mockExecSync).toHaveBeenNthCalledWith(3, `${CONTAINER_RUNTIME_BIN} stop -t 1 nanoclaw-group2-222`, {
|
|
stdio: 'pipe',
|
|
});
|
|
expect(logger.info).toHaveBeenCalledWith(
|
|
{ count: 2, names: ['nanoclaw-group1-111', 'nanoclaw-group2-222'] },
|
|
'Stopped orphaned containers',
|
|
);
|
|
});
|
|
|
|
it('does nothing when no orphans exist', () => {
|
|
mockExecSync.mockReturnValueOnce('');
|
|
|
|
cleanupOrphans();
|
|
|
|
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
expect(logger.info).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns and continues when ps fails', () => {
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw new Error('docker not available');
|
|
});
|
|
|
|
cleanupOrphans(); // should not throw
|
|
|
|
expect(logger.warn).toHaveBeenCalledWith(
|
|
expect.objectContaining({ err: expect.any(Error) }),
|
|
'Failed to clean up orphaned containers',
|
|
);
|
|
});
|
|
|
|
it('continues stopping remaining containers when one stop fails', () => {
|
|
mockExecSync.mockReturnValueOnce('nanoclaw-a-1\nnanoclaw-b-2\n');
|
|
// First stop fails
|
|
mockExecSync.mockImplementationOnce(() => {
|
|
throw new Error('already stopped');
|
|
});
|
|
// Second stop succeeds
|
|
mockExecSync.mockReturnValueOnce('');
|
|
|
|
cleanupOrphans(); // should not throw
|
|
|
|
expect(mockExecSync).toHaveBeenCalledTimes(3);
|
|
expect(logger.info).toHaveBeenCalledWith(
|
|
{ count: 2, names: ['nanoclaw-a-1', 'nanoclaw-b-2'] },
|
|
'Stopped orphaned containers',
|
|
);
|
|
});
|
|
});
|