open-design/apps/daemon/tests/plugins-handoff.test.ts
Cursor Agent e6eaa62294
feat(plugins): handoff atom + ArtifactManifest provenance fields
Plan N3 / spec §11.5.1 / §21.5.

@open-design/contracts ArtifactManifest gains the spec §11.5.1
provenance + downstream-distribution surface as additive optional
fields:

  sourcePluginSnapshotId / sourcePluginId / sourcePluginVersion /
    sourceTaskKind / sourceRunId / sourceProjectId / parentArtifactId
  artifactKind / renderKind / handoffKind
  exportTargets[] / deployTargets[]

Spec §11.5.1 invariants:
  - sourcePluginSnapshotId NEVER changes after first write.
  - exportTargets[] / deployTargets[] are append-only.
  - handoffKind promotes monotonically along
    design-only < implementation-plan < patch < deployable-app.

apps/daemon/src/plugins/atoms/handoff.ts ships the daemon-side
helper:

  recordHandoff({ manifest, exportTarget?, deployTarget?,
                  handoffKind?, enforceMonotonicHandoff? })
    → { manifest, changed }

  - Idempotent: a (surface, target) pair only ever lands once on
    exportTargets[]; same for (provider, location) on deployTargets[].
  - handoffKind defaults to monotonic; pass enforceMonotonicHandoff:
    false on a rollback path.

  isDeployableAppEligible({ manifest, buildPassing, testsPassing })
    → boolean

  Spec §11.5.1 promotion rule for the deployable-app tier: requires
  build.passing + tests.passing AND at least one exportTargets[]
  entry on docker / cli surface. Centralises the rule so plugins
  don't reimplement it.

packages/contracts/src/index.ts now uses .js extensions on every
re-export so the daemon's NodeNext moduleResolution picks up the
new types end-to-end.

Daemon tests: 1534 → 1543 (+9 cases on plugins-handoff: appends
exportTargets / deployTargets, idempotency, monotonic handoffKind
promotion, downgrade refusal vs. rollback escape, deployable-app
eligibility rule).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 14:48:29 +00:00

99 lines
3.9 KiB
TypeScript

// Phase 7-8 entry slice / spec §11.5.1 — handoff atom helper.
import { describe, expect, it } from 'vitest';
import type { ArtifactManifest } from '@open-design/contracts';
import { isDeployableAppEligible, recordHandoff } from '../src/plugins/atoms/handoff.js';
const baseManifest = (extra: Partial<ArtifactManifest> = {}): ArtifactManifest => ({
version: 1,
kind: 'html',
title: 'Fixture',
entry: 'index.html',
renderer: 'html',
exports: [],
...extra,
});
describe('recordHandoff — append-only contracts', () => {
it('appends a new exportTargets entry', () => {
const out = recordHandoff({
manifest: baseManifest(),
exportTarget: { surface: 'cli', target: '/workspace/od/x.html', exportedAt: 1000 },
});
expect(out.changed).toContain('exportTargets');
expect(out.manifest.exportTargets).toEqual([
{ surface: 'cli', target: '/workspace/od/x.html', exportedAt: 1000 },
]);
});
it('is idempotent on identical (surface, target) pairs', () => {
const first = recordHandoff({
manifest: baseManifest(),
exportTarget: { surface: 'cli', target: '/p/a.html', exportedAt: 1 },
});
const second = recordHandoff({
manifest: first.manifest,
exportTarget: { surface: 'cli', target: '/p/a.html', exportedAt: 5 },
});
expect(second.changed).toEqual([]);
expect(second.manifest.exportTargets?.length).toBe(1);
});
it('appends deployTargets independently', () => {
const out = recordHandoff({
manifest: baseManifest(),
deployTarget: { provider: 'aws', location: 'arn:aws:ecs:...', deployedAt: 2 },
});
expect(out.changed).toEqual(['deployTargets']);
expect(out.manifest.deployTargets?.[0]?.provider).toBe('aws');
});
});
describe('recordHandoff — handoffKind monotonicity', () => {
it('records the first handoffKind unconditionally', () => {
const out = recordHandoff({
manifest: baseManifest(),
handoffKind: 'design-only',
});
expect(out.manifest.handoffKind).toBe('design-only');
expect(out.changed).toContain('handoffKind');
});
it('promotes handoffKind along the axis design-only → implementation-plan → patch → deployable-app', () => {
const a = recordHandoff({ manifest: baseManifest({ handoffKind: 'design-only' }), handoffKind: 'patch' });
expect(a.manifest.handoffKind).toBe('patch');
const b = recordHandoff({ manifest: a.manifest, handoffKind: 'deployable-app' });
expect(b.manifest.handoffKind).toBe('deployable-app');
});
it('refuses to downgrade when enforceMonotonicHandoff is on (default)', () => {
const a = recordHandoff({ manifest: baseManifest({ handoffKind: 'patch' }), handoffKind: 'design-only' });
expect(a.manifest.handoffKind).toBe('patch');
expect(a.changed).not.toContain('handoffKind');
});
it('allows downgrade when enforceMonotonicHandoff is false (rollback path)', () => {
const a = recordHandoff({
manifest: baseManifest({ handoffKind: 'deployable-app' }),
handoffKind: 'patch',
enforceMonotonicHandoff: false,
});
expect(a.manifest.handoffKind).toBe('patch');
});
});
describe('isDeployableAppEligible', () => {
it('requires both build + tests passing', () => {
const m = baseManifest({ exportTargets: [{ surface: 'docker', target: 'ghcr.io/od/x:1', exportedAt: 1 }] });
expect(isDeployableAppEligible({ manifest: m, buildPassing: true, testsPassing: true })).toBe(true);
expect(isDeployableAppEligible({ manifest: m, buildPassing: false, testsPassing: true })).toBe(false);
expect(isDeployableAppEligible({ manifest: m, buildPassing: true, testsPassing: false })).toBe(false);
});
it('requires at least one docker or cli exportTarget', () => {
const m = baseManifest({
exportTargets: [{ surface: 'figma', target: 'file/abc', exportedAt: 1 }],
});
expect(isDeployableAppEligible({ manifest: m, buildPassing: true, testsPassing: true })).toBe(false);
});
});