Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/commands/validators/config-assembly-api-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
buildRateLimitConfig,
callAssembleWith,
logger,
mockBuildConfigOnce,
setupConfigAssemblyTestSuite,
validateRateLimitFlags,
} from './config-assembly.test-utils';

describe('config-assembly', () => {
setupConfigAssemblyTestSuite();

describe('rate limit validation', () => {
it('should exit if rate limit config build fails', () => {
mockBuildConfigOnce({
enableApiProxy: true,
});

(buildRateLimitConfig as jest.Mock).mockReturnValueOnce({
error: 'Invalid rate limit configuration',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Invalid rate limit configuration'),
);
});

it('should exit if rate limit flags are used without --enable-api-proxy', () => {
(validateRateLimitFlags as jest.Mock).mockReturnValueOnce({
valid: false,
error: 'Rate limit flags require --enable-api-proxy',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
'Rate limit flags require --enable-api-proxy',
);
});

it('should set rate limit config when API proxy is enabled', () => {
mockBuildConfigOnce({
enableApiProxy: true,
});

const mockRateLimitConfig = {
enabled: true,
rpm: 100,
rph: 1000,
bytesPm: 10000,
};

(buildRateLimitConfig as jest.Mock).mockReturnValueOnce({
config: mockRateLimitConfig,
});

const result = callAssembleWith();

expect(result.rateLimitConfig).toEqual(mockRateLimitConfig);
expect(logger.debug).toHaveBeenCalledWith(
expect.stringContaining('Rate limiting: enabled=true'),
);
});
});

describe('API proxy configuration', () => {
it('should log API proxy status when enabled', () => {
mockBuildConfigOnce({
enableApiProxy: true,
openaiApiKey: 'sk-test',
anthropicApiKey: 'test-key',
});

(buildRateLimitConfig as jest.Mock).mockReturnValueOnce({
config: { enabled: false },
});

callAssembleWith();

expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('API proxy enabled: OpenAI=true, Anthropic=true'),
);
});
});
});
168 changes: 168 additions & 0 deletions src/commands/validators/config-assembly-docker-host.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
callAssembleWith,
getMockExit,
logger,
mockBuildConfigOnce,
setupConfigAssemblyTestSuite,
} from './config-assembly.test-utils';

describe('config-assembly', () => {
setupConfigAssemblyTestSuite();

describe('docker-host validation', () => {
it('should reject non-loopback tcp:// docker host URIs', () => {
mockBuildConfigOnce({
awfDockerHost: 'tcp://192.168.1.100:2375',
dockerHostPathPrefix: undefined,
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('--docker-host must be a unix:// socket URI or a loopback TCP URI'),
);
});

it('should accept loopback tcp:// docker host URIs (ARC/DinD)', () => {
mockBuildConfigOnce({
awfDockerHost: 'tcp://localhost:2375',
dockerHostPathPrefix: undefined,
});

const result = callAssembleWith();

expect(result).toBeDefined();
expect(getMockExit()).not.toHaveBeenCalled();
});

it('should accept tcp://127.0.0.1 docker host URIs (ARC/DinD)', () => {
mockBuildConfigOnce({
awfDockerHost: 'tcp://127.0.0.1:2375',
dockerHostPathPrefix: undefined,
});

const result = callAssembleWith();

expect(result).toBeDefined();
expect(getMockExit()).not.toHaveBeenCalled();
});

it('should accept unix:// docker host URIs', () => {
mockBuildConfigOnce({
awfDockerHost: 'unix:///var/run/docker.sock',
dockerHostPathPrefix: undefined,
});

const result = callAssembleWith();

expect(result).toBeDefined();
expect(getMockExit()).not.toHaveBeenCalled();
});

it('should reject relative docker-host-path-prefix', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: 'relative/path',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('--docker-host-path-prefix must be an absolute path'),
);
});

it('should accept absolute docker-host-path-prefix', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: '/host',
});

const result = callAssembleWith();

expect(result).toBeDefined();
expect(getMockExit()).not.toHaveBeenCalled();
});

it('should reject relative chroot binaries source path', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: undefined,
chrootBinariesSourcePath: 'relative/path',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('chroot.binariesSourcePath must be an absolute path'),
);
});

it('should accept absolute chroot binaries source path', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: undefined,
chrootBinariesSourcePath: '/tmp/gh-aw/runner-bin',
});

const result = callAssembleWith();

expect(result).toBeDefined();
expect(getMockExit()).not.toHaveBeenCalled();
});

it('should reject chroot binaries source path set to root', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: undefined,
chrootBinariesSourcePath: '/',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('chroot.binariesSourcePath cannot be "/"'),
);
});

it('should reject chroot binaries source path containing a colon', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: undefined,
chrootBinariesSourcePath: '/tmp/bin:/extra',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('chroot.binariesSourcePath must not contain ":" or newline characters'),
);
});

it('should reject chroot binaries source path containing a newline', () => {
mockBuildConfigOnce({
awfDockerHost: undefined,
dockerHostPathPrefix: undefined,
chrootBinariesSourcePath: '/tmp/bin\n/extra',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('chroot.binariesSourcePath must not contain ":" or newline characters'),
);
});
});
});
87 changes: 87 additions & 0 deletions src/commands/validators/config-assembly-flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
callAssembleWith,
getMockExit,
logger,
mockBuildConfigOnce,
setupConfigAssemblyTestSuite,
validateEnableTokenSteeringFlag,
validateSkipPullWithBuildLocal,
} from './config-assembly.test-utils';

describe('config-assembly', () => {
setupConfigAssemblyTestSuite();

describe('feature flag validation', () => {
it('should exit if --enable-token-steering is used without --enable-api-proxy', () => {
(validateEnableTokenSteeringFlag as jest.Mock).mockReturnValueOnce({
valid: false,
error: '--enable-token-steering requires --enable-api-proxy',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
'--enable-token-steering requires --enable-api-proxy',
);
});
});

describe('environment variable warnings', () => {
it('should warn when --env-all is used', () => {
mockBuildConfigOnce({
envAll: true,
});

callAssembleWith();

expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('Using --env-all'),
);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining('may expose sensitive credentials'),
);
});

it('should log debug message when --env-file is used', () => {
mockBuildConfigOnce({
envFile: '/tmp/test.env',
});

callAssembleWith();

expect(logger.debug).toHaveBeenCalledWith(
expect.stringContaining('Loading environment variables from file'),
);
});
});

describe('skip-pull validation', () => {
it('should exit if --skip-pull is used with --build-local', () => {
(validateSkipPullWithBuildLocal as jest.Mock).mockReturnValueOnce({
valid: false,
error: '--skip-pull and --build-local are incompatible',
});

expect(() => {
callAssembleWith();
}).toThrow('process.exit(1)');

expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('--skip-pull and --build-local are incompatible'),
);
});
});

describe('successful config assembly', () => {
it('should return assembled config when all validations pass', () => {
const config = callAssembleWith();

expect(config).toBeDefined();
expect(config.agentCommand).toBe('echo test');
expect(config.logLevel).toBe('info');
expect(getMockExit()).not.toHaveBeenCalled();
});
});
});
Loading
Loading