Skip to content
This repository was archived by the owner on Feb 1, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { TaskQueue } from '../../../src/queue/task-queue.js';
import type { CapturePagePayload } from '../../../src/queue/types.js';
import { closeDatabase, createDatabase } from '../../../src/storage/database.js';

interface CountResult {
count: number;
}

describe('Queue Concurrency', () => {
let testDir: string;
let dbPath: string;
Expand Down Expand Up @@ -253,10 +257,14 @@ describe('Queue Concurrency', () => {
await Promise.all(updatePromises);

const completed = (
db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('completed') as any
db
.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?')
.get('completed') as CountResult
).count;
const failed = (
db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('failed') as any
db
.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?')
.get('failed') as CountResult
).count;
expect(completed + failed).toBe(10);
});
Expand Down Expand Up @@ -351,10 +359,10 @@ describe('Queue Concurrency', () => {

const pendingTasks = db
.prepare('SELECT * FROM tasks WHERE status = ?')
.all('pending') as any[];
.all('pending') as CountResult[];
const processingTasks = db
.prepare('SELECT * FROM tasks WHERE status = ?')
.all('processing') as any[];
.all('processing') as CountResult[];

expect(pendingTasks.length + processingTasks.length).toBe(100);
});
Expand Down Expand Up @@ -405,7 +413,8 @@ describe('Queue Concurrency', () => {
config: {},
};

const initialCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count;
const initialCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult)
.count;

try {
db.transaction(() => {
Expand All @@ -417,7 +426,8 @@ describe('Queue Concurrency', () => {
// Expected error
}

const finalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count;
const finalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult)
.count;
expect(finalCount).toBe(initialCount);
});
});
Expand Down Expand Up @@ -507,7 +517,8 @@ describe('Queue Concurrency', () => {

await Promise.all(operations);

const totalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as any).count;
const totalCount = (db.prepare('SELECT COUNT(*) as count FROM tasks').get() as CountResult)
.count;
expect(totalCount).toBe(50);
});

Expand All @@ -532,10 +543,14 @@ describe('Queue Concurrency', () => {
await Promise.all(updatePromises);

const completedCount = (
db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('completed') as any
db
.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?')
.get('completed') as CountResult
).count;
const pendingCount = (
db.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?').get('pending') as any
db
.prepare('SELECT COUNT(*) as count FROM tasks WHERE status = ?')
.get('pending') as CountResult
).count;

expect(completedCount).toBe(10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,11 @@ describe('Storage Concurrency', () => {
isBaseline: false,
});

const statuses = ['processing', 'completed', 'failed', 'processing', 'completed'];
const statuses = ['in_progress', 'completed', 'interrupted', 'in_progress', 'completed'];
const updatePromises = statuses.map((status) =>
Promise.resolve(
runRepo.update(run.id, {
status: status as any,
status: status as 'new' | 'in_progress' | 'interrupted' | 'completed',
}),
),
);
Expand All @@ -253,7 +253,7 @@ describe('Storage Concurrency', () => {

const updated = runRepo.findById(run.id);
expect(updated).not.toBeNull();
expect(['processing', 'completed', 'failed']).toContain(updated?.status);
expect(['new', 'in_progress', 'interrupted', 'completed']).toContain(updated?.status);
});

// FIXME: Test needs refactoring for Drizzle async methods and transaction API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ describe('Advanced Path Traversal Security', () => {
url: '/api/v1/artifacts/../../../etc/passwd/screenshot',
});

// Security violation should return 400 (validation error) or 404 (route not found)
// Both indicate successful blocking
expect([400, 404]).toContain(response.statusCode);

// Verify no sensitive data leaked
if (response.statusCode === 200) {
const body = response.body;
expect(body).not.toContain('root:');
expect(response.body).not.toContain('root:');
}
});

Expand All @@ -104,6 +106,7 @@ describe('Advanced Path Traversal Security', () => {
url: '/api/v1/artifacts/%252e%252e%252f%252e%252e%252fetc%252fpasswd/screenshot',
});

// Both 400 and 404 indicate successful blocking
expect([400, 404]).toContain(response.statusCode);
});

Expand All @@ -113,6 +116,7 @@ describe('Advanced Path Traversal Security', () => {
url: '/api/v1/artifacts/\u002e\u002e\u002f\u002e\u002e\u002fetc\u002fpasswd/screenshot',
});

// Both 400 and 404 indicate successful blocking
expect([400, 404]).toContain(response.statusCode);
});

Expand All @@ -122,6 +126,7 @@ describe('Advanced Path Traversal Security', () => {
url: '/api/v1/artifacts/..\\..\\..\\etc\\passwd/screenshot',
});

// Both 400 and 404 indicate successful blocking
expect([400, 404]).toContain(response.statusCode);
});
});
Expand Down Expand Up @@ -299,6 +304,7 @@ describe('Advanced Path Traversal Security', () => {
const originalExists = existsSync;

try {
// biome-ignore lint/suspicious/noExplicitAny: Necessary for monkey-patching global in tests
(global as any).existsSync = (path: string) => {
accessLog.push(path);
return originalExists(path);
Expand All @@ -312,6 +318,7 @@ describe('Advanced Path Traversal Security', () => {
const accessedPaths = accessLog.map((p) => resolve(p));
expect(accessedPaths.every((p) => p.startsWith(artifactsDir))).toBe(true);
} finally {
// biome-ignore lint/suspicious/noExplicitAny: Necessary for restoring global in tests
(global as any).existsSync = originalExists;
}
});
Expand Down
10 changes: 9 additions & 1 deletion packages/backend/tests/unit/crawler/browser-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Tests browser instance lifecycle, pooling, and error handling
*/

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { BrowserManager } from '../../../src/crawler/browser-manager.js';

describe('BrowserManager', () => {
Expand Down Expand Up @@ -37,15 +37,23 @@ describe('BrowserManager', () => {
});

it('should handle concurrent getBrowser calls safely', async () => {
// Create spy to verify browser is launched only once
const playwrightModule = await import('playwright');
const launchSpy = vi.spyOn(playwrightModule.chromium, 'launch');

const [browser1, browser2, browser3] = await Promise.all([
browserManager.getBrowser(),
browserManager.getBrowser(),
browserManager.getBrowser(),
]);

// Should create browser only once despite concurrent requests
expect(launchSpy).toHaveBeenCalledTimes(1);
expect(browser1).toBe(browser2);
expect(browser2).toBe(browser3);
expect(browser1.isConnected()).toBe(true);

launchSpy.mockRestore();
});

it('should throw error when manager is closing', async () => {
Expand Down
36 changes: 19 additions & 17 deletions packages/backend/tests/unit/crawler/page-capturer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ describe('PageCapturer', () => {
});
});

describe('capture - 404 pages', () => {
it('should handle 404 pages correctly', async () => {
describe('capture - HTTP status codes', () => {
it('should capture 404 pages as valid responses', async () => {
const result = await pageCapturer.capture({
url: `${baseUrl}/404`,
pageId: randomUUID(),
Expand All @@ -136,10 +136,26 @@ describe('PageCapturer', () => {
collectHar: false,
});

// 404 is a valid HTTP response, not an error
expect(result.httpStatus).toBe(404);
expect(result.htmlHash).toBeDefined();
expect(result.screenshotPath).toBeDefined();
expect(result.error).toBeUndefined();
expect(result.error).toBeUndefined(); // Not an error
});

it('should differentiate 404 from network errors', async () => {
const result = await pageCapturer.capture({
url: 'http://invalid-url-that-does-not-exist-12345.com',
pageId: randomUUID(),
viewport: { width: 1920, height: 1080 },
waitAfterLoad: 0,
collectHar: false,
});

// Real network error
expect(result.httpStatus).toBe(0); // No status received
expect(result.error).toBeDefined();
expect(result.error).toContain('net::ERR');
});
});

Expand Down Expand Up @@ -447,20 +463,6 @@ describe('PageCapturer', () => {
});

describe('capture - error handling', () => {
it('should handle invalid URLs gracefully', async () => {
const result = await pageCapturer.capture({
url: 'http://invalid-url-that-does-not-exist-12345.com',
pageId: randomUUID(),
viewport: { width: 1920, height: 1080 },
waitAfterLoad: 0,
collectHar: false,
});

expect(result.httpStatus).toBe(0);
expect(result.error).toBeDefined();
expect(result.error).toContain('net::ERR');
});

it('should handle timeout scenarios', async () => {
// This test verifies that very slow pages eventually timeout
// The PageCapturer has a 30s timeout configured
Expand Down
34 changes: 28 additions & 6 deletions packages/backend/tests/unit/domain/visual-comparator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,37 @@ describe('VisualComparator', () => {
expect(result.thresholdExceeded).toBe(false);
});

it('should handle antialiasing tolerance in pixelmatch', () => {
// Small color variations should be ignored with default settings
it('should apply threshold configuration correctly', () => {
// Create images with measurable difference (10x10 black region = 1% of 100x100)
const baseline = createTestImage(100, 100, [255, 255, 255, 255]);
const slightlyDifferent = createTestImage(100, 100, [254, 254, 254, 255]);
const withDifference = createImageWithRegion(
100,
100,
[255, 255, 255, 255],
0,
0,
10,
10,
[0, 0, 0, 255],
);

// With low threshold (strict) - 1% diff should exceed 0.5% threshold
const strictResult = VisualComparator.compare(baseline, withDifference, {
threshold: 0.5, // 0.5% tolerance
});

// With high threshold (permissive) - 1% diff should not exceed 2% threshold
const permissiveResult = VisualComparator.compare(baseline, withDifference, {
threshold: 2, // 2% tolerance
});

const result = VisualComparator.compare(baseline, slightlyDifferent);
// Verify threshold behavior
expect(strictResult.thresholdExceeded).toBe(true);
expect(permissiveResult.thresholdExceeded).toBe(false);

// With default pixelmatch threshold, very small differences might be ignored
expect(result.diffPercentage).toBeLessThan(100);
// Both should have same diff percentage (1%)
expect(strictResult.diffPercentage).toBe(permissiveResult.diffPercentage);
expect(strictResult.diffPercentage).toBe(1);
});
});

Expand Down
15 changes: 13 additions & 2 deletions packages/backend/tests/unit/queue/task-queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,23 @@ describe('TaskQueue.dequeue()', () => {
config: {},
};

// Enqueue in reverse priority order
// Enqueue with explicit timestamps to ensure deterministic ordering
const lowId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'low' });
// Set explicit timestamp for low priority task
db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run('2026-01-01T10:00:00Z', lowId);

const normalId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'normal' });
// Set timestamp 1 second later but higher priority
db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run(
'2026-01-01T10:00:01Z',
normalId,
);

const highId = taskQueue.enqueue({ type: 'capture-page', payload, priority: 'high' });
// Set timestamp latest but highest priority
db.prepare('UPDATE tasks SET created_at = ? WHERE id = ?').run('2026-01-01T10:00:02Z', highId);

// Should dequeue high priority first
// Should dequeue high priority first (despite being created last)
const task1 = taskQueue.dequeue();
expect(task1?.id).toBe(highId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* Tests for keyboard navigation, focus management, and screen reader support
*/

import type { ComponentMountingOptions } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Component } from 'vue';
import ProjectCard from '../../../src/components/ProjectCard.vue';
import ProjectForm from '../../../src/components/ProjectForm.vue';
import RunCard from '../../../src/components/RunCard.vue';
Expand Down Expand Up @@ -53,7 +55,10 @@ describe('Keyboard Navigation Accessibility', () => {
});

// Helper to mount component
const mountComponent = (component: any, options: any = {}) => {
const mountComponent = <T extends Component>(
component: T,
options: ComponentMountingOptions<T> = {},
) => {
return mount(component, options);
};

Expand Down