- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
514 lines
24 KiB
TypeScript
Executable File
514 lines
24 KiB
TypeScript
Executable File
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
defaultProjectContextDirectory,
|
|
MAX_CONTEXT_RECORDS,
|
|
normalizeContextKeywords,
|
|
normalizeContextText,
|
|
ProjectContextRepository,
|
|
searchProjectContext,
|
|
} from "../src/context/project-context-store.js";
|
|
import {
|
|
buildContextDocumentFromSnapshot,
|
|
enrichContextDocument,
|
|
getProjectContextTools,
|
|
MAX_CONCURRENT_SOURCE_FINGERPRINTS,
|
|
type PremiereContextSnapshot,
|
|
} from "../src/tools/project-context.js";
|
|
|
|
const temporaryDirectories: string[] = [];
|
|
const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
|
|
const supportsNodeSqlite = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 5);
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
|
});
|
|
|
|
function snapshot(overrides: Partial<PremiereContextSnapshot["sequence"]["clips"][number]> = {}): PremiereContextSnapshot {
|
|
return {
|
|
projectName: "Documentary",
|
|
projectPath: "D:/Projects/documentary.prproj",
|
|
sequence: {
|
|
id: "sequence-1",
|
|
name: "Assembly",
|
|
durationSeconds: 120,
|
|
truncated: false,
|
|
clips: [
|
|
{
|
|
nodeId: "timeline-1",
|
|
name: "Interview A",
|
|
startSeconds: 10,
|
|
endSeconds: 20,
|
|
inPointSeconds: 30,
|
|
outPointSeconds: 40,
|
|
speed: 1,
|
|
trackType: "video",
|
|
trackIndex: 0,
|
|
sourceId: "source-1",
|
|
sourceName: "Interview A.mov",
|
|
mediaPath: "D:/Media/interview-a.mov",
|
|
offline: false,
|
|
...overrides,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("project context index", () => {
|
|
it("separates source invalidation from timeline placement revisions", async () => {
|
|
let mediaVersion = 1;
|
|
const fingerprint = async () => ({ mediaPathHash: "path-hash", size: 1_000, modifiedMs: mediaVersion });
|
|
const first = await buildContextDocumentFromSnapshot(snapshot(), undefined, fingerprint);
|
|
const source = first.document.records.find((record) => record.kind === "source")!;
|
|
const enriched = enrichContextDocument(first.document, [
|
|
{
|
|
kind: "transcript",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
start_seconds: 31,
|
|
end_seconds: 35,
|
|
text: "The budget needs approval by Friday.",
|
|
keywords: ["budget", "approval"],
|
|
},
|
|
]).document;
|
|
|
|
const moved = await buildContextDocumentFromSnapshot(
|
|
snapshot({ startSeconds: 45, endSeconds: 55 }),
|
|
enriched,
|
|
fingerprint,
|
|
);
|
|
expect(moved.document.sourceRevision).toBe(first.document.sourceRevision);
|
|
expect(moved.document.timelineRevision).not.toBe(first.document.timelineRevision);
|
|
expect(moved.document.records.some((record) => record.kind === "transcript")).toBe(true);
|
|
expect(moved.invalidatedRecords).toBe(0);
|
|
|
|
mediaVersion = 2;
|
|
const changedSource = await buildContextDocumentFromSnapshot(snapshot(), moved.document, fingerprint);
|
|
expect(changedSource.document.sourceRevision).not.toBe(first.document.sourceRevision);
|
|
expect(changedSource.document.records.some((record) => record.kind === "transcript")).toBe(false);
|
|
expect(changedSource.invalidatedRecords).toBe(1);
|
|
});
|
|
|
|
it("bounds concurrent source fingerprint work during a large capture", async () => {
|
|
const largeSnapshot = snapshot();
|
|
largeSnapshot.sequence.clips = Array.from({ length: MAX_CONCURRENT_SOURCE_FINGERPRINTS * 3 }, (_, index) => ({
|
|
...largeSnapshot.sequence.clips[0]!,
|
|
nodeId: `timeline-${index}`,
|
|
sourceId: `source-${index}`,
|
|
mediaPath: `D:/Media/source-${index}.mov`,
|
|
}));
|
|
let active = 0;
|
|
let peak = 0;
|
|
await buildContextDocumentFromSnapshot(largeSnapshot, undefined, async () => {
|
|
active += 1;
|
|
peak = Math.max(peak, active);
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
active -= 1;
|
|
return {};
|
|
});
|
|
expect(peak).toBe(MAX_CONCURRENT_SOURCE_FINGERPRINTS);
|
|
});
|
|
|
|
it("ranks bounded transcript and audio evidence while preserving revision provenance", async () => {
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
const source = built.document.records.find((record) => record.kind === "source")!;
|
|
const enriched = enrichContextDocument(built.document, [
|
|
{
|
|
id: "budget-transcript",
|
|
kind: "transcript",
|
|
name: "Budget answer",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
start_seconds: 31,
|
|
end_seconds: 35,
|
|
text: "The budget needs approval by Friday.",
|
|
keywords: ["budget", "approval"],
|
|
},
|
|
{
|
|
id: "room-tone",
|
|
kind: "audio",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
text: "Quiet room tone suitable for dialogue padding.",
|
|
keywords: ["room tone"],
|
|
},
|
|
]).document;
|
|
|
|
const results = searchProjectContext(enriched, { query: "budget approval", limit: 1 });
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].record.id).toBe("budget-transcript");
|
|
expect(results[0].record.sourceRevision).toBe(source.sourceRevision);
|
|
expect(results[0].matchedTerms).toEqual(expect.arrayContaining(["budget", "approval"]));
|
|
});
|
|
|
|
it("persists locally without retaining a native media path", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const repository = new ProjectContextRepository({ backend: "json", directory });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "redacted-path-hash" }));
|
|
await repository.put(built.document);
|
|
|
|
const restored = await repository.get(built.document.projectId);
|
|
expect(restored).toEqual(built.document);
|
|
expect(await repository.backendName()).toBe("json");
|
|
const files = await readdir(directory);
|
|
const serialized = await readFile(path.join(directory, files.find((name) => name.endsWith(".json"))!), "utf8");
|
|
expect(serialized).not.toContain("D:/Media/interview-a.mov");
|
|
expect(serialized).toContain("redacted-path-hash");
|
|
});
|
|
|
|
it.runIf(supportsNodeSqlite)("uses SQLite when requested and supports complete repository lifecycle", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-sqlite-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const repository = new ProjectContextRepository({ backend: "sqlite", directory });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
|
|
expect(await repository.backendName()).toBe("sqlite");
|
|
expect(await repository.get("missing")).toBeUndefined();
|
|
await repository.put(built.document);
|
|
expect(await repository.get(built.document.projectId)).toEqual(built.document);
|
|
expect(await repository.list()).toEqual([
|
|
expect.objectContaining({ projectId: built.document.projectId, recordCount: built.document.records.length }),
|
|
]);
|
|
const updated = { ...built.document, projectName: "Updated documentary" };
|
|
await repository.put(updated);
|
|
expect((await repository.get(built.document.projectId))?.projectName).toBe("Updated documentary");
|
|
expect(await repository.delete(built.document.projectId)).toBe(true);
|
|
expect(await repository.delete(built.document.projectId)).toBe(false);
|
|
await repository.close();
|
|
});
|
|
|
|
it("handles missing and corrupt JSON entries without hiding valid project summaries", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-json-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const repository = new ProjectContextRepository({ backend: "json", directory });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
|
|
|
|
expect(await repository.get("missing")).toBeUndefined();
|
|
expect(await repository.delete("missing")).toBe(false);
|
|
await repository.put(built.document);
|
|
await writeFile(path.join(directory, "corrupt.json"), "not json", "utf8");
|
|
expect(await repository.list()).toEqual([
|
|
expect.objectContaining({ projectId: built.document.projectId }),
|
|
]);
|
|
|
|
const validFile = (await readdir(directory)).find((name) => name !== "corrupt.json")!;
|
|
await writeFile(path.join(directory, validFile), JSON.stringify({ schemaVersion: 999 }), "utf8");
|
|
await expect(repository.get(built.document.projectId)).rejects.toThrow("Unsupported project context schema version");
|
|
});
|
|
|
|
it("validates repository backend, record bounds, and context directory overrides", async () => {
|
|
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-env-test-"));
|
|
temporaryDirectories.push(directory);
|
|
vi.stubEnv("PREMIERE_CONTEXT_DIR", directory);
|
|
expect(defaultProjectContextDirectory()).toBe(path.resolve(directory));
|
|
await expect(new ProjectContextRepository({ backend: "invalid" as any }).backendName())
|
|
.rejects.toThrow("must be auto, sqlite, json, or memory");
|
|
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
await expect(repository.put({
|
|
...built.document,
|
|
records: Array.from({ length: MAX_CONTEXT_RECORDS + 1 }, () => built.document.records[0]),
|
|
})).rejects.toThrow(`limited to ${MAX_CONTEXT_RECORDS}`);
|
|
expect(await repository.get("missing")).toBeUndefined();
|
|
expect(await repository.list()).toEqual([]);
|
|
expect(await repository.delete("missing")).toBe(false);
|
|
});
|
|
|
|
it("normalizes bounded text and filters search by sequence, kind, and result limits", async () => {
|
|
expect(normalizeContextText(null)).toBe("");
|
|
expect(normalizeContextText(" multiple\n spaces ")).toBe("multiple spaces");
|
|
expect(normalizeContextKeywords("not-an-array")).toEqual([]);
|
|
expect(normalizeContextKeywords([" budget ", "budget", 4])).toEqual(["budget"]);
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
|
|
expect(() => searchProjectContext(built.document, { query: " " })).toThrow("must not be empty");
|
|
expect(searchProjectContext(built.document, {
|
|
query: "interview",
|
|
sequenceId: "different-sequence",
|
|
})).toEqual([]);
|
|
const sourceOnly = searchProjectContext(built.document, {
|
|
query: "interview",
|
|
kinds: ["source"],
|
|
limit: 100,
|
|
});
|
|
expect(sourceOnly).toHaveLength(1);
|
|
expect(sourceOnly[0].record.kind).toBe("source");
|
|
expect(searchProjectContext(built.document, { query: "not-present", limit: -4 })).toEqual([]);
|
|
});
|
|
|
|
it("supports enrich, status, search, plan, and clear through the MCP tool contracts", async () => {
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
await repository.put(built.document);
|
|
const tools = getProjectContextTools({}, { repository });
|
|
const source = built.document.records.find((record) => record.kind === "source")!;
|
|
|
|
const enrich = await (tools.manage_project_context.handler as any)({
|
|
action: "enrich",
|
|
project_id: built.document.projectId,
|
|
records: [
|
|
{
|
|
kind: "transcript",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
start_seconds: 31,
|
|
end_seconds: 35,
|
|
text: "The budget needs approval by Friday.",
|
|
keywords: ["budget"],
|
|
metadata: { confidence: 0.97, source_path: "D:/private.mov", api_token: "secret" },
|
|
},
|
|
],
|
|
});
|
|
expect(enrich).toMatchObject({ success: true, data: { upserted: 1 } });
|
|
|
|
const status = await (tools.manage_project_context.handler as any)({
|
|
action: "status",
|
|
project_id: built.document.projectId,
|
|
});
|
|
expect(status).toMatchObject({ success: true, data: { backend: "memory", counts: { transcript: 1 } } });
|
|
|
|
const search = await (tools.search_project_context.handler as any)({
|
|
project_id: built.document.projectId,
|
|
query: "budget",
|
|
});
|
|
expect(search.data.results[0]).toMatchObject({ kind: "transcript", sourceId: "source-1" });
|
|
expect(search.data.results[0].metadata).toEqual({ confidence: 0.97 });
|
|
await expect((tools.search_project_context.handler as any)({
|
|
project_id: built.document.projectId,
|
|
query: "budget",
|
|
max_results: 51,
|
|
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("1 through 50") });
|
|
|
|
const plan = await (tools.create_context_edit_plan.handler as any)({
|
|
project_id: built.document.projectId,
|
|
intent: "Use the budget answer",
|
|
});
|
|
expect(plan).toMatchObject({
|
|
success: true,
|
|
data: {
|
|
applied: false,
|
|
expectedTimelineRevision: built.document.timelineRevision,
|
|
candidates: [{ kind: "transcript", sourceId: "source-1" }],
|
|
},
|
|
});
|
|
expect(plan.data.nextSteps.join(" ")).toContain("preview_edit_plan");
|
|
|
|
const cleared = await (tools.manage_project_context.handler as any)({
|
|
action: "clear",
|
|
project_id: built.document.projectId,
|
|
});
|
|
expect(cleared).toEqual({ success: true, data: { projectId: built.document.projectId, cleared: true } });
|
|
});
|
|
|
|
it("imports explicit revision-bound editorial evidence without opening frames or calling a provider", async () => {
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
await repository.put(built.document);
|
|
const source = built.document.records.find((record) => record.kind === "source")!;
|
|
const tools = getProjectContextTools({}, { repository });
|
|
|
|
const result = await (tools.manage_project_context.handler as any)({
|
|
action: "import_evidence",
|
|
project_id: built.document.projectId,
|
|
evidence: [
|
|
{
|
|
type: "transcript_passage",
|
|
text: "The budget needs approval by Friday.",
|
|
speaker_label: "Presenter",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
sequence_id: "sequence-1",
|
|
timeline_item_id: "timeline-1",
|
|
timeline_revision: built.document.timelineRevision,
|
|
start_seconds: 31,
|
|
end_seconds: 35,
|
|
metadata: { confidence: 0.97, source_path: "D:/private.mov", api_token: "secret" },
|
|
},
|
|
{
|
|
type: "shot_log",
|
|
text: "Medium interview framing with a visible product sample.",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
},
|
|
{
|
|
type: "audio_observation",
|
|
text: "Clean dialogue; brief room-tone gap before the answer.",
|
|
},
|
|
{
|
|
type: "operator_note",
|
|
text: "Prefer this explanation after the opening question.",
|
|
},
|
|
{
|
|
type: "frame_reference",
|
|
frame_reference_id: "contact-sheet-003",
|
|
sequence_id: "sequence-1",
|
|
timeline_revision: built.document.timelineRevision,
|
|
},
|
|
{
|
|
type: "speaker_label",
|
|
speaker_label: "Presenter",
|
|
source_id: "source-1",
|
|
source_revision: source.sourceRevision,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
success: true,
|
|
data: {
|
|
applied: false,
|
|
upserted: 6,
|
|
importedEvidenceTypes: expect.arrayContaining(["transcript_passage", "frame_reference"]),
|
|
},
|
|
});
|
|
const current = await repository.get(built.document.projectId);
|
|
const transcript = current!.records.find((record) => record.kind === "transcript")!;
|
|
const frame = current!.records.find((record) => record.metadata?.frameReferenceId === "contact-sheet-003")!;
|
|
expect(transcript).toMatchObject({
|
|
sourceRevision: source.sourceRevision,
|
|
timelineRevision: built.document.timelineRevision,
|
|
metadata: { evidenceType: "transcript_passage", speakerLabel: "Presenter", confidence: 0.97 },
|
|
});
|
|
expect(transcript.metadata).not.toHaveProperty("source_path");
|
|
expect(transcript.metadata).not.toHaveProperty("api_token");
|
|
expect(frame).toMatchObject({ kind: "shot", timelineRevision: built.document.timelineRevision });
|
|
|
|
const moved = await buildContextDocumentFromSnapshot(
|
|
snapshot({ startSeconds: 45, endSeconds: 55 }),
|
|
current,
|
|
async () => ({ mediaPathHash: "hash" }),
|
|
);
|
|
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "transcript_passage")).toBe(false);
|
|
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "frame_reference")).toBe(false);
|
|
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "shot_log")).toBe(true);
|
|
expect(moved.invalidatedRecords).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("rejects unguarded, stale, unknown, and path-shaped editorial evidence references", async () => {
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
await repository.put(built.document);
|
|
const tools = getProjectContextTools({}, { repository });
|
|
const importEvidence = (evidence: unknown[]) => (tools.manage_project_context.handler as any)({
|
|
action: "import_evidence",
|
|
project_id: built.document.projectId,
|
|
evidence,
|
|
});
|
|
|
|
await expect(importEvidence([{ type: "shot_log", text: "Shot", source_id: "source-1" }]))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("source_revision") });
|
|
await expect(importEvidence([{ type: "operator_note", text: "Note", sequence_id: "sequence-1" }]))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timeline_revision") });
|
|
await expect(importEvidence([{ type: "frame_reference", frame_reference_id: "D:/private/frame.png" }]))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("opaque identifier") });
|
|
await expect(importEvidence([{ type: "operator_note", text: "Note", timeline_revision: "stale" }]))
|
|
.resolves.toMatchObject({ success: false, error: expect.stringContaining("requires sequence_id") });
|
|
});
|
|
|
|
it("captures through the MCP contract and preserves bridge failures", async () => {
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
const captureSnapshot = vi.fn().mockResolvedValue({ success: true, data: snapshot() });
|
|
const tools = getProjectContextTools({}, { repository, captureSnapshot });
|
|
const captured = await (tools.manage_project_context.handler as any)({ action: "capture" });
|
|
expect(captured).toMatchObject({
|
|
success: true,
|
|
data: { backend: "memory", projectName: "Documentary", recordCount: 4, invalidatedRecords: 0 },
|
|
});
|
|
expect(captureSnapshot).toHaveBeenCalledOnce();
|
|
|
|
const statusList = await (tools.manage_project_context.handler as any)({ action: "status" });
|
|
expect(statusList.data.projects).toHaveLength(1);
|
|
const failedTools = getProjectContextTools({}, {
|
|
repository,
|
|
captureSnapshot: async () => ({ success: false, error: "Premiere offline" }),
|
|
});
|
|
await expect((failedTools.manage_project_context.handler as any)({ action: "capture" }))
|
|
.resolves.toEqual({ success: false, error: "Premiere offline" });
|
|
});
|
|
|
|
it("returns bounded not-found and unsupported-action errors", async () => {
|
|
const repository = new ProjectContextRepository({ backend: "memory" });
|
|
const tools = getProjectContextTools({}, { repository });
|
|
await expect((tools.manage_project_context.handler as any)({ action: "status", project_id: "missing" }))
|
|
.resolves.toEqual({ success: false, error: "Project context not found" });
|
|
await expect((tools.manage_project_context.handler as any)({ action: "enrich", project_id: "missing", records: [] }))
|
|
.resolves.toEqual({ success: false, error: "Project context not found; capture it before enrichment" });
|
|
await expect((tools.search_project_context.handler as any)({ project_id: "missing", query: "x" }))
|
|
.resolves.toEqual({ success: false, error: "Project context not found" });
|
|
await expect((tools.create_context_edit_plan.handler as any)({ project_id: "missing", intent: "x" }))
|
|
.resolves.toEqual({ success: false, error: "Project context not found" });
|
|
await expect((tools.manage_project_context.handler as any)({ action: "unknown" }))
|
|
.resolves.toEqual({ success: false, error: "Unsupported project context action: unknown" });
|
|
await expect((tools.manage_project_context.handler as any)({ action: "clear", project_id: "" }))
|
|
.rejects.toThrow("project_id is required");
|
|
});
|
|
|
|
it("rejects stale enrichment revisions", async () => {
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
|
|
expect(() => enrichContextDocument(built.document, [
|
|
{
|
|
kind: "transcript",
|
|
source_id: "source-1",
|
|
source_revision: "stale",
|
|
text: "Old analysis",
|
|
},
|
|
])).toThrow("stale source_revision");
|
|
});
|
|
|
|
it("validates enrichment shapes, bounds, replacement, and timeline revisions", async () => {
|
|
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
|
|
expect(() => enrichContextDocument(built.document, [])).toThrow("at least one enrichment");
|
|
expect(() => enrichContextDocument(built.document, Array.from({ length: 513 }, () => ({
|
|
kind: "note" as const,
|
|
text: "note",
|
|
})))).toThrow("limited to 512");
|
|
expect(() => enrichContextDocument(built.document, [{ kind: "source", text: "bad" }]))
|
|
.toThrow("kind must be transcript, shot, audio, or note");
|
|
expect(() => enrichContextDocument(built.document, [{ kind: "note", text: " " }]))
|
|
.toThrow("text must not be empty");
|
|
expect(() => enrichContextDocument(built.document, [{ kind: "audio", source_id: "missing", text: "quiet" }]))
|
|
.toThrow("unknown source_id");
|
|
expect(() => enrichContextDocument(built.document, [{
|
|
kind: "note",
|
|
timeline_revision: "stale",
|
|
text: "stale note",
|
|
}])).toThrow("stale timeline_revision");
|
|
expect(() => enrichContextDocument(built.document, [{ kind: "shot", start_seconds: -1, text: "bad" }]))
|
|
.toThrow("start_seconds must be non-negative");
|
|
expect(() => enrichContextDocument(built.document, [{ kind: "shot", start_seconds: 5, end_seconds: 4, text: "bad" }]))
|
|
.toThrow("end_seconds must be at or after");
|
|
|
|
const first = enrichContextDocument(built.document, [{ id: "replace-me", kind: "note", text: "first" }]).document;
|
|
const replaced = enrichContextDocument(first, [{
|
|
id: "replace-me",
|
|
kind: "note",
|
|
text: "second",
|
|
timeline_item_id: "timeline-1",
|
|
timeline_revision: first.timelineRevision,
|
|
track_type: "video",
|
|
track_index: 0,
|
|
metadata: { approved: true, score: 1, nullable: null, nested: { ignored: true } },
|
|
}], true).document;
|
|
expect(replaced.records.filter((record) => record.kind === "note")).toEqual([
|
|
expect.objectContaining({ text: "second", timelineRevision: first.timelineRevision }),
|
|
]);
|
|
});
|
|
|
|
it("validates incomplete and oversized Premiere snapshots", async () => {
|
|
await expect(buildContextDocumentFromSnapshot({ projectName: "bad" } as any))
|
|
.rejects.toThrow("incomplete context snapshot");
|
|
const oversized = snapshot();
|
|
oversized.sequence.clips = Array.from({ length: 2_001 }, (_, index) => ({
|
|
...snapshot().sequence.clips[0],
|
|
nodeId: `clip-${index}`,
|
|
}));
|
|
await expect(buildContextDocumentFromSnapshot(oversized))
|
|
.rejects.toThrow("limited to 2000 timeline items");
|
|
});
|
|
});
|