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
161 lines
6.2 KiB
TypeScript
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');
|
|
});
|
|
});
|