{ "verdict": "APPROVED", "rejection_reason": "NONE", "audit_details": { "target_invoked": true, "pre_conditions_tested": true, "post_conditions_tested": true, "test_data_used": true }, "feedback": "Both test files have successfully passed the audit. The 'task_log_viewer.test.js' suite now correctly imports and mounts the real Svelte component using Test Library, fully eliminating the logic mirror/tautology issue. The 'test_logger.py' suite now properly implements negative tests for the @PRE constraint in 'belief_scope' and fully verifies all @POST effects triggered by 'configure_logger'." }
This commit is contained in:
213
frontend/src/lib/api/__tests__/reports_api.test.js
Normal file
213
frontend/src/lib/api/__tests__/reports_api.test.js
Normal file
@@ -0,0 +1,213 @@
|
||||
// [DEF:frontend.src.lib.api.__tests__.reports_api:Module]
|
||||
// @TIER: CRITICAL
|
||||
// @SEMANTICS: tests, reports, api-client, query-string, error-normalization
|
||||
// @PURPOSE: Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.
|
||||
// @LAYER: Infra (Tests)
|
||||
// @RELATION: TESTS -> frontend.src.lib.api.reports
|
||||
// @INVARIANT: Pure functions produce deterministic output. Async wrappers propagate structured errors.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock SvelteKit environment modules before any source imports
|
||||
vi.mock('$env/static/public', () => ({
|
||||
PUBLIC_WS_URL: 'ws://localhost:8000'
|
||||
}));
|
||||
|
||||
// Mock toasts to prevent import side-effects
|
||||
vi.mock('../../toasts.js', () => ({
|
||||
addToast: vi.fn()
|
||||
}));
|
||||
|
||||
// Mock the api module
|
||||
vi.mock('../../api.js', () => ({
|
||||
api: {
|
||||
fetchApi: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import { buildReportQueryString, normalizeApiError } from '../reports.js';
|
||||
|
||||
// [DEF:TestBuildReportQueryString:Class]
|
||||
// @PURPOSE: Validate query string construction from filter options.
|
||||
// @PRE: Options object with various filter fields.
|
||||
// @POST: Correct URLSearchParams string produced.
|
||||
describe('buildReportQueryString', () => {
|
||||
it('returns empty string for empty options', () => {
|
||||
expect(buildReportQueryString()).toBe('');
|
||||
expect(buildReportQueryString({})).toBe('');
|
||||
});
|
||||
|
||||
it('serializes page and page_size', () => {
|
||||
const qs = buildReportQueryString({ page: 2, page_size: 10 });
|
||||
expect(qs).toContain('page=2');
|
||||
expect(qs).toContain('page_size=10');
|
||||
});
|
||||
|
||||
it('serializes task_types array', () => {
|
||||
const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });
|
||||
expect(qs).toContain('task_types=backup%2Cmigration');
|
||||
});
|
||||
|
||||
it('serializes statuses array', () => {
|
||||
const qs = buildReportQueryString({ statuses: ['success', 'failed'] });
|
||||
expect(qs).toContain('statuses=success%2Cfailed');
|
||||
});
|
||||
|
||||
it('ignores empty arrays', () => {
|
||||
const qs = buildReportQueryString({ task_types: [], statuses: [] });
|
||||
expect(qs).toBe('');
|
||||
});
|
||||
|
||||
it('serializes time range and search', () => {
|
||||
const qs = buildReportQueryString({
|
||||
time_from: '2024-01-01',
|
||||
time_to: '2024-12-31',
|
||||
search: 'backup'
|
||||
});
|
||||
expect(qs).toContain('time_from=2024-01-01');
|
||||
expect(qs).toContain('time_to=2024-12-31');
|
||||
expect(qs).toContain('search=backup');
|
||||
});
|
||||
|
||||
it('serializes sort options', () => {
|
||||
const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });
|
||||
expect(qs).toContain('sort_by=status');
|
||||
expect(qs).toContain('sort_order=asc');
|
||||
});
|
||||
|
||||
it('handles all options combined', () => {
|
||||
const qs = buildReportQueryString({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
task_types: ['backup'],
|
||||
statuses: ['success'],
|
||||
search: 'test',
|
||||
sort_by: 'updated_at',
|
||||
sort_order: 'desc'
|
||||
});
|
||||
expect(qs).toContain('page=1');
|
||||
expect(qs).toContain('page_size=20');
|
||||
expect(qs).toContain('task_types=backup');
|
||||
expect(qs).toContain('statuses=success');
|
||||
expect(qs).toContain('search=test');
|
||||
});
|
||||
});
|
||||
// [/DEF:TestBuildReportQueryString:Class]
|
||||
|
||||
// [DEF:TestNormalizeApiError:Class]
|
||||
// @PURPOSE: Validate error normalization for UI-state mapping.
|
||||
// @PRE: Various error types (Error, string, object).
|
||||
// @POST: Always returns {message, code, retryable} object.
|
||||
describe('normalizeApiError', () => {
|
||||
it('extracts message from Error object', () => {
|
||||
const result = normalizeApiError(new Error('Connection failed'));
|
||||
expect(result.message).toBe('Connection failed');
|
||||
expect(result.code).toBe('REPORTS_API_ERROR');
|
||||
expect(result.retryable).toBe(true);
|
||||
});
|
||||
|
||||
it('uses string error directly', () => {
|
||||
const result = normalizeApiError('Something went wrong');
|
||||
expect(result.message).toBe('Something went wrong');
|
||||
});
|
||||
|
||||
it('falls back to default message for null/undefined', () => {
|
||||
expect(normalizeApiError(null).message).toBe('Failed to load reports');
|
||||
expect(normalizeApiError(undefined).message).toBe('Failed to load reports');
|
||||
});
|
||||
|
||||
it('falls back for object without message', () => {
|
||||
const result = normalizeApiError({ status: 500 });
|
||||
expect(result.message).toBe('Failed to load reports');
|
||||
});
|
||||
|
||||
it('always includes code and retryable fields', () => {
|
||||
const result = normalizeApiError('test');
|
||||
expect(result).toHaveProperty('code');
|
||||
expect(result).toHaveProperty('retryable');
|
||||
});
|
||||
});
|
||||
// [/DEF:TestNormalizeApiError:Class]
|
||||
|
||||
// [DEF:TestGetReportsAsync:Class]
|
||||
// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.
|
||||
// @PRE: api.fetchApi is mocked via vi.mock.
|
||||
// @POST: Functions call correct endpoints and propagate results/errors.
|
||||
|
||||
describe('getReports', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const apiModule = await import('../../api.js');
|
||||
api = apiModule.api;
|
||||
});
|
||||
|
||||
it('calls fetchApi with correct endpoint', async () => {
|
||||
const { getReports } = await import('../reports.js');
|
||||
const mockResponse = { items: [], total: 0 };
|
||||
api.fetchApi.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getReports();
|
||||
expect(api.fetchApi).toHaveBeenCalledWith('/reports');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('appends query string when options provided', async () => {
|
||||
const { getReports } = await import('../reports.js');
|
||||
api.fetchApi.mockResolvedValue({ items: [] });
|
||||
|
||||
await getReports({ page: 2, page_size: 5 });
|
||||
const call = api.fetchApi.mock.calls[0][0];
|
||||
expect(call).toContain('/reports?');
|
||||
expect(call).toContain('page=2');
|
||||
expect(call).toContain('page_size=5');
|
||||
});
|
||||
|
||||
it('throws normalized error on failure', async () => {
|
||||
const { getReports } = await import('../reports.js');
|
||||
api.fetchApi.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(getReports()).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
message: 'Network error',
|
||||
code: 'REPORTS_API_ERROR'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReportDetail', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const apiModule = await import('../../api.js');
|
||||
api = apiModule.api;
|
||||
});
|
||||
|
||||
it('calls fetchApi with correct endpoint', async () => {
|
||||
const { getReportDetail } = await import('../reports.js');
|
||||
const mockDetail = { report: { report_id: 'r1' } };
|
||||
api.fetchApi.mockResolvedValue(mockDetail);
|
||||
|
||||
const result = await getReportDetail('r1');
|
||||
expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');
|
||||
expect(result).toEqual(mockDetail);
|
||||
});
|
||||
|
||||
it('throws normalized error on failure', async () => {
|
||||
const { getReportDetail } = await import('../reports.js');
|
||||
api.fetchApi.mockRejectedValue(new Error('Not found'));
|
||||
|
||||
await expect(getReportDetail('nonexistent')).rejects.toEqual(
|
||||
expect.objectContaining({
|
||||
message: 'Not found',
|
||||
code: 'REPORTS_API_ERROR'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
// [/DEF:TestGetReportsAsync:Class]
|
||||
|
||||
// [/DEF:frontend.src.lib.api.__tests__.reports_api:Module]
|
||||
6
frontend/src/lib/stores/__tests__/mocks/env_public.js
Normal file
6
frontend/src/lib/stores/__tests__/mocks/env_public.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// [DEF:mock_env_public:Module]
|
||||
// @TIER: STANDARD
|
||||
// @PURPOSE: Mock for $env/static/public SvelteKit module in vitest
|
||||
// @LAYER: UI (Tests)
|
||||
export const PUBLIC_WS_URL = 'ws://localhost:8000';
|
||||
// [/DEF:mock_env_public:Module]
|
||||
Reference in New Issue
Block a user