164 lines
5.3 KiB
TypeScript
164 lines
5.3 KiB
TypeScript
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([]);
|
|
});
|
|
});
|