Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

161 lines
6.2 KiB
TypeScript

// Snapshot writer integration test (spec §8.2.1).
//
// The applied_plugin_snapshots table has exactly one writer: the
// `snapshots.ts` module. This test exercises createSnapshot →
// linkSnapshotToRun → markSnapshotStale and asserts:
// - Inserted rows expose the AppliedPluginSnapshot shape.
// - Linking to a run pins expires_at = NULL.
// - Marking stale flips status to 'stale'.
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import Database from 'better-sqlite3';
import { migratePlugins } from '../src/plugins/persistence.js';
import {
createSnapshot,
getSnapshot,
linkSnapshotToRun,
linkSnapshotToProject,
markSnapshotStale,
restoreProjectSnapshotLink,
} from '../src/plugins/snapshots.js';
let db: Database.Database;
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), 'od-snapshot-'));
db = new Database(path.join(tmpDir, 'test.sqlite'));
// Minimal projects/conversations tables so the FK ON DELETE clauses
// don't blow up the migration.
db.exec(`
CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);
CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT, title TEXT);
`);
migratePlugins(db);
});
afterEach(async () => {
db.close();
await rm(tmpDir, { recursive: true, force: true });
});
const baseInput = (extra: Partial<Parameters<typeof createSnapshot>[1]> = {}) => ({
projectId: 'project-1',
conversationId: null,
runId: null,
pluginId: 'sample-plugin',
pluginVersion: '1.0.0',
pluginTitle: 'Sample Plugin',
pluginDescription: 'Fixture',
manifestSourceDigest: 'digest-1',
sourceMarketplaceId: null,
pinnedRef: null,
taskKind: 'new-generation' as const,
inputs: { topic: 'design' },
resolvedContext: { items: [] },
pipeline: undefined,
genuiSurfaces: [],
capabilitiesGranted: ['prompt:inject'],
capabilitiesRequired: ['prompt:inject'],
assetsStaged: [],
connectorsRequired: [],
connectorsResolved: [],
mcpServers: [],
query: 'Make a deck',
...extra,
});
describe('snapshots writer', () => {
it('createSnapshot inserts a row and round-trips via getSnapshot', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput({
sourceMarketplaceId: 'official',
sourceMarketplaceEntryName: 'open-design/sample-plugin',
sourceMarketplaceEntryVersion: '1.0.0',
marketplaceTrust: 'official',
resolvedSource: 'github:open-design/plugins@abc123/sample-plugin',
resolvedRef: 'abc123',
archiveIntegrity: 'sha512-fixture',
}));
expect(snap.snapshotId).toMatch(/^[0-9a-f-]{36}$/);
const fetched = getSnapshot(db, snap.snapshotId);
expect(fetched).not.toBeNull();
expect(fetched!.pluginId).toBe('sample-plugin');
expect(fetched!.manifestSourceDigest).toBe('digest-1');
expect(fetched!.sourceMarketplaceId).toBe('official');
expect(fetched!.sourceMarketplaceEntryName).toBe('open-design/sample-plugin');
expect(fetched!.sourceMarketplaceEntryVersion).toBe('1.0.0');
expect(fetched!.marketplaceTrust).toBe('official');
expect(fetched!.resolvedSource).toBe('github:open-design/plugins@abc123/sample-plugin');
expect(fetched!.resolvedRef).toBe('abc123');
expect(fetched!.archiveIntegrity).toBe('sha512-fixture');
expect(fetched!.status).toBe('fresh');
});
it('persists apply-time craft requirements on the immutable snapshot', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput({
craftRequires: ['typography', 'color', 'anti-ai-slop'],
}));
const fetched = getSnapshot(db, snap.snapshotId);
expect(fetched?.craftRequires).toEqual(['typography', 'color', 'anti-ai-slop']);
});
it('linkSnapshotToRun pins expires_at to NULL', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput());
const beforeRow = db.prepare('SELECT expires_at FROM applied_plugin_snapshots WHERE id = ?').get(snap.snapshotId) as { expires_at: number | null };
// Either null (TTL=0) or a numeric expiry — we care that linking nulls it.
void beforeRow;
linkSnapshotToRun(db, snap.snapshotId, 'run-1');
const after = db.prepare('SELECT run_id, expires_at FROM applied_plugin_snapshots WHERE id = ?').get(snap.snapshotId) as { run_id: string; expires_at: number | null };
expect(after.run_id).toBe('run-1');
expect(after.expires_at).toBeNull();
});
it('restoreProjectSnapshotLink makes an unlinked discarded snapshot expirable again', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const previous = createSnapshot(db, baseInput({ query: 'Previous {{topic}}' }));
linkSnapshotToProject(db, previous.snapshotId, 'project-1');
const discarded = createSnapshot(db, baseInput({ query: 'Discarded {{topic}}' }));
linkSnapshotToProject(db, discarded.snapshotId, 'project-1');
restoreProjectSnapshotLink(
db,
'project-1',
discarded.snapshotId,
previous.snapshotId,
'run-that-was-never-linked',
);
const project = db.prepare(
`SELECT applied_plugin_snapshot_id AS appliedPluginSnapshotId
FROM projects
WHERE id = ?`,
).get('project-1') as { appliedPluginSnapshotId: string | null };
const discardedRow = db.prepare(
`SELECT run_id AS runId, expires_at AS expiresAt
FROM applied_plugin_snapshots
WHERE id = ?`,
).get(discarded.snapshotId) as { runId: string | null; expiresAt: number | null };
expect(project.appliedPluginSnapshotId).toBe(previous.snapshotId);
expect(discardedRow.runId).toBeNull();
expect(discardedRow.expiresAt).not.toBeNull();
});
it('markSnapshotStale flips status', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput());
markSnapshotStale(db, snap.snapshotId);
const fetched = getSnapshot(db, snap.snapshotId);
expect(fetched!.status).toBe('stale');
});
});