mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
694289295a
Plan O1 / spec §23.3.3.
When a plugin omits `od.pipeline`, applyPlugin() now consults the
bundled scenario plugins registered by the daemon's bundled boot
walker (apps/daemon/src/plugins/bundled.ts) and copies the matching
scenario's pipeline verbatim into the snapshot. Match key:
`od.taskKind` (defaulting to 'new-generation' when absent).
Pure-side (packages/plugin-runtime):
- new resolveAppliedPipeline({ manifest, scenarios }) →
{ pipeline, source: 'declared' | 'scenario' | 'none', scenarioId? }
- RegistryView gains optional `scenarios?: ScenarioRegistryEntry[]`
so the daemon registry view propagates the lookup table without
coupling the pure resolver to SQLite.
Daemon-side (apps/daemon):
- applyPlugin() calls resolveAppliedPipeline and stores the result on
BOTH `ApplyResult.pipeline` and `AppliedPluginSnapshot.pipeline`
so a replay reproduces the same stages without re-consulting the
scenario registry.
- server.loadPluginRegistryView() now collects bundled scenarios
from `installed_plugins` (source_kind='bundled', od.kind='scenario',
non-empty pipeline). Third-party scenarios are intentionally NOT
eligible — only rows seeded by the bundled boot walker.
Invariants preserved:
- Apply stays pure: scenarios are read once at the registry-view
construction site and passed in; apply never opens a DB itself.
- Scenario plugins themselves never fall back to themselves
(resolveAppliedPipeline returns 'none' when od.kind='scenario').
- manifestSourceDigest is unaffected by the fallback (digest is over
manifest + inputs + resolvedContextRefs); replay invariance held.
Tests: +15 (plugin-runtime: 7 cases on resolveAppliedPipeline;
daemon: 8 cases on plugins-scenario-fallback covering declared
pipeline win, scenario fallback by taskKind, default to
new-generation, scenario plugins don't recurse, no scenarios →
pipeline=undefined, manifestSourceDigest stability across applies,
DB collector round-trip).
Co-authored-by: Tom Huang <1043269994@qq.com>
86 lines
3.1 KiB
TypeScript
86 lines
3.1 KiB
TypeScript
// Spec §23.3.3 — bundled-scenario pipeline fallback.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { PluginManifest } from '@open-design/contracts';
|
|
import { resolveAppliedPipeline, type ScenarioRegistryEntry } from '../src/index.js';
|
|
|
|
const baseManifest = (od: NonNullable<PluginManifest['od']> | undefined): PluginManifest => ({
|
|
$schema: 'https://open-design.ai/schemas/plugin.v1.json',
|
|
name: 'fixture',
|
|
version: '0.0.1',
|
|
...(od ? { od } : {}),
|
|
}) as PluginManifest;
|
|
|
|
const scenarios: ScenarioRegistryEntry[] = [
|
|
{
|
|
id: 'od-new-generation',
|
|
taskKind: 'new-generation',
|
|
pipeline: { stages: [{ id: 'discovery', atoms: ['discovery-question-form'] }] },
|
|
},
|
|
{
|
|
id: 'od-code-migration',
|
|
taskKind: 'code-migration',
|
|
pipeline: { stages: [{ id: 'import', atoms: ['code-import'] }] },
|
|
},
|
|
];
|
|
|
|
describe('resolveAppliedPipeline', () => {
|
|
it('returns the declared pipeline when the manifest carries one', () => {
|
|
const manifest = baseManifest({
|
|
taskKind: 'new-generation',
|
|
pipeline: { stages: [{ id: 'custom', atoms: ['todo-write'] }] },
|
|
});
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('declared');
|
|
expect(out.pipeline?.stages?.[0]?.id).toBe('custom');
|
|
expect(out.scenarioId).toBeUndefined();
|
|
});
|
|
|
|
it('falls back to the bundled scenario matching taskKind', () => {
|
|
const manifest = baseManifest({ taskKind: 'code-migration' });
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('scenario');
|
|
expect(out.scenarioId).toBe('od-code-migration');
|
|
expect(out.pipeline?.stages?.[0]?.id).toBe('import');
|
|
});
|
|
|
|
it("defaults taskKind to 'new-generation' when the manifest omits it", () => {
|
|
const manifest = baseManifest({});
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('scenario');
|
|
expect(out.scenarioId).toBe('od-new-generation');
|
|
});
|
|
|
|
it("returns source='none' when the manifest is itself a scenario", () => {
|
|
const manifest = baseManifest({
|
|
kind: 'scenario',
|
|
taskKind: 'new-generation',
|
|
});
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('none');
|
|
expect(out.pipeline).toBeUndefined();
|
|
});
|
|
|
|
it("returns source='none' when no scenario list is supplied", () => {
|
|
const manifest = baseManifest({ taskKind: 'code-migration' });
|
|
const out = resolveAppliedPipeline({ manifest });
|
|
expect(out.source).toBe('none');
|
|
});
|
|
|
|
it("returns source='none' when no scenario matches the taskKind", () => {
|
|
const manifest = baseManifest({ taskKind: 'tune-collab' });
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('none');
|
|
});
|
|
|
|
it('treats an empty stages[] declared pipeline as missing and falls back', () => {
|
|
const manifest = baseManifest({
|
|
taskKind: 'new-generation',
|
|
pipeline: { stages: [] },
|
|
});
|
|
const out = resolveAppliedPipeline({ manifest, scenarios });
|
|
expect(out.source).toBe('scenario');
|
|
expect(out.scenarioId).toBe('od-new-generation');
|
|
});
|
|
});
|