open-design/apps/daemon/tests/plugins-canon.test.ts
Cursor Agent 501fdf9e3a
feat(plugins): od plugin canon <snapshotId> show prompt block (Phase 4)
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>
2026-05-09 17:10:04 +00:00

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');
});
});