import { describe, expect, it } from 'vitest'; import { isPluginLifecycleTask, parseFrontmatter, pathMatchesAny, } from '../../harness/src/specs.mjs'; import { scanBackendCommunicationBoundary, touchesCommunicationPath, validateGatewayTaskSpec, validatePluginLifecycleTaskSpec, } from '../../harness/src/rules.mjs'; describe('harness specs', () => { it('parses Markdown frontmatter with arrays and nested docs', () => { const spec = parseFrontmatter(`--- id: example requiredProfiles: - fast - comms docs: required: false --- Body`); expect(spec.data.id).toBe('example'); expect(spec.data.requiredProfiles).toEqual(['fast', 'comms']); expect(spec.data.docs).toEqual({ required: false }); }); it('matches repository glob paths', () => { expect(pathMatchesAny('src/stores/chat/history-actions.ts', ['src/stores/chat/**'])).toBe(true); expect(pathMatchesAny('src/lib/api-client.ts', ['src/lib/api-client.ts'])).toBe(true); expect(pathMatchesAny('src/pages/Chat/index.tsx', ['electron/gateway/**'])).toBe(false); }); it('requires gateway backend communication tasks to run fast and comms', () => { const taskSpec = { path: 'harness/specs/tasks/example.md', data: { id: 'example', title: 'Example', scenario: 'gateway-backend-communication', taskType: 'runtime-bridge', intent: 'Adjust backend communication.', touchedAreas: ['src/lib/api-client.ts'], expectedUserBehavior: ['Visible state remains consistent.'], requiredProfiles: ['fast'], acceptance: ['Comms compare passes.'], docs: { required: false }, }, }; const scenarioSpec = { data: { requiredProfiles: ['fast', 'comms'], ownedPaths: ['src/lib/api-client.ts'], }, }; expect(validateGatewayTaskSpec(taskSpec, scenarioSpec)).toContain( 'harness/specs/tasks/example.md: requiredProfiles must include "comms"', ); }); it('detects plugin lifecycle task specs for strict validation', () => { expect(isPluginLifecycleTask({ data: { scenario: 'plugin-lifecycle-management', }, })).toBe(true); expect(isPluginLifecycleTask({ data: { scenarios: ['plugin-lifecycle-management'], }, })).toBe(true); expect(isPluginLifecycleTask({ data: { scenario: 'gateway-backend-communication', }, })).toBe(false); }); it('requires plugin lifecycle tasks to declare strict task fields', () => { const taskSpec = { path: 'harness/specs/tasks/plugin-example.md', data: { id: 'plugin-example', title: 'Plugin Example', scenario: 'plugin-lifecycle-management', taskType: 'plugin-lifecycle', intent: 'Adjust plugin lifecycle behavior.', requiredProfiles: [], docs: { required: false }, }, }; const scenarioSpec = { data: { requiredProfiles: ['fast'], ownedPaths: ['electron/utils/plugin-install.ts'], }, }; expect(validatePluginLifecycleTaskSpec(taskSpec, scenarioSpec)).toEqual( expect.arrayContaining([ 'harness/specs/tasks/plugin-example.md: requiredProfiles must include "fast"', 'harness/specs/tasks/plugin-example.md: touchedAreas must declare affected paths', 'harness/specs/tasks/plugin-example.md: expectedUserBehavior must declare visible behavior', 'harness/specs/tasks/plugin-example.md: acceptance must declare completion criteria', ]), ); }); it('rejects plugin lifecycle tasks with the wrong scenario or task type', () => { const taskSpec = { path: 'harness/specs/tasks/plugin-example.md', data: { id: 'plugin-example', title: 'Plugin Example', scenario: 'gateway-backend-communication', taskType: 'runtime-bridge', intent: 'Adjust plugin lifecycle behavior.', touchedAreas: ['electron/utils/plugin-install.ts'], expectedUserBehavior: ['Plugin remains usable.'], requiredProfiles: ['fast'], acceptance: ['Validation passes.'], docs: { required: false }, }, }; expect(validatePluginLifecycleTaskSpec(taskSpec, null)).toEqual( expect.arrayContaining([ 'harness/specs/tasks/plugin-example.md: plugin lifecycle tasks must set scenario: plugin-lifecycle-management', 'harness/specs/tasks/plugin-example.md: plugin lifecycle tasks must set taskType: plugin-lifecycle', ]), ); }); it('detects communication path changes', () => { expect(touchesCommunicationPath(['electron/gateway/manager.ts'])).toBe(true); expect(touchesCommunicationPath(['README.md'])).toBe(false); }); it('blocks direct Gateway HTTP in renderer files', async () => { const failures = await scanBackendCommunicationBoundary(['src/pages/Chat/index.tsx']); expect(failures).toEqual([]); }); it('allows fallback flags only in their boundary modules', async () => { const failures = await scanBackendCommunicationBoundary([ 'src/lib/api-client.ts', 'src/lib/host-api.ts', 'src/lib/host-events.ts', ]); expect(failures).toEqual([]); }); it('allows pages and components to display gatewayReady state', async () => { const failures = await scanBackendCommunicationBoundary(['src/components/layout/Sidebar.tsx']); expect(failures).toEqual([]); }); });