{ "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:
2026-02-24 21:55:13 +03:00
parent 95ae9c6af1
commit 1c362f4092
13 changed files with 2170 additions and 270 deletions

View 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]

View 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]