Files
jhonny-editor/bm/premiere-pro-mcp-main/tests/uxp/stable-workflows.test.ts
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

1089 lines
62 KiB
TypeScript
Executable File

import { createRequire } from "node:module";
import { describe, expect, it, vi } from "vitest";
const require = createRequire(import.meta.url);
const Commands = require("../../uxp-plugin/commands.cjs");
const Protocol = require("../../uxp-plugin/protocol.cjs");
function stableHost() {
const makeComponent = (matchName: string, displayName = matchName) => ({
getMatchName: vi.fn(async () => matchName),
getDisplayName: vi.fn(async () => displayName),
getParamCount: vi.fn(() => 2),
});
const components = [makeComponent("Intrinsic", "Intrinsic")];
const chain = {
getComponentCount: vi.fn(() => components.length),
getComponentAtIndex: vi.fn((index: number) => components[index]),
createAppendComponentAction: vi.fn((component: ReturnType<typeof makeComponent>) => ({ apply: () => components.push(component) })),
createInsertComponentAction: vi.fn((component: ReturnType<typeof makeComponent>, index: number) => ({ apply: () => components.splice(index, 0, component) })),
createRemoveComponentAction: vi.fn((component: ReturnType<typeof makeComponent>) => ({ apply: () => components.splice(components.indexOf(component), 1) })),
};
const sourceClip = {
isClip: true,
name: "Interview.mov",
getId: vi.fn(async () => "source-1"),
canProxy: vi.fn(async () => true),
hasProxy: vi.fn(async () => proxyPath.length > 0),
getProxyPath: vi.fn(async () => proxyPath),
attachProxy: vi.fn(async (path: string) => { proxyPath = path; return true; }),
canChangeMediaPath: vi.fn(async () => true),
isOffline: vi.fn(async () => offline),
getMediaFilePath: vi.fn(async () => mediaPath),
changeMediaFilePath: vi.fn(async (path: string) => { mediaPath = path; offline = false; return true; }),
refreshMedia: vi.fn(async () => true),
getFootageInterpretation: vi.fn(async () => footage),
createSetFootageInterpretationAction: vi.fn(() => ({ apply: () => undefined })),
getInputLUTID: vi.fn(async () => inputLutId),
createSetInputLUTIDAction: vi.fn((value: string) => ({ apply: () => { inputLutId = value; } })),
getEmbeddedLUTID: vi.fn(async () => "embedded-lut"),
};
let proxyPath = "", mediaPath = "D:/Approved/missing.mov", offline = true, inputLutId = "";
const footageValues: Record<string, unknown> = {
frameRate: 23.976, pixelAspectRatio: 1, fieldType: 1, removePullDown: false,
alphaUsage: 0, ignoreAlpha: false, invertAlpha: false, vrConform: 0,
vrLayout: 0, vrHorzView: 180, vrVertView: 180, footageInputLutId: "",
};
const getterNames: Record<string, string> = {
getFrameRate: "frameRate", getPixelAspectRatio: "pixelAspectRatio", getFieldType: "fieldType",
getRemovePullDown: "removePullDown", getAlphaUsage: "alphaUsage", getIgnoreAlpha: "ignoreAlpha",
getInvertAlpha: "invertAlpha", getVrConform: "vrConform", getVrLayout: "vrLayout",
getVrHorzView: "vrHorzView", getVrVertView: "vrVertView", getInputLUTID: "footageInputLutId",
};
const setterNames: Record<string, string> = {
setFrameRate: "frameRate", setPixelAspectRatio: "pixelAspectRatio", setFieldType: "fieldType",
setRemovePullDown: "removePullDown", setAlphaUsage: "alphaUsage", setIgnoreAlpha: "ignoreAlpha",
setInvertAlpha: "invertAlpha", setVrConform: "vrConform", setVrLayout: "vrLayout",
setVrHorzView: "vrHorzView", setVrVertView: "vrVertView", setInputLUTID: "footageInputLutId",
};
const footage: Record<string, (...args: unknown[]) => unknown> = {};
for (const [method, key] of Object.entries(getterNames)) footage[method] = vi.fn(() => footageValues[key]);
for (const [method, key] of Object.entries(setterNames)) footage[method] = vi.fn((value: unknown) => { footageValues[key] = value; return true; });
const videoItem = {
name: "Interview V",
getTrackIndex: vi.fn(async () => 0),
getMatchName: vi.fn(async () => "clip-video-match"),
getType: vi.fn(async () => 1),
getMediaType: vi.fn(async () => "video-media-guid"),
getIsSelected: vi.fn(async () => true),
getComponentChain: vi.fn(async () => chain),
getProjectItem: vi.fn(async () => sourceClip),
getStartTime: vi.fn(async () => ({ seconds: 10 })),
getEndTime: vi.fn(async () => ({ seconds: 20 })),
};
const audioComponents = [makeComponent("Volume", "Volume")];
const audioChain = {
getComponentCount: vi.fn(() => audioComponents.length),
getComponentAtIndex: vi.fn((index: number) => audioComponents[index]),
createAppendComponentAction: vi.fn((component: ReturnType<typeof makeComponent>) => ({ apply: () => audioComponents.push(component) })),
createInsertComponentAction: vi.fn((component: ReturnType<typeof makeComponent>, index: number) => ({ apply: () => audioComponents.splice(index, 0, component) })),
createRemoveComponentAction: vi.fn((component: ReturnType<typeof makeComponent>) => ({ apply: () => audioComponents.splice(audioComponents.indexOf(component), 1) })),
};
const audioItem = {
name: "Interview A",
getTrackIndex: vi.fn(async () => 0),
getMatchName: vi.fn(async () => "clip-audio-match"),
getType: vi.fn(async () => 1),
getMediaType: vi.fn(async () => "audio-media-guid"),
getIsSelected: vi.fn(async () => false),
getComponentChain: vi.fn(async () => audioChain),
getProjectItem: vi.fn(async () => sourceClip),
getStartTime: vi.fn(async () => ({ seconds: 10 })),
getEndTime: vi.fn(async () => ({ seconds: 20 })),
};
const videoTrack = { getTrackItems: vi.fn(async () => [videoItem]) };
const audioTrack = { getTrackItems: vi.fn(async () => [audioItem]) };
type MockTrackItem = typeof videoItem | typeof audioItem;
let selectedItems: MockTrackItem[] = [videoItem];
const makeSelection = (initial: MockTrackItem[]) => {
const items = [...initial];
return {
items,
addItem: vi.fn((item: MockTrackItem, skipDuplicateCheck = false) => {
if (!skipDuplicateCheck && items.includes(item)) return false;
items.push(item);
return true;
}),
removeItem: vi.fn((item: MockTrackItem) => {
const index = items.indexOf(item);
if (index < 0) return false;
items.splice(index, 1);
return true;
}),
getTrackItems: vi.fn(async () => [...items]),
};
};
const sequence = {
guid: "sequence-1",
getSelection: vi.fn(async () => makeSelection(selectedItems)),
setSelection: vi.fn((selection: ReturnType<typeof makeSelection>) => {
selectedItems = [...selection.items];
return true;
}),
clearSelection: vi.fn(async () => { selectedItems = []; return true; }),
getVideoTrackCount: vi.fn(async () => 1),
getVideoTrack: vi.fn(async () => videoTrack),
getAudioTrackCount: vi.fn(async () => 1),
getAudioTrack: vi.fn(async () => audioTrack),
};
const markerValues: Array<{ guid: string }> = [];
const markers = { getMarkers: vi.fn(async () => markerValues) };
const root = { isFolder: true, getItems: vi.fn(async () => [sourceClip]) };
let projectMetadata = "project-before", xmpMetadata = "xmp-before", projectColumnsMetadata = "columns-before", projectPanelMetadata = "panel-before", ingestEnabled = false;
const metadataSchemaFields: Array<{ name: string; label: string; type: number }> = [];
const ingestSettings = {
getIsIngestEnabled: vi.fn(async () => ingestEnabled),
setIngestEnabled: vi.fn(async (value: boolean) => { ingestEnabled = value; return true; }),
};
const scratchPaths = new Map<number, string>([[0, "capture-before"], [1, "audio-before"], [2, "video-before"]]);
const scratchSettings = {
getScratchDiskPath: vi.fn((type: number) => scratchPaths.get(type) ?? "unset"),
setScratchDiskPath: vi.fn((type: number, destination: number) => { scratchPaths.set(type, `destination-${destination}`); return true; }),
};
const addAction = vi.fn((action: { apply?: () => void }) => { action.apply?.(); return true; });
const project = {
guid: "project-1",
getActiveSequence: vi.fn(async () => sequence),
getRootItem: vi.fn(async () => root),
getColorSettings: vi.fn(async () => ({
getGraphicsWhiteLuminance: vi.fn(async () => 203),
getSupportedGraphicsWhiteLuminances: vi.fn(async () => [100, 203, 300]),
})),
lockedAccess: vi.fn((callback: () => void) => callback()),
executeTransaction: vi.fn((callback: (compound: { addAction: typeof addAction }) => void) => { callback({ addAction }); return true; }),
};
let monitorItem: typeof sourceClip | null = null, monitorPosition = 0;
const ppro = {
Project: { getActiveProject: vi.fn(async () => project) },
Utils: { isAEInstalled: vi.fn(async () => true) },
ProjectItem: { cast: vi.fn((item: unknown) => item) },
ClipProjectItem: { cast: vi.fn((item: { isClip?: boolean }) => { if (!item.isClip) throw new Error("not clip"); return item; }) },
FolderItem: { cast: vi.fn((item: { isFolder?: boolean }) => { if (!item.isFolder) throw new Error("not folder"); return item; }) },
ProjectUtils: { getSelection: vi.fn(async () => ({ getItems: vi.fn(async () => [sourceClip]) })) },
TrackItemSelection: {
createEmptySelection: vi.fn((callback: (selection: ReturnType<typeof makeSelection>) => void) => {
callback(makeSelection([]));
return true;
}),
},
Constants: {
TrackItemType: { CLIP: 1 },
ScratchDiskFolderType: { CAPTURE: 0, AUDIO_PREVIEW: 1, VIDEO_PREVIEW: 2, AUTO_SAVE: 3, CCL_LIBRARIES: 4, CAPSULE_MEDIA: 5 },
ScratchDiskFolder: { SAME_AS_PROJECT: 10, MY_DOCUMENTS: 11 },
},
VideoFilterFactory: {
getMatchNames: vi.fn(async () => ["PR.Test"]),
getDisplayNames: vi.fn(async () => ["Test Video Effect"]),
createComponent: vi.fn(async (name: string) => makeComponent(name, "Test Video Effect")),
},
AudioFilterFactory: {
getDisplayNames: vi.fn(async () => ["Test Audio Effect"]),
createComponentByDisplayName: vi.fn(async (name: string) => makeComponent(name, name)),
},
SequenceUtils: {
SEQUENCE_OPERATION_APPLYCUT: "ApplyCuts",
SEQUENCE_OPERATION_CREATEMARKER: "CreateMarkers",
SEQUENCE_OPERATION_CREATESUBCLIP: "CreateSubclips",
performSceneEditDetectionOnSelection: vi.fn(async (operation: string) => {
if (operation === "CreateMarkers") markerValues.push({ guid: `marker-${markerValues.length + 1}` });
return true;
}),
},
Markers: { getMarkers: vi.fn(async () => markers) },
ProjectSettings: {
getIngestSettings: vi.fn(async () => ingestSettings),
createSetIngestSettingsAction: vi.fn(() => ({ apply: () => undefined })),
getScratchDiskSettings: vi.fn(async () => scratchSettings),
createSetScratchDiskSettingsAction: vi.fn(() => ({ apply: () => undefined })),
},
Metadata: {
getProjectMetadata: vi.fn(async () => projectMetadata),
getXMPMetadata: vi.fn(async () => xmpMetadata),
getProjectColumnsMetadata: vi.fn(async () => projectColumnsMetadata),
getProjectPanelMetadata: vi.fn(async () => projectPanelMetadata),
setProjectPanelMetadata: vi.fn(async (value: string) => { projectPanelMetadata = value; return true; }),
METADATA_TYPE_INTEGER: 1,
METADATA_TYPE_REAL: 2,
METADATA_TYPE_TEXT: 3,
METADATA_TYPE_BOOLEAN: 4,
addPropertyToProjectMetadataSchema: vi.fn(async (name: string, label: string, type: number) => {
metadataSchemaFields.push({ name, label, type });
projectPanelMetadata += `|schema:${name}:${label}:${type}`;
return true;
}),
createSetProjectMetadataAction: vi.fn((_item: unknown, value: string) => ({ apply: () => { projectMetadata = value; } })),
createSetXMPMetadataAction: vi.fn((_item: unknown, value: string) => ({ apply: () => { xmpMetadata = value; } })),
},
SourceMonitor: {
getPosition: vi.fn(async () => ({ seconds: monitorPosition })),
setPosition: vi.fn(async (value: { seconds: number }) => { monitorPosition = value.seconds; return true; }),
getProjectItem: vi.fn(async () => monitorItem),
openProjectItem: vi.fn(async (item: typeof sourceClip) => { monitorItem = item; return true; }),
openFilePath: vi.fn(async () => true),
play: vi.fn(async () => true),
closeClip: vi.fn(async () => { monitorItem = null; return true; }),
closeAllClips: vi.fn(async () => { monitorItem = null; return true; }),
},
TickTime: { createWithSeconds: vi.fn((seconds: number) => ({ seconds })) },
PRProduction: { getActiveProduction: vi.fn(() => ({ getScratchDiskSettings: vi.fn(async () => scratchSettings) })) },
};
const workspace = {
status: vi.fn(() => ({ configured: true, accessMode: "request", rootName: "Approved", persistent: true, pathDisclosure: "redacted", canonicalPathValidation: "available" })),
assertPathAllowed: vi.fn((path: string) => path.replace(/\\/g, "/")),
};
return {
registry: Commands.createCommandRegistry({ ppro, Protocol, workspace }),
ppro, project, sequence, videoItem, audioItem, components, audioComponents, sourceClip, workspace, metadataSchemaFields,
setProjectPanelMetadataValue: (value: string) => { projectPanelMetadata = value; },
selectedItems: () => [...selectedItems],
selectMany: (count: number) => { selectedItems = Array.from({ length: count }, () => videoItem); },
selectAudio: () => { selectedItems = [audioItem]; },
};
}
describe("stable Premiere UXP workflow expansion", () => {
it("advertises each workflow from runtime probes and labels workspace-bound commands", async () => {
const value = stableHost();
const capabilities = await value.registry.capabilities();
expect(Object.keys(capabilities.commands)).toEqual(expect.arrayContaining([
"effects.catalog", "effects.chain.add", "trackItem.identity.inspect", "selection.inspect", "selection.fingerprints.inspect", "selection.targets.inspect", "selection.update", "effects.selection.add",
"sceneEdit.detect", "proxy.attach", "ingest.configure", "media.relink",
"metadata.update", "metadata.columns.get", "metadata.projectPanel.get", "metadata.projectPanel.update", "metadata.projectSchema.inspect", "metadata.projectSchema.create", "color.preflight", "environment.inspect", "footage.conform", "sourceMonitor.open",
"storage.preflight", "scratch.configure", "workspace.status",
]));
expect(capabilities.commands["effects.selection.add"]).toMatchObject({
supported: true, documented: true, destructive: true, undoable: true,
});
expect(capabilities.commands["sceneEdit.detect"]).toMatchObject({
supported: true, minHostVersion: "26.3.0",
});
expect(capabilities.commands["selection.update"]).toMatchObject({
supported: true, documented: true, readOnly: false, destructive: false,
undoable: false, idempotent: true, minHostVersion: "25.6.0",
});
expect(capabilities.commands["selection.targets.inspect"]).toMatchObject({
supported: true, documented: true, readOnly: true, targetCapabilityProbe: "invocation",
});
expect(capabilities.commands["media.relink"]).toMatchObject({
supported: true, undoable: false, workspaceRequired: true, targetCapabilityProbe: "invocation",
});
expect(capabilities.workspace).toMatchObject({ configured: true, pathDisclosure: "redacted" });
});
it("probes Project-panel metadata accessors independently", async () => {
const missingPanelMetadata = stableHost();
Reflect.deleteProperty(missingPanelMetadata.ppro.Metadata, "getProjectPanelMetadata");
const withoutPanelMetadata = await missingPanelMetadata.registry.capabilities();
expect(withoutPanelMetadata.commands["metadata.columns.get"]).toMatchObject({ supported: true });
expect(withoutPanelMetadata.commands["metadata.projectPanel.get"]).toMatchObject({ supported: false });
expect(withoutPanelMetadata.commands["metadata.projectPanel.update"]).toMatchObject({ supported: false });
expect(withoutPanelMetadata.commands["metadata.projectSchema.inspect"]).toMatchObject({ supported: false });
expect(withoutPanelMetadata.commands["metadata.projectSchema.create"]).toMatchObject({ supported: false });
const missingColumnsMetadata = stableHost();
Reflect.deleteProperty(missingColumnsMetadata.ppro.Metadata, "getProjectColumnsMetadata");
const withoutColumnsMetadata = await missingColumnsMetadata.registry.capabilities();
expect(withoutColumnsMetadata.commands["metadata.columns.get"]).toMatchObject({ supported: false });
expect(withoutColumnsMetadata.commands["metadata.projectPanel.get"]).toMatchObject({ supported: true });
expect(withoutColumnsMetadata.commands["metadata.projectPanel.update"]).toMatchObject({ supported: true });
const missingPanelSetter = stableHost();
Reflect.deleteProperty(missingPanelSetter.ppro.Metadata, "setProjectPanelMetadata");
const withoutPanelSetter = await missingPanelSetter.registry.capabilities();
expect(withoutPanelSetter.commands["metadata.projectPanel.get"]).toMatchObject({ supported: true });
expect(withoutPanelSetter.commands["metadata.projectPanel.update"]).toMatchObject({ supported: false });
const missingSchemaCreator = stableHost();
Reflect.deleteProperty(missingSchemaCreator.ppro.Metadata, "addPropertyToProjectMetadataSchema");
const withoutSchemaCreator = await missingSchemaCreator.registry.capabilities();
expect(withoutSchemaCreator.commands["metadata.projectSchema.inspect"]).toMatchObject({ supported: false });
expect(withoutSchemaCreator.commands["metadata.projectSchema.create"]).toMatchObject({ supported: false });
});
it("probes Source Monitor state, play, and close commands independently", async () => {
const missingPlay = stableHost();
Reflect.deleteProperty(missingPlay.ppro.SourceMonitor, "play");
const withoutPlay = await missingPlay.registry.capabilities();
expect(withoutPlay.commands["sourceMonitor.state"]).toMatchObject({ supported: true });
expect(withoutPlay.commands["sourceMonitor.play"]).toMatchObject({ supported: false });
expect(withoutPlay.commands["sourceMonitor.close"]).toMatchObject({ supported: true });
const missingCloseClip = stableHost();
Reflect.deleteProperty(missingCloseClip.ppro.SourceMonitor, "closeClip");
const withoutCloseClip = await missingCloseClip.registry.capabilities();
expect(withoutCloseClip.commands["sourceMonitor.state"]).toMatchObject({ supported: true });
expect(withoutCloseClip.commands["sourceMonitor.play"]).toMatchObject({ supported: true });
expect(withoutCloseClip.commands["sourceMonitor.close"]).toMatchObject({ supported: false });
const missingCloseAll = stableHost();
Reflect.deleteProperty(missingCloseAll.ppro.SourceMonitor, "closeAllClips");
const withoutCloseAll = await missingCloseAll.registry.capabilities();
expect(withoutCloseAll.commands["sourceMonitor.close"]).toMatchObject({ supported: false });
});
it("keeps selection inspection available when mutation APIs are unavailable", async () => {
const missingFactory = stableHost();
Reflect.deleteProperty(missingFactory.ppro.TrackItemSelection, "createEmptySelection");
const withoutFactory = await missingFactory.registry.capabilities();
expect(withoutFactory.commands["selection.inspect"]).toMatchObject({ supported: true, readOnly: true });
expect(withoutFactory.commands["selection.update"]).toMatchObject({ supported: false, readOnly: false });
const missingSet = stableHost();
Reflect.deleteProperty(missingSet.sequence, "setSelection");
const withoutSet = await missingSet.registry.capabilities();
expect(withoutSet.commands["selection.inspect"]).toMatchObject({ supported: true });
expect(withoutSet.commands["selection.update"]).toMatchObject({ supported: false });
const noProject = stableHost();
noProject.ppro.Project.getActiveProject.mockResolvedValue(null as never);
const withoutProject = await noProject.registry.capabilities();
expect(withoutProject.commands["selection.update"]).toMatchObject({ supported: true });
const noSequence = stableHost();
noSequence.project.getActiveSequence.mockResolvedValue(null as never);
const withoutSequence = await noSequence.registry.capabilities();
expect(withoutSequence.commands["selection.update"]).toMatchObject({ supported: true });
});
it("runs native effect and selection batches as verified action transactions", async () => {
const value = stableHost();
await expect(value.registry.dispatch("effects.catalog", { mediaType: "all" })).resolves.toMatchObject({
video: { matchNames: ["PR.Test"] }, audio: { displayNames: ["Test Audio Effect"] },
});
await expect(value.registry.dispatch("effects.chain.add", {
mediaType: "video", trackIndex: 0, clipIndex: 0, effectId: "PR.Test",
})).resolves.toMatchObject({ applied: true, outcome: "verified", beforeCount: 1, after: { count: 2 } });
await expect(value.registry.dispatch("effects.chain.add", {
mediaType: "audio", trackIndex: 0, clipIndex: 0, effectId: "Test Audio Effect",
})).resolves.toMatchObject({ applied: true, outcome: "verified", beforeCount: 1, after: { count: 2 } });
await expect(value.registry.dispatch("selection.inspect", {})).resolves.toMatchObject({
count: 1, items: [{ mediaType: "video", trackIndex: 0, clipIndex: 0 }],
});
await expect(value.registry.dispatch("effects.selection.add", {
mediaType: "video", effectId: "PR.Test",
})).resolves.toMatchObject({ applied: 1, outcome: "verified" });
await expect(value.registry.dispatch("effects.selection.remove", {
mediaType: "video", componentIndex: 2, expectedEffectId: "PR.Test",
})).resolves.toMatchObject({ removed: 1, outcome: "verified" });
await expect(value.registry.dispatch("effects.chain.remove", {
mediaType: "video", trackIndex: 0, clipIndex: 0, componentIndex: 1, expectedEffectId: "PR.Other",
})).rejects.toMatchObject({ code: "UXP_STALE_EFFECT_CHAIN" });
expect(value.project.executeTransaction).toHaveBeenCalledTimes(4);
expect(value.components).toHaveLength(2);
expect(value.audioComponents).toHaveLength(2);
});
it("reads one complete native track-item identity and rejects stale or incomplete snapshots", async () => {
const value = stableHost();
await expect(value.registry.dispatch("trackItem.identity.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 0, expectedSequenceGuid: "sequence-1",
})).resolves.toEqual({
sequenceGuid: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 0,
matchName: "clip-video-match", trackItemType: 1, mediaTypeGuid: "video-media-guid",
reportedTrackIndex: 0, selected: true, verificationBoundary: "active_sequence_identity_readback",
});
await expect(value.registry.dispatch("trackItem.identity.inspect", {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedSequenceGuid: "different-sequence",
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
const incomplete = stableHost();
Reflect.deleteProperty(incomplete.videoItem, "getMatchName");
await expect(incomplete.registry.dispatch("trackItem.identity.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 0,
})).rejects.toMatchObject({ code: "UXP_COMMAND_UNAVAILABLE" });
const switched = stableHost();
switched.videoItem.getMatchName.mockImplementationOnce(async () => {
switched.project.getActiveSequence.mockResolvedValueOnce({ guid: "sequence-2" });
return "clip-video-match";
});
await expect(switched.registry.dispatch("trackItem.identity.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 0,
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
});
it("constructs, updates, clears, and replays deterministic timeline selections", async () => {
const value = stableHost();
const video = {
mediaType: "video", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
const audio = {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
await expect(value.registry.dispatch("selection.fingerprints.inspect", {})).resolves.toMatchObject({
sequenceGuid: "sequence-1", count: 1,
items: [{ mediaType: "video", projectItem: { id: "source-1" }, startSeconds: 10, endSeconds: 20 }],
});
await expect(value.registry.dispatch("selection.targets.inspect", {
items: [
{ mediaType: "video", trackIndex: 0, clipIndex: 0 },
{ mediaType: "audio", trackIndex: 0, clipIndex: 0 },
],
})).resolves.toMatchObject({
sequenceGuid: "sequence-1", count: 2,
items: [
{ targetIndex: 0, mediaType: "video", projectItem: { id: "source-1" }, startSeconds: 10, endSeconds: 20 },
{ targetIndex: 1, mediaType: "audio", projectItem: { id: "source-1" }, startSeconds: 10, endSeconds: 20 },
],
});
await expect(value.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [video, audio], operationId: "selection-1",
})).resolves.toMatchObject({
updated: true, changed: true, mode: "replace", count: 2, outcome: "verified",
verified: "timeline_selection_readback",
operation: { mutatesProject: false, undo: { supported: false } },
});
await expect(value.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [video, audio], operationId: "selection-1",
})).resolves.toMatchObject({ replayed: true, count: 2 });
expect(value.sequence.setSelection).toHaveBeenCalledTimes(1);
await expect(value.registry.dispatch("selection.update", {
mode: "remove", expectedSequenceGuid: "sequence-1", items: [video], operationId: "selection-2",
})).resolves.toMatchObject({ count: 1, items: [{ mediaType: "audio" }] });
await expect(value.registry.dispatch("selection.update", {
mode: "add", expectedSequenceGuid: "sequence-1", items: [video], operationId: "selection-3",
})).resolves.toMatchObject({ count: 2 });
await expect(value.registry.dispatch("selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1", operationId: "selection-4",
})).resolves.toMatchObject({ count: 0, items: [] });
await expect(value.registry.dispatch("selection.fingerprints.inspect", {})).resolves.toMatchObject({ count: 0, items: [] });
expect(value.selectedItems()).toEqual([]);
});
it("supports the pre-26.3 Promise form of Sequence.setSelection", async () => {
const value = stableHost();
const implementation = value.sequence.setSelection.getMockImplementation();
value.sequence.setSelection.mockImplementation(((selection: unknown) =>
Promise.resolve(implementation?.(selection as never))) as never);
await expect(value.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", operationId: "selection-async",
items: [{
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
}],
})).resolves.toMatchObject({ count: 1, items: [{ mediaType: "audio" }] });
});
it("enumerates each target track once per selection resolution", async () => {
const value = stableHost();
const audioTrack = await value.sequence.getAudioTrack(0);
const secondAudioItem = {
...value.audioItem,
name: "Interview A 2",
getStartTime: vi.fn(async () => ({ seconds: 30 })),
getEndTime: vi.fn(async () => ({ seconds: 40 })),
};
audioTrack.getTrackItems.mockResolvedValue([value.audioItem, secondAudioItem]);
audioTrack.getTrackItems.mockClear();
await expect(value.registry.dispatch("selection.targets.inspect", {
items: [
{ mediaType: "audio", trackIndex: 0, clipIndex: 0 },
{ mediaType: "audio", trackIndex: 0, clipIndex: 1 },
],
})).resolves.toMatchObject({
count: 2,
items: [
{ targetIndex: 0, startSeconds: 10, endSeconds: 20 },
{ targetIndex: 1, startSeconds: 30, endSeconds: 40 },
],
});
expect(audioTrack.getTrackItems).toHaveBeenCalledTimes(1);
});
it("clears or replaces a manual selection larger than the mutation limit", async () => {
const target = {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
const cleared = stableHost();
cleared.selectMany(65);
await expect(cleared.registry.dispatch("selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1",
})).resolves.toMatchObject({ changed: true, count: 0, items: [] });
const replaced = stableHost();
replaced.selectMany(65);
await expect(replaced.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).resolves.toMatchObject({ changed: true, count: 1, items: [{ mediaType: "audio" }] });
});
it("revalidates and uses the active sequence immediately before selection mutation", async () => {
const target = {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
const refreshed = stableHost();
const getSelection = refreshed.sequence.getSelection.getMockImplementation();
const setSelection = refreshed.sequence.setSelection.getMockImplementation();
const refreshedSequence = {
...refreshed.sequence,
getSelection: vi.fn(() => getSelection?.()),
setSelection: vi.fn((selection: Parameters<typeof refreshed.sequence.setSelection>[0]) => setSelection?.(selection)),
};
refreshed.sequence.getSelection.mockImplementationOnce(async () => {
const selection = await getSelection?.();
refreshed.project.getActiveSequence.mockResolvedValue(refreshedSequence);
return selection as Awaited<ReturnType<NonNullable<typeof getSelection>>>;
});
await expect(refreshed.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).resolves.toMatchObject({ count: 1, items: [{ mediaType: "audio" }] });
expect(refreshed.sequence.setSelection).not.toHaveBeenCalled();
expect(refreshedSequence.setSelection).toHaveBeenCalledTimes(1);
expect(refreshedSequence.getSelection).toHaveBeenCalledTimes(4);
const switched = stableHost();
const originalSelection = switched.sequence.getSelection.getMockImplementation();
const inactiveSequence = { ...switched.sequence, guid: "sequence-2", setSelection: vi.fn() };
switched.sequence.getSelection.mockImplementationOnce(async () => {
const selection = await originalSelection?.();
switched.project.getActiveSequence.mockResolvedValue(inactiveSequence);
return selection as Awaited<ReturnType<NonNullable<typeof originalSelection>>>;
});
await expect(switched.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
expect(switched.sequence.setSelection).not.toHaveBeenCalled();
expect(inactiveSequence.setSelection).not.toHaveBeenCalled();
const switchedDuringPlan = stableHost();
const getPlanningSelection = switchedDuringPlan.sequence.getSelection.getMockImplementation();
const lateInactiveSequence = { ...switchedDuringPlan.sequence, guid: "sequence-2", setSelection: vi.fn() };
let planningSelectionCalls = 0;
switchedDuringPlan.sequence.getSelection.mockImplementation(async () => {
planningSelectionCalls += 1;
const selection = await getPlanningSelection?.();
if (planningSelectionCalls === 2) {
switchedDuringPlan.project.getActiveSequence.mockResolvedValue(lateInactiveSequence);
}
return selection as Awaited<ReturnType<NonNullable<typeof getPlanningSelection>>>;
});
await expect(switchedDuringPlan.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
expect(switchedDuringPlan.sequence.setSelection).not.toHaveBeenCalled();
expect(lateInactiveSequence.setSelection).not.toHaveBeenCalled();
const replacedDuringPlan = stableHost();
const getReplacedSelection = replacedDuringPlan.sequence.getSelection.getMockImplementation();
const setReplacedSelection = replacedDuringPlan.sequence.setSelection.getMockImplementation();
const finalAudioItem = { ...replacedDuringPlan.audioItem };
const finalSequence = {
...replacedDuringPlan.sequence,
getSelection: vi.fn(() => getReplacedSelection?.()),
setSelection: vi.fn((selection: Parameters<typeof replacedDuringPlan.sequence.setSelection>[0]) =>
setReplacedSelection?.(selection)),
getAudioTrack: vi.fn(async () => ({ getTrackItems: vi.fn(async () => [finalAudioItem]) })),
};
let replacedSelectionCalls = 0;
replacedDuringPlan.sequence.getSelection.mockImplementation(async () => {
replacedSelectionCalls += 1;
const selection = await getReplacedSelection?.();
if (replacedSelectionCalls === 2) replacedDuringPlan.project.getActiveSequence.mockResolvedValue(finalSequence as never);
return selection as Awaited<ReturnType<NonNullable<typeof getReplacedSelection>>>;
});
await expect(replacedDuringPlan.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).resolves.toMatchObject({ count: 1, items: [{ mediaType: "audio" }] });
expect(replacedDuringPlan.sequence.setSelection).not.toHaveBeenCalled();
expect(finalSequence.setSelection).toHaveBeenCalledWith(expect.objectContaining({ items: [finalAudioItem] }));
const switchedDuringFinalPlan = stableHost();
const getFinalPlanningSelection = switchedDuringFinalPlan.sequence.getSelection.getMockImplementation();
const setFinalPlanningSelection = switchedDuringFinalPlan.sequence.setSelection.getMockImplementation();
const lateSameGuidSequence = {
...switchedDuringFinalPlan.sequence,
setSelection: vi.fn((selection: Parameters<typeof switchedDuringFinalPlan.sequence.setSelection>[0]) =>
setFinalPlanningSelection?.(selection)),
};
let finalPlanningSelectionCalls = 0;
switchedDuringFinalPlan.sequence.getSelection.mockImplementation(async () => {
finalPlanningSelectionCalls += 1;
const selection = await getFinalPlanningSelection?.();
if (finalPlanningSelectionCalls === 3) {
switchedDuringFinalPlan.project.getActiveSequence.mockResolvedValue(lateSameGuidSequence);
}
return selection as Awaited<ReturnType<NonNullable<typeof getFinalPlanningSelection>>>;
});
await expect(switchedDuringFinalPlan.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).resolves.toMatchObject({ count: 1, items: [{ mediaType: "audio" }] });
expect(switchedDuringFinalPlan.sequence.setSelection).not.toHaveBeenCalled();
expect(lateSameGuidSequence.setSelection).toHaveBeenCalledTimes(1);
const movedDuringFinalFetch = stableHost();
let activeSequenceCalls = 0;
let targetMoved = false;
movedDuringFinalFetch.audioItem.getStartTime.mockImplementation(async () => ({ seconds: targetMoved ? 11 : 10 }));
movedDuringFinalFetch.project.getActiveSequence.mockImplementation(async () => {
activeSequenceCalls += 1;
if (activeSequenceCalls === 4) targetMoved = true;
return movedDuringFinalFetch.sequence;
});
await expect(movedDuringFinalFetch.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SELECTION_TARGET" });
expect(movedDuringFinalFetch.sequence.setSelection).not.toHaveBeenCalled();
const switchedDuringFinalPlanning = stableHost();
const getFinalSelection = switchedDuringFinalPlanning.sequence.getSelection.getMockImplementation();
const differentFinalSequence = { ...switchedDuringFinalPlanning.sequence, guid: "sequence-2", setSelection: vi.fn() };
let finalSelectionCalls = 0;
switchedDuringFinalPlanning.sequence.getSelection.mockImplementation(async () => {
finalSelectionCalls += 1;
const selection = await getFinalSelection?.();
if (finalSelectionCalls === 4) {
switchedDuringFinalPlanning.project.getActiveSequence.mockResolvedValue(differentFinalSequence);
}
return selection as Awaited<ReturnType<NonNullable<typeof getFinalSelection>>>;
});
await expect(switchedDuringFinalPlanning.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
expect(switchedDuringFinalPlanning.sequence.setSelection).not.toHaveBeenCalled();
expect(differentFinalSequence.setSelection).not.toHaveBeenCalled();
const changedDuringFinalPlanning = stableHost();
const getTargetStart = changedDuringFinalPlanning.audioItem.getStartTime.getMockImplementation();
let targetStartCalls = 0;
changedDuringFinalPlanning.audioItem.getStartTime.mockImplementation(async () => {
targetStartCalls += 1;
const start = await getTargetStart?.();
if (targetStartCalls === 7) changedDuringFinalPlanning.selectAudio();
return start as Awaited<ReturnType<NonNullable<typeof getTargetStart>>>;
});
await expect(changedDuringFinalPlanning.registry.dispatch("selection.update", {
mode: "add", expectedSequenceGuid: "sequence-1", items: [{
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
}],
})).rejects.toMatchObject({ code: "UXP_STALE_SELECTION" });
expect(changedDuringFinalPlanning.sequence.setSelection).not.toHaveBeenCalled();
});
it("revalidates same-sequence targets and requires native timeline fingerprints", async () => {
const target = {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
const changed = stableHost();
changed.audioItem.getStartTime
.mockResolvedValueOnce({ seconds: 10 })
.mockResolvedValueOnce({ seconds: 10 })
.mockResolvedValue({ seconds: 11 });
await expect(changed.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SELECTION_TARGET" });
expect(changed.sequence.setSelection).not.toHaveBeenCalled();
const unavailable = stableHost();
const sourceFallback = vi.fn(async () => ({ seconds: 10 }));
Object.assign(unavailable.audioItem, { getInPoint: sourceFallback });
unavailable.audioItem.getStartTime.mockRejectedValueOnce(new Error("timeline time unavailable"));
await expect(unavailable.registry.dispatch("selection.targets.inspect", {
items: [{ mediaType: "audio", trackIndex: 0, clipIndex: 0 }],
})).rejects.toMatchObject({ code: "UXP_SELECTION_FINGERPRINT_UNAVAILABLE" });
expect(sourceFallback).not.toHaveBeenCalled();
const missingId = stableHost();
missingId.sourceClip.getId.mockResolvedValueOnce("");
await expect(missingId.registry.dispatch("selection.targets.inspect", {
items: [{ mediaType: "video", trackIndex: 0, clipIndex: 0 }],
})).rejects.toMatchObject({ code: "UXP_SELECTION_FINGERPRINT_UNAVAILABLE" });
const currentUnavailable = stableHost();
const currentSourceFallback = vi.fn(async () => ({ seconds: 10 }));
Object.assign(currentUnavailable.videoItem, { getInPoint: currentSourceFallback });
currentUnavailable.videoItem.getStartTime.mockRejectedValue(new Error("timeline time unavailable"));
await expect(currentUnavailable.registry.dispatch("selection.fingerprints.inspect", {}))
.rejects.toMatchObject({ code: "UXP_SELECTION_FINGERPRINT_UNAVAILABLE" });
expect(currentSourceFallback).not.toHaveBeenCalled();
await expect(currentUnavailable.registry.dispatch("selection.inspect", {})).resolves.toMatchObject({
count: 1, items: [{ mediaType: "video", startSeconds: 10 }],
});
expect(currentSourceFallback).toHaveBeenCalledTimes(1);
const unclassified = stableHost();
unclassified.sequence.getVideoTrack.mockResolvedValue({ getTrackItems: vi.fn(async () => []) } as never);
unclassified.sequence.getAudioTrack.mockResolvedValue({ getTrackItems: vi.fn(async () => []) } as never);
await expect(unclassified.registry.dispatch("selection.fingerprints.inspect", {}))
.rejects.toMatchObject({ code: "UXP_UNCLASSIFIED_SELECTION" });
await expect(unclassified.registry.dispatch("selection.inspect", {})).resolves.toMatchObject({
count: 1, items: [{ mediaType: "unknown", trackIndex: 0, clipIndex: null }],
});
const relative = stableHost();
const relativeSelection = relative.sequence.getSelection.getMockImplementation();
let relativeSelectionCalls = 0;
relative.sequence.getSelection.mockImplementation(async () => {
relativeSelectionCalls += 1;
if (relativeSelectionCalls === 4) relative.selectAudio();
return relativeSelection?.() as ReturnType<NonNullable<typeof relativeSelection>>;
});
await expect(relative.registry.dispatch("selection.update", {
mode: "add", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SELECTION" });
expect(relative.sequence.setSelection).not.toHaveBeenCalled();
});
it("rejects stale, duplicate, oversized, rejected, and mismatched selection updates", async () => {
const target = {
mediaType: "audio", trackIndex: 0, clipIndex: 0, expectedProjectItemId: "source-1",
expectedStartSeconds: 10, expectedEndSeconds: 20,
};
const staleSequence = stableHost();
await expect(staleSequence.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-old", items: [target],
})).rejects.toMatchObject({ code: "UXP_STALE_SEQUENCE" });
const staleTarget = stableHost();
await expect(staleTarget.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1",
items: [{ ...target, expectedProjectItemId: "source-old" }],
})).rejects.toMatchObject({ code: "UXP_STALE_SELECTION_TARGET" });
expect(staleTarget.sequence.setSelection).not.toHaveBeenCalled();
const invalid = stableHost();
await expect(invalid.registry.dispatch("selection.update", {
mode: "replace", items: [target],
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1", items: null,
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1",
items: [{ ...target, expectedStartSeconds: 21 }],
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target, target],
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.targets.inspect", {
items: [
{ mediaType: "video", trackIndex: 0, clipIndex: 0 },
{ mediaType: "video", trackIndex: 0, clipIndex: 0 },
],
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(invalid.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1",
items: Array.from({ length: 65 }, (_, clipIndex) => ({ ...target, clipIndex })),
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
const rejected = stableHost();
rejected.sequence.setSelection.mockReturnValueOnce(false);
await expect(rejected.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_SELECTION_REJECTED" });
const clearRejected = stableHost();
clearRejected.sequence.clearSelection.mockResolvedValueOnce(false);
await expect(clearRejected.registry.dispatch("selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1",
})).rejects.toMatchObject({ code: "UXP_SELECTION_REJECTED" });
const mismatch = stableHost();
mismatch.sequence.setSelection.mockImplementationOnce(() => true);
await expect(mismatch.registry.dispatch("selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", items: [target],
})).rejects.toMatchObject({ code: "UXP_VERIFICATION_FAILED" });
});
it("uses the native selection for all scene-edit modes without claiming undo", async () => {
const value = stableHost();
for (const [mode, hostOperation] of [
["applyCuts", "ApplyCuts"],
["createMarkers", "CreateMarkers"],
["createSubclips", "CreateSubclips"],
]) {
await expect(value.registry.dispatch("sceneEdit.detect", { mode })).resolves.toMatchObject({
detected: true, mode, selectedItemCount: 1, outcome: mode === "createMarkers" ? "verified" : "committed_unverified",
operation: { mutatesProject: true, undo: { supported: false } },
});
expect(value.ppro.SequenceUtils.performSceneEditDetectionOnSelection)
.toHaveBeenLastCalledWith(hostOperation, expect.any(Object));
if (mode === "createMarkers") {
expect(value.ppro.Markers.getMarkers).toHaveBeenCalledWith(value.sourceClip);
}
}
});
it("refuses to report scene-marker detection when marker readback is unchanged", async () => {
const value = stableHost();
value.ppro.SequenceUtils.performSceneEditDetectionOnSelection.mockImplementationOnce(async () => true);
await expect(value.registry.dispatch("sceneEdit.detect", { mode: "createMarkers" }))
.rejects.toMatchObject({ code: "UXP_VERIFICATION_FAILED" });
});
it("guards non-undoable proxy/relink calls and verifies host readback", async () => {
const value = stableHost();
await expect(value.registry.dispatch("proxy.attach", {
projectItemId: "source-1", mediaPath: "D:/Approved/proxy.mov",
})).rejects.toMatchObject({ code: "UXP_CONFIRMATION_REQUIRED" });
await expect(value.registry.dispatch("proxy.attach", {
projectItemId: "source-1", mediaPath: "D:/Approved/proxy.mov", confirmNonUndoable: true,
})).resolves.toMatchObject({ attached: true, outcome: "verified", after: { hasProxy: true } });
await expect(value.registry.dispatch("proxy.attach", {
projectItemId: "source-1", mediaPath: "D:/Approved/proxy-2.mov", confirmNonUndoable: true,
})).rejects.toMatchObject({ code: "UXP_PROXY_ALREADY_ATTACHED" });
await expect(value.registry.dispatch("ingest.configure", { enabled: true })).resolves.toMatchObject({
configured: true, outcome: "verified", enabled: true,
});
await expect(value.registry.dispatch("media.relink", {
projectItemId: "source-1", newPath: "D:/Approved/online.mov",
expectedCurrentPath: "D:/Approved/missing.mov", confirmNonUndoable: true,
})).resolves.toMatchObject({ relinked: true, outcome: "verified", after: { offline: false } });
expect(value.workspace.assertPathAllowed).toHaveBeenCalledTimes(3);
});
it("updates metadata and footage conformance transactionally with readback", async () => {
const value = stableHost();
await expect(value.registry.dispatch("metadata.update", {
projectItemId: "source-1", projectMetadata: "project-after", xmpMetadata: "xmp-after", updatedFields: ["Column.Intrinsic.LogNote"],
})).resolves.toMatchObject({ updated: true, outcome: "verified", metadata: { projectMetadata: "project-after", xmpMetadata: "xmp-after" } });
await expect(value.registry.dispatch("color.preflight", { projectItemId: "source-1" })).resolves.toMatchObject({
project: { graphicsWhiteLuminance: 203 }, clip: { frameRate: 23.976, embeddedLutId: "embedded-lut" },
});
await expect(value.registry.dispatch("footage.conform", {
projectItemId: "source-1", frameRate: 24, pixelAspectRatio: 1.2, inputLutId: "lut-guid",
})).resolves.toMatchObject({ conformed: true, outcome: "verified", after: { frameRate: 24, pixelAspectRatio: 1.2, inputLutId: "lut-guid" } });
});
it("reads bounded native Project-panel schema and item-column metadata without calling the setter", async () => {
const value = stableHost();
await expect(value.registry.dispatch("metadata.columns.get", { projectItemId: "source-1" })).resolves.toEqual({
projectItemId: "source-1", name: "Interview.mov", projectColumnsMetadata: "columns-before",
});
await expect(value.registry.dispatch("metadata.projectPanel.get", {})).resolves.toEqual({
projectGuid: "project-1", projectName: "", projectPanelMetadata: "panel-before",
});
expect(value.ppro.Metadata.getProjectColumnsMetadata).toHaveBeenCalledWith(value.sourceClip);
expect(value.ppro.Metadata.getProjectPanelMetadata).toHaveBeenCalledTimes(1);
expect(value.ppro.Metadata.setProjectPanelMetadata).not.toHaveBeenCalled();
});
it("guardedly replaces bounded Project-panel metadata with exact readback and operation replay", async () => {
const value = stableHost();
const args = {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before",
projectPanelMetadata: "<panel><column id=\"review\"/></panel>", confirmUpdate: true, operationId: "panel-metadata-1",
};
await expect(value.registry.dispatch("metadata.projectPanel.update", args)).resolves.toMatchObject({
updated: true, outcome: "verified", verified: true, projectGuid: "project-1",
projectPanelMetadata: "<panel><column id=\"review\"/></panel>",
operation: { undo: { supported: false }, cancellation: { supported: false } },
});
await expect(value.registry.dispatch("metadata.projectPanel.update", args)).resolves.toMatchObject({
updated: true, replayed: true, outcome: "verified",
});
expect(value.ppro.Metadata.setProjectPanelMetadata).toHaveBeenCalledTimes(1);
await expect(value.registry.dispatch("metadata.projectPanel.update", {
...args, expectedProjectPanelMetadata: "panel-before", projectPanelMetadata: "<panel/>", operationId: "panel-stale-1",
})).rejects.toMatchObject({ code: "UXP_STALE_PROJECT_PANEL_METADATA" });
await expect(value.registry.dispatch("metadata.projectPanel.update", {
...args, expectedProjectPanelMetadata: "<panel><column id=\"review\"/></panel>", projectPanelMetadata: "<panel/>",
confirmUpdate: false, operationId: "panel-confirmation-1",
})).rejects.toMatchObject({ code: "UXP_CONFIRMATION_REQUIRED" });
await expect(value.registry.dispatch("metadata.projectPanel.update", {
...args, expectedProjectPanelMetadata: "<panel><column id=\"review\"/></panel>", projectPanelMetadata: "<panel/>", confirmUpdate: true, operationId: "*",
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
expect(value.ppro.Metadata.setProjectPanelMetadata).toHaveBeenCalledTimes(1);
});
it("serializes different guarded Project-panel metadata operations and fails the queued stale request", async () => {
const value = stableHost();
let releaseFirst: (() => void) | undefined;
const firstAccepted = new Promise<boolean>((resolve) => { releaseFirst = () => resolve(true); });
value.ppro.Metadata.setProjectPanelMetadata.mockImplementationOnce(async (xml: string) => {
value.setProjectPanelMetadataValue(xml);
return firstAccepted;
});
const first = value.registry.dispatch("metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before", projectPanelMetadata: "panel-first",
confirmUpdate: true, operationId: "panel-concurrent-first",
});
await vi.waitFor(() => expect(value.ppro.Metadata.setProjectPanelMetadata).toHaveBeenCalledOnce());
const second = value.registry.dispatch("metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before", projectPanelMetadata: "panel-second",
confirmUpdate: true, operationId: "panel-concurrent-second",
});
releaseFirst?.();
await expect(first).resolves.toMatchObject({ updated: true, verified: true, projectPanelMetadata: "panel-first" });
await expect(second).rejects.toMatchObject({ code: "UXP_STALE_PROJECT_PANEL_METADATA" });
expect(value.ppro.Metadata.setProjectPanelMetadata).toHaveBeenCalledOnce();
});
it("reports committed-unverified Project-panel metadata if post-set readback differs", async () => {
const value = stableHost();
value.ppro.Metadata.setProjectPanelMetadata.mockResolvedValueOnce(true);
await expect(value.registry.dispatch("metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before", projectPanelMetadata: "panel-requested",
confirmUpdate: true, operationId: "panel-unverified-1",
})).resolves.toMatchObject({
updated: true, outcome: "committed_unverified", verified: false,
verificationBoundary: "project_panel_metadata_active_project_readback", projectPanelMetadata: "panel-before",
});
});
it("guards direct Project metadata schema-field creation with bounded snapshots, replay, and honest readback", async () => {
const value = stableHost();
const args = {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before",
fieldName: "McpReviewState", fieldLabel: "MCP Review State", fieldType: "text",
confirmCreate: true, operationId: "schema-create-1",
};
await expect(value.registry.dispatch("metadata.projectSchema.inspect", {})).resolves.toMatchObject({
projectGuid: "project-1", projectPanelMetadata: "panel-before",
verificationBoundary: "bounded_project_panel_metadata_readback",
});
await expect(value.registry.dispatch("metadata.projectSchema.create", args)).resolves.toMatchObject({
creationRequested: true, hostAccepted: true, projectGuid: "project-1",
field: { name: "McpReviewState", label: "MCP Review State", type: "text" },
panelMetadataChanged: true, outcome: "committed_unverified", verified: false,
verificationBoundary: "project_panel_metadata_change_readback",
operation: { undo: { supported: false }, cancellation: { supported: false } },
});
await expect(value.registry.dispatch("metadata.projectSchema.create", args)).resolves.toMatchObject({
creationRequested: true, replayed: true, outcome: "committed_unverified",
});
expect(value.ppro.Metadata.addPropertyToProjectMetadataSchema).toHaveBeenCalledTimes(1);
expect(value.metadataSchemaFields).toEqual([{ name: "McpReviewState", label: "MCP Review State", type: 3 }]);
await expect(value.registry.dispatch("metadata.projectSchema.create", {
...args, fieldName: "McpPriority", expectedProjectPanelMetadata: "panel-before", operationId: "schema-stale-1",
})).rejects.toMatchObject({ code: "UXP_STALE_PROJECT_METADATA_SCHEMA" });
await expect(value.registry.dispatch("metadata.projectSchema.create", {
...args, fieldName: "McpPriority", expectedProjectPanelMetadata: "panel-before|schema:McpReviewState:MCP Review State:3",
confirmCreate: false, operationId: "schema-confirmation-1",
})).rejects.toMatchObject({ code: "UXP_CONFIRMATION_REQUIRED" });
await expect(value.registry.dispatch("metadata.projectSchema.create", {
...args, fieldName: "9-invalid", expectedProjectPanelMetadata: "panel-before|schema:McpReviewState:MCP Review State:3", operationId: "schema-invalid-name-1",
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(value.registry.dispatch("metadata.projectSchema.create", {
...args, fieldName: "McpPriority", fieldType: "date", expectedProjectPanelMetadata: "panel-before|schema:McpReviewState:MCP Review State:3", operationId: "schema-invalid-type-1",
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
await expect(value.registry.dispatch("metadata.projectSchema.create", {
...args, fieldName: "McpPriority", expectedProjectPanelMetadata: "panel-before|schema:McpReviewState:MCP Review State:3", operationId: "*",
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
expect(value.ppro.Metadata.addPropertyToProjectMetadataSchema).toHaveBeenCalledTimes(1);
});
it("serializes competing Project metadata schema creates before their final stale snapshots", async () => {
const value = stableHost();
let releaseFirst: (() => void) | undefined;
const firstFinished = new Promise<void>((resolve) => { releaseFirst = resolve; });
value.ppro.Metadata.addPropertyToProjectMetadataSchema.mockImplementationOnce(async (name: string, label: string, type: number) => {
await firstFinished;
value.metadataSchemaFields.push({ name, label, type });
value.setProjectPanelMetadataValue("panel-after-first");
return true;
});
const first = value.registry.dispatch("metadata.projectSchema.create", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before",
fieldName: "McpFirst", fieldLabel: "MCP First", fieldType: "boolean", confirmCreate: true, operationId: "schema-concurrent-first",
});
await vi.waitFor(() => expect(value.ppro.Metadata.addPropertyToProjectMetadataSchema).toHaveBeenCalledOnce());
const second = value.registry.dispatch("metadata.projectSchema.create", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before",
fieldName: "McpSecond", fieldLabel: "MCP Second", fieldType: "integer", confirmCreate: true, operationId: "schema-concurrent-second",
});
releaseFirst?.();
await expect(first).resolves.toMatchObject({ creationRequested: true, panelMetadataChanged: true, verified: false });
await expect(second).rejects.toMatchObject({ code: "UXP_STALE_PROJECT_METADATA_SCHEMA" });
expect(value.ppro.Metadata.addPropertyToProjectMetadataSchema).toHaveBeenCalledOnce();
});
it("remains committed-unverified when schema creation has no observable panel XML change", async () => {
const value = stableHost();
value.ppro.Metadata.addPropertyToProjectMetadataSchema.mockResolvedValueOnce(true);
await expect(value.registry.dispatch("metadata.projectSchema.create", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before",
fieldName: "McpSilent", fieldLabel: "MCP Silent", fieldType: "real", confirmCreate: true, operationId: "schema-unverified-1",
})).resolves.toMatchObject({
hostAccepted: true, panelMetadataChanged: false, outcome: "committed_unverified", verified: false,
verificationBoundary: "metadata_schema_add_host_return",
});
});
it("fails closed for malformed or oversized native Project-panel metadata", async () => {
const value = stableHost();
value.ppro.Metadata.getProjectColumnsMetadata.mockResolvedValueOnce(null);
await expect(value.registry.dispatch("metadata.columns.get", { projectItemId: "source-1" }))
.rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
value.ppro.Metadata.getProjectPanelMetadata.mockResolvedValueOnce("\u0800".repeat(350000));
await expect(value.registry.dispatch("metadata.projectPanel.get", {}))
.rejects.toMatchObject({ code: "UXP_RESULT_TOO_LARGE" });
await expect(value.registry.dispatch("metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "panel-before", projectPanelMetadata: "x".repeat(12289),
confirmUpdate: true, operationId: "panel-too-large",
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
});
it("inspects After Effects interoperability and project color support without mutation", async () => {
const value = stableHost();
await expect(value.registry.dispatch("environment.inspect", {})).resolves.toEqual({
afterEffectsInstalled: true,
projectColor: {
graphicsWhiteLuminance: 203,
supportedGraphicsWhiteLuminances: [100, 203, 300],
},
});
Reflect.deleteProperty(value.ppro.Utils, "isAEInstalled");
const capabilities = await value.registry.capabilities();
expect(capabilities.commands["environment.inspect"]).toMatchObject({ supported: false });
const missingColorApi = stableHost();
missingColorApi.project.getColorSettings.mockResolvedValueOnce({});
const withoutColorApi = await missingColorApi.registry.capabilities();
expect(withoutColorApi.commands["environment.inspect"]).toMatchObject({ supported: false });
const noActiveProject = stableHost();
noActiveProject.ppro.Project.getActiveProject.mockResolvedValue(null);
const withoutProject = await noActiveProject.registry.capabilities();
expect(withoutProject.commands["environment.inspect"]).toMatchObject({ supported: true });
});
it("rejects metadata whose combined serialized UTF-8 result exceeds the frame budget", async () => {
const value = stableHost();
value.ppro.Metadata.getProjectMetadata.mockResolvedValueOnce("é".repeat(300000));
value.ppro.Metadata.getXMPMetadata.mockResolvedValueOnce("é".repeat(300000));
await expect(value.registry.dispatch("metadata.get", { projectItemId: "source-1" }))
.rejects.toMatchObject({ code: "UXP_RESULT_TOO_LARGE" });
});
it("covers Source Monitor audition and project/Production storage preflight", async () => {
const value = stableHost();
await expect(value.registry.dispatch("sourceMonitor.open", { projectItemId: "source-1" })).resolves.toMatchObject({
opened: true, source: "projectItem", outcome: "verified",
});
await expect(value.registry.dispatch("sourceMonitor.open", { filePath: "D:/Approved/take.mov" })).resolves.toMatchObject({
opened: true, source: "file", outcome: "committed_unverified",
});
await expect(value.registry.dispatch("sourceMonitor.play", { speed: 1.5 })).resolves.toMatchObject({ playing: true, speed: 1.5 });
await expect(value.registry.dispatch("sourceMonitor.close", { all: true })).resolves.toMatchObject({ closed: true, outcome: "verified" });
await expect(value.registry.dispatch("storage.preflight", {})).resolves.toMatchObject({
project: { ingestEnabled: false }, production: { apiAvailable: true, active: true },
});
await expect(value.registry.dispatch("scratch.configure", {
folderTypes: ["capture", "autoSave"], destination: "sameAsProject",
})).resolves.toMatchObject({ configured: true, outcome: "committed_unverified", destination: "sameAsProject" });
await expect(value.registry.dispatch("workspace.status", {})).resolves.toMatchObject({ configured: true, pathDisclosure: "redacted" });
expect(value.workspace.assertPathAllowed).toHaveBeenCalledWith("D:/Approved/take.mov", {
label: "Source Monitor filePath", kind: "file",
});
});
});