open-design/apps/daemon/tests/plugins-handoff-pipeline.test.ts
Cursor Agent 6fa42ca159
feat(plugins): runHandoffAtom() pipeline-driven bridge (Phase 8 entry slice)
Plan S1 / spec §11.5.1 / §21.5.

apps/daemon/src/plugins/atoms/handoff.ts gains the on-disk bridge
runHandoffAtom({ cwd, manifest, exportTarget?, deployTarget? })
that reads the canonical state previous atoms wrote and returns
the updated manifest with the right handoffKind / exportTargets[]
attached.

Promotion ladder (spec §11.5.1):

  decision='reject'                                       → 'design-only'
  decision='accept'/'partial' AND no build-test report    → 'implementation-plan'
  + build.passing OR tests.passing                        → 'patch'
  + build.passing AND tests.passing AND docker/cli export → 'deployable-app'

Inputs read from cwd:

  <cwd>/review/decision.json       — diff-review atom output
  <cwd>/critique/build-test.json   — build-test atom signals

Both files are optional; missing → the bridge skips the matching
ladder rung. Monotonicity enforced via recordHandoff() — a manifest
that already carries handoffKind='patch' won't demote to
'design-only' even if a follow-up reject arrives (the reject path
documents the rollback escape hatch via enforceMonotonicHandoff:
false).

Returns:

  { manifest, changed, signals: {
      decision?, buildPassing?, testsPassing?, deployable
  } }

Caller (the pipeline runner / od plugin export / docker tools-pack
hook) is responsible for persisting the updated manifest. Calls
are idempotent: re-running across already-recorded targets is a
no-op via the recordHandoff() append-only contract.

Daemon tests: 1629 → 1639 (+10 cases on plugins-handoff-pipeline:
all four ladder rungs (reject → design-only, accept-noBT →
implementation-plan, accept+oneSignal → patch, accept+bothSignals
+docker/cli → deployable-app), partial decision behaves like
accept, missing decision file leaves manifest alone, append-only
exportTargets[] dedupe across re-runs, refuses to demote across
the monotonic invariant, signals forwarding).

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

157 lines
5.9 KiB
TypeScript

// Phase 8 entry slice — runHandoffAtom() pipeline-driven bridge.
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 type { ArtifactManifest, ArtifactExportTarget } from '@open-design/contracts';
import { runHandoffAtom } from '../src/plugins/atoms/handoff.js';
let cwd: string;
beforeEach(async () => {
cwd = await mkdtemp(path.join(os.tmpdir(), 'od-handoff-pipeline-'));
});
afterEach(async () => {
await rm(cwd, { recursive: true, force: true });
});
const baseManifest = (over: Partial<ArtifactManifest> = {}): ArtifactManifest => ({
version: 1,
kind: 'react-component',
title: 'Patch artifact',
entry: 'index.tsx',
renderer: 'react-component',
exports: [],
...over,
});
async function writeBuildTestReport(buildPassing: boolean, testsPassing: boolean) {
await mkdir(path.join(cwd, 'critique'), { recursive: true });
await writeFile(
path.join(cwd, 'critique', 'build-test.json'),
JSON.stringify({
build: { status: buildPassing ? 'passing' : 'failing' },
tests: { status: testsPassing ? 'passing' : 'failing' },
signals: {
'build.passing': buildPassing,
'tests.passing': testsPassing,
'critique.score': buildPassing && testsPassing ? 5 : 1,
},
}, null, 2),
);
}
async function writeDecision(decision: 'accept' | 'reject' | 'partial') {
await mkdir(path.join(cwd, 'review'), { recursive: true });
await writeFile(
path.join(cwd, 'review', 'decision.json'),
JSON.stringify({ decision, accepted_files: [], rejected_files: [], reviewer: 'user', decidedAt: new Date().toISOString() }),
);
}
describe('runHandoffAtom — promotion ladder', () => {
it("decision='reject' \u2192 handoffKind='design-only'", async () => {
await writeDecision('reject');
const out = await runHandoffAtom({ cwd, manifest: baseManifest() });
expect(out.manifest.handoffKind).toBe('design-only');
expect(out.signals.decision).toBe('reject');
});
it("decision='accept' without build-test \u2192 handoffKind='implementation-plan'", async () => {
await writeDecision('accept');
const out = await runHandoffAtom({ cwd, manifest: baseManifest() });
expect(out.manifest.handoffKind).toBe('implementation-plan');
});
it("decision='accept' + build.passing OR tests.passing \u2192 'patch'", async () => {
await writeDecision('accept');
await writeBuildTestReport(true, false);
const a = await runHandoffAtom({ cwd, manifest: baseManifest() });
expect(a.manifest.handoffKind).toBe('patch');
await writeBuildTestReport(false, true);
const b = await runHandoffAtom({ cwd, manifest: baseManifest() });
expect(b.manifest.handoffKind).toBe('patch');
});
it("decision='accept' + both signals + docker exportTarget \u2192 'deployable-app'", async () => {
await writeDecision('accept');
await writeBuildTestReport(true, true);
const exportTarget: ArtifactExportTarget = { surface: 'docker', target: 'ghcr.io/od/x:1', exportedAt: 1 };
const out = await runHandoffAtom({ cwd, manifest: baseManifest(), exportTarget });
expect(out.manifest.handoffKind).toBe('deployable-app');
expect(out.signals.deployable).toBe(true);
expect(out.manifest.exportTargets).toEqual([exportTarget]);
});
it("decision='accept' + both signals WITHOUT docker/cli export \u2192 stays 'patch'", async () => {
await writeDecision('accept');
await writeBuildTestReport(true, true);
const out = await runHandoffAtom({
cwd,
manifest: baseManifest(),
exportTarget: { surface: 'figma', target: 'file/abc', exportedAt: 2 },
});
expect(out.manifest.handoffKind).toBe('patch');
expect(out.signals.deployable).toBe(false);
});
it('partial decision behaves like accept on the promotion ladder', async () => {
await writeDecision('partial');
await writeBuildTestReport(true, true);
const out = await runHandoffAtom({
cwd,
manifest: baseManifest(),
exportTarget: { surface: 'cli', target: '/tmp/out', exportedAt: 3 },
});
expect(out.manifest.handoffKind).toBe('deployable-app');
});
it('no decision file \u2192 leaves handoffKind alone', async () => {
const out = await runHandoffAtom({ cwd, manifest: baseManifest({ handoffKind: 'patch' }) });
expect(out.manifest.handoffKind).toBe('patch');
});
});
describe('runHandoffAtom — append-only contract', () => {
it('preserves existing exportTargets[] + appends without duplicates', async () => {
await writeDecision('accept');
const incoming: ArtifactExportTarget = { surface: 'cli', target: '/p/a.html', exportedAt: 1 };
const initial = baseManifest({
exportTargets: [{ surface: 'docker', target: 'ghcr.io/od/x:1', exportedAt: 0 }],
});
const a = await runHandoffAtom({ cwd, manifest: initial, exportTarget: incoming });
expect(a.manifest.exportTargets).toEqual([
{ surface: 'docker', target: 'ghcr.io/od/x:1', exportedAt: 0 },
incoming,
]);
// Re-record same target → no duplicate.
const b = await runHandoffAtom({ cwd, manifest: a.manifest, exportTarget: incoming });
expect(b.manifest.exportTargets?.length).toBe(2);
});
it('refuses to demote handoffKind via the monotonic invariant', async () => {
await writeDecision('reject'); // would map to 'design-only'
const out = await runHandoffAtom({
cwd,
manifest: baseManifest({ handoffKind: 'patch' }),
});
expect(out.manifest.handoffKind).toBe('patch');
});
});
describe('runHandoffAtom — signals', () => {
it('forwards parsed build/test signals when present', async () => {
await writeDecision('accept');
await writeBuildTestReport(true, false);
const out = await runHandoffAtom({ cwd, manifest: baseManifest() });
expect(out.signals).toMatchObject({
decision: 'accept',
buildPassing: true,
testsPassing: false,
deployable: false,
});
});
});