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([]);
});
});