{ "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,128 @@
// [DEF:frontend.src.components.__tests__.task_log_viewer:Module]
// @TIER: CRITICAL
// @SEMANTICS: tests, task-log, viewer, mount, components
// @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM.
// @LAYER: UI (Tests)
// @RELATION: TESTS -> frontend.src.components.TaskLogViewer
// @INVARIANT: Duplicate logs are never appended. Polling only active for in-progress tasks.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/svelte';
import TaskLogViewer from '../TaskLogViewer.svelte';
import { getTaskLogs } from '../../services/taskService.js';
vi.mock('../../services/taskService.js', () => ({
getTaskLogs: vi.fn()
}));
vi.mock('../../lib/i18n', () => ({
t: {
subscribe: (fn) => {
fn({
tasks: {
loading: 'Loading...'
}
});
return () => { };
}
}
}));
describe('TaskLogViewer Component', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders loading state initially', () => {
getTaskLogs.mockResolvedValue([]);
render(TaskLogViewer, { inline: true, taskId: 'task-123' });
expect(screen.getByText('Loading...')).toBeDefined();
});
it('fetches and displays historical logs', async () => {
getTaskLogs.mockResolvedValue([
{ timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }
]);
render(TaskLogViewer, { inline: true, taskId: 'task-123' });
await waitFor(() => {
expect(screen.getByText(/Historical log entry/)).toBeDefined();
});
expect(getTaskLogs).toHaveBeenCalledWith('task-123');
});
it('displays error message on fetch failure', async () => {
getTaskLogs.mockRejectedValue(new Error('Network error fetching logs'));
render(TaskLogViewer, { inline: true, taskId: 'task-123' });
await waitFor(() => {
expect(screen.getByText('Network error fetching logs')).toBeDefined();
expect(screen.getByText('Retry')).toBeDefined();
});
});
it('appends real-time logs passed as props', async () => {
getTaskLogs.mockResolvedValue([
{ timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }
]);
const { rerender } = render(TaskLogViewer, {
inline: true,
taskId: 'task-123',
realTimeLogs: []
});
await waitFor(() => {
expect(screen.getByText(/Historical log entry/)).toBeDefined();
});
// Simulate receiving a new real-time log
await rerender({
inline: true,
taskId: 'task-123',
realTimeLogs: [
{ timestamp: '2024-01-01T00:00:01', level: 'DEBUG', message: 'Realtime log entry' }
]
});
await waitFor(() => {
expect(screen.getByText(/Realtime log entry/)).toBeDefined();
});
});
it('deduplicates real-time logs that are already in historical logs', async () => {
getTaskLogs.mockResolvedValue([
{ timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }
]);
const { rerender } = render(TaskLogViewer, {
inline: true,
taskId: 'task-123',
realTimeLogs: []
});
await waitFor(() => {
expect(screen.getByText(/Duplicate log entry/)).toBeDefined();
});
// Pass the exact same log as realtime
await rerender({
inline: true,
taskId: 'task-123',
realTimeLogs: [
{ timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }
]
});
// Wait a bit to ensure no explosive re-renders or double additions
await new Promise((r) => setTimeout(r, 50));
// In RTL, if there were duplicates, getAllByText would return > 1 elements.
// getByText asserts there is exactly *one* match.
expect(() => screen.getByText(/Duplicate log entry/)).not.toThrow();
});
});
// [/DEF:frontend.src.components.__tests__.task_log_viewer:Module]

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]

View File

@@ -4,9 +4,7 @@ import path from 'path';
export default defineConfig({
plugins: [
svelte({
test: true
})
svelte()
],
test: {
globals: true,
@@ -33,10 +31,12 @@ export default defineConfig({
alias: [
{ find: '$app/environment', replacement: path.resolve(__dirname, './src/lib/stores/__tests__/mocks/environment.js') },
{ find: '$app/stores', replacement: path.resolve(__dirname, './src/lib/stores/__tests__/mocks/stores.js') },
{ find: '$app/navigation', replacement: path.resolve(__dirname, './src/lib/stores/__tests__/mocks/navigation.js') }
{ find: '$app/navigation', replacement: path.resolve(__dirname, './src/lib/stores/__tests__/mocks/navigation.js') },
{ find: '$env/static/public', replacement: path.resolve(__dirname, './src/lib/stores/__tests__/mocks/env_public.js') }
]
},
resolve: {
conditions: ['mode=browser', 'browser'],
alias: {
'$lib': path.resolve(__dirname, './src/lib'),
'$app': path.resolve(__dirname, './src')