Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d1a6010
feat(api-proxy): add structured logging, metrics, and request tracing
Mossaka Feb 25, 2026
61791b5
feat(api-proxy): add sliding window rate limiter with CLI integration
Mossaka Feb 25, 2026
ecd3003
ci: add API proxy unit tests to build workflow
Mossaka Feb 25, 2026
43e361c
test: add integration tests for api-proxy observability
Mossaka Feb 25, 2026
f77d987
test: extract buildRateLimitConfig and add coverage tests
Mossaka Feb 25, 2026
6191d25
test: add CI workflow for non-chroot integration tests
Mossaka Feb 25, 2026
7286fca
test: fix docker-warning tests and fragile timing dependencies
Mossaka Feb 25, 2026
391f7e2
fix: use warn+return for workDir guard in log-commands tests
Mossaka Feb 25, 2026
9ea38c1
fix: add awf-api-proxy to container cleanup
Mossaka Feb 25, 2026
74f1596
fix: copy missing JS modules in api-proxy Dockerfile
Mossaka Feb 25, 2026
2d81934
fix: fix shell quoting in api-proxy rate limit tests
Mossaka Feb 25, 2026
00b9eb2
Merge origin/main into test/fix-flaky-tests
Mossaka Feb 25, 2026
4f17835
fix: remove docker-warning from test patterns after test deletion
Mossaka Feb 25, 2026
a28373c
fix: use correct commander parse mode in default flags test
Mossaka Feb 25, 2026
ceaaa6e
Merge origin/main into test/fix-flaky-tests
Mossaka Feb 26, 2026
823de75
fix: remove duplicate rate limit options in awf-runner from auto-merge
Mossaka Feb 26, 2026
c498aaa
Merge origin/main into test/fix-flaky-tests
Mossaka Feb 26, 2026
7662287
Merge origin/main into test/fix-flaky-tests
Mossaka Feb 26, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/test-integration-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
4 changes: 2 additions & 2 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@

afterEach(() => {
// Clean up the test directory
if (fs.existsSync(testDir)) {

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found existsSync from package "fs" with non literal argument at index 0

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found existsSync from package "fs" with non literal argument at index 0

Check warning on line 51 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found existsSync from package "fs" with non literal argument at index 0
fs.rmSync(testDir, { recursive: true, force: true });
}
});

it('should parse domains from file with one domain per line', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\napi.github.com\nnpmjs.org');

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 58 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -64,7 +64,7 @@

it('should parse comma-separated domains from file', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com, api.github.com, npmjs.org');

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 67 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -73,7 +73,7 @@

it('should handle mixed formats (lines and commas)', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\napi.github.com, npmjs.org\nexample.com');

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 76 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -82,7 +82,7 @@

it('should skip empty lines', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\n\n\napi.github.com\n\nnpmjs.org');

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 85 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -91,7 +91,7 @@

it('should skip lines with only whitespace', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com\n \n\t\napi.github.com');

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 94 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -100,7 +100,7 @@

it('should skip comments starting with #', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, '# This is a comment\ngithub.com\n# Another comment\napi.github.com');

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 103 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -109,7 +109,7 @@

it('should handle inline comments (after domain)', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com # GitHub main domain\napi.github.com # API endpoint');

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 112 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -118,7 +118,7 @@

it('should handle domains with inline comments in comma-separated format', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, 'github.com, api.github.com # GitHub domains\nnpmjs.org');

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 121 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand All @@ -133,7 +133,7 @@

it('should return empty array for file with only comments and whitespace', () => {
const filePath = path.join(testDir, 'domains.txt');
fs.writeFileSync(filePath, '# Comment 1\n\n# Comment 2\n \n');

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Found writeFileSync from package "fs" with non literal argument at index 0

Check warning on line 136 in src/cli.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Found writeFileSync from package "fs" with non literal argument at index 0

const result = parseDomainsFile(filePath);

Expand Down Expand Up @@ -350,8 +350,8 @@
.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');
Expand Down
2 changes: 1 addition & 1 deletion src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
);
});
Expand Down
2 changes: 1 addition & 1 deletion src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 0 additions & 28 deletions tests/fixtures/awf-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('--');

Expand Down Expand Up @@ -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('--');

Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Cleanup {
async removeContainers(): Promise<void> {
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)
}
Expand Down
125 changes: 0 additions & 125 deletions tests/integration/docker-warning.test.ts

This file was deleted.

98 changes: 51 additions & 47 deletions tests/integration/log-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/no-docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/

/// <reference path="../jest-custom-matchers.d.ts" />
Expand Down
Loading
Loading