diff --git a/.github/workflows/test-integration-suite.yml b/.github/workflows/test-integration-suite.yml index d345f797..a1eac262 100644 --- a/.github/workflows/test-integration-suite.yml +++ b/.github/workflows/test-integration-suite.yml @@ -161,7 +161,7 @@ jobs: run: | echo "=== Running container & ops tests ===" npm run test:integration -- \ - --testPathPatterns="(container-workdir|docker-warning|environment-variables|error-handling|exit-code-propagation|log-commands|no-docker|volume-mounts)" \ + --testPathPatterns="(container-workdir|environment-variables|error-handling|exit-code-propagation|log-commands|no-docker|volume-mounts)" \ --verbose env: JEST_TIMEOUT: 180000 diff --git a/scripts/ci/cleanup.sh b/scripts/ci/cleanup.sh index ae59652e..690a4119 100755 --- a/scripts/ci/cleanup.sh +++ b/scripts/ci/cleanup.sh @@ -12,7 +12,7 @@ echo "===========================================" # First, explicitly remove containers by name (handles orphaned containers) echo "Removing awf containers by name..." -docker rm -f awf-squid awf-agent 2>/dev/null || true +docker rm -f awf-squid awf-agent awf-api-proxy 2>/dev/null || true # Cleanup diagnostic test containers echo "Stopping docker compose services..." diff --git a/src/cli.test.ts b/src/cli.test.ts index d8790945..6420ed37 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -350,8 +350,8 @@ describe('cli', () => { .option('--build-local', 'Build locally', false) .option('--env-all', 'Pass all env vars', false); - // Parse empty args to get defaults - program.parse([], { from: 'user' }); + // Parse empty args to get defaults (from: 'node' treats argv[0] as node, argv[1] as script) + program.parse(['node', 'awf'], { from: 'node' }); const opts = program.opts(); expect(opts.logLevel).toBe('info'); diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 7858d24c..a431c66c 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2106,7 +2106,7 @@ describe('docker-manager', () => { expect(mockExecaFn).toHaveBeenCalledWith( 'docker', - ['rm', '-f', 'awf-squid', 'awf-agent'], + ['rm', '-f', 'awf-squid', 'awf-agent', 'awf-api-proxy'], { reject: false } ); }); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 8f7c7235..aff4bad9 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1347,7 +1347,7 @@ export async function startContainers(workDir: string, allowedDomains: string[], // This handles orphaned containers from failed/interrupted previous runs logger.debug('Removing any existing containers with conflicting names...'); try { - await execa('docker', ['rm', '-f', 'awf-squid', 'awf-agent'], { + await execa('docker', ['rm', '-f', 'awf-squid', 'awf-agent', 'awf-api-proxy'], { reject: false, }); } catch { diff --git a/tests/fixtures/awf-runner.ts b/tests/fixtures/awf-runner.ts index aee732a0..a3b5cfeb 100644 --- a/tests/fixtures/awf-runner.ts +++ b/tests/fixtures/awf-runner.ts @@ -142,20 +142,6 @@ export class AwfRunner { } } - // Add rate limit flags - if (options.rateLimitRpm !== undefined) { - args.push('--rate-limit-rpm', String(options.rateLimitRpm)); - } - if (options.rateLimitRph !== undefined) { - args.push('--rate-limit-rph', String(options.rateLimitRph)); - } - if (options.rateLimitBytesPm !== undefined) { - args.push('--rate-limit-bytes-pm', String(options.rateLimitBytesPm)); - } - if (options.noRateLimit) { - args.push('--no-rate-limit'); - } - // Add -- separator before command args.push('--'); @@ -336,20 +322,6 @@ export class AwfRunner { } } - // Add rate limit flags - if (options.rateLimitRpm !== undefined) { - args.push('--rate-limit-rpm', String(options.rateLimitRpm)); - } - if (options.rateLimitRph !== undefined) { - args.push('--rate-limit-rph', String(options.rateLimitRph)); - } - if (options.rateLimitBytesPm !== undefined) { - args.push('--rate-limit-bytes-pm', String(options.rateLimitBytesPm)); - } - if (options.noRateLimit) { - args.push('--no-rate-limit'); - } - // Add -- separator before command args.push('--'); diff --git a/tests/fixtures/cleanup.ts b/tests/fixtures/cleanup.ts index ab4cb86e..fbfa5714 100644 --- a/tests/fixtures/cleanup.ts +++ b/tests/fixtures/cleanup.ts @@ -25,7 +25,7 @@ export class Cleanup { async removeContainers(): Promise { this.log('Removing awf containers by name...'); try { - await execa('docker', ['rm', '-f', 'awf-squid', 'awf-agent']); + await execa('docker', ['rm', '-f', 'awf-squid', 'awf-agent', 'awf-api-proxy']); } catch (error) { // Ignore errors (containers may not exist) } diff --git a/tests/integration/docker-warning.test.ts b/tests/integration/docker-warning.test.ts deleted file mode 100644 index 362fbffb..00000000 --- a/tests/integration/docker-warning.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Docker Command Warning Tests - * - * These tests verify that the Docker stub script shows helpful error messages - * when users attempt to run Docker commands inside AWF. - * Docker-in-Docker support was removed in v0.9.1. - * - * NOTE: These tests are currently skipped due to a pre-existing Docker build issue - * (Node.js installation from NodeSource is not working correctly in local builds). - * The implementation is correct and tests will be enabled once the build issue is fixed. - * - * To enable these tests: - * 1. Fix the Node.js installation in containers/agent/Dockerfile - * 2. Change describe.skip to describe - * 3. Set buildLocal: true in test options - */ - -/// - -import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; -import { createRunner, AwfRunner } from '../fixtures/awf-runner'; -import { cleanup } from '../fixtures/cleanup'; - -describe.skip('Docker Command Warning', () => { - let runner: AwfRunner; - - beforeAll(async () => { - // Run cleanup before tests to ensure clean state - await cleanup(false); - - runner = createRunner(); - }); - - afterAll(async () => { - // Clean up after all tests - await cleanup(false); - }); - - test('Test 1: docker run command shows warning', async () => { - const result = await runner.runWithSudo( - 'docker run alpine echo hello', - { - allowDomains: ['github.com'], - logLevel: 'debug', - timeout: 30000, - buildLocal: true, // Use local build with our stub script - } - ); - - // Should fail (exit code may be 127 or 1 depending on how the command is invoked) - expect(result).toFail(); - expect(result.exitCode).not.toBe(0); - - // Should contain error message about Docker-in-Docker removal - expect(result.stderr).toContain('Docker-in-Docker support was removed in AWF v0.9.1'); - expect(result.stderr).toContain('Docker commands are no longer available'); - expect(result.stderr).toContain('PR #205'); - }, 120000); - - test('Test 2: docker-compose command shows warning (docker-compose uses docker)', async () => { - const result = await runner.runWithSudo( - 'docker-compose up', - { - allowDomains: ['github.com'], - logLevel: 'debug', - timeout: 30000, - buildLocal: true, // Use local build with our stub script - } - ); - - // Should fail because docker-compose is not installed - // But if someone tries 'docker' explicitly, they'll see the warning - expect(result).toFail(); - }, 120000); - - test('Test 3: which docker shows docker stub exists', async () => { - const result = await runner.runWithSudo( - 'which docker', - { - allowDomains: ['github.com'], - logLevel: 'debug', - timeout: 30000, - buildLocal: true, // Use local build with our stub script - } - ); - - // Should succeed and show /usr/bin/docker exists - expect(result).toSucceed(); - expect(result.stdout).toContain('/usr/bin/docker'); - }, 120000); - - test('Test 4: docker --help shows warning', async () => { - const result = await runner.runWithSudo( - 'docker --help', - { - allowDomains: ['github.com'], - logLevel: 'debug', - timeout: 30000, - buildLocal: true, // Use local build with our stub script - } - ); - - // The command may succeed or fail depending on how the shell handles the exit code - // But the warning message should always be present in stderr - expect(result.stderr).toContain('Docker-in-Docker support was removed in AWF v0.9.1'); - expect(result.stderr).toContain('https://github.com/github/gh-aw-firewall#breaking-changes'); - }, 120000); - - test('Test 5: docker version shows warning', async () => { - const result = await runner.runWithSudo( - 'docker version', - { - allowDomains: ['github.com'], - logLevel: 'debug', - timeout: 30000, - buildLocal: true, // Use local build with our stub script - } - ); - - // Should fail with helpful error - expect(result).toFail(); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('ERROR: Docker-in-Docker support was removed'); - }, 120000); -}); diff --git a/tests/integration/log-commands.test.ts b/tests/integration/log-commands.test.ts index a39fd110..e3641c66 100644 --- a/tests/integration/log-commands.test.ts +++ b/tests/integration/log-commands.test.ts @@ -43,18 +43,21 @@ describe('Log Commands', () => { expect(result).toSucceed(); // Check that logs were created - if (result.workDir) { - const squidLogPath = path.join(result.workDir, 'squid-logs', 'access.log'); + // workDir extraction depends on parsing stderr logs, which may not always work + // (e.g., sudo may buffer/redirect stderr differently in CI) + if (!result.workDir) { + console.warn('WARN: workDir not extracted from stderr — skipping log file assertions'); + return; + } + const squidLogPath = path.join(result.workDir, 'squid-logs', 'access.log'); - // Logs may not be immediately available due to buffering - // Wait a moment for logs to be flushed - await new Promise(resolve => setTimeout(resolve, 1000)); + // Logs may not be immediately available due to buffering + // Wait a moment for logs to be flushed + await new Promise(resolve => setTimeout(resolve, 1000)); - if (fs.existsSync(squidLogPath)) { - const logContent = fs.readFileSync(squidLogPath, 'utf-8'); - expect(logContent.length).toBeGreaterThan(0); - } - } + expect(fs.existsSync(squidLogPath)).toBe(true); + const logContent = fs.readFileSync(squidLogPath, 'utf-8'); + expect(logContent.length).toBeGreaterThan(0); // Cleanup after test await cleanup(false); @@ -72,27 +75,27 @@ describe('Log Commands', () => { ); // First curl should succeed, second should fail - if (result.workDir) { - const squidLogPath = path.join(result.workDir, 'squid-logs', 'access.log'); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - if (fs.existsSync(squidLogPath)) { - const logContent = fs.readFileSync(squidLogPath, 'utf-8'); - const parser = createLogParser(); - const entries = parser.parseSquidLog(logContent); - - // Should have at least one entry - if (entries.length > 0) { - // Verify entry structure - const entry = entries[0]; - expect(entry).toHaveProperty('timestamp'); - expect(entry).toHaveProperty('host'); - expect(entry).toHaveProperty('statusCode'); - expect(entry).toHaveProperty('decision'); - } - } + if (!result.workDir) { + console.warn('WARN: workDir not extracted from stderr — skipping log file assertions'); + return; } + const squidLogPath2 = path.join(result.workDir, 'squid-logs', 'access.log'); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + expect(fs.existsSync(squidLogPath2)).toBe(true); + const logContent = fs.readFileSync(squidLogPath2, 'utf-8'); + const parser = createLogParser(); + const entries = parser.parseSquidLog(logContent); + + // Should have at least one entry + expect(entries.length).toBeGreaterThan(0); + // Verify entry structure + const entry = entries[0]; + expect(entry).toHaveProperty('timestamp'); + expect(entry).toHaveProperty('host'); + expect(entry).toHaveProperty('statusCode'); + expect(entry).toHaveProperty('decision'); await cleanup(false); }, 120000); @@ -108,27 +111,28 @@ describe('Log Commands', () => { } ); - if (result.workDir) { - const squidLogPath = path.join(result.workDir, 'squid-logs', 'access.log'); + if (!result.workDir) { + console.warn('WARN: workDir not extracted from stderr — skipping log file assertions'); + return; + } + const squidLogPath3 = path.join(result.workDir, 'squid-logs', 'access.log'); + + await new Promise(resolve => setTimeout(resolve, 1000)); - await new Promise(resolve => setTimeout(resolve, 1000)); + expect(fs.existsSync(squidLogPath3)).toBe(true); + const logContent3 = fs.readFileSync(squidLogPath3, 'utf-8'); + const parser3 = createLogParser(); + const entries3 = parser3.parseSquidLog(logContent3); - if (fs.existsSync(squidLogPath)) { - const logContent = fs.readFileSync(squidLogPath, 'utf-8'); - const parser = createLogParser(); - const entries = parser.parseSquidLog(logContent); + // Should have at least one entry + expect(entries3.length).toBeGreaterThan(0); - // Filter by decision - const allowed = parser.filterByDecision(entries, 'allowed'); - const blocked = parser.filterByDecision(entries, 'blocked'); + // Filter by decision + const allowed = parser3.filterByDecision(entries3, 'allowed'); + const blocked = parser3.filterByDecision(entries3, 'blocked'); - // We should have at least one allowed (github.com) and one blocked (example.com) - // Note: Log parsing depends on timing and buffering - if (entries.length > 0) { - expect(allowed.length + blocked.length).toBeGreaterThanOrEqual(1); - } - } - } + // We should have at least one allowed (github.com) and one blocked (example.com) + expect(allowed.length + blocked.length).toBeGreaterThanOrEqual(1); await cleanup(false); }, 180000); diff --git a/tests/integration/no-docker.test.ts b/tests/integration/no-docker.test.ts index c5afd10d..01f1f452 100644 --- a/tests/integration/no-docker.test.ts +++ b/tests/integration/no-docker.test.ts @@ -15,6 +15,10 @@ * * Known Issue: Building locally may fail due to NodeSource repository issues. * If tests fail with "docker found" errors, the images need to be rebuilt and published. + * + * NOTE: docker-warning.test.ts was removed as redundant — the Docker stub-script + * approach was superseded by removing docker-cli entirely. This file covers the + * Docker removal behavior (command not found, no socket, graceful failure). */ /// diff --git a/tests/integration/token-unset.test.ts b/tests/integration/token-unset.test.ts index 66700b32..bea9d410 100644 --- a/tests/integration/token-unset.test.ts +++ b/tests/integration/token-unset.test.ts @@ -26,17 +26,21 @@ describe('Token Unsetting from Entrypoint Environ', () => { test('should unset GITHUB_TOKEN from /proc/1/environ after agent starts', async () => { const testToken = 'ghp_test_token_12345678901234567890'; - // Command that checks /proc/1/environ after sleeping to allow token unsetting + // Command that polls /proc/1/environ until token is cleared (retry loop) const command = ` - # Wait for entrypoint to unset tokens (5 second delay + 2 second buffer) - sleep 7 - - # Check if GITHUB_TOKEN is still in /proc/1/environ + # Poll /proc/1/environ until GITHUB_TOKEN is cleared (up to 15 seconds) + for i in $(seq 1 15); do + if ! cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN="; then + echo "SUCCESS: GITHUB_TOKEN cleared from /proc/1/environ" + break + fi + sleep 1 + done + + # Final check - fail if still present after retries if cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN="; then - echo "ERROR: GITHUB_TOKEN still in /proc/1/environ" + echo "ERROR: GITHUB_TOKEN still in /proc/1/environ after 15 seconds" exit 1 - else - echo "SUCCESS: GITHUB_TOKEN cleared from /proc/1/environ" fi # Verify agent can still read the token (cached by one-shot-token library) @@ -66,13 +70,18 @@ describe('Token Unsetting from Entrypoint Environ', () => { const testToken = 'sk-test_openai_key_1234567890'; const command = ` - sleep 7 + # Poll /proc/1/environ until OPENAI_API_KEY is cleared (up to 15 seconds) + for i in $(seq 1 15); do + if ! cat /proc/1/environ | tr "\\0" "\\n" | grep -q "OPENAI_API_KEY="; then + echo "SUCCESS: OPENAI_API_KEY cleared from /proc/1/environ" + break + fi + sleep 1 + done if cat /proc/1/environ | tr "\\0" "\\n" | grep -q "OPENAI_API_KEY="; then - echo "ERROR: OPENAI_API_KEY still in /proc/1/environ" + echo "ERROR: OPENAI_API_KEY still in /proc/1/environ after 15 seconds" exit 1 - else - echo "SUCCESS: OPENAI_API_KEY cleared from /proc/1/environ" fi if [ -n "$OPENAI_API_KEY" ]; then @@ -101,13 +110,18 @@ describe('Token Unsetting from Entrypoint Environ', () => { const testToken = 'sk-ant-test_key_1234567890'; const command = ` - sleep 7 + # Poll /proc/1/environ until ANTHROPIC_API_KEY is cleared (up to 15 seconds) + for i in $(seq 1 15); do + if ! cat /proc/1/environ | tr "\\0" "\\n" | grep -q "ANTHROPIC_API_KEY="; then + echo "SUCCESS: ANTHROPIC_API_KEY cleared from /proc/1/environ" + break + fi + sleep 1 + done if cat /proc/1/environ | tr "\\0" "\\n" | grep -q "ANTHROPIC_API_KEY="; then - echo "ERROR: ANTHROPIC_API_KEY still in /proc/1/environ" + echo "ERROR: ANTHROPIC_API_KEY still in /proc/1/environ after 15 seconds" exit 1 - else - echo "SUCCESS: ANTHROPIC_API_KEY cleared from /proc/1/environ" fi if [ -n "$ANTHROPIC_API_KEY" ]; then @@ -134,9 +148,19 @@ describe('Token Unsetting from Entrypoint Environ', () => { test('should unset multiple tokens simultaneously', async () => { const command = ` - sleep 7 - - # Check all three tokens + # Poll /proc/1/environ until all tokens are cleared (up to 15 seconds) + for i in $(seq 1 15); do + TOKENS_FOUND=0 + cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN=" && TOKENS_FOUND=$((TOKENS_FOUND + 1)) + cat /proc/1/environ | tr "\\0" "\\n" | grep -q "OPENAI_API_KEY=" && TOKENS_FOUND=$((TOKENS_FOUND + 1)) + cat /proc/1/environ | tr "\\0" "\\n" | grep -q "ANTHROPIC_API_KEY=" && TOKENS_FOUND=$((TOKENS_FOUND + 1)) + if [ $TOKENS_FOUND -eq 0 ]; then + break + fi + sleep 1 + done + + # Final check - fail if any still present TOKENS_FOUND=0 if cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN="; then @@ -187,10 +211,16 @@ describe('Token Unsetting from Entrypoint Environ', () => { test('should work in non-chroot mode', async () => { const command = ` - sleep 7 + # Poll /proc/1/environ until GITHUB_TOKEN is cleared (up to 15 seconds) + for i in $(seq 1 15); do + if ! cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN="; then + break + fi + sleep 1 + done if cat /proc/1/environ | tr "\\0" "\\n" | grep -q "GITHUB_TOKEN="; then - echo "ERROR: GITHUB_TOKEN still in /proc/1/environ" + echo "ERROR: GITHUB_TOKEN still in /proc/1/environ after 15 seconds" exit 1 else echo "SUCCESS: GITHUB_TOKEN cleared from /proc/1/environ in non-chroot mode"