mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Plan CC1.
apps/daemon/src/server.ts: new `GET /api/applied-plugins/:snapshotId/canon`
returns the canonical `## Active plugin` block this snapshot
splices into the system prompt. Two response modes:
default \u2192 { snapshotId, pluginId, block }
Accept: text/plain \u2192 raw block body for shell pipes
Powered by the same renderPluginBlock() composeSystemPrompt() uses,
so the route output is byte-equal to what the agent reads. Useful
for:
- debugging 'why is the agent ignoring my plugin' (read what's
actually injected),
- locking byte-equality regression fixtures against the daemon's
renderPluginBlock() output,
- eyeballing what the prompt block looks like before applying.
CLI: `od plugin canon <snapshotId> [--json]`. Default output is
plain text suitable for piping; --json wraps the block in
{ snapshotId, pluginId, block }. printPluginHelp() updated.
Daemon tests: 1737 \u2192 1744 (+7 cases on plugins-canon: id +
version when title absent, pluginTitle wins when present, plugin
description appended, sorted alphabetic inputs block, byte-equal
output across calls with same snapshot (replay invariance check),
inputs block omitted when no inputs, query echoed as stylized
brief).
Co-authored-by: Tom Huang <1043269994@qq.com>
73 lines
2.9 KiB
TypeScript
73 lines
2.9 KiB
TypeScript
// Plan §3.CC1 — `od plugin canon <snapshotId>` route + helper.
|
|
//
|
|
// The CLI subcommand fetches GET /api/applied-plugins/<id>/canon,
|
|
// which calls renderPluginBlock() (re-exported as pluginPromptBlock
|
|
// from apps/daemon/src/plugins/index.ts). The route is a 4-line
|
|
// thin wrapper so this test directly exercises the helper +
|
|
// asserts the canonical block shape stays byte-deterministic.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { AppliedPluginSnapshot } from '@open-design/contracts';
|
|
import { pluginPromptBlock } from '../src/plugins/index.js';
|
|
|
|
const snapshot = (over: Partial<AppliedPluginSnapshot> = {}): AppliedPluginSnapshot => ({
|
|
snapshotId: 'snap-1',
|
|
pluginId: 'fixture',
|
|
pluginVersion: '0.1.0',
|
|
manifestSourceDigest: 'd1',
|
|
inputs: {},
|
|
resolvedContext: { items: [], itemRefs: [] } as unknown as AppliedPluginSnapshot['resolvedContext'],
|
|
capabilitiesGranted: [],
|
|
capabilitiesRequired: [],
|
|
assetsStaged: [],
|
|
taskKind: 'new-generation',
|
|
appliedAt: 1,
|
|
connectorsRequired: [],
|
|
connectorsResolved: [],
|
|
mcpServers: [],
|
|
status: 'fresh',
|
|
...over,
|
|
});
|
|
|
|
describe('pluginPromptBlock — Active plugin block shape', () => {
|
|
it('renders the plugin id + version when title is absent', () => {
|
|
const out = pluginPromptBlock(snapshot({ pluginId: 'p', pluginVersion: '1.0.0' }));
|
|
expect(out).toContain('## Active plugin');
|
|
expect(out).toContain('`p@1.0.0`');
|
|
});
|
|
|
|
it('prefers pluginTitle when present', () => {
|
|
const out = pluginPromptBlock(snapshot({ pluginTitle: 'Hero plugin', pluginId: 'p', pluginVersion: '1.0.0' }));
|
|
expect(out).toContain('**Hero plugin**');
|
|
expect(out).toContain('`p@1.0.0`');
|
|
});
|
|
|
|
it('appends pluginDescription when present', () => {
|
|
const out = pluginPromptBlock(snapshot({ pluginDescription: 'Does things' }));
|
|
expect(out).toContain('Does things');
|
|
});
|
|
|
|
it('renders Plugin inputs block sorted alphabetically by key', () => {
|
|
const out = pluginPromptBlock(snapshot({ inputs: { topic: 'cards', tone: 'warm', spacing: '16px' } }));
|
|
expect(out).toContain('## Plugin inputs');
|
|
const positions = ['spacing', 'tone', 'topic'].map((k) => out.indexOf(`**${k}**`));
|
|
expect(positions[0]).toBeGreaterThan(0);
|
|
expect(positions[0]).toBeLessThan(positions[1]!);
|
|
expect(positions[1]).toBeLessThan(positions[2]!);
|
|
});
|
|
|
|
it('returns byte-equal output across two calls with the same snapshot (replay invariance)', () => {
|
|
const snap = snapshot({ pluginTitle: 'X', inputs: { a: '1', b: '2' } });
|
|
expect(pluginPromptBlock(snap)).toBe(pluginPromptBlock(snap));
|
|
});
|
|
|
|
it('omits the Plugin inputs block when no inputs are present', () => {
|
|
const out = pluginPromptBlock(snapshot());
|
|
expect(out).not.toContain('## Plugin inputs');
|
|
});
|
|
|
|
it('echoes query as a stylized brief when present', () => {
|
|
const out = pluginPromptBlock(snapshot({ query: 'Make a deck about cats' }));
|
|
expect(out).toContain('Make a deck about cats');
|
|
});
|
|
});
|