141 lines
5.2 KiB
TypeScript
141 lines
5.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
loadSystemPrompt,
|
|
loadSystemPromptWithOverrides,
|
|
assertOverridesReachActiveSpec,
|
|
loadSkills,
|
|
} from '../src/prompt-loader.js';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
|
|
describe('Prompt Loader', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-prompt-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('loadSystemPrompt', () => {
|
|
it('returns null when no system-prompt.md exists', () => {
|
|
const result = loadSystemPrompt(tmpDir);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('loads system-prompt.md from waggle dir', () => {
|
|
fs.writeFileSync(path.join(tmpDir, 'system-prompt.md'), 'You are a helpful bot.');
|
|
const result = loadSystemPrompt(tmpDir);
|
|
expect(result).toBe('You are a helpful bot.');
|
|
});
|
|
|
|
it('trims whitespace', () => {
|
|
fs.writeFileSync(path.join(tmpDir, 'system-prompt.md'), ' Hello \n\n');
|
|
const result = loadSystemPrompt(tmpDir);
|
|
expect(result).toBe('Hello');
|
|
});
|
|
});
|
|
|
|
describe('loadSkills', () => {
|
|
it('returns empty array when no skills dir', () => {
|
|
const result = loadSkills(tmpDir);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('loads .md files from skills directory', () => {
|
|
const skillsDir = path.join(tmpDir, 'skills');
|
|
fs.mkdirSync(skillsDir);
|
|
fs.writeFileSync(path.join(skillsDir, 'coding.md'), '# Coding\nYou are a coder.');
|
|
fs.writeFileSync(path.join(skillsDir, 'research.md'), '# Research\nYou research things.');
|
|
|
|
const result = loadSkills(tmpDir);
|
|
expect(result).toHaveLength(2);
|
|
expect(result.map(s => s.name)).toContain('coding');
|
|
expect(result.map(s => s.name)).toContain('research');
|
|
});
|
|
|
|
it('ignores non-md files', () => {
|
|
const skillsDir = path.join(tmpDir, 'skills');
|
|
fs.mkdirSync(skillsDir);
|
|
fs.writeFileSync(path.join(skillsDir, 'coding.md'), '# Coding');
|
|
fs.writeFileSync(path.join(skillsDir, 'notes.txt'), 'not a skill');
|
|
|
|
const result = loadSkills(tmpDir);
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
// ── H-08 G2 — override-aware loader ──────────────────────────
|
|
describe('loadSystemPromptWithOverrides', () => {
|
|
it('returns baseline when no overrides exist', () => {
|
|
const composed = loadSystemPromptWithOverrides(tmpDir);
|
|
expect(composed.userPromptMd).toBeNull();
|
|
expect(composed.behavioralSpec).toBeDefined();
|
|
// Baseline should contain at least one known section string
|
|
const flattened = Object.values(composed.behavioralSpec).join('\n');
|
|
expect(flattened.length).toBeGreaterThan(0);
|
|
expect(composed.customPersonas).toEqual([]);
|
|
});
|
|
|
|
it('surfaces a custom system-prompt.md alongside baseline spec', () => {
|
|
fs.writeFileSync(path.join(tmpDir, 'system-prompt.md'), 'hello from user');
|
|
const composed = loadSystemPromptWithOverrides(tmpDir);
|
|
expect(composed.userPromptMd).toBe('hello from user');
|
|
});
|
|
|
|
it('applies behavioral-spec overrides into the active spec', () => {
|
|
const overrideDir = path.join(tmpDir, 'behavioral-overrides');
|
|
fs.mkdirSync(overrideDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(overrideDir, 'coreLoop.json'),
|
|
JSON.stringify({
|
|
section: 'coreLoop',
|
|
text: 'CUSTOM CORE LOOP CONTENT FOR TEST',
|
|
deployedAt: new Date().toISOString(),
|
|
}),
|
|
);
|
|
|
|
const composed = loadSystemPromptWithOverrides(tmpDir);
|
|
const flattened = Object.values(composed.behavioralSpec).join('\n');
|
|
expect(flattened).toContain('CUSTOM CORE LOOP CONTENT FOR TEST');
|
|
});
|
|
});
|
|
|
|
describe('assertOverridesReachActiveSpec', () => {
|
|
it('is quiet when no overrides are deployed', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const composed = loadSystemPromptWithOverrides(tmpDir);
|
|
assertOverridesReachActiveSpec(tmpDir, composed.behavioralSpec);
|
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('warns when disk has overrides but activeSpec does not contain them', () => {
|
|
const overrideDir = path.join(tmpDir, 'behavioral-overrides');
|
|
fs.mkdirSync(overrideDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(overrideDir, 'coreLoop.json'),
|
|
JSON.stringify({
|
|
section: 'coreLoop',
|
|
text: 'UNIQUE MARKER STRING FOR G2 TEST — LONG ENOUGH TO MATCH',
|
|
deployedAt: new Date().toISOString(),
|
|
}),
|
|
);
|
|
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
// Simulate using the BARE loader — activeSpec is the compile-time
|
|
// baseline with no overrides folded in.
|
|
const fakeActiveSpec = loadSystemPromptWithOverrides(path.join(tmpDir, 'nothing')).behavioralSpec;
|
|
assertOverridesReachActiveSpec(tmpDir, fakeActiveSpec);
|
|
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
const message = warnSpy.mock.calls[0]?.[0] as string;
|
|
expect(message).toContain('coreLoop');
|
|
warnSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|