open-design/apps/daemon/tests/plugins-atom-bodies.test.ts
Cursor Agent 847304ebc5
feat(plugins): atom SKILL.md body loader + renderActiveStageBlock (spec §23.4)
Plan J3 / spec §23.3.2 patch 2 / §23.4.

Lays the substrate slice for migrating prompt fragments out of
`apps/daemon/src/prompts/system.ts` and into the bundled atom
SKILL.md bodies registered by §3.I3.

apps/daemon/src/plugins/atom-bodies.ts owns the daemon-side loader:

  loadAtomBodies(db, atomIds) → AtomBodyEntry[]

The function looks each atom id up in installed_plugins (bundled
rows win), reads the matching fsPath/SKILL.md, strips
front-matter, and returns the raw body. Atoms with no installed
plugin or unreadable SKILL.md are silently skipped — the caller
drops empty entries from the prompt.

packages/contracts/src/prompts/atom-block.ts ships the pure
renderer:

  renderActiveStageBlock({ stageId, bodies, iteration? }) → string

Mirrors spec §23.4's composeSystemPrompt sketch. Empty bodies
return ''; multiple bodies are separated by '---' with no trailing
separator. Lives in contracts so the daemon-side composer and any
future contracts-side composer share one definition (§11.8 PB1
single-import guarantee).

The composeSystemPrompt() rewiring itself is the next PR — this
commit gives that PR zero scaffolding to build: the helpers are
reachable, tested, and the bundled atom plugins from §3.I3 already
have the matching SKILL.md bodies on disk.

Tests: contracts 8 → 12 (+4 cases on atom-block); daemon
1482 → 1486 (+4 cases on plugins-atom-bodies covering the
end-to-end loadAtomBodies → renderActiveStageBlock path).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 13:15:52 +00:00

94 lines
4 KiB
TypeScript

// Phase 4 / spec §23.3.2 patch 2 — atom SKILL.md body loader.
//
// The substrate slice for lifting `composeSystemPrompt`'s prompt
// constants into the bundled atom plugins. The daemon-side helper
// reads `<bundled-fsPath>/SKILL.md` and strips frontmatter; the
// pure renderer in @open-design/contracts then assembles the stage
// prompt block. This test pins both halves of the contract so a
// future PR that lifts system.ts has zero scaffolding to build.
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import Database from 'better-sqlite3';
import { migratePlugins } from '../src/plugins/persistence.js';
import { registerBundledPlugins } from '../src/plugins/bundled.js';
import { loadAtomBodies } from '../src/plugins/atom-bodies.js';
import { renderActiveStageBlock } from '@open-design/contracts';
const SAMPLE_MANIFEST = (id: string) =>
JSON.stringify({
$schema: 'https://open-design.ai/schemas/plugin.v1.json',
name: id,
title: id,
version: '0.1.0',
description: `${id} fixture`,
license: 'MIT',
od: { kind: 'atom', capabilities: ['prompt:inject'] },
});
const SAMPLE_SKILL = (id: string, body: string) =>
`---\nname: ${id}\ndescription: ${id} fixture\n---\n\n# ${id}\n\n${body}\n`;
let db: Database.Database;
let tmpRoot: string;
beforeEach(async () => {
tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'od-atom-bodies-'));
db = new Database(':memory:');
db.exec(`
CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);
CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT, title TEXT);
`);
migratePlugins(db);
// Build a minimal bundled root with two atom plugins so the loader has
// something to find.
const atomA = path.join(tmpRoot, 'atoms', 'discovery-question-form');
const atomB = path.join(tmpRoot, 'atoms', 'todo-write');
await mkdir(atomA, { recursive: true });
await mkdir(atomB, { recursive: true });
await writeFile(path.join(atomA, 'open-design.json'), SAMPLE_MANIFEST('discovery-question-form'));
await writeFile(path.join(atomA, 'SKILL.md'), SAMPLE_SKILL('discovery-question-form', 'Ask the user about audience.'));
await writeFile(path.join(atomB, 'open-design.json'), SAMPLE_MANIFEST('todo-write'));
await writeFile(path.join(atomB, 'SKILL.md'), SAMPLE_SKILL('todo-write', 'Commit a numbered plan.'));
await registerBundledPlugins({ db, bundledRoot: tmpRoot });
});
afterEach(async () => {
db.close();
await rm(tmpRoot, { recursive: true, force: true });
});
describe('loadAtomBodies', () => {
it('reads SKILL.md bodies for bundled atoms (frontmatter stripped)', async () => {
const out = await loadAtomBodies(db, ['discovery-question-form', 'todo-write']);
expect(out.map((e) => e.atomId)).toEqual(['discovery-question-form', 'todo-write']);
expect(out[0]!.body).toContain('# discovery-question-form');
expect(out[0]!.body).toContain('Ask the user about audience.');
expect(out[0]!.body.startsWith('---')).toBe(false);
});
it('skips ids without an installed plugin or readable SKILL.md', async () => {
const out = await loadAtomBodies(db, ['unknown-atom', 'todo-write']);
expect(out.map((e) => e.atomId)).toEqual(['todo-write']);
});
it('returns an empty array for an empty input', async () => {
expect(await loadAtomBodies(db, [])).toEqual([]);
});
});
describe('renderActiveStageBlock + loadAtomBodies (end-to-end stage block)', () => {
it('builds a `## Active stage` header followed by every atom body', async () => {
const bodies = await loadAtomBodies(db, ['discovery-question-form', 'todo-write']);
const block = renderActiveStageBlock({ stageId: 'plan', bodies });
expect(block).toContain('## Active stage: plan');
expect(block).toContain('### discovery-question-form');
expect(block).toContain('Ask the user about audience.');
expect(block).toContain('### todo-write');
expect(block).toContain('Commit a numbered plan.');
});
});