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
This commit is contained in:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
import { describe, expect, it, vi } from "vitest";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createServer } from "../../src/server.js";
import { getUxpTools } from "../../src/tools/uxp.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
type UxpTool = {
parameters: {
type?: string;
properties?: Record<string, Record<string, unknown>>;
required?: string[];
};
handler: (args: Record<string, unknown>) => Promise<unknown>;
};
const ADOBE_26_3_TOOLS = [
"rename_track_uxp",
"create_subclip_uxp",
"list_markers_uxp",
"set_source_monitor_position_uxp",
"inspect_frame_alignment_uxp",
"calculate_tick_time_uxp",
"manage_sequence_preview_frame_uxp",
"inspect_source_media_provenance_uxp",
"inspect_source_proxy_uxp",
"manage_source_media_overrides_uxp",
"inspect_track_item_identity_uxp",
"slide_track_item_uxp",
"duplicate_track_item_uxp",
"ripple_delete_track_item_uxp",
"create_empty_sequence_uxp",
"audit_object_masks_uxp",
"inspect_unique_object_identity_uxp",
"inspect_effect_parameter_catalog_uxp",
"has_transcript_uxp",
"export_aaf_uxp",
] as const;
function catalog(request = vi.fn().mockResolvedValue({ outcome: "verified" })) {
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
return { request, tools: getUxpTools(bridge) as Record<string, UxpTool> };
}
describe("Adobe Premiere 26.3 UXP public MCP catalog", () => {
it("registers the sampled capability-gated tools only with a UXP bridge", async () => {
const bridge = {
request: vi.fn(),
getState: vi.fn(() => ({ status: "listening", connected: false })),
} as unknown as UxpWebSocketBridge;
const server = createServer({}, { uxpBridge: bridge });
const client = new Client({ name: "adobe-26-3-catalog-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
const listed = await client.listTools();
expect(listed.tools.map((tool) => tool.name)).toEqual(
expect.arrayContaining(ADOBE_26_3_TOOLS),
);
expect(listed.tools).toHaveLength(440);
} finally {
await client.close();
await server.close();
}
});
it("publishes bounded schemas for Adobe's documented 26.3 APIs", () => {
const { tools } = catalog();
expect(Object.keys(tools)).toEqual(expect.arrayContaining(ADOBE_26_3_TOOLS));
expect(tools.rename_track_uxp.parameters).toMatchObject({
type: "object",
required: ["track_type", "track_index", "name"],
properties: {
track_type: { type: "string", enum: ["audio", "video", "caption"] },
track_index: { type: "integer" },
name: { type: "string" },
operation_id: { type: "string" },
},
});
expect(tools.create_subclip_uxp.parameters).toMatchObject({
type: "object",
required: ["name", "start_seconds", "end_seconds"],
properties: {
project_item_id: { type: "string" },
project_item_name: { type: "string" },
name: { type: "string" },
start_seconds: { type: "number" },
end_seconds: { type: "number" },
hard_boundaries: { type: "boolean" },
take_video: { type: "boolean" },
take_audio: { type: "boolean" },
operation_id: { type: "string" },
},
});
expect(tools.list_markers_uxp.parameters).toMatchObject({
type: "object",
properties: {
scope: { type: "string", enum: ["sequence", "project_item"] },
project_item_id: { type: "string" },
project_item_name: { type: "string" },
filters: { type: "array", items: { type: "string" } },
include_web_links: { type: "boolean" },
include_color_values: { type: "boolean" },
},
});
expect(tools.set_source_monitor_position_uxp.parameters).toMatchObject({
type: "object",
required: ["seconds"],
properties: { seconds: { type: "number" }, operation_id: { type: "string" } },
});
expect(tools.inspect_frame_alignment_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["action", "frame_rate"],
properties: {
action: { type: "string", enum: ["align", "frame"] },
frame_rate: { type: "number", minimum: 1, maximum: 240 },
seconds: { type: "number", minimum: 0, maximum: 86400 },
frame_count: { type: "integer", minimum: 0, maximum: 20736000 },
},
});
expect(tools.calculate_tick_time_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["operation", "base_ticks"],
properties: {
operation: { type: "string", enum: ["add", "subtract", "multiply", "divide"] },
base_ticks: { type: "string", pattern: "^(?:0|-?[1-9][0-9]{0,17})$" },
operand_ticks: { type: "string", pattern: "^(?:0|-?[1-9][0-9]{0,17})$" },
factor: { type: "integer", minimum: -1_000_000, maximum: 1_000_000 },
},
});
expect(tools.manage_sequence_preview_frame_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["action", "sequence_id"],
properties: {
action: { type: "string", enum: ["inspect", "update"] },
sequence_id: { type: "string", minLength: 1, maxLength: 128 },
preview_width: { type: "integer", minimum: 16, maximum: 10240 },
preview_height: { type: "integer", minimum: 16, maximum: 8192 },
expected_snapshot: { type: "object", additionalProperties: false, required: ["project_guid", "sequence_id", "preview_width", "preview_height"] },
confirm_set_preview_frame: { type: "boolean" }, operation_id: { type: "string" },
},
});
expect(tools.inspect_source_media_provenance_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["project_item_id"],
properties: {
project_item_id: { type: "string", minLength: 1, maxLength: 512 },
include_media_file_path: { type: "boolean" },
include_originating_project_path: { type: "boolean" },
},
});
expect(tools.inspect_source_proxy_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["project_item_id"],
properties: {
project_item_id: { type: "string", minLength: 1, maxLength: 512 },
include_proxy_path: { type: "boolean" },
},
});
expect(tools.create_empty_sequence_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["name", "confirm_non_undoable", "operation_id"],
properties: {
name: { type: "string" },
confirm_non_undoable: { type: "boolean" },
operation_id: { type: "string" },
},
});
expect(tools.audit_object_masks_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: {
expected_project_guid: { type: "string", minLength: 1, maxLength: 512 },
sequence_ids: {
type: "array", minItems: 1, maxItems: 64, uniqueItems: true,
items: { type: "string", minLength: 1, maxLength: 512 },
},
},
});
expect(tools.inspect_unique_object_identity_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false,
properties: {
project_item_id: { type: "string", minLength: 1, maxLength: 512 },
sequence_guid: { type: "string", minLength: 1, maxLength: 512 },
expected_project_guid: { type: "string", minLength: 1, maxLength: 512 },
expected_unique_id: { type: "string", minLength: 1, maxLength: 512 },
},
oneOf: [{ required: ["project_item_id"] }, { required: ["sequence_guid"] }],
});
expect(tools.inspect_track_item_identity_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["media_type", "track_index", "clip_index"],
properties: {
media_type: { type: "string", enum: ["video", "audio"] },
track_index: { type: "integer", minimum: 0 },
clip_index: { type: "integer", minimum: 0 },
expected_sequence_guid: { type: "string", maxLength: 512 },
},
});
expect(tools.inspect_effect_parameter_catalog_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false,
required: ["media_type", "track_index", "clip_index", "component_index"],
properties: {
media_type: { enum: ["video", "audio"] },
track_index: { maximum: 511 }, clip_index: { maximum: 511 }, component_index: { maximum: 511 },
expected_sequence_guid: { maxLength: 512 }, expected_component_id: { maxLength: 512 },
},
});
expect(tools.has_transcript_uxp.parameters).toMatchObject({
type: "object",
properties: {
project_item_id: { type: "string" },
project_item_name: { type: "string" },
},
});
expect(tools.export_aaf_uxp.parameters).toMatchObject({
type: "object",
required: ["output_file_path"],
properties: {
output_file_path: { type: "string" },
operation_id: { type: "string" },
options: {
type: "object",
properties: {
mixdown_video: { type: "boolean" },
explode_to_mono: { type: "boolean" },
embed_audio: { type: "boolean" },
trim_sources: { type: "boolean" },
render_audio_effects: { type: "boolean" },
interleave_without_effects: { type: "boolean" },
preserve_parent_folder: { type: "boolean" },
sample_rate: { type: "integer", enum: [32000, 44100, 48000, 88200, 96000] },
bits_per_sample: { type: "integer", enum: [16, 24, 32] },
audio_file_format: { type: "string", enum: ["aiff", "wav"] },
handle_frames: { type: "integer" },
video_mixdown_preset_path: { type: "string" },
},
},
},
});
});
it("translates the public snake_case arguments to protocol arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const { tools } = catalog(request);
await tools.rename_track_uxp.handler({
track_type: "caption", track_index: 2, name: "Dialogue", operation_id: "track-1",
});
await tools.create_subclip_uxp.handler({
project_item_id: "clip-17", name: "Sentence", start_seconds: 1.25, end_seconds: 4.75,
hard_boundaries: true, take_video: false, take_audio: true, operation_id: "subclip-1",
});
await tools.list_markers_uxp.handler({
scope: "project_item", project_item_id: "clip-17", filters: ["Comment", "Chapter"], include_web_links: true, include_color_values: true,
});
await tools.set_source_monitor_position_uxp.handler({ seconds: 12.5, operation_id: "source-1" });
await tools.inspect_frame_alignment_uxp.handler({ action: "align", frame_rate: 24, seconds: 1.03 });
await tools.calculate_tick_time_uxp.handler({ operation: "add", base_ticks: "1", operand_ticks: "2" });
await tools.manage_sequence_preview_frame_uxp.handler({
action: "update", sequence_id: "sequence-1", preview_width: 1920, preview_height: 1080,
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", preview_width: 640, preview_height: 360 },
confirm_set_preview_frame: true, operation_id: "preview-frame-1",
});
await tools.inspect_source_media_provenance_uxp.handler({
project_item_id: "clip-17", include_media_file_path: true, include_originating_project_path: false,
});
await tools.inspect_source_proxy_uxp.handler({ project_item_id: "clip-17", include_proxy_path: true });
await tools.create_empty_sequence_uxp.handler({
name: "Empty Assembly", confirm_non_undoable: true, operation_id: "empty-1",
});
await tools.audit_object_masks_uxp.handler({
expected_project_guid: "project-1", sequence_ids: ["sequence-2", "sequence-1"],
});
await tools.inspect_track_item_identity_uxp.handler({
media_type: "video", track_index: 1, clip_index: 2, expected_sequence_guid: "sequence-1",
});
await tools.has_transcript_uxp.handler({ project_item_name: "Interview A" });
await tools.export_aaf_uxp.handler({
output_file_path: "/exports/turnover.aaf",
options: {
mixdown_video: true, explode_to_mono: true, embed_audio: false, trim_sources: true,
render_audio_effects: true, interleave_without_effects: false, preserve_parent_folder: true,
sample_rate: 48000, bits_per_sample: 24, audio_file_format: "wav", handle_frames: 12,
video_mixdown_preset_path: "/presets/prores.epr",
},
operation_id: "aaf-1",
});
expect(request).toHaveBeenNthCalledWith(1, "track.rename", {
trackType: "caption", trackIndex: 2, name: "Dialogue", operationId: "track-1",
});
expect(request).toHaveBeenNthCalledWith(2, "subclip.create", {
projectItemId: "clip-17", name: "Sentence", startSeconds: 1.25, endSeconds: 4.75,
hasHardBoundaries: true, takeVideo: false, takeAudio: true, operationId: "subclip-1",
});
expect(request).toHaveBeenNthCalledWith(3, "marker.list", {
scope: "projectItem", projectItemId: "clip-17", filters: ["Comment", "Chapter"], includeWebLinks: true, includeColorValues: true,
});
expect(request).toHaveBeenNthCalledWith(4, "sourceMonitor.position.set", {
seconds: 12.5, operationId: "source-1",
});
expect(request).toHaveBeenNthCalledWith(5, "time.frameAlignment.inspect", {
action: "align", frameRate: 24, seconds: 1.03,
});
expect(request).toHaveBeenNthCalledWith(6, "time.tickArithmetic.inspect", {
operation: "add", baseTicks: "1", operandTicks: "2",
});
expect(request).toHaveBeenNthCalledWith(7, "sequence.previewFrame.update", {
sequenceId: "sequence-1", previewWidth: 1920, previewHeight: 1080, confirmSetPreviewFrame: true, operationId: "preview-frame-1",
expectedSnapshot: { projectGuid: "project-1", sequenceId: "sequence-1", previewWidth: 640, previewHeight: 360 },
});
expect(request).toHaveBeenNthCalledWith(8, "source.provenance.inspect", {
projectItemId: "clip-17", includeMediaFilePath: true, includeOriginatingProjectPath: false,
});
expect(request).toHaveBeenNthCalledWith(9, "source.proxy.inspect", {
projectItemId: "clip-17", includeProxyPath: true,
});
expect(request).toHaveBeenNthCalledWith(10, "sequences.createEmpty", {
name: "Empty Assembly", confirmNonUndoable: true, operationId: "empty-1",
});
expect(request).toHaveBeenNthCalledWith(11, "objectMask.audit", {
expectedProjectGuid: "project-1", sequenceIds: ["sequence-2", "sequence-1"],
});
expect(request).toHaveBeenNthCalledWith(12, "trackItem.identity.inspect", {
mediaType: "video", trackIndex: 1, clipIndex: 2, expectedSequenceGuid: "sequence-1",
});
expect(request).toHaveBeenNthCalledWith(13, "transcript.has", { projectItemName: "Interview A" });
expect(request).toHaveBeenNthCalledWith(14, "interchange.aaf.export", {
outputFilePath: "/exports/turnover.aaf",
options: {
mixdownVideo: true, explodeToMono: true, embedAudio: false, trimSources: true,
renderAudioEffects: true, interleaveWithoutEffects: false, preserveParentFolder: true,
sampleRate: 48000, bitsPerSample: 24, audioFileFormat: "wav", handleFrames: 12,
videoMixdownPresetPath: "/presets/prores.epr",
},
operationId: "aaf-1",
});
});
});
+196
View File
@@ -0,0 +1,196 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getAudioTools, parseEbur128Summary } from "../../src/tools/audio.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tools = getAudioTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
beforeEach(() => vi.clearAllMocks());
function enqueueFfmpegResult(stderr = "") {
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr });
}
function enqueueFfmpegFailure(error: Error) {
mockedExecFileAsync.mockRejectedValueOnce(error);
}
function createMediaFixture() {
const directory = mkdtempSync(join(tmpdir(), "premiere-loudness-"));
const mediaPath = join(directory, "dialogue.wav");
writeFileSync(mediaPath, "audio-fixture");
return mediaPath;
}
const measurableSummary = `
Summary:
Integrated loudness:
I: -16.2 LUFS
Loudness range:
LRA: 4.1 LU
True peak:
Peak: -1.3 dBFS
`;
describe("parseEbur128Summary", () => {
it("extracts the final integrated loudness, range, and true peak", () => {
const output = `
Summary:
Integrated loudness:
I: -16.2 LUFS
Threshold: -26.2 LUFS
Loudness range:
LRA: 4.1 LU
True peak:
Peak: -1.3 dBFS
`;
expect(parseEbur128Summary(output)).toEqual({
integratedLufs: -16.2,
loudnessRangeLu: 4.1,
truePeakDbfs: -1.3,
});
});
it("reports digital silence as unmeasurable instead of fabricating a number", () => {
const output = `Integrated loudness:\n I: -inf LUFS\nLoudness range:\n LRA: 0.0 LU\nTrue peak:\n Peak: -inf dBFS`;
expect(parseEbur128Summary(output)).toEqual({
integratedLufs: null,
loudnessRangeLu: 0,
truePeakDbfs: null,
});
});
});
describe("analyze_loudness argument boundary", () => {
it("rejects invalid targets before contacting Premiere or FFmpeg", async () => {
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", target_lufs: 4 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("target_lufs") });
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", tolerance_lu: -1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("tolerance_lu") });
await expect(tools.analyze_loudness.handler({ media_path: "missing.wav", max_true_peak_dbfs: 1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("max_true_peak_dbfs") });
await expect(tools.analyze_loudness.handler({}))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("media_path") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("resolves project-item paths through one read-only bridge call", async () => {
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "Project item not found" });
const result = await tools.analyze_loudness.handler({ project_item_id: "missing" });
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
expect(mockedSendCommand.mock.calls[0][0]).toContain("getMediaPath");
expect(result).toMatchObject({ success: false, error: "Project item not found" });
});
it("returns a local EBU R128 measurement and evaluates an optional delivery target", async () => {
const mediaPath = createMediaFixture();
enqueueFfmpegResult();
enqueueFfmpegResult(measurableSummary);
await expect(tools.analyze_loudness.handler({
media_path: mediaPath,
target_lufs: -16,
tolerance_lu: 0.5,
max_true_peak_dbfs: -1,
})).resolves.toMatchObject({
success: true,
data: {
mediaPath,
integratedLufs: -16.2,
loudnessRangeLu: 4.1,
truePeakDbfs: -1.3,
target: {
lufs: -16,
toleranceLu: 0.5,
deltaLu: -0.2,
loudnessPass: true,
truePeakPass: true,
passes: true,
},
},
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(2);
});
it("resolves a project item to a local file before measuring it", async () => {
const mediaPath = createMediaFixture();
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { mediaPath, name: "Dialogue" } });
enqueueFfmpegResult();
enqueueFfmpegResult(measurableSummary);
await expect(tools.analyze_loudness.handler({ project_item_id: "dialogue-item" }))
.resolves.toMatchObject({ success: true, data: { mediaPath, projectItemName: "Dialogue", target: null } });
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
});
it("reports a failed target and true-peak check without changing the media", async () => {
const mediaPath = createMediaFixture();
enqueueFfmpegResult();
enqueueFfmpegResult(measurableSummary);
await expect(tools.analyze_loudness.handler({
media_path: mediaPath,
target_lufs: -14,
tolerance_lu: 0.1,
max_true_peak_dbfs: -2,
})).resolves.toMatchObject({
success: true,
data: {
target: {
loudnessPass: false,
truePeakPass: false,
passes: false,
},
},
});
});
it("reports unavailable analysis prerequisites, timeouts, and digital silence without making up a metric", async () => {
const mediaPath = createMediaFixture();
enqueueFfmpegFailure(new Error("not found"));
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on PATH") });
enqueueFfmpegResult();
enqueueFfmpegFailure(Object.assign(new Error("timed out"), { killed: true }));
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
enqueueFfmpegResult();
enqueueFfmpegResult("Integrated loudness:\n I: -inf LUFS");
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("did not return measurable") });
});
it("does not invoke FFmpeg for an unavailable file and preserves an analysis error detail", async () => {
await expect(tools.analyze_loudness.handler({ media_path: "missing-dialogue.wav" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("Media file not found") });
expect(mockedExecFileAsync).not.toHaveBeenCalled();
const mediaPath = createMediaFixture();
enqueueFfmpegResult();
enqueueFfmpegFailure(Object.assign(new Error("decode failed"), { stderr: "invalid stream" }));
await expect(tools.analyze_loudness.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid stream") });
});
});
+80
View File
@@ -0,0 +1,80 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import { getExportTools, parseVideoQcOutput } from "../../src/tools/export.js";
describe("parseVideoQcOutput", () => {
it("returns exact black and freeze intervals", () => {
const stderr = [
"[blackdetect] black_start:0 black_end:1.25 black_duration:1.25",
"[freezedetect] lavfi.freezedetect.freeze_start: 2.5",
"[freezedetect] lavfi.freezedetect.freeze_duration: 1.5",
"[freezedetect] lavfi.freezedetect.freeze_end: 4",
].join("\n");
expect(parseVideoQcOutput(stderr)).toEqual({
blackFrames: [{ start: 0, end: 1.25, duration: 1.25 }],
freezes: [{ start: 2.5, end: 4, duration: 1.5 }],
});
});
});
describe("analyze_video_qc boundary", () => {
const tool = getExportTools({ tempDir: "/tmp/test" }).analyze_video_qc;
beforeEach(() => vi.clearAllMocks());
it("rejects invalid thresholds and missing files before FFmpeg", async () => {
await expect(tool.handler({ media_path: "missing.mp4", minimum_black_seconds: 0 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("minimum_black_seconds") });
await expect(tool.handler({ media_path: "missing.mp4" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
await expect(tool.handler({ media_path: "missing.mp4", minimum_freeze_seconds: 0 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("minimum_freeze_seconds") });
});
it("reports decoded findings without modifying the delivery file", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-video-qc-"));
const mediaPath = join(directory, "delivery.mp4");
writeFileSync(mediaPath, "video-fixture");
mockedExecFileAsync.mockResolvedValueOnce({
stdout: "",
stderr: [
"black_start:0 black_end:1.25 black_duration:1.25",
"lavfi.freezedetect.freeze_start: 2.5",
"lavfi.freezedetect.freeze_end: 4",
].join("\n"),
});
await expect(tool.handler({ media_path: mediaPath, minimum_black_seconds: 1, minimum_freeze_seconds: 1 }))
.resolves.toMatchObject({
success: true,
data: {
mediaPath,
passes: false,
blackFrames: [{ start: 0, end: 1.25, duration: 1.25 }],
freezes: [{ start: 2.5, end: 4, duration: 1.5 }],
},
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(1);
});
it("preserves a non-timeout FFmpeg failure detail", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-video-qc-error-"));
const mediaPath = join(directory, "delivery.mp4");
writeFileSync(mediaPath, "video-fixture");
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode failed"), { stderr: "invalid stream" }));
await expect(tool.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid stream") });
});
});
+311
View File
@@ -0,0 +1,311 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createHash } from "node:crypto";
import { mkdtempSync, rmSync, writeFileSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const { mockExecFile } = vi.hoisted(() => ({
mockExecFile: vi.fn(),
}));
vi.mock("node:child_process", () => ({ execFile: mockExecFile }));
vi.mock("node:util", async (importOriginal) => {
const original = await importOriginal<typeof import("node:util")>();
return {
...original,
promisify: () => (...args: any[]) => new Promise((resolve, reject) => {
mockExecFile(...args, (error: Error | null, stdout: string, stderr: string) => {
if (error) reject(error);
else resolve({ stdout, stderr });
});
}),
};
});
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn(),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getAudioTools } from "../../src/tools/audio.js";
import { confirmationToken, getEditPlanTools, validateEditPlan } from "../../src/tools/edit-plans.js";
import {
getExportTools,
inspectExportPresetFile,
MAX_CAPTURE_FRAME_BYTES,
verifyDeliveryFile,
} from "../../src/tools/export.js";
const mockedSendCommand = vi.mocked(sendCommand);
const temporaryDirectories: string[] = [];
const bridgeOptions = { tempDir: "/tmp/coverage-tools", timeoutMs: 500 };
function temporaryPath(name: string, contents = "test content"): string {
const directory = mkdtempSync(join(tmpdir(), "premiere-tool-coverage-"));
temporaryDirectories.push(directory);
const path = join(directory, name);
writeFileSync(path, contents);
return path;
}
function temporaryDirectory(): string {
const directory = mkdtempSync(join(tmpdir(), "premiere-tool-coverage-"));
temporaryDirectories.push(directory);
return directory;
}
function execSucceeds(stderr = ""): void {
mockExecFile.mockImplementationOnce((...args: any[]) => {
args.at(-1)(null, "", stderr);
});
}
function execFails(error: Error): void {
mockExecFile.mockImplementationOnce((...args: any[]) => {
args.at(-1)(error, "", "");
});
}
beforeEach(() => {
vi.clearAllMocks();
mockedSendCommand.mockResolvedValue({ success: true, data: { host: "ok" } });
});
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
describe("audio tool analysis coverage", () => {
it("resolves a project item, runs ffmpeg, and returns silent and retained ranges", async () => {
const mediaPath = temporaryPath("speech.mp4");
mockedSendCommand.mockResolvedValueOnce({
success: true,
data: { mediaPath, name: "Interview" },
});
execSucceeds();
execSucceeds([
"Duration: 00:00:12.00, start: 0.000000",
"[silencedetect] silence_start: 2",
"[silencedetect] silence_end: 4.5 | silence_duration: 2.5",
"[silencedetect] silence_start: 10",
].join("\n"));
const result = await getAudioTools(bridgeOptions).detect_silence.handler({
project_item_id: "interview-item",
noise_threshold_db: -42,
min_duration_seconds: 0.75,
});
expect(mockedSendCommand).toHaveBeenCalledOnce();
expect(mockedSendCommand.mock.calls[0][0]).toContain("getMediaPath");
expect(mockExecFile).toHaveBeenCalledTimes(2);
expect(result).toMatchObject({
success: true,
data: {
mediaPath,
projectItemName: "Interview",
noiseThresholdDb: -42,
minDurationSeconds: 0.75,
totalDurationSeconds: 12,
silenceIntervals: [
{ start: 2, end: 4.5, duration: 2.5 },
{ start: 10, end: 12, duration: 2 },
],
segments: [
{ start: 0, end: 2, duration: 2 },
{ start: 4.5, end: 10, duration: 5.5 },
],
silentSeconds: 4.5,
},
});
});
it("reports missing ffmpeg, timeouts, and useful analyser failure details", async () => {
const mediaPath = temporaryPath("speech.mp4");
const tools = getAudioTools(bridgeOptions);
execFails(new Error("ENOENT"));
await expect(tools.detect_silence.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("ffmpeg was not found") });
execSucceeds();
execFails(Object.assign(new Error("slow"), { killed: true }));
await expect(tools.detect_silence.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out after 300s") });
execSucceeds();
execFails(Object.assign(new Error("failed"), {
stderr: "first line\n\nsecond line\nthird line\nfourth line",
}));
await expect(tools.detect_silence.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("second line third line fourth line") });
});
it("renders audio adjustment, keyframe, and unmute options into the bridge script", async () => {
const tools = getAudioTools(bridgeOptions);
await tools.adjust_audio_levels.handler({ node_id: "clip\"id", level_db: -6 });
// Premiere's Volume > Level uses +15 dB as its normalized maximum.
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("0.08912509381337455");
await tools.add_audio_keyframes.handler({
node_id: "clip",
keyframes: [{ time_seconds: 1.25, level_db: -Infinity }],
});
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("1e-7");
await tools.mute_track.handler({ track_index: 3, muted: false });
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("track.setMute(0)");
});
});
describe("edit plan validation and apply coverage", () => {
it.each([
[null, "plan must be an object"],
[{}, "plan.operations must contain at least one operation"],
[{ operations: Array.from({ length: 101 }, () => ({ type: "remove_clip", node_id: "clip" })) }, "limited to 100"],
[{ operations: [null] }, "operation 0 must be an object"],
[{ operations: [{ type: "insert_clip", start_seconds: 0 }] }, "requires item_id"],
[{ operations: [{ type: "insert_clip", item_id: "item", start_seconds: Number.NaN }] }, "non-negative number"],
[{ operations: [{ type: "insert_clip", item_id: "item", start_seconds: 0, video_track_index: -1 }] }, "video_track_index"],
[{ operations: [{ type: "insert_clip", item_id: "item", start_seconds: 0, audio_track_index: 1.5 }] }, "audio_track_index"],
[{ operations: [{ type: "remove_clip", node_id: "" }] }, "requires node_id"],
])("rejects %j", (value, message) => {
expect(() => validateEditPlan(value)).toThrow(message);
});
it("previews destructive and non-destructive work with stable confirmation", async () => {
const plan = {
operations: [
{ type: "insert_clip" as const, item_id: "source", start_seconds: 2 },
{ type: "remove_clip" as const, node_id: "old-clip", ripple: true },
],
};
const tools = getEditPlanTools(bridgeOptions, {
capabilities: { capabilities: new Set(["inspect"]), source: "explicit" },
operationIdFactory: () => "preview-mixed",
});
const result = await tools.preview_edit_plan.handler({ plan });
expect(result).toMatchObject({
success: true,
data: {
operationId: "preview-mixed",
confirmationToken: confirmationToken(plan),
changes: [
{ type: "insert_clip", target: "source", destructive: false },
{ type: "remove_clip", target: "old-clip", destructive: true },
],
},
});
expect(confirmationToken({ ...plan, sequence_id: "other" })).not.toBe(confirmationToken(plan));
});
it("builds a remove plan against a named sequence and decorates bridge failures", async () => {
const plan = {
sequence_id: "seq\"one",
operations: [
{ type: "remove_clip" as const, node_id: "old-clip", ripple: true },
{ type: "insert_clip" as const, item_id: "new-item", start_seconds: 3, video_track_index: 2, audio_track_index: 4 },
],
};
const auditSink = vi.fn();
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "Premiere rejected the edit" });
const tools = getEditPlanTools(bridgeOptions, {
capabilities: { capabilities: new Set(["inspect", "edit"]), source: "explicit" },
auditSink,
operationIdFactory: () => "apply-remove",
});
const result = await tools.apply_edit_plan.handler({
plan,
confirmation_token: confirmationToken(plan),
});
const script = String(mockedSendCommand.mock.calls[0][0]);
expect(script).toContain('__findSequence("seq\\"one")');
expect(script).toContain("function __planFindClip");
expect(script).toContain("found0.remove(true, true)");
expect(result).toEqual({ success: false, error: "Premiere rejected the edit (operation apply-remove)" });
expect(auditSink.mock.calls.map(([event]) => event.outcome)).toEqual(["started", "failed"]);
});
});
describe("export verification and host-result coverage", () => {
it("validates a SHA-512 delivery and rejects malformed size constraints", async () => {
const delivery = temporaryPath("delivery.mov", "delivery data");
const checksum = createHash("sha512").update("delivery data").digest("hex");
await expect(verifyDeliveryFile(delivery, { minimumSizeBytes: -1 })).rejects.toThrow("minimum_size_bytes");
await expect(verifyDeliveryFile(delivery, { expectedSizeBytes: 1.5 })).rejects.toThrow("expected_size_bytes");
await expect(verifyDeliveryFile(temporaryDirectory())).rejects.toThrow("not a regular file");
await expect(verifyDeliveryFile(delivery, {
algorithm: "sha512",
expectedChecksum: ` ${checksum.toUpperCase()} `,
expectedSizeBytes: 13,
minimumSizeBytes: 0,
})).resolves.toMatchObject({
checksum: { algorithm: "sha512", value: checksum },
matchesExpectedChecksum: true,
matchesExpectedSize: true,
valid: true,
});
});
it("surfaces local preset and delivery errors, then merges a host-validated preset response", async () => {
const tools = getExportTools(bridgeOptions);
const invalid = temporaryPath("preset.xml", "preset");
await expect(tools.validate_export_preset.handler({ preset_path: invalid }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining(".epr") });
expect(mockedSendCommand).not.toHaveBeenCalled();
const preset = temporaryPath("preset.epr", "preset");
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "No active sequence" });
await expect(tools.validate_export_preset.handler({ preset_path: preset }))
.resolves.toEqual({ success: false, error: "No active sequence" });
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { outputExtension: ".mov" } });
await expect(tools.validate_export_preset.handler({ preset_path: preset }))
.resolves.toMatchObject({ success: true, data: { path: preset, outputExtension: ".mov", hostValidated: true } });
expect(inspectExportPresetFile(preset).sizeBytes).toBe(6);
await expect(tools.verify_delivery_file.handler({ output_path: join(tmpdir(), "missing-delivery.mov") }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("does not exist") });
});
it("reads a host-exported frame, reports unreadable output, and covers proxy script choices", async () => {
const tools = getExportTools(bridgeOptions);
const frame = temporaryPath("frame.png", "png bytes");
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { outputPath: frame } });
await expect(tools.capture_frame.handler({ time_seconds: 3 })).resolves.toMatchObject({
success: true,
data: { captured: true, mimeType: "image/png", base64: Buffer.from("png bytes").toString("base64") },
});
expect(existsSync(frame)).toBe(false);
const oversizedFrame = temporaryPath("oversized-frame.png", "x".repeat(MAX_CAPTURE_FRAME_BYTES + 1));
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { outputPath: oversizedFrame } });
await expect(tools.capture_frame.handler({})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("inline response limit"),
});
expect(existsSync(oversizedFrame)).toBe(false);
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { outputPath: temporaryDirectory() } });
await expect(tools.capture_frame.handler({})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("Failed to read captured frame"),
});
await tools.manage_proxies.handler({ item_id: "item", action: "create" });
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("output_path is required");
await tools.manage_proxies.handler({ item_id: "item", action: "create", output_path: "proxy.mov" });
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("__findProxyPreset()");
await tools.manage_proxies.handler({ item_id: "item", action: "create", output_path: "proxy.mov", preset_path: "proxy.epr" });
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain('var presetPath = "proxy.epr"');
await tools.manage_proxies.handler({ item_id: "item", action: "attach" });
expect(mockedSendCommand.mock.calls.at(-1)?.[0]).toContain("proxy_path is required for attach action");
});
});
+72
View File
@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getAvSettingsTools } from "../../src/tools/av-settings.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tools = getAvSettingsTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
describe("AV feature support", () => {
it("identifies documented support and UI-only boundaries without a host call", async () => {
const result = await tools.get_av_feature_support.handler();
expect(result.success).toBe(true);
expect(result.data.supported.projectItemAudioChannelMapping.status).toBe("supported_with_limitations");
expect(result.data.blocked.enhanceSpeech).toMatchObject({
status: "ui_only",
reason: expect.stringContaining("No documented"),
});
expect(result.data.blocked.audioTrackMixerRoutingAndSubmixes.status).toBe("unsupported");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("AV inspection scripts", () => {
beforeEach(() => vi.clearAllMocks());
it("inspects documented sequence audio and color/render fields", async () => {
await tools.inspect_sequence_av_settings.handler();
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("audioChannelCount");
expect(script).toContain("autoToneMapEnabled");
expect(script).toContain("compositeLinearColor");
expect(script).toContain("sequenceWorkingColorSpace");
});
it("inspects project item color spaces, LUTs, override options, and channel shape", async () => {
await tools.inspect_project_item_av_metadata.handler({ item_id: 'clip "A"' });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('clip \\"A\\"');
expect(script).toContain("getColorSpace");
expect(script).toContain("getOriginalColorSpace");
expect(script).toContain("getOverrideColorSpaceList");
expect(script).toContain("getAudioChannelMapping");
});
});
describe("audio channel mapping", () => {
beforeEach(() => vi.clearAllMocks());
it("rejects non-integer and negative indexes before bridge execution", async () => {
await expect(tools.set_project_item_audio_channel_mapping.handler({
item_id: "clip", channel_index: -1, source_channel_index: 0,
})).resolves.toEqual({ success: false, error: "channel_index must be a non-negative integer" });
await expect(tools.set_project_item_audio_channel_mapping.handler({
item_id: "clip", channel_index: 0, source_channel_index: 1.5,
})).resolves.toEqual({ success: false, error: "source_channel_index must be a non-negative integer" });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("requires Premiere to accept the documented mapping call", async () => {
await tools.set_project_item_audio_channel_mapping.handler({
item_id: "clip", channel_index: 1, source_channel_index: 0,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("mapping.setMappingForChannel(1, 0)");
expect(script).toContain("accepted !== true");
expect(script).toContain('verification: "api-return"');
});
});
+26
View File
@@ -0,0 +1,26 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({ sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }) }));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getExportTools } from "../../src/tools/export.js";
const mocked = vi.mocked(sendCommand);
const tool = getExportTools({ tempDir: "/tmp/test" }).export_sequence_clip_review_frames;
beforeEach(() => vi.clearAllMocks());
describe("export_sequence_clip_review_frames", () => {
it("samples up to twenty clip midpoints in one bridge round trip", async () => {
await tool.handler({ output_dir: "/tmp/review" });
expect(mocked).toHaveBeenCalledTimes(1);
const script = mocked.mock.calls[0][0];
expect(script).toContain("Math.min(track.clips.numItems, 20)");
expect(script).toContain("clip.start.seconds + ((clip.end.seconds - clip.start.seconds) / 2)");
expect(script).toContain("__exportStillFrame(");
});
it("rejects unsafe bounds before contacting Premiere", async () => {
await expect(tool.handler({ output_dir: "/tmp", track_index: -1 })).resolves.toMatchObject({ success: false });
await expect(tool.handler({ output_dir: "/tmp", limit: 51 })).resolves.toMatchObject({ success: false });
expect(mocked).not.toHaveBeenCalled();
});
});
+235
View File
@@ -0,0 +1,235 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BridgeOptions } from "../../src/bridge/file-bridge.js";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getCompetitorGapTools } from "../../src/tools/competitor-gaps.js";
const bridgeOptions: BridgeOptions = { tempDir: "/tmp/competitor-gap-tests", timeoutMs: 5000 };
const mockedSendCommand = vi.mocked(sendCommand);
describe("competitor-gap default-profile tools", () => {
beforeEach(() => vi.clearAllMocks());
it("exposes the requested competitor-parity names", () => {
const tools = getCompetitorGapTools(bridgeOptions);
expect(Object.keys(tools)).toEqual(expect.arrayContaining([
"import_edl",
"add_to_timeline_batch",
"crop_clip",
"setup_ducking",
"validate_project_for_export",
"read_sequence_captions",
"set_clip_properties_batch",
"detect_scene_edits",
]));
});
it("does not attempt an unattended EDL import", async () => {
const result = await getCompetitorGapTools(bridgeOptions).import_edl.handler({ file_path: "D:/cuts/spot.edl" });
expect(result.success).toBe(false);
expect(result.error).toContain("No mutation was attempted");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("rejects ambiguous batch insert positions before writing", async () => {
const result = await getCompetitorGapTools(bridgeOptions).add_to_timeline_batch.handler({
clips: [
{ item_id: "a", track_index: 0, start_seconds: 4 },
{ item_id: "b", track_index: 0, start_seconds: 4 },
],
});
expect(result.success).toBe(false);
expect(result.error).toContain("shares video track");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("preflights and reads back batch insertions", async () => {
await getCompetitorGapTools(bridgeOptions).add_to_timeline_batch.handler({
clips: [
{ item_id: "later", track_index: 1, start_seconds: 10, audio_track_index: 0 },
{ item_id: "first", track_index: 0, start_seconds: 2, audio_track_index: 0 },
],
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("Preflight every mutable dependency");
expect(script).toContain("seq.insertClip");
expect(script).toContain("actualStart");
expect(script.indexOf('itemId: "first"')).toBeLessThan(script.indexOf('itemId: "later"'));
});
it("requires a meaningful crop change before adding an effect", async () => {
const result = await getCompetitorGapTools(bridgeOptions).crop_clip.handler({ node_id: "video-1" });
expect(result.success).toBe(false);
expect(result.error).toContain("requires at least one");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("uses the QE crop catalog only when needed and verifies requested values", async () => {
await getCompetitorGapTools(bridgeOptions).crop_clip.handler({ node_id: "video-1", left: 12, zoom: true });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('__getQeEffectCatalog("video")');
expect(script).toContain("addVideoEffect");
expect(script).toContain('name: "Left"');
expect(script).toContain("getValue");
});
it("rejects overlapping ducking windows before writing automation", async () => {
const result = await getCompetitorGapTools(bridgeOptions).setup_ducking.handler({
node_id: "music-1",
ducking_windows: [
{ start_seconds: 1, end_seconds: 3, ducked_db: -20 },
{ start_seconds: 2, end_seconds: 4, ducked_db: -24 },
],
});
expect(result.success).toBe(false);
expect(result.error).toContain("must not overlap");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("generates verified relative audio keyframes for non-overlapping windows", async () => {
await getCompetitorGapTools(bridgeOptions).setup_ducking.handler({
node_id: "music-1",
base_db: -4,
fade_seconds: 0.25,
ducking_windows: [{ start_seconds: 2, end_seconds: 4, ducked_db: -28 }],
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("setup_ducking only supports audio");
expect(script).toContain("setTimeVarying");
expect(script).toContain("getValueAtTime");
expect(script).toContain("duckingWindowCount");
});
it("generates a non-mutating export readiness audit", async () => {
await getCompetitorGapTools(bridgeOptions).validate_project_for_export.handler({
sequence_id: "edit-1",
output_path: "D:/exports/final.mp4",
preset_path: "D:/presets/h264.epr",
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("OFFLINE_MEDIA");
expect(script).toContain("TIMELINE_GAPS");
expect(script).toContain("readyForExport");
expect(script).not.toContain("encodeSequence");
});
it("makes caption-read limitations explicit instead of treating no tracks as no captions", async () => {
await getCompetitorGapTools(bridgeOptions).read_sequence_captions.handler({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("captionReadSupported");
expect(script).toContain("do not prove this sequence has no captions");
});
it("fails property batch speed requests before touching any clip", async () => {
const result = await getCompetitorGapTools(bridgeOptions).set_clip_properties_batch.handler({
items: [{ node_id: "clip-1", speed: 150 }],
});
expect(result.success).toBe(false);
expect(result.error).toContain("speed");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("preflights, verifies, and restores batch clip properties on mismatch", async () => {
await getCompetitorGapTools(bridgeOptions).set_clip_properties_batch.handler({
items: [{ node_id: "clip-1", opacity: 80, scale: 110, position_x: 400, rotation: 12 }],
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("No batch mutation was attempted");
expect(script).toContain("restoreAll");
expect(script).toContain("No partial batch is reported as verified");
});
it("withholds unsafe CEP scene detection without an authenticated UXP bridge", async () => {
const result = await getCompetitorGapTools(bridgeOptions).detect_scene_edits.handler({
mode: "create_markers",
confirm_non_undoable: true,
});
expect(result.success).toBe(false);
expect(result.error).toContain("authenticated Premiere UXP bridge");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("routes confirmed scene detection through UXP with a stable mode mapping", async () => {
const request = vi.fn().mockResolvedValue({ detected: true });
const bridge = { request } as any;
const result = await getCompetitorGapTools(bridgeOptions, bridge).detect_scene_edits.handler({
mode: "create_subclips",
confirm_non_undoable: true,
operation_id: "scene-42",
});
expect(result).toMatchObject({ success: true, data: { backend: "uxp" } });
expect(request).toHaveBeenCalledWith("sceneEdit.detect", { mode: "createSubclips", operationId: "scene-42" });
});
it("rejects malformed or unsafe inputs before generating a CEP command", async () => {
const tools = getCompetitorGapTools(bridgeOptions);
await expect(tools.add_to_timeline_batch.handler({ clips: [] })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("clips must contain"),
});
await expect(tools.add_to_timeline_batch.handler({
clips: Array.from({ length: 33 }, (_, index) => ({ item_id: `clip-${index}` })),
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("between 1 and 32") });
await expect(tools.add_to_timeline_batch.handler({ clips: [{ item_id: "" }] })).resolves
.toMatchObject({ success: false, error: expect.stringContaining("item_id") });
await expect(tools.add_to_timeline_batch.handler({
clips: [{ item_id: "clip", track_index: -1 }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("non-negative integer") });
await expect(tools.crop_clip.handler({ node_id: "clip", right: 101 })).resolves.toMatchObject({ success: false, error: expect.stringContaining("percentages") });
await expect(tools.setup_ducking.handler({
node_id: "music", fade_seconds: 0, ducking_windows: [],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("positive fade_seconds") });
await expect(tools.setup_ducking.handler({
node_id: "music", ducking_windows: Array.from({ length: 33 }, () => ({ start_seconds: 0, end_seconds: 1, ducked_db: -12 })),
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("0–32 ducking windows") });
await expect(tools.setup_ducking.handler({
node_id: "music", ducking_windows: [{ start_seconds: 3, end_seconds: 3, ducked_db: -12 }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("greater than start_seconds") });
await expect(tools.set_clip_properties_batch.handler({ items: [] })).resolves.toMatchObject({ success: false, error: expect.stringContaining("between 1 and 16") });
await expect(tools.set_clip_properties_batch.handler({ items: [{ node_id: "" }] })).resolves
.toMatchObject({ success: false, error: expect.stringContaining("node_id") });
await expect(tools.set_clip_properties_batch.handler({
items: [{ node_id: "clip" }, { node_id: "clip", opacity: 80 }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("must specify at least one") });
await expect(tools.set_clip_properties_batch.handler({
items: [{ node_id: "clip", opacity: -1 }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid property") });
await expect(tools.set_clip_properties_batch.handler({
items: [{ node_id: "clip", opacity: 80 }, { node_id: "clip", opacity: 60 }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("duplicate node_id") });
await expect(tools.detect_scene_edits.handler({ mode: "create_markers", confirm_non_undoable: false })).resolves
.toMatchObject({ success: false, error: expect.stringContaining("confirm_non_undoable") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("covers optional export, caption, and UXP-scene request variants", async () => {
const tools = getCompetitorGapTools(bridgeOptions);
await tools.validate_project_for_export.handler({ require_non_empty_timeline: false, check_gaps: false });
expect(String(mockedSendCommand.mock.calls[0][0])).toContain("requireNonEmptyTimeline: false");
expect(String(mockedSendCommand.mock.calls[0][0])).toContain("PRESET_NOT_PROVIDED");
vi.clearAllMocks();
await tools.read_sequence_captions.handler({ sequence_id: "sequence-1" });
expect(String(mockedSendCommand.mock.calls[0][0])).toContain('__findSequence("sequence-1")');
const rejectedBridge = { request: vi.fn().mockRejectedValue(new Error("UXP disconnected")) } as any;
await expect(getCompetitorGapTools(bridgeOptions, rejectedBridge).detect_scene_edits.handler({
mode: "apply_cuts", confirm_non_undoable: true,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("UXP disconnected") });
await expect(getCompetitorGapTools(bridgeOptions, { request: vi.fn() } as any).detect_scene_edits.handler({
mode: "not-supported" as any, confirm_non_undoable: true,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("Unsupported scene-edit mode") });
const bridge = { request: vi.fn().mockResolvedValue({ applied: true }) } as any;
await getCompetitorGapTools(bridgeOptions, bridge).detect_scene_edits.handler({
mode: "create_markers", confirm_non_undoable: true,
});
expect(bridge.request).toHaveBeenCalledWith("sceneEdit.detect", { mode: "createMarkers" });
});
});
+284
View File
@@ -0,0 +1,284 @@
import { appendFileSync, mkdtempSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import {
evaluateDeliveryConformance,
getExportTools,
parseRationalRate,
validateDeliveryConformanceContract,
} from "../../src/tools/export.js";
const probe = {
format: { format_name: "mov,mp4,m4a", duration: "10.020", bit_rate: "12000000" },
streams: [
{ codec_type: "video", codec_name: "h264", width: 1920, height: 1080, avg_frame_rate: "30000/1001", bit_rate: "10000000" },
{ codec_type: "audio", codec_name: "aac", sample_rate: "48000", channels: 2 },
],
};
describe("delivery conformance comparisons", () => {
it("parses rational frame rates and rejects malformed contracts", () => {
expect(parseRationalRate("30000/1001")).toBeCloseTo(29.97003, 5);
expect(parseRationalRate("0/0")).toBeNull();
expect(parseRationalRate("0/1")).toBeNull();
expect(parseRationalRate(0)).toBeNull();
expect(validateDeliveryConformanceContract({})).toContain("At least one");
expect(validateDeliveryConformanceContract({ width: -1 })).toContain("positive integer");
expect(validateDeliveryConformanceContract({ minimumVideoBitrateKbps: 20, maximumVideoBitrateKbps: 10 })).toContain("cannot exceed");
expect(parseRationalRate(24)).toBe(24);
expect(parseRationalRate("bad")).toBeNull();
expect(validateDeliveryConformanceContract({ allowedContainerNames: [] })).toContain("container name");
expect(validateDeliveryConformanceContract({ audioChannels: 1.5 })).toContain("positive integer");
expect(validateDeliveryConformanceContract({ durationSeconds: 0 })).toContain("greater than 0");
expect(validateDeliveryConformanceContract({ durationToleranceSeconds: -1 })).toContain("non-negative");
expect(validateDeliveryConformanceContract({ targetLufs: 1 })).toContain("target_lufs");
expect(validateDeliveryConformanceContract({ maximumTruePeakDbfs: 1 })).toContain("maximum_true_peak");
expect(validateDeliveryConformanceContract({ frameRateTolerance: 0.01 })).toContain("frame_rate_tolerance requires frame_rate");
expect(validateDeliveryConformanceContract({ durationToleranceSeconds: 0.01 })).toContain("duration_tolerance_seconds requires duration_seconds");
expect(validateDeliveryConformanceContract({ loudnessToleranceLu: 1 })).toContain("loudness_tolerance_lu requires target_lufs");
expect(validateDeliveryConformanceContract({ videoCodec: "h264", frameRateTolerance: 0.01 })).toContain("frame_rate_tolerance requires frame_rate");
expect(validateDeliveryConformanceContract({ videoCodec: " " })).toContain("non-empty");
expect(validateDeliveryConformanceContract({ audioCodec: "" })).toContain("non-empty");
});
it("falls back from unusable frame-rate metadata and does not fail missing measurements", () => {
const checks = evaluateDeliveryConformance({
format: { bit_rate: "12000000" },
streams: [{ codec_type: "video", avg_frame_rate: "0/1", r_frame_rate: "24/1", bit_rate: "N/A" }],
}, {
frameRate: 24, minimumVideoBitrateKbps: 11000, targetLufs: -23, maximumTruePeakDbfs: -1,
}, { integratedLufs: -23, truePeakDbfs: null });
expect(checks.find(check => check.id === "frame_rate")?.status).toBe("pass");
expect(checks.find(check => check.id === "minimum_video_bitrate")).toMatchObject({
status: "not_evaluated", detail: "Video bitrate metadata was unavailable",
});
expect(checks.find(check => check.id === "integrated_loudness")?.status).toBe("pass");
expect(checks.find(check => check.id === "true_peak")?.status).toBe("not_evaluated");
});
it("reports matching and mismatching fields independently", () => {
const checks = evaluateDeliveryConformance(probe, {
allowedContainerNames: ["mp4"], videoCodec: "h264", audioCodec: "pcm_s24le",
width: 1920, height: 720, frameRate: 29.97, frameRateTolerance: 0.01,
durationSeconds: 10, durationToleranceSeconds: 0.05,
minimumVideoBitrateKbps: 9000, maximumVideoBitrateKbps: 11000,
audioSampleRateHz: 48000, audioChannels: 2,
});
expect(checks.find(check => check.id === "container_demuxer_family")).toMatchObject({
status: "pass", detail: expect.stringContaining("do not identify an exact container subtype"),
});
expect(checks.find(check => check.id === "audio_codec")?.status).toBe("fail");
expect(checks.find(check => check.id === "height")?.status).toBe("fail");
expect(checks.find(check => check.id === "frame_rate")?.status).toBe("pass");
expect(checks.find(check => check.id === "maximum_video_bitrate")?.status).toBe("pass");
});
it("fails expected streams that are missing and preserves unavailable loudness", () => {
const checks = evaluateDeliveryConformance({ format: {}, streams: [] }, {
videoCodec: "prores", audioChannels: 2, targetLufs: -23, maximumTruePeakDbfs: -1,
}, null, "No audio stream was available for EBU R128 analysis");
expect(checks.find(check => check.id === "video_codec")?.status).toBe("fail");
expect(checks.find(check => check.id === "audio_channels")?.status).toBe("fail");
expect(checks.find(check => check.id === "integrated_loudness")?.status).toBe("not_evaluated");
expect(checks.find(check => check.id === "true_peak")?.status).toBe("not_evaluated");
});
it("evaluates loudness failures and does not evaluate video bitrate without video", () => {
const checks = evaluateDeliveryConformance({ format: { format_name: "matroska", bit_rate: "5000000" }, streams: [{ codec_type: "audio", codec_name: "aac" }] }, {
allowedContainerNames: ["mov"], minimumVideoBitrateKbps: 1000, maximumVideoBitrateKbps: 2000,
targetLufs: -23, loudnessToleranceLu: 0.5, maximumTruePeakDbfs: -1,
}, { integratedLufs: -20, truePeakDbfs: -0.5 });
expect(checks.find(check => check.id === "minimum_video_bitrate")).toMatchObject({
status: "not_evaluated", detail: expect.stringContaining("No video stream"),
});
expect(checks.find(check => check.id === "maximum_video_bitrate")?.status).toBe("not_evaluated");
expect(checks.filter(check => !check.id.includes("video_bitrate")).every(check => check.status === "fail")).toBe(true);
const missingBitrate = evaluateDeliveryConformance({ format: {}, streams: [{ codec_type: "video", bit_rate: "N/A" }] }, {
minimumVideoBitrateKbps: 1000,
});
expect(missingBitrate.find(check => check.id === "minimum_video_bitrate")).toMatchObject({
status: "not_evaluated", detail: "Video bitrate metadata was unavailable",
});
const coverArtOnly = evaluateDeliveryConformance({
format: { bit_rate: "5000000" },
streams: [{ codec_type: "video", codec_name: "mjpeg", width: 1000, disposition: { attached_pic: 1 } }],
}, { videoCodec: "mjpeg", width: 1000, minimumVideoBitrateKbps: 1000 });
expect(coverArtOnly.find(check => check.id === "video_codec")?.status).toBe("fail");
expect(coverArtOnly.find(check => check.id === "width")?.status).toBe("fail");
expect(coverArtOnly.find(check => check.id === "minimum_video_bitrate")?.status).toBe("not_evaluated");
});
it("does not treat unavailable numeric metadata as a mismatch", () => {
const checks = evaluateDeliveryConformance({
format: { duration: "N/A", bit_rate: "5000000" },
streams: [{ codec_type: "video", width: "N/A", height: "N/A", avg_frame_rate: "0/0", r_frame_rate: "0/0" }],
}, {
width: 1920, height: 1080, frameRate: 24, durationSeconds: 10,
minimumVideoBitrateKbps: 1000,
});
for (const id of ["width", "height", "frame_rate", "duration", "minimum_video_bitrate"]) {
expect(checks.find(check => check.id === id)?.status).toBe("not_evaluated");
}
});
it("does not evaluate video bitrate from aggregate format bitrate", () => {
const checks = evaluateDeliveryConformance({
format: { bit_rate: "5000000" },
streams: [{ codec_type: "video", bit_rate: "1000000" }],
}, { minimumVideoBitrateKbps: 3000, maximumVideoBitrateKbps: 3000 });
expect(checks.find(check => check.id === "minimum_video_bitrate")?.status).toBe("fail");
expect(checks.find(check => check.id === "maximum_video_bitrate")?.status).toBe("pass");
});
});
describe("verify_delivery_conformance boundary", () => {
const tool = getExportTools({ tempDir: "/tmp/test" }).verify_delivery_conformance;
beforeEach(() => vi.clearAllMocks());
it("reports its local filesystem-only operational boundary", () => {
expect(tool.operationalCapability).toMatchObject({
backend: "local", backends: ["local"], authority: "filesystem",
verificationBoundary: "local_filesystem", hostVerificationRequired: false,
minimumPremiereVersion: null,
});
});
function mediaFile() {
const directory = mkdtempSync(join(tmpdir(), "premiere-delivery-conformance-"));
const mediaPath = join(directory, "delivery.mp4");
writeFileSync(mediaPath, "delivery-fixture");
return mediaPath;
}
it("returns an explicit failure for a missing baseline file", async () => {
const missingPath = join(tmpdir(), `missing-delivery-${Date.now()}.mp4`);
await expect(tool.handler({ output_path: missingPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on disk") });
expect(mockedExecFileAsync).not.toHaveBeenCalled();
});
it("returns a complete local conformance report", async () => {
const mediaPath = mediaFile();
mockedExecFileAsync
.mockResolvedValueOnce({ stdout: JSON.stringify(probe), stderr: "" })
.mockResolvedValueOnce({ stdout: "", stderr: "Summary:\n Integrated loudness:\n I: -23.0 LUFS\n True peak:\n Peak: -2.0 dBFS" });
await expect(tool.handler({
output_path: mediaPath, allowed_container_names: ["mp4"], video_codec: "h264",
frame_rate: 29.97, frame_rate_tolerance: 0.01, target_lufs: -23,
maximum_true_peak_dbfs: -1,
})).resolves.toMatchObject({
success: true,
data: { mediaPath, conforms: true, evaluated: 5, notEvaluated: 0 },
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(2);
expect(mockedExecFileAsync).toHaveBeenNthCalledWith(1, "ffprobe", expect.arrayContaining(["-protocol_whitelist", "file,crypto,data"]), expect.any(Object));
expect(mockedExecFileAsync).toHaveBeenNthCalledWith(2, "ffmpeg", expect.arrayContaining(["-protocol_whitelist", "file,crypto,data", "-map", "0:a:0"]), expect.any(Object));
});
it("keeps ffprobe process failures explicit", async () => {
const mediaPath = mediaFile();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("probe failed"), { stderr: "invalid media" }));
await expect(tool.handler({ output_path: mediaPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid media") });
const emptyStderrPath = mediaFile();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("probe failed"), { stderr: "" }));
await expect(tool.handler({ output_path: emptyStderrPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("probe failed") });
const noisyStderrPath = mediaFile();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("probe failed"), { stderr: "x".repeat(5_000_000) }));
const result = await tool.handler({ output_path: noisyStderrPath, video_codec: "h264" });
expect(result).toMatchObject({ success: false, error: expect.stringContaining("ffprobe delivery conformance inspection failed:") });
expect(result.error).toHaveLength(4_096 + "ffprobe delivery conformance inspection failed: ".length);
expect(result.error.endsWith("…")).toBe(true);
});
it("reports ffprobe timeouts and refuses a file that changes during inspection", async () => {
const timedOutPath = mediaFile();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timeout"), { killed: true }));
await expect(tool.handler({ output_path: timedOutPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
const changingPath = mediaFile();
mockedExecFileAsync.mockImplementationOnce(async () => {
appendFileSync(changingPath, "changed");
return { stdout: JSON.stringify(probe), stderr: "" };
});
await expect(tool.handler({ output_path: changingPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("changed during") });
const disappearingPath = mediaFile();
mockedExecFileAsync.mockImplementationOnce(async () => {
unlinkSync(disappearingPath);
return { stdout: JSON.stringify(probe), stderr: "" };
});
await expect(tool.handler({ output_path: disappearingPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("changed during") });
const rejectedAfterDeletionPath = mediaFile();
mockedExecFileAsync.mockImplementationOnce(async () => {
unlinkSync(rejectedAfterDeletionPath);
throw Object.assign(new Error("probe failed"), { stderr: "invalid media" });
});
await expect(tool.handler({ output_path: rejectedAfterDeletionPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("changed during") });
const invalidAfterChangePath = mediaFile();
mockedExecFileAsync.mockImplementationOnce(async () => {
appendFileSync(invalidAfterChangePath, "changed");
return { stdout: "null", stderr: "" };
});
await expect(tool.handler({ output_path: invalidAfterChangePath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("changed during") });
});
it("handles missing tools, invalid probe JSON, and unavailable loudness without overclaiming", async () => {
const missingToolPath = mediaFile();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("missing"), { code: "ENOENT" }));
await expect(tool.handler({ output_path: missingToolPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("ffprobe was not found") });
const invalidJsonPath = mediaFile();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "null", stderr: "" });
await expect(tool.handler({ output_path: invalidJsonPath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid delivery report") });
const incompleteProbePath = mediaFile();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: JSON.stringify({ format: {} }), stderr: "" });
await expect(tool.handler({ output_path: incompleteProbePath, video_codec: "h264" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid delivery report") });
const silentPath = mediaFile();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: JSON.stringify({ format: {}, streams: [] }), stderr: "" });
await expect(tool.handler({ output_path: silentPath, target_lufs: -23 }))
.resolves.toMatchObject({ success: true, data: { conforms: false, evaluated: 0, notEvaluated: 1 } });
const unreadableAudioPath = mediaFile();
mockedExecFileAsync
.mockResolvedValueOnce({ stdout: JSON.stringify(probe), stderr: "" })
.mockRejectedValueOnce(Object.assign(new Error("decode"), { stderr: "no measurement" }));
await expect(tool.handler({ output_path: unreadableAudioPath, target_lufs: -23 }))
.resolves.toMatchObject({ success: true, data: { conforms: false, notEvaluated: 1 } });
const partialAudioPath = mediaFile();
mockedExecFileAsync
.mockResolvedValueOnce({ stdout: JSON.stringify(probe), stderr: "" })
.mockRejectedValueOnce(Object.assign(new Error("decode"), { stderr: "Summary:\n Integrated loudness:\n I: -23.0 LUFS\n True peak:\n Peak: -2.0 dBFS" }));
await expect(tool.handler({ output_path: partialAudioPath, target_lufs: -23, maximum_true_peak_dbfs: -1 }))
.resolves.toMatchObject({ success: true, data: { conforms: false, evaluated: 0, notEvaluated: 2 } });
});
});
+112
View File
@@ -0,0 +1,112 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
deliveryFileChangedDuringHash,
getExportTools,
inspectExportPresetFile,
verifyDeliveryFile,
} from "../../src/tools/export.js";
const temporaryDirectories: string[] = [];
function temporaryFile(name: string, contents: string | Buffer): string {
const directory = mkdtempSync(join(tmpdir(), "premiere-delivery-test-"));
temporaryDirectories.push(directory);
const path = join(directory, name);
writeFileSync(path, contents);
return path;
}
afterEach(() => {
vi.restoreAllMocks();
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
describe("delivery file verification", () => {
it("returns deterministic SHA-256 metadata", async () => {
const path = temporaryFile("delivery.mov", "premiere delivery\n");
const result = await verifyDeliveryFile(path);
expect(result).toMatchObject({
path,
exists: true,
regularFile: true,
sizeBytes: 18,
checksum: {
algorithm: "sha256",
value: "beabfc0259a7213ae09fffc062f2bdfc69fc0e702ab19ff44b50259b1f02d843",
},
valid: true,
});
});
it("reports expected checksum and size mismatches without claiming validity", async () => {
const path = temporaryFile("delivery.mp4", "video");
const result = await verifyDeliveryFile(path, {
expectedChecksum: "0".repeat(64),
expectedSizeBytes: 99,
});
expect(result).toMatchObject({
matchesExpectedChecksum: false,
matchesExpectedSize: false,
valid: false,
});
});
it("detects a file replacement or write during hashing", () => {
const snapshot = { dev: 1, ino: 2, size: 100, mtimeMs: 500 };
expect(deliveryFileChangedDuringHash(snapshot, snapshot)).toBe(false);
expect(deliveryFileChangedDuringHash(snapshot, { ...snapshot, size: 101 })).toBe(true);
expect(deliveryFileChangedDuringHash(snapshot, { ...snapshot, mtimeMs: 501 })).toBe(true);
expect(deliveryFileChangedDuringHash(snapshot, { ...snapshot, ino: 3 })).toBe(true);
});
it("returns mismatch details through the MCP tool as a completed verification", async () => {
const path = temporaryFile("delivery.mp4", "video");
const result = await getExportTools({}).verify_delivery_file.handler({
output_path: path,
expected_checksum: "0".repeat(64),
expected_size_bytes: 99,
});
expect(result).toMatchObject({
success: true,
data: {
path,
valid: false,
matchesExpectedChecksum: false,
matchesExpectedSize: false,
},
});
});
it("rejects missing, empty, and invalid delivery targets", async () => {
await expect(verifyDeliveryFile("/definitely/missing/delivery.mov")).rejects.toThrow("does not exist");
const empty = temporaryFile("empty.mov", "");
await expect(verifyDeliveryFile(empty)).rejects.toThrow("expected at least 1");
const path = temporaryFile("delivery.mov", "content");
await expect(verifyDeliveryFile(path, { algorithm: "md5" as never })).rejects.toThrow("sha256");
await expect(verifyDeliveryFile(path, { expectedChecksum: "not-hex" })).rejects.toThrow("64-character");
});
});
describe("export preset inspection", () => {
it("accepts a non-empty regular .epr file", () => {
const path = temporaryFile("YouTube.epr", "<preset />");
expect(inspectExportPresetFile(path)).toMatchObject({
path,
exists: true,
regularFile: true,
sizeBytes: 10,
});
});
it("rejects wrong extensions and empty presets", () => {
const wrongExtension = temporaryFile("preset.xml", "<preset />");
expect(() => inspectExportPresetFile(wrongExtension)).toThrow(".epr");
const empty = temporaryFile("empty.epr", "");
expect(() => inspectExportPresetFile(empty)).toThrow("empty");
});
});
+94
View File
@@ -0,0 +1,94 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("../../src/bridge/file-bridge.js", () => ({ sendCommand: vi.fn() }));
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import { analyzeBeatPcm, getAudioTools } from "../../src/tools/audio.js";
const tools = getAudioTools({ tempDir: "/tmp/beat-tests" });
beforeEach(() => vi.clearAllMocks());
function pulsePcm(seconds = 12) {
const sampleRate = 200;
const samples = new Int16Array(sampleRate * seconds);
for (let at = 0; at < samples.length; at += sampleRate / 2) {
for (let width = 0; width < 4; width++) samples[at + width] = 30_000;
}
return { sampleRate, samples };
}
function sampleBuffer(samples: Int16Array) {
const buffer = Buffer.alloc(samples.length * 2);
for (let index = 0; index < samples.length; index++) buffer.writeInt16LE(samples[index], index * 2);
return buffer;
}
function createMediaFixture() {
const directory = mkdtempSync(join(tmpdir(), "premiere-beats-"));
const mediaPath = join(directory, "music.wav");
writeFileSync(mediaPath, "audio-fixture");
return mediaPath;
}
describe("detect_beats analysis", () => {
it("recovers a phase-aligned 120 BPM pulse train", () => {
const { sampleRate, samples } = pulsePcm();
const result = analyzeBeatPcm(samples, sampleRate);
expect(result.bpm).toBeCloseTo(120, 0);
expect(result.confidence).toBeGreaterThan(0.5);
expect(result.beatTimesSeconds.slice(0, 4)).toEqual([0, 0.5, 1, 1.5]);
});
it("rejects short or silent audio instead of fabricating a grid", () => {
expect(() => analyzeBeatPcm(new Int16Array(100), 200)).toThrow("at least two seconds");
expect(() => analyzeBeatPcm(new Int16Array(800), 200)).toThrow("No repeating beat evidence");
});
it("validates each input before invoking FFmpeg", async () => {
await expect(tools.detect_beats.handler({ media_path: "" }))
.resolves.toMatchObject({ success: false, error: "media_path is required." });
await expect(tools.detect_beats.handler({ media_path: "missing.wav", max_beats: 0 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("max_beats") });
await expect(tools.detect_beats.handler({ media_path: "definitely-missing.wav" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
expect(mockedExecFileAsync).not.toHaveBeenCalled();
});
it("decodes a bounded stream and reports truncation without mutating Premiere", async () => {
const mediaPath = createMediaFixture();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: sampleBuffer(pulsePcm().samples), stderr: Buffer.alloc(0) });
await expect(tools.detect_beats.handler({ media_path: mediaPath, max_beats: 3 })).resolves.toMatchObject({
success: true,
data: { bpm: 120, beatTimesSeconds: [0, 0.5, 1], beatTimesTruncated: true, reliable: true },
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(1);
const args = mockedExecFileAsync.mock.calls[0][1] as string[];
expect(args).toEqual(expect.arrayContaining(["-t", "1800", "-ar", "200", "pipe:1"]));
});
it("distinguishes missing FFmpeg, timeout, and decode failures", async () => {
const mediaPath = createMediaFixture();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("missing"), { code: "ENOENT" }));
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on PATH") });
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timeout"), { killed: true }));
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode"), { stderr: "invalid stream" }));
await expect(tools.detect_beats.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid stream") });
});
});
+259
View File
@@ -0,0 +1,259 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import {
getAudioTools,
invertToSegments,
mapSilenceIntervalsToTimeline,
parseDurationSeconds,
parseSilenceDetectOutput,
} from "../../src/tools/audio.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tools = getAudioTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
beforeEach(() => vi.clearAllMocks());
describe("parseSilenceDetectOutput", () => {
it("pairs each silence_start with its silence_end", () => {
const stderr = [
"[silencedetect @ 0x1] silence_start: 2.5",
"[silencedetect @ 0x1] silence_end: 6.25 | silence_duration: 3.75",
"[silencedetect @ 0x1] silence_start: 10",
"[silencedetect @ 0x1] silence_end: 12.5 | silence_duration: 2.5",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)).toEqual([
{ start: 2.5, end: 6.25, duration: 3.75 },
{ start: 10, end: 12.5, duration: 2.5 },
]);
});
it("closes a silence that runs to the end of the file at the media duration", () => {
// ffmpeg emits no silence_end when the file ends mid-silence.
const stderr = [
"[silencedetect @ 0x1] silence_start: 2",
"[silencedetect @ 0x1] silence_end: 5 | silence_duration: 3",
"[silencedetect @ 0x1] silence_start: 17.5",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)).toEqual([
{ start: 2, end: 5, duration: 3 },
{ start: 17.5, end: 20, duration: 2.5 },
]);
});
it("drops an unterminated silence when the duration is unknown", () => {
const stderr = "[silencedetect @ 0x1] silence_start: 17.5";
expect(parseSilenceDetectOutput(stderr, null)).toEqual([]);
});
it("clamps a negative silence_start to zero", () => {
const stderr = [
"[silencedetect @ 0x1] silence_start: -0.001",
"[silencedetect @ 0x1] silence_end: 4 | silence_duration: 4",
].join("\n");
expect(parseSilenceDetectOutput(stderr, 20)[0].start).toBe(0);
});
it("returns nothing for output containing no silence markers", () => {
expect(parseSilenceDetectOutput("frame= 480 fps=0.0 q=-0.0", 20)).toEqual([]);
});
});
describe("parseDurationSeconds", () => {
it("reads the duration banner", () => {
expect(parseDurationSeconds(" Duration: 00:00:20.00, start: 0.000000")).toBe(20);
});
it("handles hours and fractional seconds", () => {
expect(parseDurationSeconds(" Duration: 01:02:03.50, bitrate: 1000 kb/s")).toBeCloseTo(3723.5, 3);
});
it("returns null when no duration is present", () => {
expect(parseDurationSeconds("no banner here")).toBeNull();
});
});
describe("invertToSegments", () => {
it("returns the ranges between silences", () => {
const silences = [
{ start: 2, end: 5, duration: 3 },
{ start: 10, end: 12, duration: 2 },
];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 0, end: 2, duration: 2 },
{ start: 5, end: 10, duration: 5 },
{ start: 12, end: 20, duration: 8 },
]);
});
it("omits a leading segment when the media opens on silence", () => {
const silences = [{ start: 0, end: 4, duration: 4 }];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 4, end: 20, duration: 16 },
]);
});
it("omits a trailing segment when the media ends on silence", () => {
const silences = [{ start: 16, end: 20, duration: 4 }];
expect(invertToSegments(silences, 20)).toEqual([
{ start: 0, end: 16, duration: 16 },
]);
});
it("returns the whole media when there is no silence", () => {
expect(invertToSegments([], 20)).toEqual([
{ start: 0, end: 20, duration: 20 },
]);
});
it("returns nothing usable when the duration is unknown", () => {
expect(invertToSegments([], null)).toEqual([]);
});
it("collapses fully silent media to no segments", () => {
const silences = [{ start: 0, end: 20, duration: 20 }];
expect(invertToSegments(silences, 20)).toEqual([]);
});
});
describe("mapSilenceIntervalsToTimeline", () => {
it("clips source ranges and maps the retained intervals onto a 1x placement", () => {
expect(mapSilenceIntervalsToTimeline([
{ start: 0, end: 4, duration: 4 },
{ start: 7, end: 12, duration: 5 },
], {
sourceInSeconds: 2,
sourceOutSeconds: 10,
timelineStartSeconds: 100,
maxCandidates: 50,
})).toEqual({
totalCandidateCount: 2,
truncated: false,
candidates: [
{
sourceStartSeconds: 2,
sourceEndSeconds: 4,
durationSeconds: 2,
timelineStartSeconds: 100,
timelineEndSeconds: 102,
},
{
sourceStartSeconds: 7,
sourceEndSeconds: 10,
durationSeconds: 3,
timelineStartSeconds: 105,
timelineEndSeconds: 108,
},
],
});
});
it("keeps counting bounded candidates after its response limit", () => {
const result = mapSilenceIntervalsToTimeline([
{ start: 1, end: 2, duration: 1 },
{ start: 3, end: 4, duration: 1 },
], {
sourceInSeconds: 0,
sourceOutSeconds: 10,
timelineStartSeconds: 0,
maxCandidates: 1,
});
expect(result.totalCandidateCount).toBe(2);
expect(result.truncated).toBe(true);
expect(result.candidates).toHaveLength(1);
});
});
describe("detect_silence argument handling", () => {
it("requires either a media path or a project item", async () => {
const result = await tools.detect_silence.handler({});
expect(result.success).toBe(false);
expect(result.error).toContain("media_path or project_item_id");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("rejects a positive noise threshold", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/whatever.mp4",
noise_threshold_db: 12,
});
expect(result.success).toBe(false);
expect(result.error).toContain("dBFS");
});
it("rejects a non-positive minimum duration", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/whatever.mp4",
min_duration_seconds: 0,
});
expect(result.success).toBe(false);
expect(result.error).toContain("greater than 0");
});
it("reports a missing file before shelling out to ffmpeg", async () => {
const result = await tools.detect_silence.handler({
media_path: "/tmp/definitely-not-here-9f3a2b.mp4",
});
expect(result.success).toBe(false);
expect(result.error).toContain("not found on disk");
});
it("resolves a project item's media path through the bridge", async () => {
mockedSendCommand.mockResolvedValueOnce({
success: false,
error: "Project item not found: missing-clip",
});
const result = await tools.detect_silence.handler({
project_item_id: "missing-clip",
});
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
expect(mockedSendCommand.mock.calls[0][0]).toContain("getMediaPath");
expect(result.success).toBe(false);
expect(result.error).toContain("missing-clip");
});
});
describe("plan_silence_review_markers argument handling", () => {
const plan = tools.plan_silence_review_markers;
it("rejects invalid placement or response bounds before decoding media", async () => {
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: -1,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("timeline_start_seconds") });
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: 0,
source_in_seconds: 5,
source_out_seconds: 5,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("source_out_seconds") });
await expect(plan.handler({
media_path: "/tmp/does-not-matter.mov",
timeline_start_seconds: 0,
max_candidates: 201,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("max_candidates") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("detect_silence distribution prerequisites", () => {
it("installs ffmpeg in the production container", () => {
const dockerfile = readFileSync(join(process.cwd(), "Dockerfile"), "utf8");
expect(dockerfile).toMatch(/apk add --no-cache ffmpeg/);
});
it("documents the local ffmpeg prerequisite", () => {
const readme = readFileSync(join(process.cwd(), "README.md"), "utf8");
expect(readme).toContain("`detect_silence`");
expect(readme).toContain("winget install Gyan.FFmpeg");
expect(readme).toContain("brew install ffmpeg");
});
});
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { analyzeDialogueEditCandidates, getDialogueAnalysisTools } from "../../src/tools/dialogue-analysis.js";
const revision = `sha256:${"a".repeat(64)}`;
describe("dialogue candidate analysis", () => {
it("returns stable review candidates without applying edits", () => {
const input = { segments: [
{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 1, text: "Um hello world" },
{ id: "b", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 1, end_seconds: 2, text: "hello world again" },
{ id: "c", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 2, end_seconds: 3, text: "hello world again" },
], fillerWords: ["um"], silenceRanges: [{ source_project_item_id: "clip", start_seconds: 3, end_seconds: 4 }], minimumSilenceSeconds: .7 };
const first = analyzeDialogueEditCandidates(input), second = analyzeDialogueEditCandidates(input);
expect(first.analysis_revision).toBe(second.analysis_revision);
expect(first.candidates.map((x) => x.reason)).toEqual(["filler_word", "repeated_phrase", "long_silence"]);
expect(first.applied).toBe(false);
});
it("rejects unbound transcript input", () => expect(() => analyzeDialogueEditCandidates({ segments: [{ id: "a", source_project_item_id: "clip", transcript_revision: "bad", start_seconds: 0, end_seconds: 1, text: "hello" }] })).toThrow(/sha256/));
it.each([
[[], /between 1/], [[null], /must be an object/],
[[{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 1, text: "x", extra: 1 }], /unknown field/],
[[{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 1, end_seconds: 1, text: "x" }], /greater/],
[[{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 1, text: 1 }], /text must/],
])("rejects malformed segments", (segments, message) => expect(() => analyzeDialogueEditCandidates({ segments })).toThrow(message));
it("validates optional analysis controls", () => {
const segments = [{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 1, text: "okay" }];
expect(() => analyzeDialogueEditCandidates({ segments, fillerWords: "um" })).toThrow(/filler_words/);
expect(() => analyzeDialogueEditCandidates({ segments, fillerWords: ["um", "um"] })).toThrow(/duplicates/);
expect(() => analyzeDialogueEditCandidates({ segments, minimumSilenceSeconds: 40 })).toThrow(/between/);
expect(() => analyzeDialogueEditCandidates({ segments, silenceRanges: "bad" })).toThrow(/silence_ranges/);
expect(() => analyzeDialogueEditCandidates({ segments, silenceRanges: [null] })).toThrow(/must be an object/);
expect(() => analyzeDialogueEditCandidates({ segments, silenceRanges: [{ source_project_item_id: "clip", start_seconds: 0, end_seconds: 1, extra: true }] })).toThrow(/unknown field/);
});
it("deduplicates time removal and ignores short silence", () => {
const segments = [
{ id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 2, text: "um okay" },
{ id: "b", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 3, end_seconds: 4, text: "different" },
];
const result = analyzeDialogueEditCandidates({ segments, fillerWords: ["um"], silenceRanges: [{ source_project_item_id: "clip", start_seconds: 1, end_seconds: 3 }, { source_project_item_id: "clip", start_seconds: 5, end_seconds: 5.2 }], minimumSilenceSeconds: .5 });
expect(result.projected_removed_seconds).toBe(3);
expect(result.candidate_count).toBe(2);
});
it("rejects duplicate ids and invalid labels", () => {
const base = { id: "a", source_project_item_id: "clip", transcript_revision: revision, start_seconds: 0, end_seconds: 1, text: "x" };
expect(() => analyzeDialogueEditCandidates({ segments: [base, { ...base, start_seconds: 1, end_seconds: 2 }] })).toThrow(/duplicate id/);
expect(() => analyzeDialogueEditCandidates({ segments: [{ ...base, speaker_label: "" }] })).toThrow(/speaker_label/);
});
it("bounds very large candidate sets", () => {
const segments = Array.from({ length: 520 }, (_, index) => ({ id: String(index), source_project_item_id: "clip", transcript_revision: revision, start_seconds: index, end_seconds: index + .5, text: "um repeated phrase" }));
const result = analyzeDialogueEditCandidates({ segments, fillerWords: ["um"] });
expect(result.candidate_count).toBe(512);
expect(result.truncated).toBe(true);
});
it("normalizes handler failures", async () => {
expect((await getDialogueAnalysisTools().analyze_dialogue_edit_candidates.handler({ segments: [] })).success).toBe(false);
});
});
+59
View File
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({ sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }) }));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getInspectionTools } from "../../src/tools/inspection.js";
const mocked = vi.mocked(sendCommand);
const tool = getInspectionTools({ tempDir: "/tmp/test" }).inspect_edit_readiness;
const reviewReport = getInspectionTools({ tempDir: "/tmp/test" }).inspect_sequence_review_report;
beforeEach(() => vi.clearAllMocks());
describe("inspect_edit_readiness", () => {
it("checks five structural defect classes in one read-only bridge call", async () => {
await tool.handler({ primary_video_track: 0, maximum_scale_percent: 125 });
expect(mocked).toHaveBeenCalledTimes(1);
const script = mocked.mock.calls[0][0];
for (const marker of ["PRIMARY_TRACK_GAP", "EMPTY_VIDEO_TIMELINE", "DISABLED_CLIP", "MUTED_TRACK", "EXCESSIVE_SCALE"]) {
expect(script).toContain(marker);
}
expect(script).not.toMatch(/setMute\(|setValue\(|remove\(|insertClip\(/);
});
it("rejects invalid thresholds locally", async () => {
await expect(tool.handler({ primary_video_track: -1 })).resolves.toMatchObject({ success: false });
await expect(tool.handler({ maximum_scale_percent: 0 })).resolves.toMatchObject({ success: false });
expect(mocked).not.toHaveBeenCalled();
});
});
describe("inspect_sequence_review_report", () => {
it("builds one read-only, privacy-bounded handoff report", async () => {
await reviewReport.handler({ sequence_id: "Final Cut", primary_video_track: 1, max_markers: 25 });
expect(mocked).toHaveBeenCalledTimes(1);
const script = mocked.mock.calls[0][0];
for (const marker of [
"sequence-review-handoff",
"PRIMARY_VIDEO_TRACK_UNAVAILABLE",
"DISABLED_CLIP",
"MUTED_TRACK",
"isOffline",
"markersTruncated",
]) {
expect(script).toContain(marker);
}
expect(script).not.toContain("getMediaPath");
expect(script).not.toContain("entry.comments");
expect(script).not.toMatch(/setValue\(|remove\(|insertClip\(|save\(/);
});
it("includes private marker comments only after explicit opt-in", async () => {
await reviewReport.handler({ include_marker_comments: true });
expect(mocked.mock.calls[0][0]).toContain("entry.comments");
});
it("rejects invalid bounded input before using the bridge", async () => {
await expect(reviewReport.handler({ primary_video_track: -1 })).resolves.toMatchObject({ success: false });
await expect(reviewReport.handler({ max_markers: 201 })).resolves.toMatchObject({ success: false });
expect(mocked).not.toHaveBeenCalled();
});
});
+116
View File
@@ -0,0 +1,116 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../src/bridge/file-bridge.js")>();
return { ...actual, sendCommand: vi.fn() };
});
vi.mock("../../src/telemetry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../src/telemetry.js")>();
return { ...actual, captureActivationEvent: vi.fn() };
});
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getHealthTools } from "../../src/tools/health.js";
const mockedSendCommand = vi.mocked(sendCommand);
afterEach(() => vi.clearAllMocks());
describe("health connection diagnostics", () => {
it("returns an explicit local feature-support report and safely surfaces invalid version input", async () => {
const tools = getHealthTools(
{ tempDir: "/tmp/health" },
undefined,
() => ({}),
);
await expect(tools.get_advanced_feature_support.handler({
backend: "uxp",
premiere_version: "26.3.0",
frameio_entitled: true,
generative_ai_entitled: true,
network_available: true,
})).resolves.toMatchObject({
success: true,
data: {
context: { backend: "uxp", premiereVersion: "26.3.0" },
features: { productions: { staticEligibility: { eligible: true } } },
},
});
await expect(tools.get_advanced_feature_support.handler({
premiere_version: "26.beta",
})).resolves.toEqual({
success: false,
error: "premiere_version must contain only numeric version components",
});
});
it("reports an unavailable UXP bridge without falling back to CEP", async () => {
const telemetry = { enabled: true, capture: vi.fn(), shutdown: vi.fn() };
const result = await getHealthTools({ tempDir: "/tmp/health" }, undefined, undefined, { telemetry })
.verify_premiere_connection.handler({ backend: "uxp" });
expect(result).toMatchObject({
success: true,
data: {
backend: "uxp",
overall: "needs_attention",
components: expect.arrayContaining([
expect.objectContaining({ id: "premiere_connector", state: "needs_attention" }),
]),
},
});
expect(mockedSendCommand).not.toHaveBeenCalled();
expect(telemetry.capture).not.toHaveBeenCalled();
});
it("treats a malformed UXP state response as a reachable host with no open project", async () => {
const request = vi.fn().mockResolvedValue(null);
const result = await getHealthTools(
{ tempDir: "/tmp/health" },
undefined,
undefined,
{ uxpBridge: { request } as any },
).verify_premiere_connection.handler({ backend: "uxp" });
expect(request).toHaveBeenCalledWith("state.get");
expect(result).toMatchObject({
success: true,
data: {
backend: "uxp",
overall: "needs_attention",
components: expect.arrayContaining([
expect.objectContaining({ id: "premiere_connector", state: "ready" }),
expect.objectContaining({ id: "active_project", state: "needs_attention" }),
expect.objectContaining({ id: "active_sequence", state: "needs_attention" }),
]),
},
});
});
it("reports an unreachable UXP bridge when its read-only request rejects", async () => {
const request = vi.fn().mockRejectedValue(new Error("panel closed"));
const result = await getHealthTools(
{ tempDir: "/tmp/health" },
undefined,
undefined,
{ uxpBridge: { request } as any },
).verify_premiere_connection.handler({ backend: "uxp" });
expect(result).toMatchObject({
success: true,
data: { backend: "uxp", overall: "needs_attention" },
});
});
it("handles failed and thrown CEP probes as unavailable diagnostics", async () => {
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "CEP unavailable" });
const tools = getHealthTools({ tempDir: "/tmp/health" });
await expect(tools.verify_premiere_connection.handler({ backend: "cep" }))
.resolves.toMatchObject({ success: true, data: { backend: "cep", overall: "needs_attention" } });
mockedSendCommand.mockRejectedValueOnce(new Error("bridge directory unreadable"));
await expect(tools.verify_premiere_connection.handler({ backend: "cep" }))
.resolves.toMatchObject({ success: true, data: { backend: "cep", overall: "needs_attention" } });
});
});
+31
View File
@@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({ sendCommand: vi.fn().mockResolvedValue({ success: true }) }));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getEffectsTools } from "../../src/tools/effects.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tool = getEffectsTools({ tempDir: "/tmp/test" }).inspect_stabilizer_status;
beforeEach(() => vi.clearAllMocks());
describe("inspect_stabilizer_status", () => {
it("builds a read-only all-clips inspection with an explicit unknown-state boundary", async () => {
await tool.handler({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('requestedNodeId = ""');
expect(script).toContain("analysisStatus = \"unknown\"");
expect(script).toContain("unknown is not proof of a completed solve");
expect(script).not.toContain("setValue(");
expect(script).not.toContain("addVideoEffect(");
});
it("escapes and limits inspection to a requested video clip", async () => {
await tool.handler({ node_id: 'clip"1' });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('requestedNodeId = "clip\\\"1"');
expect(script).toContain("__findClip(requestedNodeId)");
expect(script).toContain('found.trackType !== "video"');
});
});
+191
View File
@@ -0,0 +1,191 @@
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import {
compareCmx3600Edls,
getInterchangeAnalysisTools,
inspectFcpxml,
parseCmx3600Edl,
timecodeToFrames,
validateCmx3600Edl,
} from "../../src/tools/interchange-analysis.js";
const tools = getInterchangeAnalysisTools({ tempDir: "/tmp/interchange-analysis" });
const BASE_EDL = [
"TITLE: Round trip",
"FCM: NON-DROP FRAME",
"001 CAM_A V C 00:00:00:00 00:00:01:00 01:00:00:00 01:00:01:00",
"002 CAM_B V D 024 00:00:01:00 00:00:02:00 01:00:01:00 01:00:02:00",
].join("\n");
function writeFixture(name: string, contents: string): string {
const directory = mkdtempSync(join(tmpdir(), "premiere-interchange-"));
const path = join(directory, name);
writeFileSync(path, contents);
return path;
}
describe("CMX 3600 parsing and validation", () => {
it("parses CMX headers, cut and dissolve events, and bounded malformed numeric lines", () => {
const parsed = parseCmx3600Edl(`${BASE_EDL}\n003 incomplete event`);
expect(parsed).toMatchObject({
title: "Round trip",
frameCodeMode: "NON-DROP FRAME",
events: [
{ eventNumber: 1, reel: "CAM_A", transition: "C", transitionDurationFrames: null },
{ eventNumber: 2, reel: "CAM_B", transition: "D", transitionDurationFrames: 24 },
],
unrecognizedEventLines: [{ line: 5, text: "003 incomplete event" }],
});
});
it("converts non-drop and valid drop-frame timecodes while rejecting invalid forms", () => {
expect(timecodeToFrames("00:00:01:00", 24)).toBe(24);
expect(timecodeToFrames("00:01:00;02", 29.97)).toBe(1_800);
expect(timecodeToFrames("00:01:00;04", 59.94)).toBe(3_600);
expect(() => timecodeToFrames("00:00:00;00", 24)).toThrow("Drop-frame separator");
expect(() => timecodeToFrames("00:01:00;00", 29.97)).toThrow("Invalid dropped frame");
expect(() => timecodeToFrames("00:00:60:00", 24)).toThrow("Invalid timecode fields");
expect(() => timecodeToFrames("not timecode", 24)).toThrow("Invalid timecode");
});
it("detects duplicate IDs, invalid durations, overlaps, gaps, and invalid event timecodes", () => {
const parsed = parseCmx3600Edl([
"001 CAM_A V C 00:00:01:00 00:00:00:00 01:00:00:00 01:00:01:00",
"001 CAM_B V C 00:00:00:00 00:00:01:00 01:00:00:12 01:00:01:12",
"003 CAM_C V C 00:00:00:00 00:00:01:00 01:00:02:00 01:00:03:00",
"004 CAM_D V C 00:00:60:00 00:00:01:00 01:00:03:00 01:00:04:00",
].join("\n"));
const validation = validateCmx3600Edl(parsed, 24);
expect(validation.valid).toBe(false);
expect(validation.errors.join(" ")).toContain("duplicated");
expect(validation.errors.join(" ")).toContain("non-positive source");
expect(validation.errors.join(" ")).toContain("overlaps");
expect(validation.errors.join(" ")).toContain("Invalid timecode fields");
expect(validation.warnings.join(" ")).toContain("record gap");
expect(validateCmx3600Edl(parseCmx3600Edl(BASE_EDL), 24)).toMatchObject({
valid: true,
totalRecordFrames: 48,
});
expect(validateCmx3600Edl(parseCmx3600Edl("001 malformed"), 24)).toMatchObject({
valid: false,
errors: ["No CMX 3600 event records were found"],
warnings: ["1 numeric line(s) did not match the supported CMX 3600 event grammar"],
totalRecordFrames: null,
});
});
it("compares event additions, removals, and edits without mutating either EDL", () => {
const before = parseCmx3600Edl(BASE_EDL);
const after = parseCmx3600Edl([
"001 CAM_A V C 00:00:00:00 00:00:01:00 01:00:00:00 01:00:01:00",
"002 CAM_B V D 012 00:00:01:00 00:00:02:00 01:00:01:00 01:00:02:00",
"004 CAM_D A C 00:00:02:00 00:00:03:00 01:00:02:00 01:00:03:00",
].join("\n"));
expect(compareCmx3600Edls(before, after)).toEqual({
beforeEventCount: 2,
afterEventCount: 3,
changes: [
{ eventNumber: 2, type: "changed" },
{ eventNumber: 4, type: "added" },
],
truncated: false,
});
});
});
describe("FCPXML inspection and approved-root reference verification", () => {
it("inspects text only, reports entities defensively, and keeps FCPXML facts bounded", () => {
const report = inspectFcpxml([
"<!DOCTYPE fcpxml [<!ENTITY media SYSTEM 'untrusted'>]>",
"<fcpxml version=\"1.10\"><resources>",
"<asset id=\"r1\" name=\"Camera\" src=\"file:///safe.mov\" />",
"<asset id=\"r2\" name=\"Generator\" />",
"</resources><library><event><project><sequence><spine>",
"<asset-clip ref=\"r1\" /><video /><audio />",
"</spine></sequence></project></event></library></fcpxml>",
].join(""));
expect(report).toMatchObject({
format: "FCPXML",
version: "1.10",
sequenceCount: 1,
clipElementCount: 3,
assetCount: 2,
assets: [
{ id: "r1", name: "Camera", source: "file:///safe.mov" },
{ id: "r2", name: "Generator", source: null },
],
warnings: ["DOCTYPE is present; this tool inspects text only and never resolves entities"],
});
expect(inspectFcpxml("<sequence />").warnings).toContain("No <fcpxml> root element was found");
});
it("verifies only file URLs inside caller-approved roots and hides denied paths", async () => {
const root = mkdtempSync(join(tmpdir(), "premiere-interchange-root-"));
const allowedMedia = join(root, "media.mov");
writeFileSync(allowedMedia, "media");
const nested = join(root, "nested");
mkdirSync(nested);
const missingMedia = join(nested, "missing.mov");
const outsideMedia = writeFixture("private.mov", "not for inspection");
const fcpxml = writeFixture("roundtrip.fcpxml", [
"<fcpxml version=\"1.10\"><resources>",
`<asset id=\"r1\" name=\"inside\" src=\"${pathToFileURL(allowedMedia)}\" />`,
`<asset id=\"r2\" name=\"missing\" src=\"${pathToFileURL(missingMedia)}\" />`,
`<asset id=\"r3\" name=\"outside\" src=\"${pathToFileURL(outsideMedia)}\" />`,
"<asset id=\"r4\" name=\"remote\" src=\"https://example.test/media.mov\" />",
"<asset id=\"r5\" name=\"unset\" /></resources></fcpxml>",
].join(""));
const result = await tools.verify_fcpxml_media_references.handler({ path: fcpxml, allowed_roots: [root] });
expect(result).toMatchObject({
success: true,
data: {
checkedReferenceCount: 5,
references: [
{ assetId: "r1", status: "available", path: allowedMedia },
{ assetId: "r2", status: "missing", path: missingMedia },
{ assetId: "r3", status: "outside_allowed_roots" },
{ assetId: "r4", status: "non_file_url" },
{ assetId: "r5", status: "missing_source" },
],
},
});
expect(JSON.stringify(result)).not.toContain(outsideMedia);
});
});
describe("interchange tool contracts", () => {
it("returns parsed, validated, compared, and inspected local results", async () => {
const beforePath = writeFixture("before.edl", BASE_EDL);
const afterPath = writeFixture("after.edl", BASE_EDL.replace("CAM_B", "CAM_C"));
const xmlPath = writeFixture("before.xml", "<fcpxml version=\"1.8\"><sequence /></fcpxml>");
await expect(tools.inspect_cmx3600_edl.handler({ path: beforePath })).resolves.toMatchObject({ success: true, data: { eventCount: 2 } });
await expect(tools.validate_cmx3600_edl.handler({ path: beforePath, frame_rate: 24 })).resolves.toMatchObject({ success: true, data: { valid: true } });
await expect(tools.compare_cmx3600_edls.handler({ before_path: beforePath, after_path: afterPath })).resolves.toMatchObject({ success: true, data: { changes: [{ eventNumber: 2, type: "changed" }] } });
await expect(tools.inspect_fcpxml_interchange.handler({ path: xmlPath })).resolves.toMatchObject({ success: true, data: { version: "1.8" } });
});
it("fails closed for invalid rates, unsafe roots, missing paths, and incorrect extensions", async () => {
const textPath = writeFixture("not-interchange.txt", "not interchange");
await expect(tools.inspect_cmx3600_edl.handler({ path: "missing.edl" })).resolves.toMatchObject({ success: false, error: expect.stringContaining("does not exist") });
await expect(tools.inspect_cmx3600_edl.handler({ path: textPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining(".edl") });
await expect(tools.validate_cmx3600_edl.handler({ path: "missing.edl", frame_rate: 23 as never })).resolves.toEqual({ success: false, error: "frame_rate must be 24, 25, 29.97, 30, 50, 59.94, or 60" });
await expect(tools.compare_cmx3600_edls.handler({ before_path: textPath, after_path: textPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining(".edl") });
await expect(tools.inspect_fcpxml_interchange.handler({ path: textPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining(".fcpxml") });
await expect(tools.verify_fcpxml_media_references.handler({ path: textPath, allowed_roots: [] })).resolves.toMatchObject({ success: false, error: expect.stringContaining(".fcpxml") });
const xmlPath = writeFixture("empty.fcpxml", "<fcpxml />");
await expect(tools.verify_fcpxml_media_references.handler({ path: xmlPath, allowed_roots: ["relative-root"] })).resolves.toMatchObject({ success: false, error: expect.stringContaining("absolute directory") });
await expect(tools.verify_fcpxml_media_references.handler({ path: xmlPath, allowed_roots: [join(tmpdir(), "missing-root")] })).resolves.toMatchObject({ success: false, error: expect.stringContaining("not an existing directory") });
});
});
+180
View File
@@ -0,0 +1,180 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getHelpersSource } from "../../src/bridge/script-builder.js";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getClipboardTools } from "../../src/tools/clipboard.js";
import { getDiscoveryTools } from "../../src/tools/discovery.js";
import { getHealthTools } from "../../src/tools/health.js";
import { getMetadataTools } from "../../src/tools/metadata.js";
import { getProjectTools } from "../../src/tools/project.js";
import { getSequenceTools } from "../../src/tools/sequence.js";
import { getUtilityTools } from "../../src/tools/utility.js";
const mockedSendCommand = vi.mocked(sendCommand);
const bridgeOptions = { tempDir: "/tmp/test-bridge", timeoutMs: 5_000 };
async function scriptFor(tool: { handler: (args: any) => Promise<unknown> }, args: unknown) {
mockedSendCommand.mockClear();
await tool.handler(args);
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
return String(mockedSendCommand.mock.calls[0][0]);
}
beforeEach(() => vi.clearAllMocks());
describe("issue #239 — metadata writes require a readback-capable payload", () => {
const metadata = getMetadataTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
it("refuses the old field/value form before it reaches Premiere", async () => {
const result = await metadata.set_metadata.handler({
item_id: "clip-1",
field_name: "Column.Intrinsic.Description",
value: "unique value",
});
expect(result).toMatchObject({ success: false, error: expect.stringContaining("partial field_name/value") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("writes complete Project Metadata XML only with field paths and exact readback", async () => {
const script = await scriptFor(metadata.set_metadata, {
item_id: "clip-1",
metadata_xml: "<xmpmeta><Description>unique value</Description></xmpmeta>",
updated_fields: ["Column.Intrinsic.Description"],
});
expect(script).toContain("item.setProjectMetadata(requestedMetadata, updatedFields)");
expect(script).toContain("String(readback) !== requestedMetadata");
expect(script).toContain("verified: true");
});
it("verifies project-panel metadata and labels schema creation as non-value-writing", async () => {
const panelScript = await scriptFor(project.set_project_panel_metadata, { metadata_xml: "<columns />" });
expect(panelScript).toContain("app.project.getProjectPanelMetadata()");
expect(panelScript).toContain("String(readback) !== requestedMetadata");
const schemaScript = await scriptFor(project.add_custom_metadata_field, {
field_name: "ReviewStatus", field_label: "Review Status", field_type: 2,
});
expect(schemaScript).toContain("outcome: \"committed_unverified\"");
expect(schemaScript).toContain("perItemValue");
});
});
describe("issue #240 — legacy structural tools verify or refuse the operation", () => {
const utility = getUtilityTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
const sequence = getSequenceTools(bridgeOptions);
it("uses documented bin/sequence deletion paths and checks target absence", async () => {
const itemScript = await scriptFor(utility.delete_project_item, { item_id: "item-1" });
expect(itemScript).not.toContain("deleteProjectItem(");
expect(itemScript).toContain("item.deleteBin()");
expect(itemScript).toContain("app.project.deleteSequence(sequence)");
expect(itemScript).toContain("if (__findProjectItem(nodeId))");
const manyScript = await scriptFor(utility.delete_multiple_project_items, { item_ids: ["item-1", "item-2"] });
expect(manyScript).toContain("No project items were deleted");
expect(manyScript).toContain("The batch is not reported as successful");
const binScript = await scriptFor(project.delete_bin, { bin_id: "bin-1" });
expect(binScript).toContain("if (__findProjectItem(nodeId))");
const sequenceScript = await scriptFor(sequence.delete_sequence, { sequence_id: "sequence-1" });
expect(sequenceScript).toContain("var seq = __findSequence");
expect(sequenceScript).toContain("if (__findSequence(sequenceId))");
});
it("does not misrepresent createSubsequence as timeline nesting", async () => {
const subsequenceScript = await scriptFor(sequence.create_subsequence, {});
expect(subsequenceScript).toContain("nested: false");
expect(subsequenceScript).toContain("The source timeline selection is intentionally unchanged");
expect(subsequenceScript).toContain("before.indexOf(newId) !== -1");
mockedSendCommand.mockClear();
const result = await utility.nest_clips.handler({ name: "Verified nest" });
expect(result).toMatchObject({ success: false, error: expect.stringContaining("does not replace the original clips") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("refuses a linked A/V unnest before it can leave an audio reference behind", async () => {
const script = await scriptFor(sequence.unnest_sequence, { node_id: "nested-clip" });
expect(script).toContain("collectLinkedReferences(seq.videoTracks, \"video\")");
expect(script).toContain("collectLinkedReferences(seq.audioTracks, \"audio\")");
expect(script).toContain("linkedReferences.length !== 1");
expect(script).toContain("No clips were changed");
});
});
describe("issue #241 — sequence rates use Time durations and readback", () => {
const utility = getUtilityTools(bridgeOptions);
it("converts a requested audio Hz value to ticks per sample and verifies it", async () => {
const script = await scriptFor(utility.set_sequence_audio_settings, { sample_rate: 48_000 });
expect(script).toContain("TICKS_PER_SECOND / requestedSampleRate");
expect(script).toContain("var sampleDuration = new Time()");
expect(script).toContain("settings.audioSampleRate = sampleDuration");
expect(script).toContain("Math.abs(appliedTicksPerSample - requestedTicksPerSample) > 1");
expect(script).not.toContain("settings.audioSampleRate = 48000");
});
it("rejects unsafe audio-rate inputs without contacting Premiere", async () => {
const result = await utility.set_sequence_audio_settings.handler({ sample_rate: 0 });
expect(result).toMatchObject({ success: false, error: expect.stringContaining("between 1 and 768000") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("issue #242 — stale active sequences are not surfaced or trusted", () => {
const discovery = getDiscoveryTools(bridgeOptions);
const health = getHealthTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
it("defines one current-project active-sequence guard", () => {
const helpers = getHelpersSource();
expect(helpers).toContain("function __isCurrentProjectSequence(sequence)");
expect(helpers).toContain("function __getCurrentActiveSequence()");
});
it("uses the guard in project info, active-sequence inspection, and ping", async () => {
const projectInfo = await scriptFor(discovery.get_project_info, {});
expect(projectInfo).toContain("var activeSeq = __getCurrentActiveSequence()");
const activeSequence = await scriptFor(discovery.get_active_sequence, {});
expect(activeSequence).toContain("var seq = __getCurrentActiveSequence()");
expect(activeSequence).toContain("No active sequence in the current project");
const ping = await scriptFor(health.ping, {});
expect(ping).toContain("var activeSequence = app.project ? __getCurrentActiveSequence() : null");
});
it("verifies project creation and sequence activation before reporting success", async () => {
const create = await scriptFor(project.create_project, { path: "/tmp/New project.prproj" });
expect(create).toContain("project.sequences.numSequences === 0 && active");
expect(create).toContain("activeSequence: active ?");
const activate = await scriptFor(project.set_active_sequence, { sequence_id: "sequence-1" });
expect(activate).toContain("String(active.sequenceID) !== String(seq.sequenceID)");
expect(activate).toContain("verified: true");
});
});
describe("issue #243 — copy_effect_values refuses unsafe Blend Mode writes", () => {
const clipboard = getClipboardTools(bridgeOptions);
it("does not copy Blend Mode through the generic enum setter and verifies every copied scalar", async () => {
const script = await scriptFor(clipboard.copy_effect_values, {
source_node_id: "source-clip", target_node_id: "target-clip", effect_name: "Opacity",
});
expect(script).toContain('srcProp.displayName === "Blend Mode"');
expect(script).toContain("Legacy CEP enum writes can corrupt Blend Mode; no write was attempted.");
expect(script).toContain("var readback = tgtComp.properties[q].getValue(0, 0)");
expect(script).toContain("Effect-value copy was not fully verified");
});
});
+232
View File
@@ -0,0 +1,232 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BridgeOptions } from "../../src/bridge/file-bridge.js";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: { covered: true } }),
sendRawCommand: vi.fn().mockResolvedValue({ success: true, data: { covered: true } }),
}));
import { sendCommand, sendRawCommand } from "../../src/bridge/file-bridge.js";
import { getAdvancedTools } from "../../src/tools/advanced.js";
import { getAudioTools } from "../../src/tools/audio.js";
import { getExportTools } from "../../src/tools/export.js";
import { getHealthTools } from "../../src/tools/health.js";
import { getInspectionTools } from "../../src/tools/inspection.js";
import { getClipboardTools } from "../../src/tools/clipboard.js";
import { getCaptionTools } from "../../src/tools/captions.js";
import { getDiscoveryTools } from "../../src/tools/discovery.js";
import { getEffectsTools } from "../../src/tools/effects.js";
import { getMarkerTools } from "../../src/tools/markers.js";
import { getMediaTools } from "../../src/tools/media.js";
import { getMetadataTools } from "../../src/tools/metadata.js";
import { getProjectManagerTools } from "../../src/tools/project-manager.js";
import { getProjectTools } from "../../src/tools/project.js";
import { getRecoveryTools } from "../../src/tools/recovery.js";
import { getScriptingTools } from "../../src/tools/scripting.js";
import { getSelectionTools } from "../../src/tools/selection.js";
import { getSequenceTools } from "../../src/tools/sequence.js";
import { getSourceMonitorTools } from "../../src/tools/source-monitor.js";
import { getTextTools } from "../../src/tools/text.js";
import { getTimelineTools } from "../../src/tools/timeline.js";
import { getTrackTools } from "../../src/tools/tracks.js";
import { getTrackTargetingTools } from "../../src/tools/track-targeting.js";
import { getUtilityTools } from "../../src/tools/utility.js";
type Schema = {
type?: string;
enum?: unknown[];
properties?: Record<string, Schema>;
required?: string[];
items?: Schema;
default?: unknown;
};
type Tool = {
parameters: Schema;
handler: (args: Record<string, unknown>) => Promise<unknown>;
};
const bridgeOptions: BridgeOptions = { tempDir: "/tmp/handler-coverage", timeoutMs: 25 };
const mockedSendCommand = vi.mocked(sendCommand);
const mockedSendRawCommand = vi.mocked(sendRawCommand);
const FIELD_VALUES: Record<string, unknown> = {
path: "/tmp/coverage.prproj",
file_path: "/tmp/coverage.mov",
output_path: "/tmp/coverage.json",
folder_path: "/tmp",
node_id: "coverage-node",
clip_node_id: "coverage-node",
project_item_node_id: "coverage-item",
sequence_id: "coverage-sequence",
sequence_name: "Coverage Sequence",
track_index: 1,
source_track_index: 0,
destination_track_index: 1,
time_seconds: 2.5,
start_seconds: 1,
end_seconds: 3,
duration_seconds: 2,
frame_number: 12,
name: "Coverage Name",
text: "Coverage text",
script: "return __result({ covered: true });",
code: "return __result({ covered: true });",
};
function valueFor(schema: Schema, field: string, includeOptional: boolean): unknown {
if (field in FIELD_VALUES) return FIELD_VALUES[field];
if (schema.enum?.length) return includeOptional ? schema.enum.at(-1) : schema.enum[0];
if (schema.default !== undefined) return schema.default;
switch (schema.type) {
case "boolean": return includeOptional;
case "number": return 1;
case "array": return [valueFor(schema.items ?? { type: "string" }, field, includeOptional)];
case "object": return argsFor(schema, includeOptional);
default: return `coverage-${field}`;
}
}
function argsFor(schema: Schema, includeOptional: boolean): Record<string, unknown> {
const args: Record<string, unknown> = {};
const required = new Set(schema.required ?? []);
for (const [field, property] of Object.entries(schema.properties ?? {})) {
if (includeOptional || required.has(field)) args[field] = valueFor(property, field, includeOptional);
}
return args;
}
const modules: Array<[string, () => Record<string, Tool>, Set<string>?]> = [
["advanced", () => getAdvancedTools(bridgeOptions) as Record<string, Tool>],
["audio", () => getAudioTools(bridgeOptions) as Record<string, Tool>, new Set(["detect_silence", "detect_beats", "analyze_loudness", "normalize_loudness_file"])],
["export", () => getExportTools(bridgeOptions) as Record<string, Tool>, new Set(["validate_export_preset", "verify_delivery_file", "analyze_video_qc", "detect_source_scene_changes"])],
["clipboard", () => getClipboardTools(bridgeOptions) as Record<string, Tool>],
["captions", () => getCaptionTools(bridgeOptions) as Record<string, Tool>],
["discovery", () => getDiscoveryTools(bridgeOptions) as Record<string, Tool>],
["effects", () => getEffectsTools(bridgeOptions) as Record<string, Tool>],
["inspection", () => getInspectionTools(bridgeOptions) as Record<string, Tool>],
["markers", () => getMarkerTools(bridgeOptions) as Record<string, Tool>],
["media", () => getMediaTools(bridgeOptions) as Record<string, Tool>],
["metadata", () => getMetadataTools(bridgeOptions) as Record<string, Tool>],
["project-manager", () => getProjectManagerTools(bridgeOptions) as Record<string, Tool>],
["project", () => getProjectTools(bridgeOptions) as Record<string, Tool>],
["recovery", () => getRecoveryTools(bridgeOptions) as Record<string, Tool>, new Set(["get_bridge_telemetry"])],
["scripting", () => getScriptingTools(bridgeOptions) as Record<string, Tool>],
["selection", () => getSelectionTools(bridgeOptions) as Record<string, Tool>],
["sequence", () => getSequenceTools(bridgeOptions) as Record<string, Tool>],
["source-monitor", () => getSourceMonitorTools(bridgeOptions) as Record<string, Tool>],
["text", () => getTextTools(bridgeOptions) as Record<string, Tool>],
["timeline", () => getTimelineTools(bridgeOptions) as Record<string, Tool>],
["track-targeting", () => getTrackTargetingTools(bridgeOptions) as Record<string, Tool>],
["tracks", () => getTrackTools(bridgeOptions) as Record<string, Tool>],
["utility", () => getUtilityTools(bridgeOptions) as Record<string, Tool>],
];
describe("large tool handler coverage", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedSendCommand.mockResolvedValue({ success: true, data: { covered: true } });
mockedSendRawCommand.mockResolvedValue({ success: true, data: { covered: true } });
});
for (const [moduleName, getTools, excludedTools = new Set<string>()] of modules) {
describe(moduleName, () => {
for (const [toolName, tool] of Object.entries(getTools())) {
if (excludedTools.has(toolName)) continue;
it.each([
["required arguments", false],
["all arguments", true],
])(`${toolName} handles %s`, async (_label, includeOptional) => {
const commandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
const result = await tool.handler(argsFor(tool.parameters, includeOptional));
expect(result).toBeDefined();
const nextCommandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
if (nextCommandCount === commandCount) {
expect(result).toMatchObject({ success: false });
return;
}
const script = mockedSendCommand.mock.calls.at(-1)?.[0]
?? mockedSendRawCommand.mock.calls.at(-1)?.[0];
expect(script).toEqual(expect.any(String));
expect(script.length).toBeGreaterThan(20);
});
for (const [field, property] of Object.entries(tool.parameters.properties ?? {})) {
for (const enumValue of property.enum?.slice(1, -1) ?? []) {
it(`${toolName} handles ${field}=${String(enumValue)}`, async () => {
const args = argsFor(tool.parameters, true);
args[field] = enumValue;
const commandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
const result = await tool.handler(args);
const nextCommandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
if (nextCommandCount === commandCount) {
expect(result).toMatchObject({ success: false });
return;
}
expect(nextCommandCount).toBeGreaterThan(commandCount);
});
}
if (property.type === "boolean") {
it(`${toolName} handles ${field}=false`, async () => {
const args = argsFor(tool.parameters, true);
args[field] = false;
const commandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
const result = await tool.handler(args);
const nextCommandCount = mockedSendCommand.mock.calls.length + mockedSendRawCommand.mock.calls.length;
if (nextCommandCount === commandCount) {
expect(result).toMatchObject({ success: false });
return;
}
expect(nextCommandCount).toBeGreaterThan(commandCount);
});
}
}
}
});
}
it.each([Number.NaN, 0, 241])("rejects invalid sequence frame rate %s locally", async (frameRate) => {
const tool = getUtilityTools(bridgeOptions).set_sequence_frame_rate;
const result = await tool.handler({ frame_rate: frameRate });
expect(result).toEqual({
success: false,
error: "frame_rate must be a finite value between 1 and 240 fps",
});
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it.each([
["smoothness only", { node_id: "coverage-node", smoothness: 25 }],
["method only", { node_id: "coverage-node", method: "Subspace Warp" }],
])("stabilizes with %s", async (_label, args) => {
await getEffectsTools(bridgeOptions).stabilize_clip.handler(args);
expect(mockedSendCommand).toHaveBeenCalledWith(expect.stringContaining("Warp Stabilizer"), bridgeOptions);
});
describe("health diagnostic failure branches", () => {
it("reports successful, primitive, and rejected UXP diagnostic responses", async () => {
const request = vi.fn()
.mockResolvedValueOnce({ projectOpen: true, sequenceOpen: true })
.mockResolvedValueOnce("unexpected")
.mockRejectedValueOnce(new Error("offline"));
const tools = getHealthTools(bridgeOptions, undefined, undefined, {
uxpBridge: { request } as never,
});
await expect(tools.verify_premiere_connection.handler({ backend: "uxp" }))
.resolves.toMatchObject({ success: true, data: { overall: "ready" } });
await expect(tools.verify_premiere_connection.handler({ backend: "uxp" }))
.resolves.toMatchObject({ success: true, data: { overall: "needs_attention" } });
await expect(tools.verify_premiere_connection.handler({ backend: "uxp" }))
.resolves.toMatchObject({ success: true, data: { overall: "needs_attention" } });
});
it("turns a thrown CEP diagnostic into a completed needs-attention report", async () => {
mockedSendCommand.mockRejectedValueOnce(new Error("CEP unavailable"));
const result = await getHealthTools(bridgeOptions).verify_premiere_connection.handler({ backend: "cep" });
expect(result).toMatchObject({ success: true, data: { overall: "needs_attention" } });
});
});
});
+55
View File
@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getExportTools } from "../../src/tools/export.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tool = getExportTools({ tempDir: "/tmp/test", timeoutMs: 1000 }).export_sequence_marker_review_frames;
beforeEach(() => vi.clearAllMocks());
describe("export_sequence_marker_review_frames", () => {
it("exports chronological marker anchors in one bounded bridge request", async () => {
await tool.handler({
output_dir: "/tmp/review",
marker_type: "Comment",
start_seconds: 2,
end_seconds: 18,
limit: 8,
});
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('var requiredType = "Comment"');
expect(script).toContain("matched.sort(function(a, b)");
expect(script).toContain("var requested = Math.min(matched.length, 8)");
expect(script).toContain("marker_review_");
expect(script).toContain("__exportStillFrame(");
expect(script).toContain("truncated: matched.length > requested");
});
it("rejects invalid filters and bounds before contacting Premiere", async () => {
await expect(tool.handler({ output_dir: "/tmp/review", limit: 25 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("1 through 24") });
await expect(tool.handler({ output_dir: "/tmp/review", marker_type: " " }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("non-empty") });
await expect(tool.handler({ output_dir: "/tmp/review", start_seconds: 9, end_seconds: 2 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("greater than") });
await expect(tool.handler({ output_dir: "", limit: 1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("non-empty") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("reports file verification and partial-export boundaries in the generated request", async () => {
await tool.handler({ output_dir: "/tmp/review", limit: 2 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("failures.push");
expect(script).toContain("complete: frames.length === requested");
expect(script).toContain("verified on disk by the Premiere bridge at the matched marker start");
});
});
+229
View File
@@ -0,0 +1,229 @@
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import {
analyzeRgbScopes,
compareScopeReadings,
getMediaAnalysisTools,
parseCropDetectOutput,
parseIdetOutput,
parseMediaProbeJson,
parseMotionPeakCandidates,
parseTransientCandidates,
} from "../../src/tools/media-analysis.js";
const tools = getMediaAnalysisTools({ tempDir: "/tmp/test" });
beforeEach(() => vi.clearAllMocks());
function fixture(extension = ".mov") {
const directory = mkdtempSync(join(tmpdir(), "premiere-media-analysis-"));
const path = join(directory, `fixture${extension}`);
writeFileSync(path, "fixture");
return path;
}
describe("media analysis parsers", () => {
it("normalizes ffprobe JSON", () => {
expect(parseMediaProbeJson(JSON.stringify({
format: { duration: "2.0" }, streams: [{ codec_type: "video" }, null], chapters: [{ id: 1 }],
}))).toEqual({ format: { duration: "2.0" }, streams: [{ codec_type: "video" }], chapters: [{ id: 1 }] });
expect(parseMediaProbeJson("{}")).toEqual({ format: {}, streams: [], chapters: [] });
expect(parseMediaProbeJson(JSON.stringify({ format: "bad", streams: "bad", chapters: [null, "bad"] })))
.toEqual({ format: {}, streams: [], chapters: [] });
});
it("keeps the strongest transient inside the minimum interval", () => {
const output = [
"frame:0 pts_time:0.10", "lavfi.astats.Overall.Peak_level=-8",
"frame:1 pts_time:0.20", "lavfi.astats.Overall.Peak_level=-3",
"frame:2 pts_time:1.00", "lavfi.astats.Overall.Peak_level=-20",
"frame:3 pts_time:1.50", "lavfi.astats.Overall.Peak_level=-6",
].join("\n");
expect(parseTransientCandidates(output, -12, 0.25)).toEqual([
{ timeSeconds: 0.2, peakDbfs: -3 }, { timeSeconds: 1.5, peakDbfs: -6 },
]);
});
it("keeps local motion maxima and the strongest peak inside the minimum interval", () => {
const output = [
"frame:0 pts_time:0", "lavfi.signalstats.YAVG=2",
"frame:1 pts_time:0.25", "lavfi.signalstats.YAVG=15",
"frame:2 pts_time:0.5", "lavfi.signalstats.YAVG=2",
"frame:3 pts_time:0.75", "lavfi.signalstats.YAVG=20",
"frame:4 pts_time:1", "lavfi.signalstats.YAVG=2",
"frame:5 pts_time:2", "lavfi.signalstats.YAVG=18",
].join("\n");
expect(parseMotionPeakCandidates(output, 12, 1)).toEqual([
{ timeSeconds: 0.75, difference: 20 }, { timeSeconds: 2, difference: 18 },
]);
});
it("represents an equal-valued motion plateau with one peak", () => {
const output = [
"frame:0 pts_time:0", "lavfi.signalstats.YAVG=2",
"frame:1 pts_time:0.25", "lavfi.signalstats.YAVG=15",
"frame:2 pts_time:0.5", "lavfi.signalstats.YAVG=15",
"frame:3 pts_time:0.75", "lavfi.signalstats.YAVG=2",
].join("\n");
expect(parseMotionPeakCandidates(output, 12, 0.1)).toEqual([
{ timeSeconds: 0.25, difference: 15 },
]);
});
it("computes waveform, parade, saturation, and RGB endpoint occupancy", () => {
const reading = analyzeRgbScopes(Uint8Array.from([0, 0, 0, 255, 255, 255, 255, 0, 0]));
expect(reading).toMatchObject({
pixels: 3,
waveform: { black: 0, white: 100 },
rgbParade: { red: { high: 100 }, green: { low: 0 }, blue: { median: 0 } },
saturation: { high: 100 },
rgbExtremes: { nearBlackPercent: 33.33, nearWhitePercent: 33.33 },
});
expect(() => analyzeRgbScopes(Uint8Array.from([0, 1]))).toThrow("complete RGB24 pixels");
});
it("compares scope readings with target-minus-reference deltas and coarse directions", () => {
const reference = analyzeRgbScopes(Uint8Array.from([100, 100, 100, 120, 120, 120]));
const target = analyzeRgbScopes(Uint8Array.from([60, 70, 80, 70, 80, 90]));
expect(compareScopeReadings(reference, target)).toMatchObject({
targetMinusReference: { medianLuma: expect.any(Number), redBlueBalance: expect.any(Number) },
suggestedDirections: { exposure: "raise", warmth: "warmer", saturation: "lower" },
});
});
it("keeps the warmth direction stable across proportional exposure changes", () => {
const reference = analyzeRgbScopes(Uint8Array.from([100, 75, 50]));
const target = analyzeRgbScopes(Uint8Array.from([200, 150, 100]));
expect(compareScopeReadings(reference, target)).toMatchObject({
targetMinusReference: { redBlueBalance: -0.01 },
suggestedDirections: { exposure: "lower", warmth: "hold", saturation: "hold" },
});
});
it("classifies progressive, mixed, and absent idet summaries", () => {
expect(parseIdetOutput("Multi frame detection: TFF: 1 BFF: 0 Progressive: 99 Undetermined: 0").classification).toBe("progressive");
expect(parseIdetOutput("Multi frame detection: TFF: 50 BFF: 0 Progressive: 50 Undetermined: 0").classification).toBe("mixed");
expect(parseIdetOutput("").classification).toBe("undetermined");
expect(parseIdetOutput("Multi frame detection: TFF: 90 BFF: 5 Progressive: 5 Undetermined: 1").classification).toBe("tff");
expect(parseIdetOutput("Multi frame detection: TFF: 5 BFF: 90 Progressive: 5 Undetermined: 1").classification).toBe("bff");
});
it("selects the most frequent active-picture rectangle", () => {
const output = "crop=1920:800:0:140\ncrop=1920:800:0:140\ncrop=1900:800:10:140";
expect(parseCropDetectOutput(output)).toEqual({ width: 1920, height: 800, x: 0, y: 140, samples: 2 });
expect(parseCropDetectOutput("no crop data")).toBeNull();
});
});
describe("media analysis tool contracts", () => {
it("rejects missing files and unsafe output arguments before spawning", async () => {
await expect(tools.inspect_media_streams.handler({ media_path: "" })).resolves.toMatchObject({ success: false });
const directory = mkdtempSync(join(tmpdir(), "premiere-media-directory-"));
mkdirSync(join(directory, "nested"));
await expect(tools.inspect_media_streams.handler({ media_path: join(directory, "nested") })).resolves.toMatchObject({ success: false });
await expect(tools.inspect_media_streams.handler({ media_path: "missing.mov" })).resolves.toMatchObject({ success: false });
const mediaPath = fixture();
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: "sheet.jpg" })).resolves.toMatchObject({ success: false, error: expect.stringContaining(".png") });
const existingOutput = fixture(".png");
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: existingOutput })).resolves.toMatchObject({ success: false, error: expect.stringContaining("already exists") });
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: join(directory, "sheet.png"), columns: 1 })).resolves.toMatchObject({ success: false });
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: join(directory, "sheet.png"), thumbnail_width: 20 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_audio_transients.handler({ media_path: mediaPath, threshold_dbfs: 1 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_audio_transients.handler({ media_path: mediaPath, minimum_interval_seconds: 20 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_audio_transients.handler({ media_path: mediaPath, maximum_events: 0 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, sample_seconds: 0 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, samples_per_second: 11 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, threshold: 256 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, minimum_interval_seconds: 0 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, maximum_events: 0 })).resolves.toMatchObject({ success: false });
await expect(tools.read_video_scopes.handler({ media_path: mediaPath, time_seconds: -1 })).resolves.toMatchObject({ success: false });
await expect(tools.plan_shot_match.handler({ reference_media_path: mediaPath, target_media_path: mediaPath, target_time_seconds: -1 })).resolves.toMatchObject({ success: false });
await expect(tools.analyze_video_interlacing.handler({ media_path: mediaPath, sample_seconds: 0 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_active_picture_bounds.handler({ media_path: mediaPath, sample_seconds: 301 })).resolves.toMatchObject({ success: false });
await expect(tools.detect_active_picture_bounds.handler({ media_path: mediaPath, limit: 300 })).resolves.toMatchObject({ success: false });
expect(mockedExecFileAsync).not.toHaveBeenCalled();
});
it("returns structured media inspection", async () => {
const mediaPath = fixture();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: JSON.stringify({ format: { format_name: "mov" }, streams: [{ codec_name: "h264" }] }), stderr: "" });
await expect(tools.inspect_media_streams.handler({ media_path: mediaPath })).resolves.toMatchObject({
success: true, data: { mediaPath, streamCount: 1, format: { format_name: "mov" } },
});
});
it("returns bounded transient, interlace, and picture findings", async () => {
const mediaPath = fixture();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "pts_time:1\nlavfi.astats.Overall.Peak_level=-4" });
await expect(tools.detect_audio_transients.handler({ media_path: mediaPath, maximum_events: 1 })).resolves.toMatchObject({ success: true, data: { candidates: [{ timeSeconds: 1, peakDbfs: -4 }] } });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "pts_time:1\nlavfi.signalstats.YAVG=20" });
await expect(tools.detect_motion_peaks.handler({ media_path: mediaPath, threshold: 12, maximum_events: 1 })).resolves.toMatchObject({ success: true, data: { candidates: [{ timeSeconds: 1, difference: 20 }] } });
expect(mockedExecFileAsync.mock.calls.at(-1)?.[1]).toEqual(expect.arrayContaining(["-t", "60", "-an", "-f", "null", "-"]));
mockedExecFileAsync.mockResolvedValueOnce({ stdout: Buffer.alloc(320 * 180 * 3, 128), stderr: Buffer.alloc(0) });
await expect(tools.read_video_scopes.handler({ media_path: mediaPath, time_seconds: 2 })).resolves.toMatchObject({ success: true, data: { timeSeconds: 2, pixels: 57_600, sampleSize: { width: 320, height: 180 } } });
expect(mockedExecFileAsync.mock.calls.at(-1)?.[1]).toEqual(expect.arrayContaining(["-ss", "2", "-frames:v", "1", "rgb24", "pipe:1"]));
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timeout"), { killed: true }));
await expect(tools.read_video_scopes.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out after 60 seconds") });
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode"), { stderr: Buffer.from("invalid video stream") }));
await expect(tools.read_video_scopes.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining("invalid video stream") });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: Buffer.alloc(320 * 180 * 3, 100), stderr: Buffer.alloc(0) });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: Buffer.alloc(320 * 180 * 3, 80), stderr: Buffer.alloc(0) });
await expect(tools.plan_shot_match.handler({ reference_media_path: mediaPath, target_media_path: mediaPath })).resolves.toMatchObject({ success: true, data: { comparison: { suggestedDirections: { exposure: "raise" } } } });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "Multi frame detection: TFF: 0 BFF: 0 Progressive: 20 Undetermined: 0" });
await expect(tools.analyze_video_interlacing.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: true, data: { classification: "progressive", passesProgressiveDelivery: true } });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "crop=1920:800:0:140" });
await expect(tools.detect_active_picture_bounds.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: true, data: { activePicture: { width: 1920, height: 800 } } });
});
it("creates and verifies a new contact sheet without overwriting", async () => {
const mediaPath = fixture();
const outputPath = join(mkdtempSync(join(tmpdir(), "premiere-contact-sheet-")), "sheet.png");
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "4", stderr: "" });
mockedExecFileAsync.mockImplementationOnce(async (_command: string, args: string[]) => {
writeFileSync(args.at(-1)!, "png-output");
return { stdout: "", stderr: "" };
});
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: outputPath }))
.resolves.toMatchObject({ success: true, data: { outputPath, verified: true, grid: { requestedFrames: 12 } } });
});
it("fails closed when contact-sheet duration or output verification is unavailable", async () => {
const mediaPath = fixture();
const directory = mkdtempSync(join(tmpdir(), "premiere-contact-sheet-failure-"));
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "not-a-duration", stderr: "" });
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: join(directory, "duration.png") }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("positive media duration") });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "2", stderr: "" }).mockResolvedValueOnce({ stdout: "", stderr: "" });
await expect(tools.generate_media_contact_sheet.handler({ media_path: mediaPath, output_path: join(directory, "missing.png") }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("non-empty") });
});
it("fails closed on tool errors and missing measurements", async () => {
const mediaPath = fixture();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timed out"), { killed: true }));
await expect(tools.inspect_media_streams.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "" });
await expect(tools.detect_active_picture_bounds.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining("no active-picture") });
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode failed"), { stderr: "bad stream" }));
await expect(tools.analyze_video_interlacing.handler({ media_path: mediaPath })).resolves.toMatchObject({ success: false, error: expect.stringContaining("bad stream") });
});
});
+91
View File
@@ -0,0 +1,91 @@
import { mkdirSync, mkdtempSync, utimesSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { MediaWatchRegistry, getMediaWatchTools } from "../../src/tools/media-watch.js";
const registries: MediaWatchRegistry[] = [];
afterEach(() => registries.splice(0).forEach((registry) => registry.close()));
describe("media watch", () => {
it("proposes new media without disclosing paths by default", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-"));
writeFileSync(path.join(root, "existing.mp4"), "old");
const registry = new MediaWatchRegistry(); registries.push(registry);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"] }) as any;
writeFileSync(path.join(root, "new.mp4"), "new");
const preview = registry.preview({ watch_id: started.watch_id }) as any;
expect(preview.proposed_count).toBe(1);
expect(preview.proposed_imports[0]).not.toHaveProperty("media_path");
expect(preview.applied).toBe(false);
});
it("supports disclosure, rescan, stop, and handler failures", async () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-"));
const registry = new MediaWatchRegistry(); registries.push(registry);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: [".MP4"], recursive: false }) as any;
writeFileSync(path.join(root, "new.mp4"), "new");
const preview = registry.preview({ watch_id: started.watch_id, include_paths: true, known_media_path_hashes: [] }) as any;
expect(preview.proposed_imports[0].media_path).toContain("new.mp4");
expect((registry.rescan() as any).baseline_file_count).toBe(1);
registry.close(); expect(registry.status()).toMatchObject({ active: false });
expect(() => registry.preview({ watch_id: started.watch_id })).toThrow(/no media watch/);
});
it("rejects unsafe starts and stale preview arguments", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-")), outside = mkdtempSync(path.join(tmpdir(), "outside-"));
const registry = new MediaWatchRegistry(); registries.push(registry);
expect(() => registry.start({ approved_workspace_path: root, watch_path: outside, allowed_extensions: ["mp4"] })).toThrow(/contained/);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"] }) as any;
expect(() => registry.start({})).toThrow(/already active/);
expect(() => registry.preview({ watch_id: "wrong" })).toThrow(/does not match/);
expect(() => registry.preview({ watch_id: started.watch_id, include_paths: "yes" })).toThrow(/boolean/);
expect(() => registry.preview({ watch_id: started.watch_id, known_media_path_hashes: ["bad"] })).toThrow(/sha256/);
});
it("validates paths, extensions, recursion, bins, and inactive scans", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-"));
for (const args of [
{ approved_workspace_path: "relative", watch_path: root, allowed_extensions: ["mp4"] },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: [] },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: [1] },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: ["bad!"] },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4", ".MP4"] },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"], recursive: "yes" },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"], target_bin_id: "" },
{ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"], target_bin_id: "x".repeat(513) },
]) expect(() => new MediaWatchRegistry().start(args)).toThrow();
expect(() => new MediaWatchRegistry().rescan()).toThrow(/no media watch/);
});
it("routes every management action through the public handlers", async () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-")), registry = new MediaWatchRegistry(); registries.push(registry);
const tools = getMediaWatchTools(registry);
const started = await tools.manage_media_watch.handler({ action: "start", approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"] }) as any;
expect((await tools.manage_media_watch.handler({ action: "status" })).success).toBe(true);
expect((await tools.manage_media_watch.handler({ action: "scan" })).success).toBe(true);
expect((await tools.preview_watched_media_import.handler({ watch_id: started.data.watch_id })).success).toBe(true);
expect((await tools.manage_media_watch.handler({ action: "unknown" })).success).toBe(false);
expect((await tools.manage_media_watch.handler({ action: "stop" })).success).toBe(true);
expect((await tools.preview_watched_media_import.handler({ watch_id: started.data.watch_id })).success).toBe(false);
});
it("filters known hashes and proposes changed baseline files to a bin", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-")), file = path.join(root, "clip.mp4"); writeFileSync(file, "a");
const registry = new MediaWatchRegistry(); registries.push(registry);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"], target_bin_id: "bin" }) as any;
writeFileSync(file, "changed-size");
const first = registry.preview({ watch_id: started.watch_id }) as any, hash = first.proposed_imports[0].path_hash;
expect(first.proposed_imports[0].target_bin_id).toBe("bin");
expect((registry.preview({ watch_id: started.watch_id, known_media_path_hashes: [hash] }) as any).proposed_count).toBe(0);
expect(() => registry.preview({ watch_id: started.watch_id, known_media_path_hashes: "bad" })).toThrow(/at most 5000/);
});
it("does not propose an unchanged baseline file", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-")); writeFileSync(path.join(root, "clip.mp4"), "same");
const registry = new MediaWatchRegistry(); registries.push(registry);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"] }) as any;
expect((registry.preview({ watch_id: started.watch_id }) as any).proposed_count).toBe(0);
});
it("detects same-size timestamp changes and recursively scans subfolders", () => {
const root = mkdtempSync(path.join(tmpdir(), "premiere-watch-")), nested = path.join(root, "nested"); mkdirSync(nested);
const file = path.join(nested, "clip.mp4"); writeFileSync(file, "same");
const registry = new MediaWatchRegistry(); registries.push(registry);
const started = registry.start({ approved_workspace_path: root, watch_path: root, allowed_extensions: ["mp4"], recursive: true }) as any;
const changed = new Date(Date.now() + 10_000); utimesSync(file, changed, changed);
expect((registry.preview({ watch_id: started.watch_id }) as any).proposed_count).toBe(1);
});
});
+188
View File
@@ -0,0 +1,188 @@
import { describe, expect, it } from "vitest";
import { getMogrtAuthoringTools } from "../../src/tools/mogrt-authoring.js";
const bridgeOptions = { tempDir: "D:/PremiereBridge", timeoutMs: 5000 };
describe("MOGRT authoring tools", () => {
it("issues a bounded lower-third preview and consumes its token exactly once", async () => {
const scripts: string[] = [];
const tools = getMogrtAuthoringTools(bridgeOptions, {
directoryExists: () => true,
tokenFactory: () => "preview-token",
send: async (script) => {
scripts.push(script);
return { success: true, data: { exportRequested: true, hostExportReturn: false } };
},
artifactStatus: () => ({ exists: false, size_bytes: null, zip_header_valid: false }),
operationIdFactory: () => "operation-1",
});
const preview = await tools.preview_mogrt_recipe.handler({
recipe: "lower_third",
template_name: "Launch Title",
headline: "A headline",
subtitle: "A subtitle",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
});
expect(preview).toMatchObject({
success: true,
data: {
applied: false,
previewToken: "preview-token",
plan: { output_path: "D:\\Approved\\templates\\Launch Title.mogrt" },
},
});
const created = await tools.create_mogrt_recipe.handler({
preview_token: "preview-token",
confirm_export: true,
});
expect(created).toMatchObject({ success: true, data: { operationId: "operation-1" } });
expect(scripts).toHaveLength(1);
expect(scripts[0]).toContain("exportAsMotionGraphicsTemplate");
expect(scripts[0]).toContain("addToMotionGraphicsTemplate");
expect(scripts[0]).toContain("Open a saved After Effects project inside approved_workspace_path");
expect(scripts[0]).toContain("existing.exists");
await expect(tools.create_mogrt_recipe.handler({
preview_token: "preview-token",
confirm_export: true,
})).rejects.toThrow("already used");
});
it("fails closed when an output path leaves its approved workspace", async () => {
const tools = getMogrtAuthoringTools(bridgeOptions, { directoryExists: () => true });
await expect(tools.preview_mogrt_recipe.handler({
template_name: "Outside",
headline: "Nope",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Elsewhere",
})).rejects.toThrow("inside approved_workspace_path");
});
it("supports only the bounded recipe library and applies contained brand-kit defaults", async () => {
const tools = getMogrtAuthoringTools(bridgeOptions, { directoryExists: () => true });
await expect(tools.preview_mogrt_recipe.handler({
recipe: "social_end_card",
template_name: "Brand End Card",
headline: "Follow us",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
brand_kit: { name: "Brand", name_prefix: "Brand", accent_color: "#102030", text_color: "#FFFFFF", safe_margin_percent: 0.1 },
})).resolves.toMatchObject({
success: true,
data: { plan: { recipe: "social_end_card", accent_color: "#102030", text_color: "#FFFFFF" } },
});
await expect(tools.preview_mogrt_recipe.handler({
recipe: "arbitrary_script",
template_name: "Nope",
headline: "Nope",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
})).rejects.toThrow("recipe must be one of");
});
it("does not report a valid ZIP header as visual or import verification", async () => {
const tools = getMogrtAuthoringTools(bridgeOptions, {
artifactStatus: () => ({ exists: true, size_bytes: 42, zip_header_valid: true }),
operationIdFactory: () => "operation-verify",
});
await expect(tools.verify_mogrt_artifact.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
})).resolves.toMatchObject({
success: true,
data: { visualVerified: false, importVerified: false, artifact: { zip_header_valid: true } },
});
});
it("rejects unapproved preview inputs before a connector call", async () => {
const tools = getMogrtAuthoringTools(bridgeOptions, { directoryExists: () => false });
await expect(tools.preview_mogrt_recipe.handler(null)).rejects.toThrow("arguments must be an object");
await expect(tools.preview_mogrt_recipe.handler({
template_name: "Missing output",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
})).rejects.toThrow("must already exist");
await expect(tools.preview_mogrt_recipe.handler({
template_name: "Bad.",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
})).rejects.toThrow("template_name");
await expect(tools.preview_mogrt_recipe.handler({
template_name: "Safe",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
unexpected: true,
})).rejects.toThrow("unsupported field");
const existingDirectory = getMogrtAuthoringTools(bridgeOptions, { directoryExists: () => true });
await expect(existingDirectory.preview_mogrt_recipe.handler({
recipe: "not_a_recipe",
template_name: "Safe",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
})).rejects.toThrow("recipe must be one of");
await expect(existingDirectory.preview_mogrt_recipe.handler({
template_name: "Safe",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "/approved/templates",
})).rejects.toThrow("same path format");
await expect(existingDirectory.preview_mogrt_recipe.handler({
template_name: "Safe",
headline: "No file",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
frame_rate: 31,
})).rejects.toThrow("frame_rate must be one of");
});
it("requires confirmation, preserves bridge errors, and reports local artifact failures", async () => {
let now = 0;
const tools = getMogrtAuthoringTools(bridgeOptions, {
directoryExists: () => true,
now: () => now,
tokenFactory: () => "expiring-token",
operationIdFactory: () => "operation-error",
send: async () => ({ success: false, error: "connector unavailable" }),
artifactStatus: () => ({ exists: true, size_bytes: 10, zip_header_valid: false }),
});
await tools.preview_mogrt_recipe.handler({
template_name: "Safe",
headline: "A headline",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
});
await expect(tools.create_mogrt_recipe.handler({
preview_token: "expiring-token",
confirm_export: false,
})).rejects.toThrow("confirm_export");
await expect(tools.create_mogrt_recipe.handler({
preview_token: "expiring-token",
confirm_export: true,
})).resolves.toMatchObject({ success: false, error: "connector unavailable (operation operation-error)" });
await expect(tools.verify_mogrt_artifact.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
})).resolves.toMatchObject({ success: false, error: "The artifact is not a recognizable ZIP-based .mogrt file" });
await tools.preview_mogrt_recipe.handler({
template_name: "Expired",
headline: "A headline",
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
});
now = 10 * 60 * 1000;
await expect(tools.create_mogrt_recipe.handler({
preview_token: "expiring-token",
confirm_export: true,
})).rejects.toThrow("expired");
});
});
+485
View File
@@ -0,0 +1,485 @@
import { describe, expect, it } from "vitest";
import { getMogrtStudioTools } from "../../src/tools/mogrt-studio.js";
const bridgeOptions = { tempDir: "D:/PremiereBridge", timeoutMs: 5000 };
const artifact = { exists: true, size_bytes: 64, zip_header_valid: true };
describe("MOGRT studio tools", () => {
it("validates a contained brand kit and serially creates a bounded JSON batch", async () => {
const afterEffectsScripts: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("templates.json"),
readText: () => JSON.stringify([
{ recipe: "title_card", template_name: "Brand Intro", headline: "Hello" },
{ recipe: "callout", template_name: "Brand Tip", headline: "Tip" },
]),
artifactStatus: () => artifact,
tokenFactory: (() => { const tokens = ["batch-preview", "unused"]; return () => tokens.shift() ?? "token"; })(),
operationIdFactory: () => "operation-1",
sendAfterEffects: async (script) => {
afterEffectsScripts.push(script);
return { success: true, data: { exportRequested: true } };
},
});
await expect(tools.validate_mogrt_brand_kit.handler({
approved_workspace_path: "D:/Approved",
brand_kit: { name: "Brand", name_prefix: "Brand", font_family: "Inter", accent_color: "#112233", text_color: "#FFFFFF", safe_margin_percent: 0.12 },
})).resolves.toMatchObject({ success: true, data: { brandKit: { name: "Brand", accent_color: "#112233", safe_margin_percent: 0.12 } } });
const preview = await tools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/templates.json",
brand_kit: { name: "Brand", name_prefix: "Brand", accent_color: "#112233", safe_margin_percent: 0.1 },
});
expect(preview).toMatchObject({ success: true, data: { previewToken: "batch-preview", plans: [{ recipe: "title_card", accent_color: "#112233" }, { recipe: "callout", accent_color: "#112233" }] } });
await expect(tools.create_mogrt_batch.handler({ preview_token: "batch-preview", confirm_export: true })).resolves.toMatchObject({
success: true,
data: { completed: [{ templateName: "Brand Intro" }, { templateName: "Brand Tip" }], batchAtomic: false },
});
expect(afterEffectsScripts).toHaveLength(2);
expect(afterEffectsScripts[0]).toContain('recipe: "title_card"');
expect(afterEffectsScripts[1]).toContain('recipe: "callout"');
});
it("publishes only the previewed artifact to a new immutable local-library version", async () => {
const copied: Array<[string, string]> = [];
const directories: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate === "D:\\Approved\\templates\\Launch.mogrt",
artifactStatus: () => artifact,
tokenFactory: () => "library-preview",
operationIdFactory: () => "operation-library",
copyFile: (source, destination) => copied.push([source, destination]),
makeDirectory: (candidate) => directories.push(candidate),
});
const preview = await tools.preview_mogrt_library_publish.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
library_directory: "D:/Approved/library",
template_name: "Launch",
});
expect(preview).toMatchObject({ success: true, data: { previewToken: "library-preview", version: "v001" } });
await expect(tools.publish_mogrt_to_library.handler({ preview_token: "library-preview", confirm_publish: true })).resolves.toMatchObject({
success: true,
data: { version: "v001", overwrite: false },
});
expect(directories[0]).toContain("v001");
expect(copied[0]?.[1]).toContain("Launch.mogrt");
});
it("requires a disposable named Premiere target and reports import rather than visual verification", async () => {
const premiereScripts: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
artifactStatus: () => artifact,
tokenFactory: () => "handoff-preview",
operationIdFactory: () => "operation-handoff",
sendPremiere: async (script) => {
premiereScripts.push(script);
return { success: true, data: { imported: true, visualVerified: false, exposedControlDescriptors: [] } };
},
});
const preview = await tools.preview_mogrt_premiere_handoff.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
sequence_id: "sequence-1",
disposable_sequence_name: "MOGRT Verify - Launch",
});
expect(preview).toMatchObject({ success: true, data: { previewToken: "handoff-preview" } });
await expect(tools.apply_mogrt_premiere_handoff.handler({ preview_token: "handoff-preview", confirm_import: true })).resolves.toMatchObject({
success: true,
data: { visualVerified: false },
});
expect(premiereScripts[0]).toContain("MOGRT Verify - Launch");
expect(premiereScripts[0]).toContain("titleTrack.clips.numItems !== 0");
});
it("previews then queues an existing composition without starting a render", async () => {
const scripts: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: () => false,
tokenFactory: () => "render-preview",
operationIdFactory: () => "operation-render",
sendAfterEffects: async (script) => {
scripts.push(script);
return { success: true, data: { queued: true, renderStarted: false } };
},
});
const preview = await tools.preview_after_effects_render.handler({
approved_workspace_path: "D:/Approved",
composition_name: "Launch",
output_path: "D:/Approved/renders/Launch.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
});
expect(preview).toMatchObject({ success: true, data: { previewToken: "render-preview" } });
await expect(tools.enqueue_after_effects_render.handler({ preview_token: "render-preview", confirm_enqueue: true })).resolves.toMatchObject({ success: true, data: { renderStarted: false } });
expect(scripts[0]).toContain("project.renderQueue.items.add(comp)");
expect(scripts[0]).toContain("renderStarted: false");
});
it("uses AE controller-name readback only when the host exposes that API", async () => {
const scripts: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
operationIdFactory: () => "operation-inspect",
sendAfterEffects: async (script) => {
scripts.push(script);
return { success: true, data: { exposedControlsReadbackAvailable: true, exposedControlNames: ["Headline"] } };
},
});
await expect(tools.inspect_after_effects_template_source.handler({ composition_name: "Launch" })).resolves.toMatchObject({
success: true,
data: { exposedControlsReadbackAvailable: true, exposedControlNames: ["Headline"] },
});
expect(scripts[0]).toContain("motionGraphicsTemplateControllerCount");
expect(scripts[0]).toContain("getMotionGraphicsTemplateControllerName");
});
it("rejects unsafe batch input and returns partial progress without promising rollback", async () => {
const sent: string[] = [];
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("batch.json"),
readText: () => JSON.stringify([
{ recipe: "title_card", template_name: "First", headline: "One" },
{ recipe: "quote_card", template_name: "Second", headline: "Two" },
]),
tokenFactory: () => "batch-token",
operationIdFactory: () => "operation-batch-failure",
artifactStatus: () => artifact,
sendAfterEffects: async (script) => {
sent.push(script);
return sent.length === 1
? { success: true, data: { exportRequested: true } }
: { success: false, error: "After Effects refused the second export" };
},
});
await expect(tools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/batch.txt",
})).rejects.toThrow(".json or .csv");
await expect(tools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/missing.json",
})).rejects.toThrow("does not exist");
await tools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/batch.json",
});
await expect(tools.create_mogrt_batch.handler({ preview_token: "batch-token", confirm_export: false })).rejects.toThrow("confirm_export must be true");
await expect(tools.create_mogrt_batch.handler({ preview_token: "batch-token", confirm_export: true })).resolves.toMatchObject({
success: false,
data: { completed: [{ templateName: "First" }], failedTemplate: "Second", batchAtomic: false },
});
await expect(tools.create_mogrt_batch.handler({ preview_token: "batch-token", confirm_export: true })).rejects.toThrow("already used");
});
it("parses bounded CSV batches and rejects duplicate output artifacts", async () => {
const csvTools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("batch.csv"),
readText: () => "recipe,template_name,headline,duration_seconds\nquote_card,Quote,Hello,7\n",
tokenFactory: () => "csv-preview",
operationIdFactory: () => "operation-csv",
});
await expect(csvTools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/batch.csv",
})).resolves.toMatchObject({ success: true, data: { plans: [{ recipe: "quote_card", duration_seconds: 7 }] } });
const duplicateTools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("duplicate.json"),
readText: () => JSON.stringify([
{ template_name: "Same", headline: "One" },
{ template_name: "Same", headline: "Two" },
]),
});
await expect(duplicateTools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/duplicate.json",
})).rejects.toThrow("unique template_name");
});
it("keeps library publishing immutable when the source or destination changes", async () => {
let sourceIsValid = true;
let destinationExists = false;
const tokens = ["library-one", "library-two"];
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate === "D:\\Approved\\templates\\Launch.mogrt" || destinationExists,
artifactStatus: () => ({ ...artifact, zip_header_valid: sourceIsValid }),
tokenFactory: () => tokens.shift() ?? "library-extra",
operationIdFactory: () => "operation-library-race",
});
const request = {
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
library_directory: "D:/Approved/library",
template_name: "Launch",
};
await tools.preview_mogrt_library_publish.handler(request);
sourceIsValid = false;
await expect(tools.publish_mogrt_to_library.handler({ preview_token: "library-one", confirm_publish: true })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("no longer a recognizable ZIP"),
});
sourceIsValid = true;
await tools.preview_mogrt_library_publish.handler(request);
destinationExists = true;
await expect(tools.publish_mogrt_to_library.handler({ preview_token: "library-two", confirm_publish: true })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("destination now exists"),
});
});
it("lists an existing library but never treats a missing root as a publishable target", async () => {
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: (candidate) => !candidate.endsWith("missing") && !candidate.endsWith("Flat"),
listDirectory: (candidate) => candidate.endsWith("library") ? ["Launch", "Flat"] : ["v001", "notes", "v002"],
operationIdFactory: () => "operation-library-inspect",
});
await expect(tools.inspect_mogrt_library.handler({
approved_workspace_path: "D:/Approved",
library_directory: "D:/Approved/missing",
})).resolves.toMatchObject({ success: false, error: "library_directory does not exist" });
await expect(tools.inspect_mogrt_library.handler({
approved_workspace_path: "D:/Approved",
library_directory: "D:/Approved/library",
})).resolves.toMatchObject({ success: true, data: { templates: [{ name: "Launch", versions: ["v001", "v002"] }, { name: "Flat", versions: [] }] } });
});
it("requires confirmation and reports host failures for render enqueueing", async () => {
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: () => false,
tokenFactory: () => "render-token",
operationIdFactory: () => "operation-render-failure",
sendAfterEffects: async () => ({ success: false, error: "No matching composition" }),
});
const request = {
approved_workspace_path: "D:/Approved",
composition_name: "Launch",
output_path: "D:/Approved/renders/Launch.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
};
await tools.preview_after_effects_render.handler(request);
await expect(tools.enqueue_after_effects_render.handler({ preview_token: "render-token", confirm_enqueue: false })).rejects.toThrow("confirm_enqueue must be true");
await expect(tools.enqueue_after_effects_render.handler({ preview_token: "render-token", confirm_enqueue: true })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("No matching composition"),
});
});
it("fails closed when a handoff artifact changes or the disposable target is invalid", async () => {
let artifactIsValid = true;
const tools = getMogrtStudioTools(bridgeOptions, {
artifactStatus: () => ({ ...artifact, zip_header_valid: artifactIsValid }),
tokenFactory: () => "handoff-token",
operationIdFactory: () => "operation-handoff-failure",
});
const base = {
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
sequence_id: "sequence-1",
};
await expect(tools.preview_mogrt_premiere_handoff.handler({ ...base, disposable_sequence_name: "Launch" })).rejects.toThrow("must begin");
await tools.preview_mogrt_premiere_handoff.handler({ ...base, disposable_sequence_name: "MOGRT Verify - Launch" });
await expect(tools.apply_mogrt_premiere_handoff.handler({ preview_token: "handoff-token", confirm_import: false })).rejects.toThrow("confirm_import must be true");
artifactIsValid = false;
await expect(tools.apply_mogrt_premiere_handoff.handler({ preview_token: "handoff-token", confirm_import: true })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("no longer a recognizable ZIP"),
});
});
it("preserves inspection failure boundaries without creating render queue items", async () => {
const tools = getMogrtStudioTools(bridgeOptions, {
operationIdFactory: () => "operation-inspection-failure",
sendAfterEffects: async () => ({ success: false, error: "No open After Effects project" }),
});
await expect(tools.inspect_after_effects_template_source.handler({ composition_name: "Launch" })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("No open After Effects project"),
});
await expect(tools.inspect_after_effects_render_templates.handler()).resolves.toMatchObject({
success: false,
error: expect.stringContaining("No open After Effects project"),
});
});
it("rejects malformed batch files and accepts escaped CSV values without changing a project", async () => {
const preview = (contents: string) => getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("rows.csv"),
readText: () => contents,
tokenFactory: () => "csv-token",
}).preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/rows.csv",
});
await expect(preview("recipe,template_name,headline\r\nquote_card,Quote,\"Hello, \"\"world\"\"\"\r\n")).resolves.toMatchObject({
success: true,
data: { plans: [{ headline: "Hello, \"world\"" }] },
});
await expect(preview("recipe,unsupported\nquote_card,nope\n")).rejects.toThrow("unsupported column");
await expect(preview("recipe,template_name,headline\n\"quote_card,Quote,broken")).rejects.toThrow("unterminated quoted field");
await expect(preview("recipe,template_name,headline\n")).rejects.toThrow("header row and at least one template row");
});
it("rejects malformed arguments, cross-format paths, and out-of-range handoff coordinates", async () => {
const tools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("rows.json"),
readText: () => "{}",
artifactStatus: () => artifact,
});
await expect(tools.preview_mogrt_batch.handler(null)).rejects.toThrow("arguments must be an object");
await expect(tools.preview_mogrt_batch.handler({
approved_workspace_path: "relative",
output_directory: "relative/templates",
data_file_path: "relative/rows.json",
})).rejects.toThrow("Path must be absolute");
await expect(tools.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "/Approved/data/rows.json",
})).rejects.toThrow("same path format");
const handoff = {
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
sequence_id: "sequence-1",
disposable_sequence_name: "MOGRT Verify - Launch",
};
await expect(tools.preview_mogrt_premiere_handoff.handler({ ...handoff, mogrt_path: "D:/Approved/templates/Launch.txt" })).rejects.toThrow("must end in .mogrt");
await expect(tools.preview_mogrt_premiere_handoff.handler({ ...handoff, video_track_index: 1.5 })).rejects.toThrow("must be an integer");
await expect(tools.preview_mogrt_premiere_handoff.handler({ ...handoff, start_seconds: -1 })).rejects.toThrow("must be a finite number");
});
it("rejects missing render and library roots before issuing a confirmation token", async () => {
const renderTools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => false,
fileExists: () => false,
});
await expect(renderTools.preview_after_effects_render.handler({
approved_workspace_path: "D:/Approved",
composition_name: "Launch",
output_path: "D:/Approved/renders/Launch.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
})).rejects.toThrow("parent directory must already exist");
const libraryTools = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => false,
fileExists: (candidate) => candidate === "D:\\Approved\\templates\\Launch.mogrt",
artifactStatus: () => artifact,
});
await expect(libraryTools.preview_mogrt_library_publish.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
library_directory: "D:/Approved/library",
template_name: "Launch",
})).rejects.toThrow("will not create a new library root");
});
it("returns host errors after consuming only a valid Premiere handoff preview", async () => {
const tools = getMogrtStudioTools(bridgeOptions, {
artifactStatus: () => artifact,
tokenFactory: () => "handoff-host-error",
operationIdFactory: () => "operation-handoff-host-error",
sendPremiere: async () => ({ success: false, error: "The verification track is occupied" }),
sendAfterEffects: async () => ({ success: true, data: { readOnly: true } }),
});
await expect(tools.inspect_after_effects_template_source.handler()).resolves.toMatchObject({ success: true, data: { readOnly: true } });
await expect(tools.inspect_after_effects_render_templates.handler()).resolves.toMatchObject({ success: true, data: { readOnly: true } });
await tools.preview_mogrt_premiere_handoff.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Launch.mogrt",
sequence_id: "sequence-1",
disposable_sequence_name: "MOGRT Verify - Launch",
video_track_index: 2,
audio_track_index: 1,
start_seconds: 12.5,
});
await expect(tools.apply_mogrt_premiere_handoff.handler({ preview_token: "handoff-host-error", confirm_import: true })).resolves.toMatchObject({
success: false,
error: expect.stringContaining("verification track is occupied"),
});
});
it("rejects malformed authoring boundaries and expires abandoned render previews", async () => {
const guarded = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: (candidate) => candidate.endsWith("logo.png") || candidate.endsWith("Existing.mov"),
readText: () => "[]",
artifactStatus: () => ({ exists: false, size_bytes: null, zip_header_valid: false }),
});
await expect(guarded.validate_mogrt_brand_kit.handler({
approved_workspace_path: "D:/Approved",
brand_kit: { name: "Brand", logo_path: "D:/Approved/logo.png" },
})).resolves.toMatchObject({ success: true, data: { logoPathExists: true } });
await expect(guarded.preview_mogrt_batch.handler({
approved_workspace_path: "D:/Approved",
output_directory: "D:/Approved/templates",
data_file_path: "D:/Approved/data/rows.json",
unsupported: true,
})).rejects.toThrow("unsupported field");
await expect(guarded.preview_after_effects_render.handler({
approved_workspace_path: "D:/Approved",
composition_name: "Bad.",
output_path: "D:/Approved/renders/New.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
})).rejects.toThrow("composition_name must use");
await expect(guarded.preview_after_effects_render.handler({
approved_workspace_path: "D:/Approved",
composition_name: "Existing",
output_path: "D:/Approved/renders/Existing.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
})).rejects.toThrow("already exists");
await expect(guarded.preview_mogrt_premiere_handoff.handler({
approved_workspace_path: "D:/Approved",
mogrt_path: "D:/Approved/templates/Invalid.mogrt",
sequence_id: "sequence-1",
disposable_sequence_name: "MOGRT Verify - Invalid",
})).rejects.toThrow("must exist and have a ZIP header");
let now = 0;
const expiring = getMogrtStudioTools(bridgeOptions, {
directoryExists: () => true,
fileExists: () => false,
tokenFactory: () => "expired-render",
now: () => now,
});
await expiring.preview_after_effects_render.handler({
approved_workspace_path: "D:/Approved",
composition_name: "Launch",
output_path: "D:/Approved/renders/Launch.mov",
render_settings_template: "Best Settings",
output_module_template: "Lossless",
});
now = 600_000;
await expect(expiring.enqueue_after_effects_render.handler({ preview_token: "expired-render", confirm_enqueue: true })).rejects.toThrow("expired");
});
});
+138
View File
@@ -0,0 +1,138 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import { getAudioTools } from "../../src/tools/audio.js";
const tool = getAudioTools({ tempDir: "/tmp/test" }).normalize_loudness_file;
beforeEach(() => vi.clearAllMocks());
function fixture() {
const directory = mkdtempSync(join(tmpdir(), "premiere-normalize-"));
const inputPath = join(directory, "input.wav");
const outputPath = join(directory, "normalized.wav");
writeFileSync(inputPath, "input-audio");
return { inputPath, outputPath };
}
function measuredSummary(integratedLufs = -16.2, truePeakDbfs = -1.5) {
return `Summary:\n Integrated loudness:\n I: ${integratedLufs} LUFS\n Loudness range:\n LRA: 4.1 LU\n True peak:\n Peak: ${truePeakDbfs} dBFS`;
}
describe("normalize_loudness_file safety boundary", () => {
it("rejects invalid standards values before filesystem or FFmpeg work", async () => {
await expect(tool.handler({ input_path: "in.wav", output_path: "out.wav", target_lufs: 0 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("target_lufs") });
await expect(tool.handler({ input_path: "in.wav", output_path: "out.wav", max_true_peak_dbfs: 2 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("max_true_peak") });
});
it("never permits in-place normalization", async () => {
await expect(tool.handler({ input_path: "same.wav", output_path: "same.wav" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("differ") });
});
it("fails before FFmpeg when the input is missing", async () => {
await expect(tool.handler({ input_path: "missing.wav", output_path: "new.wav" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
});
it("writes a new derivative and remeasures that exact output", async () => {
const { inputPath, outputPath } = fixture();
mockedExecFileAsync.mockImplementationOnce(async () => {
writeFileSync(outputPath, "normalized-audio");
return { stdout: "", stderr: "" };
});
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: measuredSummary() });
await expect(tool.handler({
input_path: inputPath,
output_path: outputPath,
target_lufs: -16,
max_true_peak_dbfs: -1,
tolerance_lu: 0.5,
})).resolves.toMatchObject({
success: true,
data: {
inputPath,
outputPath,
verified: true,
loudnessPass: true,
truePeakPass: true,
},
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(2);
});
it("reports a new but out-of-target derivative without claiming verification", async () => {
const { inputPath, outputPath } = fixture();
mockedExecFileAsync.mockImplementationOnce(async () => {
writeFileSync(outputPath, "normalized-audio");
return { stdout: "", stderr: "" };
});
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: measuredSummary(-12, -0.5) });
await expect(tool.handler({
input_path: inputPath,
output_path: outputPath,
target_lufs: -16,
max_true_peak_dbfs: -1,
tolerance_lu: 0.5,
})).resolves.toMatchObject({
success: true,
data: { verified: false, loudnessPass: false, truePeakPass: false },
});
});
it("makes missing tools, timeouts, failed writes, and unverifiable outputs explicit", async () => {
const missingTool = fixture();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("not found"), { code: "ENOENT" }));
await expect(tool.handler({ input_path: missingTool.inputPath, output_path: missingTool.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not found on PATH") });
const timeout = fixture();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("timeout"), { killed: true }));
await expect(tool.handler({ input_path: timeout.inputPath, output_path: timeout.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timed out") });
const noOutput = fixture();
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: "" });
await expect(tool.handler({ input_path: noOutput.inputPath, output_path: noOutput.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("no non-empty output") });
const noMeasurement = fixture();
mockedExecFileAsync.mockImplementationOnce(async () => {
writeFileSync(noMeasurement.outputPath, "normalized-audio");
return { stdout: "", stderr: "" };
});
mockedExecFileAsync.mockResolvedValueOnce({ stdout: "", stderr: measuredSummary(Number.NEGATIVE_INFINITY) });
await expect(tool.handler({ input_path: noMeasurement.inputPath, output_path: noMeasurement.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("not measurable") });
});
it("rejects unsafe destinations and preserves an FFmpeg failure detail", async () => {
const unsafe = fixture();
await expect(tool.handler({ input_path: unsafe.inputPath, output_path: unsafe.outputPath, tolerance_lu: -1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("tolerance_lu") });
writeFileSync(unsafe.outputPath, "do-not-overwrite");
await expect(tool.handler({ input_path: unsafe.inputPath, output_path: unsafe.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("will not be overwritten") });
const failure = fixture();
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode failed"), { stderr: "bad audio stream" }));
await expect(tool.handler({ input_path: failure.inputPath, output_path: failure.outputPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("bad audio stream") });
});
});
+89
View File
@@ -0,0 +1,89 @@
import { describe, expect, it, vi } from "vitest";
import { getProjectIntakeTools, projectIntakeCaptureScript } from "../../src/tools/project-intake.js";
const template = {
schemaVersion: 1,
id: "facility-intake",
version: "1",
requiredBins: [{ name: "Incoming" }],
allowedExtensions: ["mov"],
allowedFrameRates: [],
proxyPolicy: "ignore",
approvedPathPrefixes: [],
requiredEvidence: [],
organizationRules: [{ id: "camera", destinationBinName: "Camera", match: { extensions: ["mov"] } }],
};
describe("preview_project_intake tool", () => {
it("returns a bounded read-only report without requiring an active sequence", async () => {
const captureSnapshot = vi.fn(async () => ({
success: true,
data: {
project: { id: "project-1", name: "Feature" },
items: [
{ id: "bin-1", name: "Incoming", type: "bin", parentId: "root", treePath: "Incoming" },
{ id: "clip-1", name: "A001.mov", type: "clip", parentId: "bin-1", mediaPath: "D:/Secret/A001.mov", offline: false },
],
truncated: false,
unavailableEvidence: [],
},
}));
const tools = getProjectIntakeTools({}, { captureSnapshot });
const result = await tools.preview_project_intake.handler({ template, max_items: 25 });
expect(captureSnapshot).toHaveBeenCalledWith(25);
expect(result).toMatchObject({
success: true,
data: {
applied: false,
capture: { pathDisclosure: "redacted" },
organizationPlan: { applied: false, proposedMoves: [{ projectItemId: "clip-1" }] },
},
});
expect(JSON.stringify(result)).not.toContain("D:/Secret");
});
it("does not abort the preview when one host item has malformed frame-rate evidence", async () => {
const captureSnapshot = vi.fn(async () => ({
success: true,
data: {
project: { id: "project-1", name: "Feature" },
items: [{ id: "clip-1", name: "A001.mov", type: "clip", frameRate: null }],
truncated: false,
unavailableEvidence: [],
},
}));
const tools = getProjectIntakeTools({}, { captureSnapshot });
const result = await tools.preview_project_intake.handler({
template: { ...template, allowedFrameRates: [29.97], requiredEvidence: ["frame_rate"] },
});
expect(result).toMatchObject({
success: true,
data: {
status: "incomplete",
findings: expect.arrayContaining([
expect.objectContaining({ code: "FRAME_RATE_UNSUPPORTED", itemId: "clip-1" }),
]),
},
});
});
it("propagates bridge failures and rejects invalid bounds before capture", async () => {
const captureSnapshot = vi.fn(async () => ({ success: false, error: "No project is open" }));
const tools = getProjectIntakeTools({}, { captureSnapshot });
await expect(tools.preview_project_intake.handler({ template })).resolves.toEqual({ success: false, error: "No project is open" });
await expect(tools.preview_project_intake.handler({ template, max_items: 0 })).resolves.toMatchObject({ success: false, error: expect.stringContaining("max_items") });
expect(captureSnapshot).toHaveBeenCalledTimes(1);
});
it("builds a read-only bounded ExtendScript capture", () => {
const script = projectIntakeCaptureScript(17);
expect(script).toContain("var maximumItems = 17");
expect(script).toContain("No project is open");
expect(script).toContain("items.length >= maximumItems");
expect(script).not.toContain("activeSequence");
expect(script).not.toMatch(/\.moveBin\(|\.createBin\(|\.deleteBin\(/);
});
});
+224
View File
@@ -0,0 +1,224 @@
import {
mkdirSync,
mkdtempSync,
rmSync,
utimesSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const fsFaults = vi.hoisted(() => ({
inaccessibleDirectories: new Set<string>(),
vanishedFiles: new Set<string>(),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
readdirSync: (...args: Parameters<typeof actual.readdirSync>) => {
if (fsFaults.inaccessibleDirectories.has(resolve(String(args[0])))) {
const error = Object.assign(new Error("permission denied"), { code: "EACCES" });
throw error;
}
return actual.readdirSync(...args);
},
statSync: (...args: Parameters<typeof actual.statSync>) => {
if (fsFaults.vanishedFiles.has(resolve(String(args[0])))) {
const error = Object.assign(new Error("file disappeared"), { code: "ENOENT" });
throw error;
}
return actual.statSync(...args);
},
};
});
vi.mock("../../src/bridge/file-bridge.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../src/bridge/file-bridge.js")>();
return { ...actual, sendCommand: vi.fn() };
});
import { sendCommand } from "../../src/bridge/file-bridge.js";
import {
collectBridgeTelemetry,
discoverAdjacentRecoveryCandidates,
getRecoveryTools,
} from "../../src/tools/recovery.js";
const mockedSendCommand = vi.mocked(sendCommand);
const temporaryDirectories: string[] = [];
function temporaryDirectory(): string {
const directory = mkdtempSync(join(tmpdir(), "premiere-recovery-edge-"));
temporaryDirectories.push(directory);
return directory;
}
afterEach(() => {
fsFaults.inaccessibleDirectories.clear();
fsFaults.vanishedFiles.clear();
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
beforeEach(() => {
vi.clearAllMocks();
});
describe("recovery discovery filesystem edges", () => {
it("keeps an autosave for a missing project path but marks its project timestamp as unknown", () => {
const directory = temporaryDirectory();
const missingProject = join(directory, "Rough Cut.prproj");
const autosaveDirectory = join(directory, "Premiere Pro Auto-Save");
mkdirSync(autosaveDirectory);
const autosave = join(autosaveDirectory, "Rough Cut backup.PRPROJ");
writeFileSync(autosave, "autosave");
mkdirSync(join(autosaveDirectory, "Rough Cut folder.prproj"));
const candidates = discoverAdjacentRecoveryCandidates(missingProject);
expect(candidates).toEqual([
expect.objectContaining({
path: autosave,
fileName: "Rough Cut backup.PRPROJ",
newerThanProjectFile: null,
}),
]);
});
it("omits inaccessible autosave directories and candidates that disappear during inspection", () => {
const directory = temporaryDirectory();
const project = join(directory, "Episode.prproj");
writeFileSync(project, "project");
const inaccessibleAutosaves = join(directory, "Adobe Premiere Pro Auto-Save");
mkdirSync(inaccessibleAutosaves);
writeFileSync(join(inaccessibleAutosaves, "Episode inaccessible.prproj"), "autosave");
fsFaults.inaccessibleDirectories.add(resolve(inaccessibleAutosaves));
const vanishing = join(directory, "Episode vanishing.prproj");
writeFileSync(vanishing, "autosave");
fsFaults.vanishedFiles.add(resolve(vanishing));
expect(discoverAdjacentRecoveryCandidates(project)).toEqual([]);
});
it("bounds discovery to fifty candidates even when more matching autosaves exist", () => {
const directory = temporaryDirectory();
const project = join(directory, "Assembly.prproj");
writeFileSync(project, "project");
const autosaveDirectory = join(directory, "Adobe Premiere Pro Auto-Save");
mkdirSync(autosaveDirectory);
for (let index = 0; index < 51; index++) {
const candidate = join(autosaveDirectory, `Assembly-${String(index).padStart(2, "0")}.prproj`);
writeFileSync(candidate, String(index));
utimesSync(candidate, new Date(1_000 + index), new Date(1_000 + index));
}
const candidates = discoverAdjacentRecoveryCandidates(project);
expect(candidates).toHaveLength(50);
expect(candidates[0].modifiedMs).toBeGreaterThanOrEqual(candidates.at(-1)!.modifiedMs);
});
});
describe("bridge telemetry filesystem edges", () => {
it("remains useful when an individual pending file disappears", () => {
const directory = temporaryDirectory();
const pending = join(directory, "cmd_transient.jsx");
writeFileSync(pending, "private script");
fsFaults.vanishedFiles.add(resolve(pending));
expect(collectBridgeTelemetry({ tempDir: directory }, 50_000)).toMatchObject({
directoryAccessible: true,
pendingCommands: 1,
pendingResponses: 0,
busyOperations: 0,
oldestPendingAgeMs: null,
healthy: false,
});
});
it("reports an inaccessible bridge directory as unhealthy without exposing its details", async () => {
const directory = temporaryDirectory();
fsFaults.inaccessibleDirectories.add(resolve(directory));
const tools = getRecoveryTools({ tempDir: directory });
const result = await tools.get_bridge_telemetry.handler();
expect(result).toMatchObject({
success: true,
data: {
directoryAccessible: false,
pendingCommands: 0,
pendingResponses: 0,
busyOperations: 0,
healthy: false,
},
});
expect(JSON.stringify(result)).not.toContain(directory);
});
it("marks an old response unhealthy while ignoring unrelated bridge files", () => {
const directory = temporaryDirectory();
const response = join(directory, "res_old.json");
writeFileSync(response, "{}");
writeFileSync(join(directory, "not-a-queue-file.txt"), "private");
utimesSync(response, new Date(1_000), new Date(1_000));
expect(collectBridgeTelemetry({ tempDir: directory }, 32_000)).toMatchObject({
directoryAccessible: true,
pendingCommands: 0,
pendingResponses: 1,
busyOperations: 0,
oldestPendingAgeMs: 31_000,
healthy: false,
});
});
});
describe("recovery inspection snapshots", () => {
it.each([
[null],
[{}],
[{ name: "Project", path: 17 }],
[{ name: 17, path: "project.prproj" }],
])("rejects an invalid Premiere snapshot: %j", async (data) => {
mockedSendCommand.mockResolvedValueOnce({ success: true, data });
const result = await getRecoveryTools({ tempDir: temporaryDirectory() }).inspect_project_recovery.handler();
expect(result).toEqual({ success: false, error: "Premiere returned an invalid project snapshot" });
});
it("preserves bridge failures instead of attempting local recovery discovery", async () => {
mockedSendCommand.mockResolvedValueOnce({ success: false, error: "No project is open" });
await expect(getRecoveryTools({ tempDir: temporaryDirectory() }).inspect_project_recovery.handler())
.resolves.toEqual({ success: false, error: "No project is open" });
});
it("explains the unsaved-project boundary and missing saved project boundary distinctly", async () => {
const tools = getRecoveryTools({ tempDir: temporaryDirectory() });
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { name: "Untitled", path: "" } });
await expect(tools.inspect_project_recovery.handler()).resolves.toMatchObject({
success: true,
data: {
project: { name: "Untitled", path: null, hasSavedPath: false, fileExists: false },
recovery: {
candidateCount: 0,
guidance: expect.stringContaining("Save the project"),
},
},
});
const missingProject = join(temporaryDirectory(), "missing.prproj");
mockedSendCommand.mockResolvedValueOnce({ success: true, data: { name: "Missing", path: missingProject } });
await expect(tools.inspect_project_recovery.handler()).resolves.toMatchObject({
success: true,
data: {
project: { name: "Missing", path: missingProject, hasSavedPath: true, fileExists: false },
recovery: {
candidateCount: 0,
guidance: expect.stringContaining("No adjacent .prproj"),
},
},
});
});
});
+161
View File
@@ -0,0 +1,161 @@
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
utimesSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../src/bridge/file-bridge.js")>();
return { ...actual, sendCommand: vi.fn() };
});
import { sendCommand } from "../../src/bridge/file-bridge.js";
import {
collectBridgeTelemetry,
discoverAdjacentRecoveryCandidates,
getRecoveryTools,
} from "../../src/tools/recovery.js";
const mockedSendCommand = vi.mocked(sendCommand);
const temporaryDirectories: string[] = [];
function temporaryDirectory() {
const directory = mkdtempSync(join(tmpdir(), "premiere-recovery-test-"));
temporaryDirectories.push(directory);
return directory;
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
describe("recovery candidate discovery", () => {
it("finds matching adjacent autosaves newest-first without modifying them", () => {
const directory = temporaryDirectory();
const project = join(directory, "Episode 01.prproj");
writeFileSync(project, "project");
const autosaveDirectory = join(directory, "Adobe Premiere Pro Auto-Save");
mkdirSync(autosaveDirectory);
const older = join(autosaveDirectory, "Episode 01-1.prproj");
const newer = join(autosaveDirectory, "Episode 01-2.prproj");
const unrelated = join(autosaveDirectory, "Other Project.prproj");
writeFileSync(older, "old");
writeFileSync(newer, "new");
writeFileSync(unrelated, "unrelated");
utimesSync(project, new Date(1_000), new Date(1_000));
utimesSync(older, new Date(2_000), new Date(2_000));
utimesSync(newer, new Date(3_000), new Date(3_000));
const candidates = discoverAdjacentRecoveryCandidates(project);
expect(candidates.map((item) => item.fileName)).toEqual([
"Episode 01-2.prproj",
"Episode 01-1.prproj",
]);
expect(candidates.every((item) => item.newerThanProjectFile)).toBe(true);
expect(readFileSync(newer, "utf8")).toBe("new");
});
it("does not scan when no saved Premiere project path exists", () => {
expect(discoverAdjacentRecoveryCandidates("")).toEqual([]);
expect(discoverAdjacentRecoveryCandidates("/tmp/not-a-project.txt")).toEqual([]);
});
});
describe("privacy-preserving bridge telemetry", () => {
it("does not report a quiet directory as healthy when no CEP heartbeat is present", () => {
const directory = temporaryDirectory();
expect(collectBridgeTelemetry({ tempDir: directory })).toMatchObject({
directoryAccessible: true,
heartbeat: { state: "unknown" },
healthy: false,
});
});
it("returns aggregate state without paths, filenames, or contents", () => {
const directory = temporaryDirectory();
writeFileSync(join(directory, "cmd_secret.jsx"), "private project script");
writeFileSync(join(directory, "res_secret.json"), '{"project":"Private"}');
writeFileSync(join(directory, "busy_secret.json"), "{}");
writeFileSync(join(directory, "bridge-heartbeat.json"), JSON.stringify({
protocolVersion: 1,
state: "running",
}));
writeFileSync(join(directory, "helpers_ignored.jsx"), "helper");
const telemetry = collectBridgeTelemetry(
{ tempDir: directory },
Date.now() + 100,
);
expect(telemetry).toMatchObject({
directoryAccessible: true,
pendingCommands: 1,
pendingResponses: 1,
busyOperations: 1,
heartbeat: { state: "running" },
healthy: false,
});
const serialized = JSON.stringify(telemetry);
expect(serialized).not.toContain(directory);
expect(serialized).not.toContain("secret");
expect(serialized).not.toContain("Private");
});
});
describe("recovery tools", () => {
beforeEach(() => vi.clearAllMocks());
it("reports the unavailable dirty flag and never emits restore code", async () => {
const directory = temporaryDirectory();
const project = join(directory, "Documentary.prproj");
writeFileSync(project, "project");
mockedSendCommand.mockResolvedValue({
success: true,
data: { name: "Documentary", path: project },
});
const tools = getRecoveryTools({ tempDir: join(directory, "bridge") });
const result = await tools.inspect_project_recovery.handler();
expect(result.success).toBe(true);
expect((result.data as any).unsavedChanges).toMatchObject({
status: "not_exposed",
dirty: null,
});
expect((result.data as any).recovery.automaticRestoreSupported).toBe(false);
const script = mockedSendCommand.mock.calls[0][0];
expect(script).not.toContain("openDocument");
expect(script).not.toContain("openProject");
expect(script).not.toContain(".save");
});
});
describe("bridge panel accessibility", () => {
it("uses semantic live regions, labels, keyboard focus, and forced-colors CSS", () => {
const cepHtml = readFileSync(
new URL("../../cep-plugin/index.html", import.meta.url),
"utf8",
);
const cepCss = readFileSync(
new URL("../../cep-plugin/styles.css", import.meta.url),
"utf8",
);
const uxpHtml = readFileSync(
new URL("../../uxp-plugin/index.html", import.meta.url),
"utf8",
);
expect(cepHtml).toContain('role="status"');
expect(cepHtml).toContain('aria-label="Bridge activity log"');
expect(cepHtml).toContain('tabindex="0"');
expect(cepCss).toContain("@media (forced-colors: active)");
expect(cepCss).toContain("focus-visible");
expect(uxpHtml).toContain('role="status"');
expect(uxpHtml).toContain('lang="en"');
});
});
+871
View File
@@ -0,0 +1,871 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { runInNewContext } from "node:vm";
import { getHelpersSource } from "../../src/bridge/script-builder.js";
import { BridgeOptions } from "../../src/bridge/file-bridge.js";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
sendRawCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
getTempDir: vi.fn().mockReturnValue("/tmp/test"),
cleanupTempDir: vi.fn(),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getMarkerTools } from "../../src/tools/markers.js";
import { getExportTools } from "../../src/tools/export.js";
import { getUtilityTools } from "../../src/tools/utility.js";
import { getTrackTargetingTools } from "../../src/tools/track-targeting.js";
import { getEffectsTools } from "../../src/tools/effects.js";
import { getClipboardTools } from "../../src/tools/clipboard.js";
import { getTimelineTools } from "../../src/tools/timeline.js";
import { getAdvancedTools } from "../../src/tools/advanced.js";
import { getProjectTools } from "../../src/tools/project.js";
import { getMediaTools } from "../../src/tools/media.js";
import { getTextTools } from "../../src/tools/text.js";
import { getKeyframeTools } from "../../src/tools/keyframes.js";
import { getCaptionTools } from "../../src/tools/captions.js";
import { getSequenceTools } from "../../src/tools/sequence.js";
import { getPlayheadTools } from "../../src/tools/playhead.js";
const mockedSendCommand = vi.mocked(sendCommand);
const bridgeOptions: BridgeOptions = { tempDir: "/tmp/test-bridge", timeoutMs: 5000 };
/** Run a tool handler and return the ExtendScript it generated. */
async function scriptFor(tool: { handler: (args: never) => Promise<unknown> }, args: unknown) {
mockedSendCommand.mockClear();
await tool.handler(args as never);
expect(mockedSendCommand).toHaveBeenCalled();
return mockedSendCommand.mock.calls[0][0] as string;
}
/**
* Same, with comments removed. The helpers name the broken APIs in prose to explain
* why they're avoided ("ProjectItem has no createProxy()"), so an assertion that a
* method is never *called* has to look at code only.
*/
async function codeFor(tool: { handler: (args: never) => Promise<unknown> }, args: unknown) {
const script = await scriptFor(tool, args);
return script.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^[ \t]*\/\/.*$/gm, "");
}
async function executePixelAspectRatioScript(sequence: unknown, ratio = "1.4222") {
const utility = getUtilityTools(bridgeOptions);
const script = await scriptFor(utility.set_sequence_pixel_aspect_ratio, { ratio });
return JSON.parse(String(runInNewContext(`${getHelpersSource()}\n${script}`, {
app: { project: { activeSequence: sequence } },
})));
}
beforeEach(() => vi.clearAllMocks());
describe("real-host social sequence regressions", () => {
const sequence = getSequenceTools(bridgeOptions);
const playhead = getPlayheadTools(bridgeOptions);
const utility = getUtilityTools(bridgeOptions);
it("calls Auto Reframe with its required five arguments and reports the derivative", async () => {
const script = await scriptFor(sequence.auto_reframe_sequence, {
sequence_id: "source-1",
target_width: 1080,
target_height: 1920,
motion_preset: "faster",
new_name: "TikTok Test",
});
expect(script).toContain('seq.autoReframeSequence(9, 16, "faster", newName, false)');
expect(script).toContain("if (!reframed) return __error");
expect(script).toContain("id: reframed.sequenceID");
});
it("sets and reads sequence in/out points in seconds with verification", async () => {
const setScript = await scriptFor(playhead.set_sequence_in_out_points, { in_seconds: 0, out_seconds: 60 });
expect(setScript).toContain("seq.setInPoint(0)");
expect(setScript).toContain("seq.setOutPoint(60)");
expect(setScript).not.toContain("__secondsToTicks(60)");
expect(setScript).toContain("Math.abs(observedOut - 60)");
const getScript = await scriptFor(playhead.get_sequence_in_out_points, {});
expect(getScript).toContain("outSeconds: Number(seq.getOutPoint())");
expect(getScript).not.toContain("__ticksToSeconds(seq.getOutPoint())");
});
it("uses documented DOM work-area accessors instead of unavailable properties and method names", async () => {
const workArea = await codeFor(playhead.get_work_area, {});
expect(workArea).toContain("seq.getWorkAreaInPoint()");
expect(workArea).toContain("seq.getWorkAreaOutPoint()");
expect(workArea).not.toContain("seq.workInPoint");
expect(workArea).not.toContain("seq.workOutPoint");
const enabled = await codeFor(sequence.is_work_area_enabled, {});
expect(enabled).toContain("seq.isWorkAreaEnabled()");
expect(enabled).not.toContain("seq.isWorkAreaBarEnabled()");
});
it("matches a sequence through projectItem.nodeId and verifies both names", async () => {
const script = await scriptFor(utility.rename_project_item, { item_id: "item-1", new_name: "Instagram Test" });
expect(script).toContain("candidate.projectItem.nodeId === item.nodeId");
expect(script).toContain('sequence.name = "Instagram Test"');
expect(script).toContain("sequence && sequence.name !==");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/6
describe("issue #6 — markers must use seconds, not ticks", () => {
const markers = getMarkerTools(bridgeOptions);
it("add_marker passes seconds straight to createMarker", async () => {
const script = await scriptFor(markers.add_marker, { time_seconds: 2.0 });
expect(script).toContain("createMarker(2)");
// The old bug: __secondsToTicks(2) -> 508032000000 handed to createMarker(),
// placing the marker ~508 billion seconds down the timeline.
expect(script).not.toContain("__secondsToTicks(2).toString()");
expect(script).not.toMatch(/createMarker\(parseFloat/);
});
it("add_marker sets marker.end in seconds when given a duration", async () => {
const script = await scriptFor(markers.add_marker, { time_seconds: 2.0, duration_seconds: 3.0 });
expect(script).toContain("marker.end = 5");
expect(script).not.toMatch(/marker\.end = __secondsToTicks/);
});
it("list_markers reads Time.seconds rather than re-converting ticks", async () => {
const script = await scriptFor(markers.list_markers, {});
expect(script).toContain("startSeconds: marker.start.seconds");
expect(script).toContain("endSeconds: marker.end.seconds");
expect(script).not.toContain("__ticksToSeconds(marker.start.ticks)");
});
it("delete_marker still compares ticks against ticks", async () => {
// This path was always correct — both sides are ticks. Guard it so the #6
// fix doesn't get over-applied here.
const script = await scriptFor(markers.delete_marker, { time_seconds: 2.0 });
expect(script).toContain("__secondsToTicks(2)");
expect(script).toContain("marker.start.ticks");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/7
describe("issue #7 — no calls to nonexistent ExtendScript methods", () => {
const exportTools = getExportTools(bridgeOptions);
const trackTargeting = getTrackTargetingTools(bridgeOptions);
it("manage_proxies never calls ProjectItem.createProxy()", async () => {
const code = await codeFor(exportTools.manage_proxies, {
item_id: "clip1",
action: "create",
output_path: "/tmp/proxy.mov",
});
// ProjectItem has no createProxy(); proxies must go through Media Encoder.
expect(code).not.toContain("createProxy");
expect(code).toContain("encodeProjectItem");
});
it("manage_proxies 'create' refuses to report false success without an output path", async () => {
const script = await scriptFor(exportTools.manage_proxies, { item_id: "clip1", action: "create" });
expect(script).toContain("output_path is required");
expect(script).not.toContain("Proxy creation started");
});
it("manage_proxies 'toggle' reports the state it actually set", async () => {
const script = await scriptFor(exportTools.manage_proxies, { item_id: "clip1", action: "toggle" });
// The old code reported !isProxyEnabled() *after* flipping it — i.e. the inverse
// of the truth, every single time.
expect(script).toContain("proxiesEnabled: enabled");
expect(script).not.toContain("proxiesEnabled: !app.project.isProxyEnabled()");
});
it("get_encoder_presets never calls encoder.getFormatList()", async () => {
const code = await codeFor(trackTargeting.get_encoder_presets, { format: "H.264" });
// EncoderManager has no getFormatList(); presets are found by scanning .epr files.
expect(code).not.toContain("getFormatList");
expect(code).toContain("__collectAllPresets()");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/9
describe("issue #9 — frame export uses the QE DOM and verifies the file landed", () => {
const exportTools = getExportTools(bridgeOptions);
const utility = getUtilityTools(bridgeOptions);
const frameTools: Array<[string, { handler: (args: never) => Promise<unknown> }, unknown]> = [
["export_frame", exportTools.export_frame, { output_path: "/tmp/f.png" }],
["capture_frame", exportTools.capture_frame, {}],
["freeze_frame", utility.freeze_frame, { output_path: "/tmp/f.png" }],
];
for (const [name, tool, args] of frameTools) {
it(`${name} does not call exportFramePNG on the public DOM sequence`, async () => {
const script = await scriptFor(tool, args);
// exportFramePNG exists only on the QE sequence. Calling it on
// app.project.activeSequence throws "seq.exportFramePNG is not a function".
expect(script).not.toMatch(/seq\.exportFramePNG/);
expect(script).toContain("__exportStillFrame(");
});
it(`${name} surfaces an error instead of claiming success when no file is written`, async () => {
const script = await scriptFor(tool, args);
expect(script).toContain("if (!res.ok) return __error(");
});
}
it("sets and restores the AME one-frame range in seconds", () => {
const helpers = getHelpersSource();
// Sequence.setInPoint/setOutPoint accept seconds. Passing ticks here made
// the fallback target an enormous range and prevented a still from landing.
expect(helpers).toContain("seq.setInPoint(__ticksToSeconds(startTicks))");
expect(helpers).toContain("seq.setOutPoint(__ticksToSeconds(startTicks + frameTicks))");
expect(helpers).toContain("seq.setInPoint(__ticksToSeconds(savedIn))");
expect(helpers).toContain("seq.setOutPoint(__ticksToSeconds(savedOut))");
expect(helpers).not.toContain("seq.setInPoint(String(startTicks))");
});
});
// Defects found while reviewing PR #3 (repair 6 broken tools on Premiere Pro 2026).
describe("PR #3 follow-ups — color_correct and export_sequence", () => {
const effects = getEffectsTools(bridgeOptions);
const exportTools = getExportTools(bridgeOptions);
it("color_correct applies a value of 0 exactly once", async () => {
const code = await codeFor(effects.color_correct, { node_id: "clip1", saturation: 0 });
// saturation: 0 is a valid full desaturate. Guarding on `!changes.saturation`
// leaves the guard permanently open for 0, re-firing setValue on every later
// Lumetri sub-section that repeats the "Saturation" display name.
expect(code).toContain("!taken.saturation");
expect(code).toContain("taken.saturation = true");
expect(code).not.toContain("!changes.saturation");
});
it("color_correct carries no dead helper", async () => {
const code = await codeFor(effects.color_correct, { node_id: "clip1", exposure: 1 });
expect(code).not.toContain("trySet");
});
it("color_correct only emits setters for the controls it was given", async () => {
const code = await codeFor(effects.color_correct, { node_id: "clip1", exposure: 1.5 });
expect(code).toContain('name === "Exposure"');
expect(code).not.toContain('name === "Saturation"');
});
it("export_sequence finds its default preset without hardcoding a version year", async () => {
const code = await codeFor(exportTools.export_sequence, { output_path: "/tmp/out.mp4" });
expect(code).toContain("__findH264Preset()");
// Hardcoded install paths rot the moment Adobe ships the next version.
expect(code).not.toContain("Adobe Media Encoder 2025");
expect(code).not.toContain("Adobe Media Encoder 2026");
});
});
describe("script-builder helpers used by the fixes are actually defined", () => {
const exportTools = getExportTools(bridgeOptions);
it("defines every helper the generated scripts call", async () => {
const script = getHelpersSource();
for (const helper of [
"function __exportStillFrame(",
"function __firstWrittenFile(",
"function __findStillPreset(",
"function __collectAllPresets(",
"function __findProxyPreset(",
"function __findH264Preset(",
"function __adobeAppFolders(",
"function __collectEprFiles(",
]) {
expect(script).toContain(helper);
}
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/189
describe("issue #189 — Premiere 26.3 capability boundaries and macOS presets", () => {
const trackTargeting = getTrackTargetingTools(bridgeOptions);
const timeline = getTimelineTools(bridgeOptions);
const advanced = getAdvancedTools(bridgeOptions);
const text = getTextTools(bridgeOptions);
const keyframes = getKeyframeTools(bridgeOptions);
const captions = getCaptionTools(bridgeOptions);
it("searches resources inside macOS application bundles and normalizes H.264 filters", async () => {
const helpers = getHelpersSource();
const code = await codeFor(trackTargeting.get_encoder_presets, { format: "H.264" });
expect(helpers).toContain("function __adobeApplicationResourceFolder(");
expect(helpers).toContain('"/Contents/"');
expect(helpers).toContain('"MediaIO/systempresets"');
expect(code).toContain('__presetSearchText("H.264")');
expect(code).toContain("__presetSearchText(p.name)");
});
it("never calls unsupported speed setters", async () => {
const variants = [
timeline.speed_change.handler({ node_id: "clip-1", speed_percent: 65 }),
advanced.set_clip_speed_qe.handler({ node_id: "clip-1", speed_percent: 65 }),
timeline.set_clip_properties.handler({ node_id: "clip-1", speed: 0.65 }),
];
await expect(Promise.all(variants)).resolves.toEqual([
expect.objectContaining({ success: false, error: expect.stringContaining("No mutation was attempted") }),
expect.objectContaining({ success: false, error: expect.stringContaining("No mutation was attempted") }),
expect.objectContaining({ success: false, error: expect.stringContaining("No mutation was attempted") }),
]);
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("fails raw-text caption creation before it can call an unsupported signature", async () => {
const result = await text.add_text_overlay.handler({ text: "TREINO UPPER" });
expect(result).toEqual(expect.objectContaining({
success: false,
error: expect.stringContaining("No mutation was attempted"),
}));
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("labels keyframe and caption results as storage or structure verification, not rendered output", async () => {
const keyframeScript = await scriptFor(keyframes.add_keyframe, {
node_id: "clip-1", effect_name: "Opacity", property_name: "Opacity", time_seconds: 0, value: 0,
});
expect(keyframeScript).toContain("getValueAtKey(time)");
expect(keyframeScript).toContain("renderVerified: false");
expect(keyframeScript).toContain("Premiere parameter readback only");
const captionScript = await scriptFor(captions.create_caption_track, { item_id: "captions.srt" });
expect(captionScript).toContain("renderVerified: false");
expect(captionScript).toContain("verify playback or exported frames");
});
it("validates and structurally verifies timeline insertion instead of trusting insertClip", async () => {
const invalid = await timeline.add_to_timeline.handler({ item_id: "clip-1", start_seconds: -1 });
expect(invalid).toEqual(expect.objectContaining({ success: false }));
expect(mockedSendCommand).not.toHaveBeenCalled();
const script = await scriptFor(timeline.add_to_timeline, {
item_id: "clip-1", track_index: 0, audio_track_index: 0, start_seconds: 3.4,
});
expect(script).toContain("beforeVideoCount");
expect(script).toContain("afterVideoCount > beforeVideoCount + 1");
expect(script).toContain("residual frame fragment");
expect(script).toContain("matchedItem");
expect(script).toContain("verified: true");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/194
describe("issue #194 — string-backed MOGRT effect properties", () => {
const keyframes = getKeyframeTools(bridgeOptions);
it("accepts and safely serializes the JSON string exposed by MOGRT text properties", async () => {
expect(keyframes.set_effect_property.parameters.properties.value).toMatchObject({
type: ["number", "string"],
});
const script = await scriptFor(keyframes.set_effect_property, {
node_id: "clip-1",
effect_name: "AE.ADBE Capsule",
property_name: "Source Text",
value: '{"textEditValue":"Hello \\"editor\\""}',
});
expect(script).toContain('var requestedValue = "{\\"textEditValue\\":\\"Hello \\\\\\\"editor\\\\\\\"\\"}";');
expect(script).toContain("prop.setValue(requestedValue, true)");
expect(script).toContain("readbackVerified: readbackAvailable && readbackValue === requestedValue");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/196
describe("issue #196 — empty Premiere 26.x QE effect catalogs", () => {
const effects = getEffectsTools(bridgeOptions);
it("probes an exact QE effect name before treating an empty catalog as unavailable", async () => {
const script = await scriptFor(effects.apply_effect, {
node_id: "clip-1",
effect_name: "Transform",
});
expect(script).toContain("qe.project.getVideoEffectByName(effectName)");
expect(script).toContain('var effectCatalog = __getQeEffectCatalog("video")');
expect(script).toContain("Direct QE lookup for");
expect(script).toContain('lookupSource: lookupSource');
const listScript = await scriptFor(effects.list_available_effects, {});
expect(listScript).toContain("var commonNames = [");
expect(listScript).toContain("qe.byName.partial");
expect(listScript).toContain("Direct lookup did not resolve any bounded fallback effects.");
const helpers = getHelpersSource();
expect(helpers).toContain("function __getQeEffectCatalog(kind)");
expect(helpers).toContain("Premiere returned an empty legacy QE");
expect(helpers).toContain("no effect was applied");
expect(helpers).toContain("manage_clip_effects_uxp");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/129
describe("issue #129 — CEP component removal is capability-gated", () => {
const effects = getEffectsTools(bridgeOptions);
const clipboard = getClipboardTools(bridgeOptions);
it.each([
["index", { node_id: "clip1", effect_index: 3 }],
["name", { node_id: "clip1", effect_name: "Amplify" }],
])("remove_effect by %s guards an unavailable Component.remove()", async (_mode, args) => {
const code = await codeFor(effects.remove_effect, args);
expect(code).toContain('typeof component.remove !== "function"');
expect(code).toContain("No safe targeted QE fallback exists");
expect(code).toContain("return __error(removal.error)");
expect(code).not.toContain("qeClip.removeEffects()");
});
it("preflights every matching component before remove_effect_by_name mutates any", async () => {
const code = await codeFor(clipboard.remove_effect_by_name, {
node_id: "clip1",
effect_name: "Amplify",
});
expect(code).toContain("var matches = []");
expect(code).toContain("if (!canRemoveComponent(component))");
expect(code).toContain("No matching components were removed");
expect(code).not.toContain("qeClip.removeEffects()");
expect(code.indexOf("if (!canRemoveComponent(component))"))
.toBeLessThan(code.lastIndexOf("component.remove()"));
});
it("documents the capability boundary in each targeted removal tool", () => {
expect(effects.remove_effect.description).toContain("capability error");
expect(clipboard.remove_effect_by_name.description).toContain("capability error");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/37
describe("issue #37 — sequence frame rate uses ticks per frame", () => {
const utility = getUtilityTools(bridgeOptions);
it("converts fps to a Time duration and verifies the applied ticks", async () => {
const script = await scriptFor(utility.set_sequence_frame_rate, { frame_rate: 30 });
expect(script).toContain("TICKS_PER_SECOND / requestedFps");
expect(script).toContain("var frameDuration = new Time()");
expect(script).toContain("frameDuration.ticks = requestedTicks.toString()");
expect(script).toContain("settings.videoFrameRate = frameDuration");
expect(script).toContain("Math.abs(appliedTicks - requestedTicks) > 1");
expect(script).not.toContain("settings.videoFrameRate = 30");
});
it("rejects invalid frame rates before sending a Premiere command", async () => {
mockedSendCommand.mockClear();
const result = await utility.set_sequence_frame_rate.handler({ frame_rate: 0 });
expect(result).toMatchObject({ success: false });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/335
describe("issue #335 — pixel aspect ratio must fail closed on unsupported CEP hosts", () => {
const utility = getUtilityTools(bridgeOptions);
it("checks host support and verifies the readback before reporting success", async () => {
const script = await codeFor(utility.set_sequence_pixel_aspect_ratio, { ratio: "1.0" });
expect(script).toContain("currentRatio = settings.videoPixelAspectRatio");
expect(script).toContain('typeof currentRatio === "undefined"');
expect(script).toContain("No sequence settings were changed");
expect(script).toContain("settings.videoPixelAspectRatio = requestedRatio");
expect(script).toContain("observed = seq.getSettings()");
expect(script).toContain("observedRatio = String(observed.videoPixelAspectRatio)");
expect(script.indexOf('typeof currentRatio === "undefined"'))
.toBeLessThan(script.indexOf("settings.videoPixelAspectRatio = requestedRatio"));
});
it("returns structured failures for unavailable and rejected host settings", async () => {
const unavailableSetSettings = vi.fn();
await expect(executePixelAspectRatioScript({
name: "Unsupported setting",
getSettings: () => ({}),
setSettings: unavailableSetSettings,
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("does not expose a writable"),
});
expect(unavailableSetSettings).not.toHaveBeenCalled();
const lockedSettings: Record<string, string> = {};
Object.defineProperty(lockedSettings, "videoPixelAspectRatio", {
get: () => "1.0",
set: () => { throw new Error("locked"); },
});
const rejectedSetSettings = vi.fn();
await expect(executePixelAspectRatioScript({
name: "Read-only setting",
getSettings: () => lockedSettings,
setSettings: rejectedSetSettings,
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("rejected the sequence pixel-aspect-ratio update"),
});
expect(rejectedSetSettings).not.toHaveBeenCalled();
await expect(executePixelAspectRatioScript({
name: "Apply failure",
getSettings: () => ({ videoPixelAspectRatio: "1.0" }),
setSettings: () => { throw new Error("host refused"); },
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("could not apply the sequence pixel-aspect-ratio update"),
});
});
it("treats a false host result and mismatched readback as unverified", async () => {
await expect(executePixelAspectRatioScript({
name: "Rejected return value",
getSettings: () => ({ videoPixelAspectRatio: "1.0" }),
setSettings: () => false,
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("rejected the sequence pixel-aspect-ratio update"),
});
const initialSettings = { videoPixelAspectRatio: "1.0" };
await expect(executePixelAspectRatioScript({
name: "Mismatched readback",
getSettings: vi.fn().mockReturnValueOnce(initialSettings).mockReturnValueOnce({ videoPixelAspectRatio: "1.0" }),
setSettings: () => undefined,
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("did not apply the requested sequence pixel aspect ratio"),
});
});
it("reports success only after an applied setting reads back exactly", async () => {
const settings = { videoPixelAspectRatio: "1.0" };
const result = await executePixelAspectRatioScript({
name: "Verified sequence",
getSettings: () => settings,
setSettings: () => true,
});
expect(result).toEqual({
success: true,
data: { ratio: "1.4222", sequence: "Verified sequence", verified: true },
});
});
it("rejects non-string aspect ratios before sending a Premiere command", async () => {
mockedSendCommand.mockClear();
const result = await utility.set_sequence_pixel_aspect_ratio.handler({ ratio: 1 as never });
expect(result).toMatchObject({ success: false });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/235
describe("issue #235 — CEP tool calls use the host's documented argument types", () => {
const utility = getUtilityTools(bridgeOptions);
const tracks = getTrackTargetingTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
it("uses a string-backed pixel aspect ratio and verifies the settings readback", async () => {
const script = await scriptFor(utility.set_sequence_pixel_aspect_ratio, { ratio: "1.0" });
expect(utility.set_sequence_pixel_aspect_ratio.parameters.properties.ratio).toMatchObject({ type: "string" });
expect(script).toContain('var requestedRatio = "1.0"');
expect(script).toContain("settings.videoPixelAspectRatio = requestedRatio");
expect(script).toContain("seq.getSettings()");
expect(script).toContain("Premiere did not apply the requested sequence pixel aspect ratio");
});
it("clears sequence points with seconds derived from their tick values", async () => {
const script = await scriptFor(tracks.clear_sequence_in_out, {});
expect(script).toContain("var zeroSeconds = __ticksToSeconds(seq.zeroPoint)");
expect(script).toContain("var endSeconds = __ticksToSeconds(seq.end)");
expect(script).toContain("seq.setInPoint(zeroSeconds)");
expect(script).toContain("seq.setOutPoint(endSeconds)");
expect(script).not.toContain("seq.zeroPoint.ticks");
});
it("passes every positional bars-and-tone argument and captures the created item", async () => {
const script = await scriptFor(project.create_bars_and_tone, {
width: 1920,
height: 1080,
pixel_aspect_numerator: 1,
pixel_aspect_denominator: 1,
audio_sample_rate: 48000,
name: "Bars",
});
expect(script).toContain("app.project.newBarsAndTone(");
expect(script).toContain("1920,");
expect(script).toContain("1080,");
expect(script).toContain("1,");
expect(script).toContain("48000,");
expect(script).toContain('"Bars"');
expect(script).toContain("if (!item) return __error");
});
it("writes the Anti-flicker numeric stream value and verifies it", async () => {
const script = await scriptFor(tracks.set_anti_alias_quality, { node_id: "clip-1", enabled: false });
expect(script).toContain("var requestedValue = 0");
expect(script).toContain("antiFlicker.setValue(requestedValue, 1)");
expect(script).toContain("antiFlicker.getValue()");
expect(script).not.toContain("Use Composition's Shutter Angle");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/236
describe("issue #236 — CEP tools supply required positional arguments", () => {
const advanced = getAdvancedTools(bridgeOptions);
const text = getTextTools(bridgeOptions);
it("passes action, linked-audio, and sensitivity to scene detection", async () => {
const script = await scriptFor(advanced.scene_edit_detection, {
action: "CreateMarkers",
apply_cuts_to_linked_audio: false,
sensitivity: "MediumSensitivity",
});
expect(script).toContain("seq.performSceneEditDetectionOnSelection(");
expect(script).toContain('"CreateMarkers"');
expect(script).toContain("false,");
expect(script).toContain('"MediumSensitivity"');
expect(script).toContain("Select at least one clip");
});
it("passes the Creative Cloud Library name before the MOGRT name", async () => {
const script = await scriptFor(text.import_mogrt_from_library, {
library_name: "Brand Library",
mogrt_name: "Lower Third",
});
expect(text.import_mogrt_from_library.parameters.required).toEqual(["library_name", "mogrt_name"]);
expect(script).toContain("seq.importMGTFromLibrary(");
expect(script).toContain('libraryName,');
expect(script).toContain('mogrtName,');
expect(script).toContain("startTicks,");
});
it("fails raw-text overlay creation before sending an unsupported bridge call", async () => {
const result = await text.add_text_overlay.handler({ text: "Title" });
expect(result).toMatchObject({ success: false, error: expect.stringContaining("No mutation was attempted") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/237
describe("issue #237 — reported mutations must be observable or fail", () => {
const advanced = getAdvancedTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
const tracks = getTrackTargetingTools(bridgeOptions);
const media = getMediaTools(bridgeOptions);
const exports = getExportTools(bridgeOptions);
it("makes trim tools read back their claimed changes", async () => {
const slide = await scriptFor(advanced.slide_edit, { node_id: "clip-1", offset_seconds: 1 });
const slip = await scriptFor(advanced.slip_edit, { node_id: "clip-1", offset_seconds: 1 });
const roll = await scriptFor(advanced.roll_edit, { node_id: "clip-1", offset_seconds: 1 });
expect(slide).toContain("The slide edit returned without an observable timeline change");
expect(roll).toContain("The roll edit returned without an observable timeline change");
expect(slip).toContain("The slip edit returned without an observable source in/out change");
expect(slip).toContain("verified: true");
});
it("uses structural receipts for import and duplicate consolidation", async () => {
const imports = await scriptFor(project.import_sequences, {
project_path: "/tmp/source.prproj",
sequence_ids: ["source-sequence-id"],
});
const duplicates = await scriptFor(project.consolidate_duplicates, {});
expect(imports).toContain("new File");
expect(imports).toContain("beforeIds");
expect(duplicates).toContain("var nodeId = String(item.nodeId || \"\")");
expect(duplicates).toContain("pathMap[mediaPath].nodeIds[nodeId]");
expect(imports).toContain("Premiere did not add any of the requested sequences");
expect(duplicates).toContain("__duplicateMediaStats");
expect(duplicates).toContain("duplicate media groups did not decrease");
});
it("checks image imports, source-point writes, disable state, and offline state", async () => {
const image = await scriptFor(tracks.import_image_sequence, { first_file_path: "/tmp/frame_001.png" });
const points = await scriptFor(tracks.set_item_in_out, { item_id: "item-1", in_seconds: 1 });
const enabled = await scriptFor(tracks.batch_enable_disable, { target: "selected", enabled: false });
const offline = await scriptFor(media.set_offline, { item_id: "item-1", offline: false });
expect(image).toContain("sourceFile.exists");
expect(image).toContain("Premiere accepted the image-sequence import but no project item was added");
expect(points).toContain("item.getInPoint");
expect(points).toContain("Premiere did not apply the requested project-item in point");
expect(enabled).toContain("track.clips[c].disabled = disabled");
expect(enabled).toContain("No clips matched target 'selected'");
expect(offline).toContain("item.refreshMedia()");
expect(offline).toContain("item.isOffline()");
});
it("labels sequence close as a tab operation and verifies an OMF file", async () => {
const close = await scriptFor(advanced.close_sequence, { sequence_id: "sequence-1" });
const omf = await scriptFor(exports.export_omf, { output_path: "/tmp/export.omf" });
expect(close).toContain("timelineTabCloseRequested: true");
expect(close).toContain("sequenceRetainedInProject");
expect(omf).toContain("outputFile.exists");
expect(omf).toContain("Premiere did not write the requested OMF file");
expect(exports.export_omf.parameters.properties.include_pan).toMatchObject({ type: "boolean" });
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/238
describe("issue #238 — AME uses canonical paths and documented encodeFile positions", () => {
const exports = getExportTools(bridgeOptions);
it("captures AME job IDs and does not present queueing as a completed encode", async () => {
const queued = await scriptFor(exports.add_to_render_queue, { output_path: "/tmp/render.mp4" });
const projectItem = await scriptFor(exports.encode_project_item, {
item_id: "item-1",
output_path: "/tmp/render.mp4",
preset_path: "/tmp/preset.epr",
});
expect(queued).toContain("var outputFile = new File");
expect(queued).toContain("var jobId = encoder.encodeSequence");
expect(queued).toContain("Queue presence and output-file creation are not verified");
expect(projectItem).toContain("outputFile.fsName");
expect(projectItem).toContain("var jobId = app.encoder.encodeProjectItem");
});
it("passes work area before removal and never passes undefined Time values", async () => {
const wholeFile = await scriptFor(exports.encode_file, {
input_path: "/tmp/source.mov",
output_path: "/tmp/render.mp4",
preset_path: "/tmp/preset.epr",
});
const range = await scriptFor(exports.encode_file, {
input_path: "/tmp/source.mov",
output_path: "/tmp/render.mp4",
preset_path: "/tmp/preset.epr",
in_seconds: 1,
out_seconds: 2,
});
expect(wholeFile).toContain("var srcIn = new Time()");
expect(wholeFile).toContain("var workArea = 0");
expect(range).toContain("var workArea = 1");
expect(range).toContain("workArea,");
expect(range).not.toContain("var srcIn = undefined");
expect(range).toContain("var jobId = app.encoder.encodeFile");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/322
describe("issue #322 — marker filtering retains the requested sequence collection", () => {
const utility = getUtilityTools(bridgeOptions);
it("resolves the requested sequence once and returns its identity with the marker set", async () => {
const script = await scriptFor(utility.get_sequence_markers_by_type, {
sequence_id: "target-sequence", marker_type: "Comment",
});
expect(script).toContain('var seq = __findSequence("target-sequence")');
expect(script).toContain("var selectedSequence = { id: String(seq.sequenceID)");
expect(script).toContain("var markerCollection = seq.markers");
expect(script).toContain("markerCollection.getFirstMarker()");
expect(script).toContain("markerCollection.getNextMarker(m)");
expect(script).toContain("sequence: selectedSequence");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/323
describe("issue #323 — AME handoffs are unverified until a queue or file readback", () => {
const exports = getExportTools(bridgeOptions);
const project = getProjectTools(bridgeOptions);
it("does not present AME acceptance as a queued, started, or completed encode", async () => {
const render = await scriptFor(exports.add_to_render_queue, { output_path: "/tmp/render.mp4" });
const item = await scriptFor(exports.encode_project_item, {
item_id: "item-1", output_path: "/tmp/render.mp4", preset_path: "/tmp/preset.epr",
});
const proxy = await scriptFor(exports.manage_proxies, {
item_id: "item-1", action: "create", output_path: "/tmp/proxy.mov", preset_path: "/tmp/proxy.epr",
});
const file = await scriptFor(exports.encode_file, {
input_path: "/tmp/source.mov", output_path: "/tmp/render.mp4", preset_path: "/tmp/preset.epr",
});
const batch = await scriptFor(project.start_batch_encode, {});
for (const script of [render, item, proxy, file]) {
expect(script).toContain('if (!jobId || String(jobId) === "0") return __error');
expect(script).toContain("accepted: true");
expect(script).toContain('outcome: "committed_unverified"');
expect(script).not.toContain("queued: true");
}
expect(batch).toContain("requested: true");
expect(batch).toContain('outcome: "committed_unverified"');
expect(batch).not.toContain("started: true");
expect(batch).toContain("var startResult = app.encoder.startBatch()");
expect(batch).toContain("if (startResult !== 1 && startResult !== true)");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/324
describe("issue #324 — duplicate media requires distinct project-item node IDs", () => {
const utility = getUtilityTools(bridgeOptions);
it("deduplicates each media path by stable node ID before forming a group", async () => {
const script = await scriptFor(utility.get_duplicate_media, {});
expect(script).toContain("var nodeId = String(item.nodeId || \"\")");
expect(script).toContain("pathMap[mp] = { items: [], nodeIds: {} }");
expect(script).toContain("if (!pathMap[mp].nodeIds[nodeId])");
expect(script).toContain("pathMap[path].items.length > 1");
expect(script).not.toContain("pathMap[path].length > 1");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/326
describe("issue #326 — sequence creation requires project-collection readback", () => {
const sequence = getSequenceTools(bridgeOptions);
it("does not report a QE-active sequence as created unless it is discoverable", async () => {
const script = await scriptFor(sequence.create_sequence, {
name: "Verified Sequence", preset_path: "/tmp/sequence.sqpreset",
});
expect(script).toContain("var beforeSequenceIds = {}");
expect(script).toContain("var sequenceId = String(seq.sequenceID)");
expect(script).toContain("if (beforeSequenceIds[sequenceId])");
expect(script).toContain("did not create a new sequence");
expect(script).toContain("var created = __findSequence(sequenceId)");
expect(script).toContain("no creation success is reported");
expect(script).toContain("verified: true");
});
});
// https://github.com/leancoderkavy/premiere-pro-mcp/issues/327
describe("issue #327 — legacy media replacement is fail-closed", () => {
const clipboard = getClipboardTools(bridgeOptions);
it("never uses overwriteClip when duration and adjacent-track preservation cannot be verified", async () => {
expect(clipboard.replace_clip_media.operationalCapability).toMatchObject({
backend: "local", backends: ["local"], status: "unsupported", hostVerificationRequired: false,
});
const result = await clipboard.replace_clip_media.handler({ clip_node_id: "clip-1", new_item_id: "item-2" } as never);
expect(result).toMatchObject({ success: false, error: expect.stringContaining("No mutation was attempted") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
+73
View File
@@ -0,0 +1,73 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
existsSync: vi.fn(),
readdirSync: vi.fn(),
statSync: vi.fn(),
}));
vi.mock("node:fs", () => ({
existsSync: mocks.existsSync,
readdirSync: mocks.readdirSync,
statSync: mocks.statSync,
}));
const originalEnv = { ...process.env };
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
process.env = { ...originalEnv };
delete process.env.PREMIERE_DEFAULT_SEQUENCE_PRESET;
mocks.existsSync.mockReturnValue(true);
mocks.statSync.mockReturnValue({ isDirectory: () => false });
});
afterEach(() => {
process.env = { ...originalEnv };
vi.restoreAllMocks();
});
describe("default sequence preset discovery", () => {
it("uses and caches an explicit existing preset", async () => {
process.env.PREMIERE_DEFAULT_SEQUENCE_PRESET = "C:\\Presets\\Explicit.sqpreset";
const { findDefaultSequencePreset } = await import("../../src/tools/sequence.js");
expect(findDefaultSequencePreset()).toBe("C:\\Presets\\Explicit.sqpreset");
mocks.existsSync.mockReturnValue(false);
expect(findDefaultSequencePreset()).toBe("C:\\Presets\\Explicit.sqpreset");
});
it("selects the preferred Windows preset while skipping unrelated installs", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
mocks.readdirSync.mockImplementation((directory) => {
const path = String(directory);
if (path === "C:\\Program Files\\Adobe") return ["Unrelated", "Adobe Premiere Pro 2026"] as never;
if (path.endsWith("SequencePresets")) {
return ["Other.sqpreset", "UHD (4K) 2160p 25 fps.sqpreset"] as never;
}
return [] as never;
});
const { findDefaultSequencePreset } = await import("../../src/tools/sequence.js");
expect(findDefaultSequencePreset()).toMatch(/UHD \(4K\) 2160p 25 fps\.sqpreset$/);
});
it("walks a macOS app bundle and falls back to any preset", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
mocks.readdirSync.mockImplementation((directory) => {
const path = String(directory);
if (path === "/Applications") return ["Adobe Premiere Pro 2026"] as never;
if (path.endsWith("Adobe Premiere Pro 2026")) return ["Adobe Premiere Pro.app", "README"] as never;
if (path.endsWith("SequencePresets")) return ["Cinema.sqpreset"] as never;
return [] as never;
});
const { findDefaultSequencePreset } = await import("../../src/tools/sequence.js");
expect(findDefaultSequencePreset()).toMatch(/Cinema\.sqpreset$/);
});
it("returns and caches null when installation discovery fails", async () => {
mocks.readdirSync.mockImplementation(() => { throw new Error("access denied"); });
const { findDefaultSequencePreset } = await import("../../src/tools/sequence.js");
expect(findDefaultSequencePreset()).toBeNull();
expect(findDefaultSequencePreset()).toBeNull();
});
});
+49
View File
@@ -0,0 +1,49 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getExportTools } from "../../src/tools/export.js";
const mockedSendCommand = vi.mocked(sendCommand);
const tools = getExportTools({ tempDir: "/tmp/test", timeoutMs: 1000 });
beforeEach(() => vi.clearAllMocks());
describe("export_sequence_review_frames", () => {
it("reduces a six-frame review from six MCP/bridge calls to one bounded bridge call", async () => {
await tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", frame_count: 6 });
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var requested = 6");
expect(script).toContain("for (var i = 0; i < requested; i++)");
expect(script).toContain("__exportStillFrame(");
expect(script).toContain("frames.length === requested");
});
it("fails invalid counts and ranges before contacting Premiere", async () => {
await expect(tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", frame_count: 25 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("2 through 24") });
await expect(tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", frame_count: 1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("2 through 24") });
await expect(tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", start_seconds: 8, end_seconds: 4 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("greater than") });
await expect(tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", start_seconds: -1 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("non-negative") });
await expect(tools.export_sequence_review_frames.handler({ output_dir: "", frame_count: 6 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("non-empty") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("reports partial export failures instead of claiming the whole review succeeded", async () => {
await tools.export_sequence_review_frames.handler({ output_dir: "/tmp/review", frame_count: 4 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("failures.push");
expect(script).toContain("complete: frames.length === requested");
expect(script).toContain("Playback, audio, and editorial quality remain unverified");
});
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { planDerivedSilenceRemoval } from "../../src/tools/silence-removal.js";
describe("derived silence-removal planner", () => {
it.each([24000 / 1001, 30000 / 1001, 60000 / 1001])("snaps removal inward at %s fps", (frameRate) => {
const plan = planDerivedSilenceRemoval({ durationSeconds: 10, frameRate, silenceRanges: [{ startSeconds: 1.001, endSeconds: 2.009 }] });
expect(plan.removalRanges).toEqual([{ startFrame: Math.ceil(1.001 * frameRate - 1e-7), endFrame: Math.floor(2.009 * frameRate + 1e-7) }]);
expect(plan.keptFrames + plan.removedFrames).toBe(plan.totalFrames);
});
it("merges overlapping frame ranges and retains requested handles", () => {
const plan = planDerivedSilenceRemoval({ durationSeconds: 6, frameRate: 30, keepHandleFrames: 2, silenceRanges: [
{ startSeconds: 1, endSeconds: 3 }, { startSeconds: 2.8, endSeconds: 4 },
] });
expect(plan.removalRanges).toEqual([{ startFrame: 32, endFrame: 118 }]);
expect(plan.keepRanges).toEqual([{ startFrame: 0, endFrame: 32 }, { startFrame: 118, endFrame: 180 }]);
});
it("merges snapped overlapping silences before applying outer handles", () => {
const plan = planDerivedSilenceRemoval({ durationSeconds: 10, frameRate: 30, keepHandleFrames: 10, silenceRanges: [
{ startSeconds: 0, endSeconds: 5 }, { startSeconds: 4.5, endSeconds: 10 },
] });
expect(plan.removalRanges).toEqual([{ startFrame: 10, endFrame: 290 }]);
expect(plan.keepRanges).toEqual([{ startFrame: 0, endFrame: 10 }, { startFrame: 290, endFrame: 300 }]);
});
it("refuses invalid, truncated, and all-silence plans", () => {
expect(() => planDerivedSilenceRemoval({ durationSeconds: 1, frameRate: 30, silenceRanges: [{ startSeconds: 0, endSeconds: 1 }] })).toThrow("entire source");
expect(() => planDerivedSilenceRemoval({ durationSeconds: 10, frameRate: 30, maximumRemovals: 1, silenceRanges: [
{ startSeconds: 1, endSeconds: 2 }, { startSeconds: 3, endSeconds: 4 },
] })).toThrow("maximumRemovals");
expect(() => planDerivedSilenceRemoval({ durationSeconds: 2, frameRate: 30, silenceRanges: [{ startSeconds: -1, endSeconds: 1 }] })).toThrow("outside");
});
});
+76
View File
@@ -0,0 +1,76 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockedExecFile, mockedExecFileAsync } = vi.hoisted(() => {
const mockedExecFileAsync = vi.fn();
const mockedExecFile = vi.fn();
mockedExecFile[Symbol.for("nodejs.util.promisify.custom")] = mockedExecFileAsync;
return { mockedExecFile, mockedExecFileAsync };
});
vi.mock("node:child_process", () => ({ execFile: mockedExecFile }));
import { getExportTools, parseSceneChangeOutput } from "../../src/tools/export.js";
describe("parseSceneChangeOutput", () => {
it("pairs times with scores and keeps the strongest nearby event", () => {
const output = [
"n:1 pts:100 pts_time:1.00",
"lavfi.scene_score=0.40",
"n:2 pts:110 pts_time:1.10",
"lavfi.scene_score=0.75",
"n:3 pts:300 pts_time:3.00",
"lavfi.scene_score=0.55",
].join("\n");
expect(parseSceneChangeOutput(output, 0.25)).toEqual([
{ timeSeconds: 1.1, score: 0.75 },
{ timeSeconds: 3, score: 0.55 },
]);
});
});
describe("detect_source_scene_changes boundary", () => {
const tool = getExportTools({ tempDir: "/tmp/test" }).detect_source_scene_changes;
beforeEach(() => vi.clearAllMocks());
it("rejects invalid thresholds and missing files", async () => {
await expect(tool.handler({ media_path: "missing.mp4", threshold: 2 })).resolves.toMatchObject({ success: false });
await expect(tool.handler({ media_path: "missing.mp4", minimum_interval_seconds: -1 })).resolves.toMatchObject({ success: false });
await expect(tool.handler({ media_path: "missing.mp4", maximum_events: 0 })).resolves.toMatchObject({ success: false });
await expect(tool.handler({ media_path: "missing.mp4" })).resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
});
it("returns bounded source-relative scene candidates from one local FFmpeg pass", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-scene-detect-"));
const mediaPath = join(directory, "source.mp4");
writeFileSync(mediaPath, "video-fixture");
mockedExecFileAsync.mockResolvedValueOnce({
stdout: "n:1 pts:100 pts_time:1.00\nlavfi.scene_score=0.40\nn:2 pts:300 pts_time:3.00\nlavfi.scene_score=0.55",
stderr: "",
});
await expect(tool.handler({ media_path: mediaPath, threshold: 0.3, minimum_interval_seconds: 0.25, maximum_events: 1 }))
.resolves.toMatchObject({
success: true,
data: {
mediaPath,
totalDetected: 2,
truncated: true,
sceneChanges: [{ timeSeconds: 1, score: 0.4 }],
},
});
expect(mockedExecFileAsync).toHaveBeenCalledTimes(1);
});
it("keeps an FFmpeg scene-detection failure explicit", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-scene-error-"));
const mediaPath = join(directory, "source.mp4");
writeFileSync(mediaPath, "video-fixture");
mockedExecFileAsync.mockRejectedValueOnce(Object.assign(new Error("decode failed"), { stderr: "bad video stream" }));
await expect(tool.handler({ media_path: mediaPath }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("bad video stream") });
});
});
+379
View File
@@ -0,0 +1,379 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
}));
import { sendCommand } from "../../src/bridge/file-bridge.js";
import { getTimelineTools } from "../../src/tools/timeline.js";
import { getTrackTargetingTools } from "../../src/tools/track-targeting.js";
import { getAdvancedTools } from "../../src/tools/advanced.js";
import { getTrackTools } from "../../src/tools/tracks.js";
const mockedSendCommand = vi.mocked(sendCommand);
const bridgeOptions = { tempDir: "/tmp/test", timeoutMs: 1000 };
const timeline = getTimelineTools(bridgeOptions);
const trackTargeting = getTrackTargetingTools(bridgeOptions);
const advanced = getAdvancedTools(bridgeOptions);
const tracks = getTrackTools(bridgeOptions);
beforeEach(() => vi.clearAllMocks());
describe("trim_clip verification", () => {
it("requires exactly one edit point instead of reporting a no-op or multi-write as success", async () => {
const result = await timeline.trim_clip.handler({ node_id: "abc" });
expect(result.success).toBe(false);
expect(result.error).toContain("new_in_seconds");
// The bridge must not be touched at all — there is nothing to apply.
expect(mockedSendCommand).not.toHaveBeenCalled();
const multiple = await timeline.trim_clip.handler({
node_id: "abc",
new_in_seconds: 2,
new_out_seconds: 8,
});
expect(multiple.success).toBe(false);
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("compares the read-back in point against the requested value", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_in_seconds: 2 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var actualIn = after.inPoint");
expect(script).toContain("Math.abs(actualIn - 2) > tolerance");
expect(script).toContain("did not apply a verified timeline trim");
expect(script).toContain("verified: true");
});
it("compares the read-back out point against the requested value", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_out_seconds: 8 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("Math.abs(actualOut - 8) > tolerance");
});
it("only writes the edge that was actually requested", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_in_seconds: 2 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("clip.inPoint = __secondsToTicks(2).toString();");
expect(script).not.toContain("clip.outPoint = __secondsToTicks(2).toString();");
});
it("derives its tolerance from the sequence timebase so frame snapping is not a failure", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_in_seconds: 2 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("seq.timebase");
expect(script).toContain("TICKS_PER_SECOND / 24");
expect(script).toContain("var tolerance = __ticksToSeconds(frameTicks)");
});
it("refuses invalid numeric edit points before contacting Premiere", async () => {
const result = await timeline.trim_clip.handler({
node_id: "abc",
new_out_seconds: Number.NaN,
});
expect(result.success).toBe(false);
expect(result.error).toContain("finite");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("verifies the visible timeline geometry as well as source metadata", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_out_seconds: 8 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var afterResult = __findClip(\"abc\")");
expect(script).toContain("var expectedStart = before.start");
expect(script).toContain("var expectedEnd = before.end + (actualOut - before.outPoint)");
expect(script).toContain("visible timeline duration does not match the applied source range");
expect(script).toContain("source metadata may have changed, but this is not reported as success");
});
it("fails closed for retimed clips and unhandled out-of-range keyframes", async () => {
await timeline.trim_clip.handler({ node_id: "abc", new_out_seconds: 8 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("does not support retimed or otherwise non-1x clips");
expect(script).toContain("__findOutOfRangeKeyframes");
expect(script).toContain("keyframe_policy: preserve");
expect(script).toContain("keyframesOutsideVisibleRange");
});
it("allows explicit keyframe preservation while reporting that it was not fully verified", async () => {
await timeline.trim_clip.handler({
node_id: "abc",
new_out_seconds: 8,
keyframe_policy: "preserve",
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('keyframePolicy: "preserve"');
expect(script).toContain("keyframesVerified:");
});
});
describe("split_clip verification", () => {
it("refuses invalid local arguments before contacting Premiere", async () => {
const invalidTime = await timeline.split_clip.handler({ time_seconds: -1 });
expect(invalidTime.success).toBe(false);
expect(mockedSendCommand).not.toHaveBeenCalled();
const invalidTrack = await timeline.split_clip.handler({
time_seconds: 4,
track_index: 1.5,
});
expect(invalidTrack.success).toBe(false);
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("requires a clip to span the requested cut before attempting QE razor", async () => {
await timeline.split_clip.handler({ time_seconds: 4, track_type: "audio" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("function __eligibleClips");
expect(script).toContain("if (!eligibleBefore.length)");
expect(script).toContain("no razor was attempted");
});
it("verifies each left and right segment, not only a count increase", async () => {
await timeline.split_clip.handler({ time_seconds: 4 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var expectedClipCount = clipCountBefore + eligibleBefore.length");
expect(script).toContain("function __hasSegment");
expect(script).toContain("left segment");
expect(script).toContain("right segment");
expect(script).toContain('keyframeSemantics: "unverified"');
});
});
describe("move_clip verification", () => {
it("re-finds the clip after the move rather than trusting a stale reference", async () => {
await timeline.move_clip.handler({ node_id: "abc", new_start_seconds: 5 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('var after = __findClip("abc")');
expect(script).toContain("Math.abs(actualStart - 5) > tolerance");
expect(script).toContain("verified: true");
});
it("rejects an out-of-range track index instead of silently skipping the move", async () => {
await timeline.move_clip.handler({
node_id: "abc",
new_start_seconds: 5,
new_track_index: 99,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("is out of range");
expect(script).toContain("targetTracks.numTracks");
});
it("attempts the track change before writing the start time, so a failure leaves no partial edit", async () => {
await timeline.move_clip.handler({
node_id: "abc",
new_start_seconds: 5,
new_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
const trackMoveAt = script.indexOf("qeClip.moveToTrack");
const startWriteAt = script.indexOf("clip.start = __secondsToTicks");
expect(trackMoveAt).toBeGreaterThan(-1);
expect(startWriteAt).toBeGreaterThan(-1);
expect(trackMoveAt).toBeLessThan(startWriteAt);
});
it("matches the QE clip by start time because QE item indices include gaps", async () => {
await timeline.move_clip.handler({
node_id: "abc",
new_start_seconds: 5,
new_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('String(cand.type) !== "Clip"');
expect(script).not.toContain("getItemAt(result.clipIndex)");
});
it("verifies the clip actually landed on the requested track", async () => {
await timeline.move_clip.handler({
node_id: "abc",
new_start_seconds: 5,
new_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("after.trackIndex !== 1");
});
it("does not emit any track-move code when no track change was requested", async () => {
await timeline.move_clip.handler({ node_id: "abc", new_start_seconds: 5 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).not.toContain("moveToTrack");
expect(script).not.toContain("after.trackIndex !==");
});
});
describe("razor_all_tracks verification", () => {
it("counts tracks whose clip count actually changed, not razor attempts", async () => {
await trackTargeting.razor_all_tracks.handler({ time_seconds: 7 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("domTrack.clips.numItems > before");
expect(script).toContain("verified: true");
});
it("treats only tracks with a clip spanning the cut point as eligible", async () => {
await trackTargeting.razor_all_tracks.handler({ time_seconds: 7 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("function __spansPoint");
expect(script).toContain("if (v > s && v < e) return true");
expect(script).toContain("if (wasEligible) eligible++");
});
it("errors when any eligible track did not split", async () => {
await trackTargeting.razor_all_tracks.handler({ time_seconds: 7 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("eligible > 0 && razored < eligible");
expect(script).toContain("only partially applied");
expect(script).toContain("no-op on some Premiere Pro 26.x");
});
it("surfaces per-track razor exceptions rather than swallowing them", async () => {
await trackTargeting.razor_all_tracks.handler({ time_seconds: 7 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("failures.push");
expect(script).toContain("failures: failures");
});
});
describe("move_clip_to_track verification", () => {
it("matches the QE clip by start time instead of the DOM clip index", async () => {
// getItemAt(result.clipIndex) returns an "Empty" gap on any track with a
// leading gap, which is what made this fail with a misleading error.
await advanced.move_clip_to_track.handler({
node_id: "abc",
target_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).not.toContain("getItemAt(result.clipIndex)");
expect(script).toContain('String(cand.type) !== "Clip"');
expect(script).toContain("Math.abs(parseFloat(cand.start.ticks) - wantStart) < 1");
});
it("rejects an out-of-range target track", async () => {
await advanced.move_clip_to_track.handler({
node_id: "abc",
target_track_index: 99,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("is out of range");
expect(script).toContain("targetTracks.numTracks");
});
it("short-circuits when the clip is already on the requested track", async () => {
await advanced.move_clip_to_track.handler({
node_id: "abc",
target_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("alreadyOnTrack: true");
});
it("confirms against the DOM rather than trusting the QE call", async () => {
await advanced.move_clip_to_track.handler({
node_id: "abc",
target_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('var after = __findClip("abc")');
expect(script).toContain("after.trackIndex !== 1");
expect(script).toContain("verified: true");
});
it("explains the QE rejection instead of surfacing a raw parameter error", async () => {
await advanced.move_clip_to_track.handler({
node_id: "abc",
target_track_index: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("catch (moveErr)");
expect(script).toContain("The clip was left untouched");
expect(script).toContain("overwriteClip");
});
});
describe("track creation verification", () => {
it("rejects invalid add_track requests locally instead of generating unsafe host code", async () => {
const result = await tracks.add_track.handler({ track_type: "audio", count: 0 });
expect(result.success).toBe(false);
expect(result.error).toContain("positive integer");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("uses a bounded QE fallback only after a public add_track failure with no mutation", async () => {
await tracks.add_track.handler({ track_type: "audio", count: 2 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('typeof seq.insertAudioTrackAt !== "function"');
expect(script).toContain("afterPublic !== expected && afterPublic !== before");
expect(script).toContain("qeSeq.addTracks(0, 2, 0, 0)");
expect(script).toContain("if (after !== expected)");
expect(script).toContain("verified: true");
});
it("does not report QE add_tracks success until exact video and audio count readback", async () => {
await advanced.add_tracks.handler({
video_tracks: 1,
audio_tracks: 2,
audio_mono_tracks: 1,
audio_51_tracks: 1,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var beforeVideo = seq.videoTracks.numTracks");
expect(script).toContain("var expectedAudio = beforeAudio + 4");
expect(script).toContain("typeof qeSeq.addTracks !== \"function\"");
expect(script).toContain("afterVideo !== expectedVideo || afterAudio !== expectedAudio");
expect(script).toContain("verified: true");
});
it("rejects an empty add_tracks request locally rather than returning a successful no-op", async () => {
const result = await advanced.add_tracks.handler({});
expect(result.success).toBe(false);
expect(result.error).toContain("at least one");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("overwrite_clip verification", () => {
it("rejects invalid indices locally before they can be interpolated into ExtendScript", async () => {
const result = await advanced.overwrite_clip.handler({
item_id: "source-1",
audio_track_index: -1,
});
expect(result.success).toBe(false);
expect(result.error).toContain("audio_track_index");
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("checks both target tracks and host capability before calling overwriteClip", async () => {
await advanced.overwrite_clip.handler({
item_id: "source-1",
track_index: 1,
audio_track_index: 2,
start_seconds: 5,
});
const script = mockedSendCommand.mock.calls[0][0];
const videoCheck = script.indexOf("Video track index 1 is out of range");
const audioCheck = script.indexOf("Audio track index 2 is out of range");
const call = script.indexOf("seq.overwriteClip(item");
expect(videoCheck).toBeGreaterThan(-1);
expect(audioCheck).toBeGreaterThan(-1);
expect(call).toBeGreaterThan(audioCheck);
expect(script).toContain('typeof seq.overwriteClip !== "function"');
});
it("proves the requested source landed on a target track and errors on no new placement", async () => {
await advanced.overwrite_clip.handler({ item_id: "source-1", start_seconds: 5 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("clip.projectItem ? String(clip.projectItem.nodeId)");
expect(script).toContain("Math.abs(actualStartTicks - wantedStartTicks) <= frameTicks");
expect(script).toContain("if (!videoPlaced && !audioPlaced)");
expect(script).toContain("produced no verifiable new placement");
expect(script).toContain("verified: true");
});
});
+979
View File
@@ -0,0 +1,979 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getHelpersSource } from "../../src/bridge/script-builder.js";
import { BridgeOptions } from "../../src/bridge/file-bridge.js";
// Mock sendCommand and sendRawCommand so tool handlers don't do real I/O
vi.mock("../../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
sendRawCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
getTempDir: vi.fn().mockReturnValue("/tmp/test"),
cleanupTempDir: vi.fn(),
}));
import { sendCommand, sendRawCommand } from "../../src/bridge/file-bridge.js";
const mockedSendCommand = vi.mocked(sendCommand);
const mockedSendRawCommand = vi.mocked(sendRawCommand);
const bridgeOptions: BridgeOptions = { tempDir: "/tmp/test-bridge", timeoutMs: 5000 };
// Import all tool modules
import { getDiscoveryTools } from "../../src/tools/discovery.js";
import { getProjectTools } from "../../src/tools/project.js";
import { getMediaTools } from "../../src/tools/media.js";
import { getSequenceTools } from "../../src/tools/sequence.js";
import { getTimelineTools } from "../../src/tools/timeline.js";
import { getEffectsTools } from "../../src/tools/effects.js";
import { getTransitionsTools } from "../../src/tools/transitions.js";
import { getAudioTools } from "../../src/tools/audio.js";
import { getTextTools } from "../../src/tools/text.js";
import { getMarkerTools } from "../../src/tools/markers.js";
import { getTrackTools } from "../../src/tools/tracks.js";
import { getPlayheadTools } from "../../src/tools/playhead.js";
import { getMetadataTools } from "../../src/tools/metadata.js";
import { getExportTools } from "../../src/tools/export.js";
import { getMediaAnalysisTools } from "../../src/tools/media-analysis.js";
import { getInterchangeAnalysisTools } from "../../src/tools/interchange-analysis.js";
import { getAdvancedTools } from "../../src/tools/advanced.js";
import { getKeyframeTools } from "../../src/tools/keyframes.js";
import { getScriptingTools } from "../../src/tools/scripting.js";
import { getInspectionTools } from "../../src/tools/inspection.js";
import { getSelectionTools } from "../../src/tools/selection.js";
import { getClipboardTools } from "../../src/tools/clipboard.js";
import { getSourceMonitorTools } from "../../src/tools/source-monitor.js";
import { getTrackTargetingTools } from "../../src/tools/track-targeting.js";
import { getUtilityTools } from "../../src/tools/utility.js";
import { getHealthTools } from "../../src/tools/health.js";
import { getWorkspaceTools } from "../../src/tools/workspace.js";
import { getCaptionTools } from "../../src/tools/captions.js";
import { getPlaybackTools } from "../../src/tools/playback.js";
import { getProjectManagerTools } from "../../src/tools/project-manager.js";
import { getRecoveryTools } from "../../src/tools/recovery.js";
import { getAvSettingsTools } from "../../src/tools/av-settings.js";
import { getProjectContextTools } from "../../src/tools/project-context.js";
import { getEditorialContextPackTools } from "../../src/tools/editorial-context-pack.js";
import { getEditorialPlanTools } from "../../src/tools/editorial-plans.js";
import { getProjectIntakeTools } from "../../src/tools/project-intake.js";
import { getCompetitorGapTools } from "../../src/tools/competitor-gaps.js";
import { getSpotWorkflowTools } from "../../src/tools/spot-workflows.js";
import { getMogrtAuthoringTools } from "../../src/tools/mogrt-authoring.js";
import { getMogrtStudioTools } from "../../src/tools/mogrt-studio.js";
import type { Telemetry, TelemetryProperties } from "../../src/telemetry.js";
interface ToolDef {
description: string;
parameters: Record<string, unknown>;
handler: (args: any) => Promise<{ success: boolean; data?: unknown; error?: string }>;
}
type ToolModule = Record<string, ToolDef>;
// All modules with their expected tool counts and names
const ALL_MODULES: Array<{
name: string;
getter: (opts: BridgeOptions) => ToolModule;
minTools: number;
}> = [
{ name: "discovery", getter: getDiscoveryTools, minTools: 8 },
{ name: "project", getter: getProjectTools, minTools: 20 },
{ name: "media", getter: getMediaTools, minTools: 12 },
{ name: "sequence", getter: getSequenceTools, minTools: 8 },
{ name: "timeline", getter: getTimelineTools, minTools: 8 },
{ name: "effects", getter: getEffectsTools, minTools: 6 },
{ name: "transitions", getter: getTransitionsTools, minTools: 3 },
{ name: "audio", getter: getAudioTools, minTools: 2 },
{ name: "text", getter: getTextTools, minTools: 2 },
{ name: "markers", getter: getMarkerTools, minTools: 3 },
{ name: "tracks", getter: getTrackTools, minTools: 3 },
{ name: "playhead", getter: getPlayheadTools, minTools: 4 },
{ name: "metadata", getter: getMetadataTools, minTools: 6 },
{ name: "export", getter: getExportTools, minTools: 10 },
{ name: "media-analysis", getter: getMediaAnalysisTools, minTools: 5 },
{ name: "interchange-analysis", getter: getInterchangeAnalysisTools, minTools: 5 },
{ name: "advanced", getter: getAdvancedTools, minTools: 20 },
{ name: "keyframes", getter: getKeyframeTools, minTools: 5 },
{ name: "scripting", getter: getScriptingTools, minTools: 3 },
{ name: "inspection", getter: getInspectionTools, minTools: 5 },
{ name: "selection", getter: getSelectionTools, minTools: 5 },
{ name: "clipboard", getter: getClipboardTools, minTools: 4 },
{ name: "source-monitor", getter: getSourceMonitorTools, minTools: 5 },
{ name: "track-targeting", getter: getTrackTargetingTools, minTools: 20 },
{ name: "utility", getter: getUtilityTools, minTools: 15 },
{ name: "health", getter: getHealthTools, minTools: 3 },
{ name: "workspace", getter: getWorkspaceTools, minTools: 2 },
{ name: "captions", getter: getCaptionTools, minTools: 1 },
{ name: "playback", getter: getPlaybackTools, minTools: 3 },
{ name: "project-manager", getter: getProjectManagerTools, minTools: 1 },
{ name: "recovery", getter: getRecoveryTools, minTools: 2 },
{ name: "av-settings", getter: getAvSettingsTools, minTools: 4 },
{ name: "project-context", getter: getProjectContextTools, minTools: 3 },
{ name: "editorial-context-pack", getter: getEditorialContextPackTools, minTools: 1 },
{ name: "editorial-plans", getter: () => getEditorialPlanTools(), minTools: 2 },
{ name: "project-intake", getter: getProjectIntakeTools, minTools: 1 },
{ name: "competitor-gaps", getter: getCompetitorGapTools, minTools: 8 },
{ name: "spot-workflows", getter: getSpotWorkflowTools, minTools: 4 },
{ name: "mogrt-authoring", getter: getMogrtAuthoringTools, minTools: 4 },
{ name: "mogrt-studio", getter: getMogrtStudioTools, minTools: 11 },
];
describe("Tool Module Structure", () => {
for (const mod of ALL_MODULES) {
describe(`${mod.name} module`, () => {
let tools: ToolModule;
beforeEach(() => {
tools = mod.getter(bridgeOptions);
});
it(`exports at least ${mod.minTools} tools`, () => {
expect(Object.keys(tools).length).toBeGreaterThanOrEqual(mod.minTools);
});
it("each tool has a description string", () => {
for (const [name, tool] of Object.entries(tools)) {
expect(typeof tool.description, `${name} description`).toBe("string");
expect(tool.description.length, `${name} description length`).toBeGreaterThan(0);
}
});
it("each tool has a parameters object", () => {
for (const [name, tool] of Object.entries(tools)) {
expect(typeof tool.parameters, `${name} parameters`).toBe("object");
}
});
it("each tool has an async handler function", () => {
for (const [name, tool] of Object.entries(tools)) {
expect(typeof tool.handler, `${name} handler`).toBe("function");
}
});
it("parameter properties all have type fields", () => {
for (const [name, tool] of Object.entries(tools)) {
const props = (tool.parameters as any).properties;
if (props) {
for (const [propName, prop] of Object.entries(props) as [string, any][]) {
expect(prop.type, `${name}.${propName} type`).toBeDefined();
const types = Array.isArray(prop.type) ? prop.type : [prop.type];
expect(
types.length > 0 && types.every((type) =>
["string", "number", "integer", "boolean", "array", "object"].includes(type)
),
`${name}.${propName} has valid type "${types.join(",")}"`
).toBe(true);
}
}
}
});
it("parameter properties all have description fields", () => {
for (const [name, tool] of Object.entries(tools)) {
const props = (tool.parameters as any).properties;
if (props) {
for (const [propName, prop] of Object.entries(props) as [string, any][]) {
expect(
typeof prop.description,
`${name}.${propName} should have description`
).toBe("string");
}
}
}
});
it("required fields reference existing properties", () => {
for (const [name, tool] of Object.entries(tools)) {
const params = tool.parameters as any;
const required = params.required || [];
const propNames = Object.keys(params.properties || {});
for (const req of required) {
expect(
propNames.includes(req),
`${name}: required field "${req}" should exist in properties`
).toBe(true);
}
}
});
});
}
});
describe("Total Tool Count", () => {
it("all modules together have 342 tools", () => {
let total = 0;
for (const mod of ALL_MODULES) {
total += Object.keys(mod.getter(bridgeOptions)).length;
}
expect(total).toBe(342);
});
it("there are 40 directly enumerated modules", () => {
expect(ALL_MODULES.length).toBe(40);
});
});
describe("Tool Handler Behavior", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedSendCommand.mockResolvedValue({ success: true, data: { mock: true } });
});
describe("health.ping", () => {
it("calls sendCommand with shortened timeout", async () => {
const tools = getHealthTools(bridgeOptions);
await (tools.ping.handler as any)({});
expect(mockedSendCommand).toHaveBeenCalledTimes(1);
const callArgs = mockedSendCommand.mock.calls[0];
// Script should contain app.version
expect(callArgs[0]).toContain("app.version");
// Should use a 5-second timeout override
expect(callArgs[1]?.timeoutMs).toBe(5000);
expect(callArgs[1]?.failFastOnUnreadyHeartbeat).toBe(true);
});
it("generates script that checks connectivity", async () => {
const tools = getHealthTools(bridgeOptions);
await (tools.ping.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("app.project");
expect(script).toContain("__result");
expect(script).toContain("connected: true");
});
});
describe("health.verify_premiere_connection", () => {
it("uses a read-only boolean check and emits activation only after readiness is confirmed", async () => {
const events: Array<{ event: string; properties?: TelemetryProperties }> = [];
const telemetry: Telemetry = {
enabled: true,
capture: (event, properties) => events.push({ event, properties }),
shutdown: async () => {},
};
mockedSendCommand.mockResolvedValue({
success: true,
data: { projectOpen: true, sequenceOpen: true },
});
const tools = getHealthTools(bridgeOptions, undefined, undefined, { telemetry });
const result = await (tools.verify_premiere_connection.handler as any)({});
const script = String(mockedSendCommand.mock.calls[0][0]);
expect(result).toMatchObject({
success: true,
data: { overall: "ready", safeCheck: { readOnly: true, mutatesProject: false } },
});
expect(script).toContain("projectOpen");
expect(script).toContain("sequenceOpen");
expect(mockedSendCommand.mock.calls[0][1]?.failFastOnUnreadyHeartbeat).toBe(true);
expect(script).not.toContain("projectName");
expect(script).not.toContain("project.path");
expect(events).toEqual([
expect.objectContaining({
event: "premiere_mcp_activation_completed",
properties: { activation_stage: "verified_connection", backend: "cep" },
}),
]);
expect(JSON.stringify(events)).not.toMatch(/prompt|path|token|project|media|argument|result|profile/i);
});
it("does not record activation when the read-only check is incomplete", async () => {
const events: Array<{ event: string; properties?: TelemetryProperties }> = [];
const telemetry: Telemetry = {
enabled: true,
capture: (event, properties) => events.push({ event, properties }),
shutdown: async () => {},
};
mockedSendCommand.mockResolvedValue({
success: true,
data: { projectOpen: true, sequenceOpen: false },
});
const tools = getHealthTools(bridgeOptions, undefined, undefined, { telemetry });
const result = await (tools.verify_premiere_connection.handler as any)({});
expect(result).toMatchObject({ success: true, data: { overall: "needs_attention" } });
expect(events).toEqual([]);
});
it("does not fall back from an unavailable UXP check to CEP", async () => {
const tools = getHealthTools(bridgeOptions);
const result = await (tools.verify_premiere_connection.handler as any)({ backend: "uxp" });
expect(result).toMatchObject({ success: true, data: { backend: "uxp", overall: "needs_attention" } });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
});
describe("project.create_project", () => {
it("rejects a directory path without invoking Premiere", async () => {
const tools = getProjectTools(bridgeOptions);
const result = await (tools.create_project.handler as any)({
path: "/tmp/film-test",
});
expect(result).toEqual({
success: false,
error:
"create_project path must be a full .prproj file path; Premiere cannot create a project from a directory path.",
});
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("verifies the active project path after creating a project", async () => {
const tools = getProjectTools(bridgeOptions);
await (tools.create_project.handler as any)({
path: "/tmp/Film Test.prproj",
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('var requestedPath = "/tmp/Film Test.prproj"');
expect(script).toContain("var beforePath = app.project");
expect(script).toContain("var actualPath = project ? String(project.path || \"\") : \"\"");
expect(script).toContain("__normalizedProjectPath(actualPath) !== __normalizedProjectPath(requestedPath)");
});
});
describe("reported tool regressions", () => {
beforeEach(() => mockedSendCommand.mockClear());
it("does not report duplicate consolidation as successful without a reduced duplicate scan", async () => {
const tools = getProjectTools(bridgeOptions);
await tools.consolidate_duplicates.handler({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("function __duplicateMediaStats()");
expect(script).toContain("var before = __duplicateMediaStats()");
expect(script).toContain("var after = __duplicateMediaStats()");
expect(script).toContain("duplicate media groups did not decrease");
expect(script).toContain("verified: true");
});
it("fails reverse_clip locally with an actionable unsupported-capability error", async () => {
const tools = getAdvancedTools(bridgeOptions);
const result = await tools.reverse_clip.handler({ node_id: "clip-1" });
expect(result).toEqual({
success: false,
error: expect.stringContaining("not exposed by Premiere's supported ExtendScript or UXP APIs"),
});
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("merges XMP patches through AdobeXMPScript and reparses host readback", async () => {
const tools = getMetadataTools(bridgeOptions);
await tools.set_xmp_metadata.handler({
item_id: "source-1",
xmp_xml: '<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"/></x:xmpmeta>',
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('new ExternalObject("lib:AdobeXMPScript")');
expect(script).toContain("var existingXmp = new XMPMeta(existingPacket)");
expect(script).toContain("XMPUtils.appendProperties(patchXmp, existingXmp, true, true, false)");
expect(script).toContain("var writtenPacket = String(item.getXMPMetadata() || \"\")");
expect(script).toContain('verification: "readback_xmp_packet_reparsed"');
});
});
describe("health.get_capabilities", () => {
it("reports platform support without requiring Premiere to be running", async () => {
const tools = getHealthTools(bridgeOptions);
const result = await tools.get_capabilities.handler();
expect(result.success).toBe(true);
expect(result.data.backends.cep.platforms).toEqual(["macOS", "Windows"]);
expect(result.data.premiere.hostVerificationRequired).toBe(true);
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("includes per-tool operational metadata from the supplied catalog", async () => {
const tools = getHealthTools(
bridgeOptions,
undefined,
() => ({
get_capabilities: { description: "Report capabilities." },
ripple_delete: { description: "Ripple delete. Uses QE DOM." },
}),
);
const result = await tools.get_capabilities.handler();
expect(result.data.tools.total).toBe(2);
expect(result.data.tools.tools).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "ripple_delete",
status: "experimental",
backend: "CEP/ExtendScript + QE",
}),
]),
);
});
});
describe("bounded read tools", () => {
it("returns a filtered, paged get_capabilities catalog without changing the capability summary", async () => {
const tools = getHealthTools(
bridgeOptions,
undefined,
() => ({
ping: { description: "Ping." },
get_capabilities: { description: "Capabilities." },
ripple_delete: { description: "Ripple delete. Uses QE DOM." },
}),
);
const result = await tools.get_capabilities.handler({ tool_names: ["ripple_delete", "missing"], tool_limit: 1 });
expect(result.success).toBe(true);
expect(result.data.tools.tools).toEqual([expect.objectContaining({ name: "ripple_delete" })]);
expect(result.data.tools.pagination).toEqual({
offset: 0,
limit: 1,
returned: 1,
totalMatching: 1,
hasMore: false,
nextOffset: null,
});
expect(result.data.backends.cep.status).toBe("production");
});
it("omits requested metadata XML payloads before invoking the CEP bridge", async () => {
const tools = getMetadataTools(bridgeOptions);
await tools.get_metadata.handler({
item_id: "source-1",
include_project_metadata: false,
include_xmp_metadata: false,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).not.toContain("item.getProjectMetadata()");
expect(script).not.toContain("item.getXMPMetadata()");
expect(script).toContain("metadata.nodeId = item.nodeId");
});
it("generates bounded overview and bin read scripts", async () => {
const tools = getInspectionTools(bridgeOptions);
await tools.get_full_project_overview.handler({
include_bin_tree: false,
sequence_offset: 5,
sequence_limit: 10,
});
const overview = mockedSendCommand.mock.calls[0][0];
expect(overview).toContain("var binTree = null");
expect(overview).toContain("var sequenceOffset = 5");
expect(overview).toContain("var sequenceLimit = 10");
expect(overview).toContain("sequencePagination");
vi.clearAllMocks();
await tools.get_bin_contents.handler({
bin_id: "Footage",
recursive: false,
offset: 10,
limit: 25,
max_depth: 0,
});
const bin = mockedSendCommand.mock.calls[0][0];
expect(bin).toContain("var allContents = walkItems(target, false, 0)");
expect(bin).toContain("allContents.slice(offset, offset + limit)");
expect(bin).toContain("totalDirectItems");
});
});
describe("workspace tools", () => {
it("get_workspaces generates correct script", async () => {
const tools = getWorkspaceTools(bridgeOptions);
await (tools.get_workspaces.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("app.getWorkspaces()");
expect(script).toContain("__result");
});
it("set_workspace escapes the workspace name", async () => {
const tools = getWorkspaceTools(bridgeOptions);
await (tools.set_workspace.handler as any)({ name: 'My "Custom" Workspace' });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('My \\"Custom\\" Workspace');
expect(script).toContain("app.setWorkspace");
});
});
describe("playback tools", () => {
it("play_timeline uses QE DOM", async () => {
const tools = getPlaybackTools(bridgeOptions);
await (tools.play_timeline.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("app.enableQE()");
expect(script).toContain("qe.startPlayback()");
expect(script).toContain("playbackRequested: true");
expect(script).toContain("playbackVerified: false");
expect(script).toContain("poll get_playhead_position");
});
it("play_timeline has no speed parameter (QE startPlayback ignores it)", async () => {
const tools = getPlaybackTools(bridgeOptions);
expect(tools.play_timeline.parameters).toEqual({});
});
it("writes and verifies the public TrackItem disabled property", async () => {
const tools = getTimelineTools(bridgeOptions);
await (tools.enable_disable_clip.handler as any)({ node_id: "clip-1", enabled: false });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("result.clip.disabled = wantDisabled");
expect(script).toContain('var verified = __findClip("clip-1")');
expect(script).toContain("!!verified.clip.disabled !== wantDisabled");
expect(script).not.toContain("setDisabled(");
});
it("does not claim legacy source-monitor or stop-playback requests are verified", async () => {
const tools = getPlaybackTools(bridgeOptions);
await (tools.stop_playback.handler as any)({});
let script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("stopRequested: true");
expect(script).toContain("playbackVerified: false");
vi.clearAllMocks();
await (tools.play_source_monitor.handler as any)({ speed: 1 });
script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("playbackRequested: true");
expect(script).toContain("poll get_source_monitor_position");
});
it("stop_playback uses QE DOM", async () => {
const tools = getPlaybackTools(bridgeOptions);
await (tools.stop_playback.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("qe.stopPlayback()");
});
it("get_source_monitor_position reads ticks", async () => {
const tools = getPlaybackTools(bridgeOptions);
await (tools.get_source_monitor_position.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("app.sourceMonitor.getPosition()");
expect(script).toContain("__ticksToSeconds");
});
});
describe("verified Premiere mutations", () => {
it("uses settings fields and readback for set_sequence_settings", async () => {
const tools = getSequenceTools(bridgeOptions);
await (tools.set_sequence_settings.handler as any)({ sequence_id: "seq-1", width: 1920, height: 1080 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("settings.videoFrameWidth = 1920");
expect(script).toContain("settings.videoFrameHeight = 1080");
expect(script).toContain("var applied = seq.getSettings()");
expect(script).toContain("Premiere did not apply the requested frame width");
expect(script).toContain("verified: true");
vi.clearAllMocks();
await (tools.set_sequence_settings.handler as any)({ width: 1920 });
expect(mockedSendCommand.mock.calls[0][0]).toContain("settings.videoFrameWidth = 1920");
expect(mockedSendCommand.mock.calls[0][0]).not.toContain("settings.videoFrameHeight =");
vi.clearAllMocks();
await (tools.set_sequence_settings.handler as any)({ height: 1080 });
expect(mockedSendCommand.mock.calls[0][0]).toContain("settings.videoFrameHeight = 1080");
expect(mockedSendCommand.mock.calls[0][0]).not.toContain("settings.videoFrameWidth =");
vi.clearAllMocks();
await expect((tools.set_sequence_settings.handler as any)({ width: 0 })).resolves.toMatchObject({ success: false });
expect(mockedSendCommand).not.toHaveBeenCalled();
await expect((tools.set_sequence_settings.handler as any)({})).resolves.toMatchObject({ success: false });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("fails unsupported legacy AAF and multiple-undo requests before mutation", async () => {
const aaf = await (getExportTools(bridgeOptions).export_aaf.handler as any)({ output_path: "/tmp/turnover.aaf" });
const undoTools = getTrackTargetingTools(bridgeOptions);
const undo = await (undoTools.multiple_undo.handler as any)({ count: 2 });
const invalidUndo = await (undoTools.multiple_undo.handler as any)({ count: 0 });
expect(aaf).toMatchObject({ success: false, error: expect.stringContaining("No export was attempted") });
expect(undo).toMatchObject({ success: false, error: expect.stringContaining("No mutation was attempted") });
expect(invalidUndo).toMatchObject({ success: false, error: expect.stringContaining("integer from 1 through 100") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("finds pan on Channel Volume layouts and verifies the readback", async () => {
const tools = getTrackTargetingTools(bridgeOptions);
await (tools.set_clip_pan.handler as any)({ node_id: "audio-clip-1", pan: -50 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('result.trackType !== "audio"');
expect(script).toContain("component.properties");
expect(script).toContain('property.displayName === "Balance"');
expect(script).toContain("panProperty.getValue()");
expect(script).toContain("Premiere did not apply the requested pan");
vi.clearAllMocks();
await expect((tools.set_clip_pan.handler as any)({ node_id: "audio-clip-1", pan: 101 })).resolves.toMatchObject({ success: false });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("supports the documented ## zero-padded rename placeholder", async () => {
const tools = getTrackTargetingTools(bridgeOptions);
await (tools.batch_rename_clips.handler as any)({ pattern: "QA_Batch_##", track_type: "video", track_index: 0 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('.split("##").join(paddedSequenceNumber)');
expect(script).toContain('while (paddedSequenceNumber.length < 2)');
expect(script).toContain('"0" + paddedSequenceNumber');
});
it("converts audio dB from Premiere's +15 dB normalized level and verifies the readback", async () => {
const tools = getAudioTools(bridgeOptions);
await (tools.adjust_audio_levels.handler as any)({ node_id: "clip-1", level_db: -6 });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("requestedLevel");
expect(script).toContain("levelProp.getValue()");
expect(script).toContain("Premiere did not apply the requested audio level");
expect(script).toContain("0.089125");
expect(script).toContain("appliedDb");
vi.clearAllMocks();
await expect((tools.adjust_audio_levels.handler as any)({ node_id: "clip-1", level_db: 16 }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("+15 dB") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("preflights and verifies every batch effect target instead of swallowing failures", async () => {
const tools = getClipboardTools(bridgeOptions);
await (tools.batch_apply_effect.handler as any)({ effect_name: "Gaussian Blur", target: "selected" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("function findQeClip(target)");
expect(script).toContain("function countEffectComponents(clip)");
expect(script).toContain("component count did not increase");
expect(script).toContain("Batch effect application was only partially verified");
expect(script).toContain("selectedIncompatible");
vi.clearAllMocks();
await expect((tools.batch_apply_effect.handler as any)({ effect_name: "Gaussian Blur", target: "track" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("track_type") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
it("does not report an empty QE audio-transition catalog as an available list", async () => {
const tools = getTransitionsTools(bridgeOptions);
await (tools.list_available_audio_transitions.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("QE reported an empty audio-transition catalog");
expect(script).toContain("no transition availability is claimed");
expect(script).toContain('source: "qe.catalog"');
});
it("uses Time objects and verifies audio keyframe values", async () => {
const tools = getAudioTools(bridgeOptions);
await (tools.add_audio_keyframes.handler as any)({
node_id: "clip-1",
keyframes: [{ time_seconds: 1.5, level_db: -12 }],
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("new Time()");
expect(script).toContain("getValueAtTime(t)");
expect(script).toContain("verificationErrors");
});
it("verifies ripple delete and passes QE razor a sequence timecode", async () => {
await (getAdvancedTools(bridgeOptions).ripple_delete.handler as any)({ node_id: "clip-1" });
let script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("if (__findClip(deletedNodeId))");
vi.clearAllMocks();
await (getTimelineTools(bridgeOptions).split_clip.handler as any)({ time_seconds: 2 });
script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var expectedClipCount = clipCountBefore + eligibleBefore.length");
expect(script).toContain("function __hasSegment");
expect(script).toContain("var __razorTc");
expect(script).toContain("track.razor(__razorTc)");
expect(script).not.toContain("track.razor(cutTicks.toString())");
vi.clearAllMocks();
await (getTrackTargetingTools(bridgeOptions).razor_all_tracks.handler as any)({ time_seconds: 2 });
script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var __razorTc");
expect(script).toContain("getVideoTrackAt(t).razor(__razorTc)");
expect(script).toContain("getAudioTrackAt(t).razor(__razorTc)");
});
it("targets the QE clip, preserves an unsupported speed boundary, and verifies track state", async () => {
const tools = getTransitionsTools(bridgeOptions);
await (tools.add_transition.handler as any)({
transition_name: "Cross Dissolve",
track_index: 0,
cut_point_seconds: 2,
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var targetClip = incomingClip || outgoingClip");
expect(script).toContain("__findQeClipByDomClip(qeTrack, targetClip)");
expect(script).toContain('typeof qeClip.addTransition !== "function"');
expect(script).toContain('qeClip.addTransition(transitionQE, targetHead, String(durationFrames), "0", 0.5, false, true)');
expect(script).not.toContain("qeTrack.addTransition(");
expect(script).toContain("transitionAtCut");
expect(script).toContain("Math.abs(((transitionStart + transitionEnd) / 2) - cutTicks) <= frameTicks / 2");
expect(script).toContain("DOM readback did not find it at the requested cut point");
vi.clearAllMocks();
await (tools.add_transition_to_clip.handler as any)({
node_id: "clip-1", transition_name: "Cross Dissolve", position: "both",
});
const clipScript = mockedSendCommand.mock.calls[0][0];
expect(clipScript).toContain('result.trackType !== "video"');
expect(clipScript).toContain("__findQeClipByDomClip(qeTrack, result.clip)");
expect(clipScript).toContain("var requestedCount = position === \"both\" ? 2 : 1");
expect(clipScript).toContain('qeClip.addTransition(transitionQE, true, String(durationFrames), "0", 0.5, false, true)');
expect(clipScript).toContain('qeClip.addTransition(transitionQE, false, String(durationFrames), "0", 0.5, false, true)');
expect(clipScript).toContain("startVerified");
expect(clipScript).toContain("endVerified");
expect(clipScript).toContain("var verifiedMidpoint = (verifiedStart + verifiedEnd) / 2");
expect(clipScript).toContain("the request was partially applied");
vi.clearAllMocks();
await (tools.batch_add_transitions.handler as any)({ transition_name: "Cross Dissolve" });
const batchScript = mockedSendCommand.mock.calls[0][0];
expect(batchScript).toContain("__findQeClipByDomClip(qeTrack, incomingClip)");
expect(batchScript).toContain('qeClip.addTransition(transitionQE, true, String(durationFrames), "0", 0.5, false, true)');
expect(batchScript).toContain("verifiedCount !== requestedCount");
expect(batchScript).toContain("Math.abs(((readStart + readEnd) / 2) - expectedCut) <= frameTicks / 2");
expect(batchScript).toContain("DOM readback did not find a transition at cut");
});
});
describe("discovery tools", () => {
it("get_project_info generates correct script", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_project_info.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("app.project");
expect(script).toContain("project.name");
expect(script).toContain("project.path");
expect(script).toContain("numSequences");
expect(script).toContain("__result");
});
it("list_project_items handles optional bin_path", async () => {
const tools = getDiscoveryTools(bridgeOptions);
// Without bin_path
await (tools.list_project_items.handler as any)({});
let script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("var binPath = null");
vi.clearAllMocks();
// With bin_path
await (tools.list_project_items.handler as any)({ bin_path: "Footage/Raw" });
script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("Footage/Raw");
});
it("list_project_items escapes bin_path", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.list_project_items.handler as any)({ bin_path: 'My "Folder"' });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('My \\"Folder\\"');
});
it("get_clip_properties escapes node_id", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_clip_properties.handler as any)({ node_id: "abc-123" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("abc-123");
expect(script).toContain("__findClip");
});
it("get_clip_at_position generates correct track lookup", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_clip_at_position.handler as any)({
time_seconds: 5.5,
track_index: 1,
track_type: "video",
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("seq.videoTracks");
expect(script).toContain("5.5");
});
it("get_clip_at_position uses audio tracks when specified", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_clip_at_position.handler as any)({
time_seconds: 2.0,
track_index: 0,
track_type: "audio",
});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("seq.audioTracks");
});
it("list_sequence_tracks defaults to active sequence", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.list_sequence_tracks.handler as any)({});
// Helpers are no longer inlined — the whole script is user code.
const userCode = mockedSendCommand.mock.calls[0][0];
expect(userCode).toContain("app.project.activeSequence");
// Should NOT call __findSequence in user code when no sequence_id is provided
expect(userCode).not.toContain('__findSequence("');
});
it("list_sequence_tracks uses __findSequence when id provided", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.list_sequence_tracks.handler as any)({ sequence_id: "seq-1" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("__findSequence");
expect(script).toContain("seq-1");
});
});
describe("caption tools", () => {
it("create_caption_track generates correct script", async () => {
const tools = getCaptionTools(bridgeOptions);
const toolNames = Object.keys(tools);
expect(toolNames).toContain("create_caption_track");
await (tools.create_caption_track.handler as any)({ item_id: "my-srt-file" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("__result");
expect(script).toContain("my-srt-file");
expect(script).toContain("createCaptionTrack");
expect(script).toContain("__captionTrackCount");
expect(script).toContain("no caption track appeared in host readback");
expect(script).toContain("accepted: true");
});
});
describe("project-manager tools", () => {
it("consolidate_and_transfer verifies a copied project in a new destination", async () => {
const tools = getProjectManagerTools(bridgeOptions);
expect(tools.consolidate_and_transfer).toBeDefined();
expect(typeof tools.consolidate_and_transfer.handler).toBe("function");
await (tools.consolidate_and_transfer.handler as any)({ destination_path: "/tmp/transfer" });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("destination_path must be a new, empty folder");
expect(script).toContain('destination.getFiles("*.prproj")');
expect(script).toContain("copiedProjectCount");
});
});
});
describe("Tool Handler Return Values", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("handlers return the result from sendCommand", async () => {
const expected = { success: true, data: { version: "24.0" } };
mockedSendCommand.mockResolvedValue(expected);
const tools = getHealthTools(bridgeOptions);
const result = await (tools.ping.handler as any)({});
expect(result).toEqual(expected);
});
it("handlers propagate sendCommand errors", async () => {
mockedSendCommand.mockRejectedValue(new Error("Connection failed"));
const tools = getHealthTools(bridgeOptions);
await expect((tools.ping.handler as any)({})).rejects.toThrow("Connection failed");
});
it("handlers propagate failure results", async () => {
const failResult = { success: false, error: "No active sequence" };
mockedSendCommand.mockResolvedValue(failResult);
const tools = getDiscoveryTools(bridgeOptions);
const result = await (tools.get_active_sequence.handler as any)({});
expect(result.success).toBe(false);
expect(result.error).toBe("No active sequence");
});
});
describe("Tool Naming Conventions", () => {
it("all tool names use snake_case", () => {
for (const mod of ALL_MODULES) {
const tools = mod.getter(bridgeOptions);
for (const name of Object.keys(tools)) {
expect(name, `${mod.name}.${name} should be snake_case`).toMatch(
/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
);
}
}
});
it("no duplicate tool names across modules", () => {
const allNames: string[] = [];
for (const mod of ALL_MODULES) {
const tools = mod.getter(bridgeOptions);
allNames.push(...Object.keys(tools));
}
const unique = new Set(allNames);
expect(unique.size).toBe(allNames.length);
});
});
describe("Script Generation Patterns", () => {
beforeEach(() => {
vi.clearAllMocks();
mockedSendCommand.mockResolvedValue({ success: true, data: {} });
});
it("all scripts contain __result or __error", async () => {
for (const mod of ALL_MODULES) {
const tools = mod.getter(bridgeOptions);
for (const [name, tool] of Object.entries(tools)) {
vi.clearAllMocks();
try {
// Call with minimal args — we only care about the generated script
await tool.handler({});
} catch {
// Some handlers may throw if required args are missing, that's OK
continue;
}
if (mockedSendCommand.mock.calls.length > 0) {
const script = mockedSendCommand.mock.calls[0][0];
const hasResult = script.includes("__result") || script.includes("__error") || script.includes("__aeResult") || script.includes("__aeError");
expect(hasResult, `${mod.name}.${name} script should use a host result helper`).toBe(true);
}
if (mockedSendRawCommand.mock.calls.length > 0) {
// Raw commands (scripting module) may not follow the pattern
continue;
}
}
}
});
it("scripts are wrapped in IIFE with try/catch", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_project_info.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain("(function() {");
expect(script).toContain("} catch(e) {");
expect(script).toContain("})();");
});
it("scripts do not inline helpers; helpers source defines them", async () => {
const tools = getDiscoveryTools(bridgeOptions);
await (tools.get_project_info.handler as any)({});
const script = mockedSendCommand.mock.calls[0][0];
// Inlining ~14KB of helpers per command overflowed the ExtendScript stack
// on long-lived engines; commands must stay lean.
expect(script).not.toContain("function __jsonStringify(obj)");
const helpers = getHelpersSource();
expect(helpers).toContain("function __result(data)");
expect(helpers).toContain("function __error(msg)");
expect(helpers).toContain("TICKS_PER_SECOND");
});
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import {
normalizeTranscriptDeletionRanges,
previewTranscriptEdit,
transcriptRevision,
} from "../../src/tools/transcript-edits.js";
describe("transcript edit range edge cases", () => {
it.each([
[null, "deletion 0 must be an object"],
["not a range", "deletion 0 must be an object"],
[{ start_seconds: -0.01, end_seconds: 1 }, "start_seconds must be a non-negative number"],
[{ start_seconds: Number.NaN, end_seconds: 1 }, "start_seconds must be a non-negative number"],
[{ start_seconds: 1, end_seconds: Number.POSITIVE_INFINITY }, "end_seconds must be greater than start_seconds"],
[{ start_seconds: 1, end_seconds: 1 }, "end_seconds must be greater than start_seconds"],
])("rejects invalid deletion input %j", (deletion, message) => {
expect(() => normalizeTranscriptDeletionRanges([deletion])).toThrow(message);
});
it("rounds normalized values and merges precisely adjacent ranges", () => {
expect(normalizeTranscriptDeletionRanges([
{ start_seconds: 2, end_seconds: 3.00000049 },
{ start_seconds: 1.00000031, end_seconds: 2 },
], 0.0000004)).toEqual([
{ start_seconds: 1, end_seconds: 3.000001 },
]);
});
it("uses end time as a stable secondary sort key before merging equal-start ranges", () => {
expect(normalizeTranscriptDeletionRanges([
{ start_seconds: 5.0000004, end_seconds: 8 },
{ start_seconds: 5.0000003, end_seconds: 6 },
])).toEqual([
{ start_seconds: 5, end_seconds: 8 },
]);
});
});
describe("transcript preview revision guard", () => {
it("rejects empty transcript payloads before creating a revision", () => {
expect(() => previewTranscriptEdit("", "", [{ start_seconds: 1, end_seconds: 2 }]))
.toThrow("Premiere returned an empty transcript");
expect(() => previewTranscriptEdit(null as unknown as string, "", [{ start_seconds: 1, end_seconds: 2 }]))
.toThrow("Premiere returned an empty transcript");
});
it("does not treat an empty requested revision as a valid transcript lock", () => {
const json = '{"segments":[{"text":"approve"}]}';
expect(transcriptRevision(json)).not.toBe("");
expect(() => previewTranscriptEdit(json, "", [{ start_seconds: 1, end_seconds: 2 }]))
.toThrow("Transcript revision does not match");
});
});
+222
View File
@@ -0,0 +1,222 @@
import { describe, expect, it, vi } from "vitest";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
import {
normalizeTranscriptDeletionRanges,
planTranscriptRoughCut,
previewTranscriptEdit,
transcriptRevision,
} from "../../src/tools/transcript-edits.js";
import { getUxpTools } from "../../src/tools/uxp.js";
describe("transcript edit planning", () => {
it("creates stable transcript revisions", () => {
expect(transcriptRevision('{"segments":[]}')).toMatch(/^sha256:[a-f0-9]{64}$/);
expect(transcriptRevision('{"segments":[]}')).toBe(transcriptRevision('{"segments":[]}'));
expect(transcriptRevision('{"segments":[1]}')).not.toBe(transcriptRevision('{"segments":[]}'));
});
it("sorts, adds handles, and merges overlapping deletion ranges", () => {
expect(normalizeTranscriptDeletionRanges([
{ start_seconds: 5, end_seconds: 7 },
{ start_seconds: 1, end_seconds: 3 },
{ start_seconds: 2.9, end_seconds: 5.1 },
], 0.1)).toEqual([{ start_seconds: 0.9, end_seconds: 7.1 }]);
});
it("rejects invalid or excessive deletion ranges", () => {
expect(() => normalizeTranscriptDeletionRanges([])).toThrow("at least one");
expect(() => normalizeTranscriptDeletionRanges([{ start_seconds: 2, end_seconds: 1 }])).toThrow("greater than");
expect(() => normalizeTranscriptDeletionRanges([{ start_seconds: 0, end_seconds: 1 }], 11)).toThrow("between 0 and 10");
expect(() => normalizeTranscriptDeletionRanges(Array.from({ length: 101 }, (_, i) => ({ start_seconds: i, end_seconds: i + 0.5 })))).toThrow("limited to 100");
});
it("revision-locks previews and returns a deterministic confirmation token", () => {
const json = '{"segments":[{"text":"remove this"}]}';
const revision = transcriptRevision(json);
const first = previewTranscriptEdit(json, revision, [
{ start_seconds: 4, end_seconds: 5 },
{ start_seconds: 1, end_seconds: 2.5 },
]);
const second = previewTranscriptEdit(json, revision, [
{ start_seconds: 1, end_seconds: 2.5 },
{ start_seconds: 4, end_seconds: 5 },
]);
expect(first).toMatchObject({ deletedSeconds: 2.5, applied: false });
expect(first.confirmationToken).toBe(second.confirmationToken);
expect(() => previewTranscriptEdit(json, transcriptRevision("different"), [{ start_seconds: 1, end_seconds: 2 }])).toThrow("does not match");
});
it("maps transcript deletions to descending verified 1x timeline cut instructions", () => {
const json = '{"segments":[]}';
const preview = previewTranscriptEdit(json, transcriptRevision(json), [
{ start_seconds: 2, end_seconds: 4 },
{ start_seconds: 8, end_seconds: 12 },
]);
const plan = planTranscriptRoughCut(preview, [{
placement_id: "clip-1",
track_type: "video",
track_index: 0,
source_in_seconds: 0,
source_out_seconds: 10,
timeline_start_seconds: 20,
timeline_end_seconds: 30,
}]);
expect(plan).toMatchObject({
applied: false,
requiresDuplicateSequence: true,
requiresPostMutationRequery: true,
ripple: true,
unmappedDeletionRanges: [],
});
expect(plan.steps.map((step) => step.timeline_range)).toEqual([
{ start_seconds: 28, end_seconds: 30 },
{ start_seconds: 22, end_seconds: 24 },
]);
});
it("rejects retimed placements instead of guessing source-to-timeline mapping", () => {
const json = '{"segments":[]}';
const preview = previewTranscriptEdit(json, transcriptRevision(json), [{ start_seconds: 1, end_seconds: 2 }]);
expect(() => planTranscriptRoughCut(preview, [{
placement_id: "retimed",
track_type: "video",
track_index: 0,
source_in_seconds: 0,
source_out_seconds: 10,
timeline_start_seconds: 0,
timeline_end_seconds: 5,
}])).toThrow("retimed");
});
it("fails closed for malformed placements and reports unmapped non-ripple ranges", () => {
const json = '{"segments":[]}';
const preview = previewTranscriptEdit(json, transcriptRevision(json), [{ start_seconds: 20, end_seconds: 21 }]);
const valid = {
placement_id: "clip-1", track_type: "audio", track_index: 0,
source_in_seconds: 0, source_out_seconds: 10,
timeline_start_seconds: 0, timeline_end_seconds: 10,
} as const;
expect(() => planTranscriptRoughCut(preview, [])).toThrow("at least one");
expect(() => planTranscriptRoughCut(preview, Array(257).fill(valid))).toThrow("limited to 256");
expect(() => planTranscriptRoughCut(preview, [null])).toThrow("must be an object");
expect(() => planTranscriptRoughCut(preview, [{ ...valid, placement_id: "" }])).toThrow("1-512");
expect(() => planTranscriptRoughCut(preview, [{ ...valid, track_type: "caption" }])).toThrow("video or audio");
expect(() => planTranscriptRoughCut(preview, [{ ...valid, track_index: -1 }])).toThrow("non-negative integer");
expect(() => planTranscriptRoughCut(preview, [{ ...valid, source_out_seconds: Number.NaN }])).toThrow("finite number");
expect(() => planTranscriptRoughCut(preview, [{ ...valid, source_out_seconds: 0 }])).toThrow("increasing");
const plan = planTranscriptRoughCut(preview, [valid], false);
expect(plan).toMatchObject({ ripple: false, steps: [], unmappedDeletionRanges: preview.deletionRanges });
});
});
describe("transcript UXP MCP tools", () => {
it("exports native transcript JSON with a stable revision", async () => {
const json = '{"segments":[]}';
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1", projectItemId: "clip-1", projectItemName: "Interview", json });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).get_clip_transcript_uxp.handler({ project_item_id: "clip-1" });
expect(request).toHaveBeenCalledWith("transcript.export", { projectItemId: "clip-1" });
expect(result).toMatchObject({
success: true,
data: { result: { projectGuid: "project-1", projectItemId: "clip-1", json, transcriptRevision: transcriptRevision(json) } },
});
});
it("maps a guarded transcript replacement to the documented UXP command", async () => {
const request = vi.fn().mockResolvedValue({ committed: true, verified: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).import_transcript_uxp.handler({
project_item_id: "clip-1",
project_guid: "project-1",
expected_transcript_revision: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
replacement_transcript_json: '{"segments":[]}',
confirm_destructive: true,
operation_id: "transcript-replace-1",
});
expect(request).toHaveBeenCalledWith("transcript.import", {
projectItemId: "clip-1",
expectedProjectGuid: "project-1",
expectedTranscriptRevision: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
json: '{"segments":[]}',
confirmDestructive: true,
operationId: "transcript-replace-1",
});
expect(result).toMatchObject({ success: true, data: { result: { committed: true, verified: true } } });
});
it("fails closed before the bridge when destructive confirmation or replay identity is missing", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).import_transcript_uxp;
await expect(tool.handler({
project_item_id: "clip-1", project_guid: "project-1", expected_transcript_revision: null,
replacement_transcript_json: '{"segments":[]}', confirm_destructive: false, operation_id: "transcript-replace-2",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("confirm_destructive") });
await expect(tool.handler({
project_item_id: "clip-1", project_guid: "project-1", expected_transcript_revision: null,
replacement_transcript_json: '{"segments":[]}', confirm_destructive: true, operation_id: "",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("operation_id") });
expect(request).not.toHaveBeenCalled();
});
it("maps bounded search arguments to the native transcript command", async () => {
const request = vi.fn().mockResolvedValue({ matches: [] });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
await getUxpTools(bridge).search_clip_transcript_uxp.handler({
project_item_name: "Interview",
query: "launch date",
case_sensitive: true,
max_results: 10,
});
expect(request).toHaveBeenCalledWith("transcript.search", {
projectItemName: "Interview",
query: "launch date",
caseSensitive: true,
maxResults: 10,
});
});
it("re-exports the transcript before producing a revision-locked preview", async () => {
const json = '{"segments":[{"text":"cut this"}]}';
const request = vi.fn().mockResolvedValue({ projectItemId: "clip-1", projectItemName: "Interview", json });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).preview_transcript_edit_uxp.handler({
project_item_id: "clip-1",
transcript_revision: transcriptRevision(json),
deletions: [{ start_seconds: 2, end_seconds: 4 }],
});
expect(request).toHaveBeenCalledWith("transcript.export", { projectItemId: "clip-1" });
expect(result).toMatchObject({
success: true,
data: { result: { projectItemId: "clip-1", deletedSeconds: 2, applied: false } },
});
});
it("builds a transcript rough-cut plan without mutating Premiere", async () => {
const json = '{"segments":[{"text":"cut this"}]}';
const request = vi.fn().mockResolvedValue({ projectItemId: "clip-1", projectItemName: "Interview", json });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).plan_transcript_rough_cut_uxp.handler({
project_item_id: "clip-1",
transcript_revision: transcriptRevision(json),
deletions: [{ start_seconds: 2, end_seconds: 4 }],
placements: [{
placement_id: "timeline-clip-1",
track_type: "video",
track_index: 0,
source_in_seconds: 0,
source_out_seconds: 10,
timeline_start_seconds: 20,
timeline_end_seconds: 30,
}],
});
expect(request).toHaveBeenCalledTimes(1);
expect(request).toHaveBeenCalledWith("transcript.export", { projectItemId: "clip-1" });
expect(result).toMatchObject({
success: true,
data: { result: { applied: false, requiresDuplicateSequence: true, steps: [{ timeline_range: { start_seconds: 22, end_seconds: 24 } }] } },
});
});
});
+446
View File
@@ -0,0 +1,446 @@
import { describe, expect, it, vi } from "vitest";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
import { getUxpTools } from "../../src/tools/uxp.js";
const ADVANCED_WORKFLOW_TOOLS = [
"inspect_project_selection_uxp",
"inspect_project_tree_uxp",
"manage_markers_uxp",
"apply_beat_markers_uxp",
"create_silence_cut_source_stringout_uxp",
"organize_project_items_uxp",
"manage_sequence_settings_uxp",
"manage_sequence_display_format_uxp",
"import_project_media_uxp",
"automate_effect_parameters_uxp",
"transform_track_item_uxp",
"edit_timeline_uxp",
"manage_sequences_uxp",
"encode_media_uxp",
] as const;
describe("advanced stable UXP workflow MCP catalog", () => {
it("publishes fourteen advanced tools with closed, bounded schemas", () => {
const bridge = { request: vi.fn(), getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge) as Record<string, { parameters: Record<string, unknown> }>;
expect(Object.keys(tools)).toEqual(expect.arrayContaining(ADVANCED_WORKFLOW_TOOLS));
expect(ADVANCED_WORKFLOW_TOOLS).toHaveLength(14);
for (const name of ADVANCED_WORKFLOW_TOOLS) {
const parameters = tools[name].parameters;
expect(parameters).toMatchObject({
type: "object",
additionalProperties: false,
});
if (name !== "inspect_project_tree_uxp") {
expect(parameters).toMatchObject({
required: expect.arrayContaining([name === "apply_beat_markers_uxp" ? "beat_times_seconds" : name === "create_silence_cut_source_stringout_uxp" ? "source_project_item_id" : "action"]),
});
}
}
expect(tools.inspect_project_selection_uxp.parameters).toMatchObject({
properties: { action: { enum: ["views", "selection"] }, view_id: { maxLength: 128 } },
});
expect(tools.inspect_project_tree_uxp.parameters).toMatchObject({
additionalProperties: false,
properties: { max_items: { minimum: 1, maximum: 512 }, max_depth: { minimum: 0, maximum: 16 } },
});
expect(tools.manage_markers_uxp.parameters).toMatchObject({
properties: {
action: { enum: ["inspect", "add", "update", "remove", "remove_many"] },
marker_snapshots: {
minItems: 1, maxItems: 128,
items: { required: ["marker_guid", "expected_name", "expected_start_seconds", "expected_duration_seconds"] },
},
confirm_destructive: { type: "boolean" },
},
});
expect(tools.manage_sequence_display_format_uxp.parameters).toMatchObject({
properties: {
action: { enum: ["inspect", "update"] },
expected_sequence_guid: { maxLength: 128 },
expected_display_formats: { required: ["audio_display_format", "video_display_format"] },
updates: { additionalProperties: false },
},
});
expect(tools.import_project_media_uxp.parameters).toMatchObject({
required: ["action", "confirm_non_undoable"],
properties: { paths: { maxItems: 100 }, confirm_non_undoable: { type: "boolean" } },
});
expect(tools.automate_effect_parameters_uxp.parameters).toMatchObject({
properties: {
action: { enum: ["inspect", "inspect_point_value", "inspect_point_displacement", "set_point_value", "inspect_color_value", "set_color_value", "inspect_keyframe", "set_value", "add_keyframe", "remove_keyframe", "remove_keyframe_range", "set_interpolation", "inspect_time_varying", "set_time_varying"] },
point: { required: ["x", "y"], additionalProperties: false },
expected_point_snapshot: { required: ["project_id", "sequence_id", "media_type", "track_index", "clip_index", "component_index", "component_id", "param_index", "param_name", "time_varying", "point"], additionalProperties: false },
color: { required: ["red", "green", "blue", "alpha"], additionalProperties: false },
expected_color_snapshot: { required: ["project_id", "sequence_id", "media_type", "track_index", "clip_index", "component_index", "component_id", "param_index", "param_name", "time_varying", "color"], additionalProperties: false },
expected_sequence_id: { maxLength: 128 }, expected_time_varying: { type: "boolean" },
expected_keyframe_times_seconds: { maxItems: 256, uniqueItems: true },
keyframe_direction: { enum: ["at", "next", "previous", "nearest"] },
time_varying: { type: "boolean" }, confirm_disable_time_varying: { type: "boolean" },
},
});
expect(tools.encode_media_uxp.parameters).toMatchObject({
properties: {
action: { enum: ["preflight", "jobs", "wait", "sequence", "project_item", "file"] },
job_id: { pattern: expect.any(String) },
timeout_ms: { maximum: 60000 },
output_file: { maxLength: 4096 },
confirm_external_write: { type: "boolean" },
},
});
});
it("maps guarded parameter keyframe and animation-mode reads to their exact UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const target = {
media_type: "video", track_index: 0, clip_index: 1, component_index: 2, param_index: 3,
expected_component_id: "ADBE Opacity", expected_param_name: "Opacity",
};
await tools.automate_effect_parameters_uxp.handler({ action: "inspect_keyframe", ...target, time_seconds: 4, keyframe_direction: "next" });
await tools.automate_effect_parameters_uxp.handler({ action: "inspect_keyframe", ...target, time_seconds: 4, end_seconds: 6, keyframe_direction: "nearest" });
await tools.automate_effect_parameters_uxp.handler({ action: "inspect_time_varying", ...target, time_seconds: 4 });
await tools.automate_effect_parameters_uxp.handler({
action: "set_time_varying", ...target, expected_sequence_id: "sequence-1", expected_time_varying: true,
expected_keyframe_times_seconds: [1, 2], time_varying: false, confirm_disable_time_varying: true,
operation_id: "parameter-animation-op",
});
expect(request.mock.calls).toEqual([
["parameters.keyframe.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Opacity", expectedParamName: "Opacity", timeSeconds: 4, direction: "next",
}],
["parameters.keyframe.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Opacity", expectedParamName: "Opacity", timeSeconds: 4, direction: "nearest", endSeconds: 6,
}],
["parameters.timeVarying.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Opacity", expectedParamName: "Opacity",
}],
["parameters.timeVarying.set", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedSequenceId: "sequence-1", expectedComponentId: "ADBE Opacity", expectedParamName: "Opacity",
expectedTimeVarying: true, expectedKeyframeTimesSeconds: [1, 2], timeVarying: false,
confirmDisableTimeVarying: true, operationId: "parameter-animation-op",
}],
]);
});
it("maps guarded static PointF inspection and update actions to the exact UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const target = {
media_type: "video", track_index: 0, clip_index: 1, component_index: 2, param_index: 3,
expected_component_id: "ADBE Motion", expected_param_name: "Position",
};
const expectedPointSnapshot = {
project_id: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 1,
component_index: 2, component_id: "ADBE Motion", param_index: 3, param_name: "Position", time_varying: false,
point: { x: 960, y: 540 },
};
await tools.automate_effect_parameters_uxp.handler({ action: "inspect_point_value", ...target });
await tools.automate_effect_parameters_uxp.handler({
action: "set_point_value", ...target, expected_point_snapshot: expectedPointSnapshot,
point: { x: 1100, y: 600 }, confirm_set_point: true, operation_id: "point-tool-op",
});
expect(request.mock.calls).toEqual([
["parameters.point.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Motion", expectedParamName: "Position",
}],
["parameters.point.set", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Motion", expectedParamName: "Position", point: { x: 1100, y: 600 },
expectedSnapshot: {
projectId: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 1,
componentIndex: 2, componentId: "ADBE Motion", paramIndex: 3, paramName: "Position", timeVarying: false,
point: { x: 960, y: 540 },
},
confirmSetPoint: true, operationId: "point-tool-op",
}],
]);
});
it("maps bounded native PointF displacement inspection to its exact UXP command", async () => {
const request = vi.fn().mockResolvedValue({ straightLineDistance: 5 });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.automate_effect_parameters_uxp.handler({
action: "inspect_point_displacement", media_type: "video", track_index: 0, clip_index: 1,
component_index: 2, param_index: 3, expected_component_id: "ADBE Motion", expected_param_name: "Position",
time_seconds: 2, end_seconds: 5,
});
expect(request).toHaveBeenCalledWith("parameters.point.displacement.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Motion", expectedParamName: "Position", startSeconds: 2, endSeconds: 5,
});
});
it("maps guarded static Color inspection and update actions to the exact UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const target = {
media_type: "video", track_index: 0, clip_index: 1, component_index: 2, param_index: 3,
expected_component_id: "ADBE Lumetri", expected_param_name: "Tint",
};
const expectedColorSnapshot = {
project_id: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 1,
component_index: 2, component_id: "ADBE Lumetri", param_index: 3, param_name: "Tint", time_varying: false,
color: { red: 0.1, green: 0.2, blue: 0.3, alpha: 1 },
};
await tools.automate_effect_parameters_uxp.handler({ action: "inspect_color_value", ...target });
await tools.automate_effect_parameters_uxp.handler({
action: "set_color_value", ...target, expected_color_snapshot: expectedColorSnapshot,
color: { red: 0.9, green: 0.8, blue: 0.7, alpha: 0.6 }, confirm_set_color: true, operation_id: "color-tool-op",
});
expect(request.mock.calls).toEqual([
["parameters.color.inspect", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Lumetri", expectedParamName: "Tint",
}],
["parameters.color.set", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Lumetri", expectedParamName: "Tint",
color: { red: 0.9, green: 0.8, blue: 0.7, alpha: 0.6 },
expectedSnapshot: {
projectId: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 1,
componentIndex: 2, componentId: "ADBE Lumetri", paramIndex: 3, paramName: "Tint", timeVarying: false,
color: { red: 0.1, green: 0.2, blue: 0.3, alpha: 1 },
},
confirmSetColor: true, operationId: "color-tool-op",
}],
]);
});
it("maps every consolidated tool to its exact camel-case UXP command contract", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.inspect_project_selection_uxp.handler({ action: "selection", view_id: "view-1" });
await tools.inspect_project_tree_uxp.handler({ max_items: 12, max_depth: 3 });
await tools.manage_markers_uxp.handler({
action: "update", owner_type: "project_item", project_item_id: "clip-1",
marker_guid: "marker-1", expected_name: "Old", name: "New", color_index: 3,
operation_id: "marker-op",
});
await tools.manage_markers_uxp.handler({
action: "remove_many", owner_type: "sequence", sequence_id: "sequence-1", marker_guid: "single-marker", expected_name: "Ignored", confirm_destructive: true,
marker_snapshots: [{ marker_guid: "marker-1", expected_name: "Old", expected_start_seconds: 1, expected_duration_seconds: 0 }],
operation_id: "marker-batch-op",
});
await tools.apply_beat_markers_uxp.handler({
beat_times_seconds: [0.5, 1, 1.5], sequence_id: "sequence-1", offset_seconds: 2,
name_prefix: "Downbeat", comments: "Detected beat grid", operation_id: "beat-grid-op",
});
await tools.create_silence_cut_source_stringout_uxp.handler({
source_project_item_id: "clip-1", sequence_name: "Interview Tight", duration_seconds: 10,
frame_rate: 30, silence_ranges: [{ start_seconds: 2, end_seconds: 4 }],
keep_handle_frames: 0, confirm_non_undoable: true, operation_id: "silence-op",
});
await tools.organize_project_items_uxp.handler({
action: "create_smart_bin", parent_bin_id: "bin-1", name: "Selects",
search_query: "rating:5", operation_id: "bin-op",
});
await tools.manage_sequence_settings_uxp.handler({
action: "update", sequence_id: "sequence-1",
updates: { maximum_bit_depth: true, video_frame_rate: 24, video_width: 1920 },
operation_id: "settings-op",
});
await tools.manage_sequence_display_format_uxp.handler({
action: "update", sequence_id: "sequence-1", expected_sequence_guid: "sequence-1",
expected_display_formats: { audio_display_format: 1, video_display_format: 20 },
updates: { audio_display_format: 2, video_display_format: 26 }, operation_id: "display-format-op",
});
await tools.import_project_media_uxp.handler({
action: "ae_comps", aep_path: "D:/Approved/graphics.aep",
comp_names: ["Lower Third"], target_bin_id: "bin-1",
confirm_non_undoable: true, operation_id: "import-op",
});
await tools.automate_effect_parameters_uxp.handler({
action: "add_keyframe", media_type: "video", track_index: 0, clip_index: 1,
component_index: 2, param_index: 3, expected_component_id: "ADBE Opacity",
expected_param_name: "Opacity", value: 75, time_seconds: 2.5,
operation_id: "keyframe-op",
});
await tools.transform_track_item_uxp.handler({
action: "update", media_type: "audio", track_index: 1, clip_index: 2,
expected_start_seconds: 10, expected_end_seconds: 20, move_by_seconds: 1.5,
disabled: false, operation_id: "track-op",
});
await tools.edit_timeline_uxp.handler({
action: "insert_mogrt_path", file_path: "D:/Approved/title.mogrt",
time_seconds: 4, video_track_index: 2, audio_track_index: 0,
confirm_non_undoable: true, operation_id: "timeline-op",
});
await tools.manage_sequences_uxp.handler({
action: "create_from_media", name: "Assembly", project_item_ids: ["clip-1", "clip-2"],
target_bin_id: "bin-1", confirm_non_undoable: true, operation_id: "sequence-op",
});
await tools.encode_media_uxp.handler({
action: "file", file_path: "D:/Approved/input.mov", output_file: "D:/Approved/output.mp4",
preset_file: "D:/Approved/h264.epr", in_seconds: 1, out_seconds: 8, work_area: 0,
remove_upon_completion: true, start_queue_immediately: false,
confirm_external_write: true, operation_id: "encode-op",
});
await tools.encode_media_uxp.handler({
action: "wait", job_id: "encode-op", timeout_ms: 5000,
});
await tools.encode_media_uxp.handler({ action: "jobs", limit: 3 });
await tools.encode_media_uxp.handler({ action: "wait", job_id: "encode-op" });
expect(request.mock.calls).toEqual([
["projectSelection.inspect", { viewId: "view-1" }],
["projectTree.inspect", { maxItems: 12, maxDepth: 3 }],
["markers.update", {
ownerType: "projectItem", projectItemId: "clip-1", markerGuid: "marker-1",
expectedName: "Old", name: "New", colorIndex: 3, operationId: "marker-op",
}],
["markers.removeMany", {
ownerType: "sequence", sequenceId: "sequence-1",
markerSnapshots: [{ markerGuid: "marker-1", expectedName: "Old", expectedStartSeconds: 1, expectedDurationSeconds: 0 }],
confirmDestructive: true, operationId: "marker-batch-op",
}],
["markers.addBeatGrid", {
beatTimesSeconds: [0.5, 1, 1.5], sequenceId: "sequence-1", offsetSeconds: 2,
namePrefix: "Downbeat", comments: "Detected beat grid", operationId: "beat-grid-op",
}],
["silence.deriveSequence", {
sourceProjectItemId: "clip-1", name: "Interview Tight",
keepRanges: [
{ startFrame: 0, endFrame: 60, startSeconds: 0, endSeconds: 2 },
{ startFrame: 120, endFrame: 300, startSeconds: 4, endSeconds: 10 },
],
confirmNonUndoable: true, operationId: "silence-op",
}],
["bins.createSmart", {
parentBinId: "bin-1", name: "Selects", searchQuery: "rating:5", operationId: "bin-op",
}],
["sequenceSettings.update", {
sequenceId: "sequence-1",
updates: { maximumBitDepth: true, videoFrameRate: 24, videoWidth: 1920 },
operationId: "settings-op",
}],
["sequence.displayFormat.update", {
sequenceId: "sequence-1", expectedSequenceGuid: "sequence-1",
expectedDisplayFormats: { audioDisplayFormat: 1, videoDisplayFormat: 20 },
updates: { audioDisplayFormat: 2, videoDisplayFormat: 26 }, operationId: "display-format-op",
}],
["project.import", {
mode: "aeComps", aepPath: "D:/Approved/graphics.aep", compNames: ["Lower Third"],
targetBinId: "bin-1", confirmNonUndoable: true, operationId: "import-op",
}],
["parameters.keyframeAdd", {
mediaType: "video", trackIndex: 0, clipIndex: 1, componentIndex: 2, paramIndex: 3,
expectedComponentId: "ADBE Opacity", expectedParamName: "Opacity", timeSeconds: 2.5,
value: 75, operationId: "keyframe-op",
}],
["trackItem.update", {
mediaType: "audio", trackIndex: 1, clipIndex: 2, expectedStartSeconds: 10,
expectedEndSeconds: 20, moveBySeconds: 1.5, disabled: false, operationId: "track-op",
}],
["timeline.mogrtPath", {
timeSeconds: 4, videoTrackIndex: 2, audioTrackIndex: 0,
filePath: "D:/Approved/title.mogrt", confirmNonUndoable: true,
operationId: "timeline-op",
}],
["sequences.createFromMedia", {
name: "Assembly", projectItemIds: ["clip-1", "clip-2"], targetBinId: "bin-1",
confirmNonUndoable: true, operationId: "sequence-op",
}],
["encoder.file", {
filePath: "D:/Approved/input.mov", outputFile: "D:/Approved/output.mp4",
presetFile: "D:/Approved/h264.epr", inSeconds: 1, outSeconds: 8, workArea: 0,
removeUponCompletion: true, startQueueImmediately: false,
confirmExternalWrite: true, operationId: "encode-op",
}],
["encoder.wait", { jobId: "encode-op", timeoutMs: 5000 }, { minimumTimeoutMs: 10000 }],
["encoder.jobs", { limit: 3 }],
["encoder.wait", { jobId: "encode-op" }, { minimumTimeoutMs: 5000 }],
]);
});
it("rejects unknown public actions without contacting Premiere", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const results = await Promise.all([
tools.inspect_project_selection_uxp.handler({ action: "destroy_everything" }),
tools.manage_markers_uxp.handler({ action: "destroy_everything" }),
tools.organize_project_items_uxp.handler({ action: "destroy_everything" }),
tools.transform_track_item_uxp.handler({ action: "destroy_everything" }),
]);
expect(results).toEqual(Array.from({ length: 4 }, () => ({
success: false,
error: "Unsupported workflow action: destroy_everything",
})));
expect(request).not.toHaveBeenCalled();
});
it("rejects unreviewed or duplicate marker batch removal before contacting Premiere", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const snapshot = { marker_guid: "marker-1", expected_name: "Beat", expected_start_seconds: 0, expected_duration_seconds: 0 };
await expect(tools.manage_markers_uxp.handler({ action: "remove_many", marker_snapshots: [snapshot] })).resolves.toMatchObject({
success: false, error: expect.stringContaining("confirm_destructive=true"),
});
await expect(tools.manage_markers_uxp.handler({
action: "remove_many", confirm_destructive: true, marker_snapshots: [snapshot, snapshot],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("duplicate marker_guid") });
await expect(tools.manage_markers_uxp.handler({
action: "remove_many", confirm_destructive: true, marker_snapshots: [null],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("marker_snapshots[0] must be an object") });
expect(request).not.toHaveBeenCalled();
});
it("normalizes Error and non-Error bridge rejections", async () => {
const request = vi.fn()
.mockRejectedValueOnce(new Error("Premiere unavailable"))
.mockRejectedValueOnce("transport closed");
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await expect(tools.inspect_project_selection_uxp.handler({ action: "views" })).resolves.toEqual({
success: false,
error: "Premiere unavailable",
});
await expect(tools.manage_sequence_settings_uxp.handler({ action: "get" })).resolves.toEqual({
success: false,
error: "transport closed",
});
});
it("rejects missing actions in mapped dispatchers before bridge access", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const results = await Promise.all([
tools.import_project_media_uxp.handler({ confirm_non_undoable: true }),
tools.automate_effect_parameters_uxp.handler({}),
tools.edit_timeline_uxp.handler({}),
tools.manage_sequences_uxp.handler({}),
tools.encode_media_uxp.handler({}),
]);
expect(results).toEqual(Array.from({ length: 5 }, () => ({
success: false,
error: "Unsupported workflow action: undefined",
})));
expect(request).not.toHaveBeenCalled();
});
});
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpCloneWorkflowTools } from "../../src/tools/uxp-clone-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const source = { project_item_id: "source-1", start_seconds: 0, end_seconds: 10, in_seconds: 20, out_seconds: 30, duration_seconds: 10, speed: 1, reversed: false };
describe("public guarded append-only track-item duplicate MCP tool", () => {
it("uses a closed complete source snapshot schema and translates snake_case apply arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const tool = getUxpCloneWorkflowTools({ request } as unknown as UxpWebSocketBridge).duplicate_track_item_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["action", "media_type", "track_index", "clip_index"],
properties: { action: { enum: ["inspect", "apply"] }, expected_snapshot: { additionalProperties: false, required: ["project_guid", "sequence_id", "media_type", "track_index", "clip_index", "track_item_count", "source"] } },
});
await tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 0, confirm_duplicate: true, operation_id: "clone-tool-1",
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 0, track_item_count: 1, source },
});
expect(request).toHaveBeenCalledWith("trackItem.clone", {
mediaType: "video", trackIndex: 0, clipIndex: 0, confirmDuplicate: true, operationId: "clone-tool-1",
expectedSnapshot: {
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 0, trackItemCount: 1,
source: { projectItemId: "source-1", startSeconds: 0, endSeconds: 10, inSeconds: 20, outSeconds: 30, durationSeconds: 10, speed: 1, reversed: false },
},
});
});
it("does not silently drop unknown fields from a reviewed source snapshot", async () => {
const request = vi.fn();
const tool = getUxpCloneWorkflowTools({ request } as unknown as UxpWebSocketBridge).duplicate_track_item_uxp;
await expect(tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 0,
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 0, track_item_count: 1, source: { ...source, unexpected: true } },
})).rejects.toThrow("expected_snapshot.source has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpDialogueWorkflowTools, validateDerivedDialoguePlan } from "../../src/tools/uxp-dialogue-workflows.js";
const json = "{\"segments\":[]}", revision = "sha256:cf84482b9efdd9291a36643471c6e09c79a69623f87a7b61265b660e54e69eaf";
describe("UXP dialogue workflow tools", () => {
it("previews then applies the exact revision-bound plan", async () => {
const request = vi.fn(async (command: string) => command === "transcript.export" ? { json, projectGuid: "project" } : { outcome: "committed_unverified" });
const tools = getUxpDialogueWorkflowTools({ request } as any);
const preview = await tools.preview_derived_dialogue_sequence_uxp.handler({ project_guid: "project", mode: "talking_head", sequence_name: "Reviewed", segments: [{ id: "s1", source_project_item_id: "clip", transcript_revision: revision, source_start_seconds: 0, source_end_seconds: 1 }] });
expect(preview.success).toBe(true);
const data = preview.data as any;
const applied = await tools.apply_derived_dialogue_sequence_uxp.handler({ plan: data.plan, confirmation_token: data.confirmation_token, operation_id: "op-1" });
expect(applied.success).toBe(true);
expect(request).toHaveBeenLastCalledWith("dialogue.deriveSequence", { plan: data.plan, operationId: "op-1" });
});
it("validates podcast plans and rejects unsafe variants", () => {
const segment = { id: "s", source_project_item_id: "camera", transcript_revision: revision, source_start_seconds: 1, source_end_seconds: 2, master_audio_start_seconds: 3, master_audio_end_seconds: 4 };
const plan = { schema_version: 1, project_guid: "project", mode: "podcast", sequence_name: "Podcast", master_audio_project_item_id: "audio", segments: [segment], output_duration_seconds: 1, original_sources_unchanged: true, render_verified: false };
expect(validateDerivedDialoguePlan(plan).mode).toBe("podcast");
const invalid = [
null, { ...plan, extra: true }, { ...plan, schema_version: 2 }, { ...plan, mode: "other" }, { ...plan, segments: [] },
{ ...plan, segments: [null] }, { ...plan, segments: [{ ...segment, extra: true }] }, { ...plan, segments: [segment, segment] },
{ ...plan, segments: [{ ...segment, source_end_seconds: 1 }] }, { ...plan, segments: [{ ...segment, transcript_revision: "bad" }] },
{ ...plan, segments: [{ ...segment, master_audio_end_seconds: 5 }] }, { ...plan, master_audio_project_item_id: undefined },
{ ...plan, output_duration_seconds: 2 }, { ...plan, original_sources_unchanged: false },
{ ...plan, target_bin_id: "" }, { ...plan, segments: [{ ...segment, speaker_label: "" }] },
{ ...plan, mode: "talking_head", segments: [{ ...segment, master_audio_start_seconds: undefined, master_audio_end_seconds: undefined }] },
];
for (const value of invalid) expect(() => validateDerivedDialoguePlan(value)).toThrow();
expect(() => validateDerivedDialoguePlan({ ...plan, mode: "talking_head", master_audio_project_item_id: undefined, segments: [{ ...segment, master_audio_start_seconds: undefined, master_audio_end_seconds: undefined }, { ...segment, id: "two", source_project_item_id: "other", master_audio_start_seconds: undefined, master_audio_end_seconds: undefined }] })).toThrow(/same source/);
});
it("fails closed for stale or unavailable transcripts and bad confirmations", async () => {
const base = { project_guid: "project", mode: "talking_head", sequence_name: "Reviewed", segments: [{ id: "s1", source_project_item_id: "clip", transcript_revision: revision, source_start_seconds: 0, source_end_seconds: 1 }] };
for (const response of [{ json, projectGuid: "other" }, { projectGuid: "project" }, { json: "changed", projectGuid: "project" }]) {
const tools = getUxpDialogueWorkflowTools({ request: vi.fn().mockResolvedValue(response) } as any);
expect((await tools.preview_derived_dialogue_sequence_uxp.handler(base)).success).toBe(false);
}
const tools = getUxpDialogueWorkflowTools({ request: vi.fn().mockResolvedValue({ json, projectGuid: "project" }) } as any);
const preview = await tools.preview_derived_dialogue_sequence_uxp.handler(base) as any;
expect((await tools.apply_derived_dialogue_sequence_uxp.handler({ plan: preview.data.plan, confirmation_token: `sha256:${"0".repeat(64)}`, operation_id: "op" })).success).toBe(false);
expect((await tools.apply_derived_dialogue_sequence_uxp.handler({ plan: preview.data.plan, confirmation_token: preview.data.confirmation_token, operation_id: "bad space" })).success).toBe(false);
});
it("normalizes non-Error bridge failures", async () => {
const tools = getUxpDialogueWorkflowTools({ request: vi.fn().mockRejectedValue("offline") } as any);
const result = await tools.preview_derived_dialogue_sequence_uxp.handler({ project_guid: "project", mode: "talking_head", sequence_name: "Reviewed", segments: [{ id: "s1", source_project_item_id: "clip", transcript_revision: revision, source_start_seconds: 0, source_end_seconds: 1 }] });
expect(result).toEqual({ success: false, error: "offline" });
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpEffectParameterCatalogWorkflowTools } from "../../src/tools/uxp-effect-parameter-catalog-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public documented UXP effect-parameter catalog tool", () => {
it("publishes a closed bounded schema and maps only parameter-catalog coordinates", async () => {
const request = vi.fn().mockResolvedValue({ parameterCount: 1 });
const tool = getUxpEffectParameterCatalogWorkflowTools({ request } as unknown as UxpWebSocketBridge)
.inspect_effect_parameter_catalog_uxp;
expect(tool.parameters).toMatchObject({
type: "object", additionalProperties: false,
required: ["media_type", "track_index", "clip_index", "component_index"],
properties: {
media_type: { type: "string", enum: ["video", "audio"] },
track_index: { type: "integer", minimum: 0, maximum: 511 },
clip_index: { type: "integer", minimum: 0, maximum: 511 },
component_index: { type: "integer", minimum: 0, maximum: 511 },
expected_sequence_guid: { minLength: 1, maxLength: 512 },
expected_component_id: { minLength: 1, maxLength: 512 },
},
});
expect(tool.description).toContain("never raw parameter values");
expect(tool.operationalCapability).toMatchObject({
backend: "UXP", minimumPremiereVersion: "25.6", hostVerificationRequired: true,
});
await tool.handler({
media_type: "audio", track_index: 3, clip_index: 2, component_index: 1,
expected_sequence_guid: "sequence-1", expected_component_id: "ADBE Volume",
});
expect(request).toHaveBeenCalledWith("parameters.catalog.inspect", {
mediaType: "audio", trackIndex: 3, clipIndex: 2, componentIndex: 1,
expectedSequenceGuid: "sequence-1", expectedComponentId: "ADBE Volume",
});
});
});
+133
View File
@@ -0,0 +1,133 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getUxpTools } from "../../src/tools/uxp.js";
import { getUxpWorkflowTools } from "../../src/tools/uxp-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
type Schema = { type?: string; enum?: unknown[]; properties?: Record<string, Schema>; required?: string[]; items?: Schema };
type Tool = { parameters: Schema; handler: (args: Record<string, unknown>) => Promise<unknown> };
const values: Record<string, unknown> = {
name: "Coverage Name", query: "coverage", preset_path: "/tmp/preset.sqpreset",
confirm_non_undoable: true, confirm_destructive: true, operation_id: "coverage-operation",
output_file_path: "/tmp/output.otio", project_item_id: "item-1", track_index: 0,
start_seconds: 1, end_seconds: 2, transcript_revision: `sha256:${"a".repeat(64)}`,
project_guid: "project-1", expected_transcript_revision: `sha256:${"a".repeat(64)}`,
replacement_transcript_json: '{"segments":[]}',
sequence_name: "Coverage Stringout", duration_seconds: 10, frame_rate: 30,
silence_ranges: [{ start_seconds: 2, end_seconds: 4 }],
placements: [{
placement_id: "coverage-placement", track_type: "video", track_index: 0,
source_in_seconds: 0, source_out_seconds: 10,
timeline_start_seconds: 20, timeline_end_seconds: 30,
}],
};
function valueFor(schema: Schema, field: string, all: boolean): unknown {
if (field in values) return values[field];
if (schema.enum?.length) return all ? schema.enum.at(-1) : schema.enum[0];
if (schema.type === "boolean") return all;
if (schema.type === "number" || schema.type === "integer") return 1;
if (schema.type === "array") return [valueFor(schema.items ?? { type: "string" }, field, all)];
if (schema.type === "object") return argsFor(schema, all);
return `coverage-${field}`;
}
function argsFor(schema: Schema, all: boolean) {
const result: Record<string, unknown> = {};
const required = new Set(schema.required ?? []);
for (const [field, property] of Object.entries(schema.properties ?? {})) {
if (all || required.has(field)) result[field] = valueFor(property, field, all);
}
return result;
}
describe("UXP tool handler coverage", () => {
const request = vi.fn();
const getState = vi.fn(() => ({ connected: true, authenticated: true }));
const bridge = { request, getState } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge) as Record<string, Tool>;
beforeEach(() => {
vi.clearAllMocks();
request.mockResolvedValue({
json: JSON.stringify({ words: [], segments: [] }),
projectItemId: "item-1",
projectItemName: "Coverage",
});
});
for (const [name, tool] of Object.entries(tools)) {
it.each([["required", false], ["all", true]])(`${name} handles %s arguments`, async (_label, all) => {
const args = argsFor(tool.parameters, all);
if (name === "manage_timeline_selection_uxp" && all) args.action = "replace";
const result = await tool.handler(args);
expect(result).toBeDefined();
if (name === "get_uxp_capabilities") expect(getState).toHaveBeenCalled();
else if (!["preview_derived_dialogue_sequence_uxp", "apply_derived_dialogue_sequence_uxp"].includes(name)) expect(request).toHaveBeenCalled();
});
}
it("normalizes Error and non-Error bridge rejections", async () => {
request.mockRejectedValueOnce(new Error("offline"));
await expect(tools.get_uxp_state.handler({})).resolves.toEqual({ success: false, error: "offline" });
request.mockRejectedValueOnce("disconnected");
await expect(tools.get_uxp_state.handler({})).resolves.toEqual({ success: false, error: "disconnected" });
});
it("rejects an empty exported transcript", async () => {
request.mockResolvedValueOnce({ json: "" });
await expect(tools.get_clip_transcript_uxp.handler({ project_item_id: "item-1" }))
.resolves.toEqual({ success: false, error: "Premiere returned an empty transcript" });
});
});
describe("UXP workflow handler branch coverage", () => {
const request = vi.fn();
const bridge = { request } as unknown as UxpWebSocketBridge;
const tools = getUxpWorkflowTools(bridge) as Record<string, Tool>;
beforeEach(() => {
vi.clearAllMocks();
request.mockResolvedValue({ covered: true });
});
for (const [name, tool] of Object.entries(tools)) {
it.each([["required", false], ["all", true]])(`${name} handles %s arguments`, async (_label, all) => {
await expect(tool.handler(argsFor(tool.parameters, all))).resolves.toBeDefined();
});
for (const [field, property] of Object.entries(tool.parameters.properties ?? {})) {
for (const enumValue of property.enum ?? []) {
it(`${name} handles ${field}=${String(enumValue)}`, async () => {
const args = argsFor(tool.parameters, true);
args[field] = enumValue;
await expect(tool.handler(args)).resolves.toBeDefined();
});
}
if (property.type === "boolean") {
it(`${name} handles ${field}=false`, async () => {
const args = argsFor(tool.parameters, true);
args[field] = false;
await expect(tool.handler(args)).resolves.toBeDefined();
});
}
}
if (tool.parameters.properties?.action?.enum) {
it(`${name} rejects an unsupported action`, async () => {
const args = argsFor(tool.parameters, true);
args.action = "unsupported-action";
await expect(tool.handler(args)).resolves.toMatchObject({ success: false });
});
}
}
it("normalizes workflow bridge failures", async () => {
request.mockRejectedValueOnce(new Error("workflow offline"));
await expect(tools.manage_clip_effects_uxp.handler({ action: "catalog" }))
.resolves.toEqual({ success: false, error: "workflow offline" });
request.mockRejectedValueOnce("disconnected");
await expect(tools.manage_clip_effects_uxp.handler({ action: "catalog" }))
.resolves.toEqual({ success: false, error: "disconnected" });
});
});
+567
View File
@@ -0,0 +1,567 @@
import { describe, expect, it, vi } from "vitest";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
import { getUxpNextWorkflowTools } from "../../src/tools/uxp-next-workflows.js";
describe("next-wave UXP MCP tools", () => {
it("publishes a closed and bounded event receipt schema", () => {
const bridge = { request: vi.fn(), getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).inspect_premiere_events_uxp;
expect(tool.description).toContain("timeline.snap.*");
expect(tool.description).toContain("operation.clip.extend.reached");
expect(tool.description).toContain("raw event payloads are never returned");
expect(tool.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["list", "wait"] },
after_revision: { maximum: Number.MAX_SAFE_INTEGER },
categories: { maxItems: 32 },
event_names: { maxItems: 32 },
limit: { maximum: 256 },
timeout_ms: { maximum: 60000 },
},
});
const readiness = getUxpNextWorkflowTools(bridge).wait_for_host_readiness_uxp;
expect(readiness.parameters).toMatchObject({
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["snapshot", "analysis", "operation"] },
operation_type: { enum: ["import", "export", "effect_drop", "generative_extend"] },
after_revision: { maximum: Number.MAX_SAFE_INTEGER },
timeout_ms: { maximum: 60000 },
poll_max_ms: { maximum: 5000 },
},
});
const projects = getUxpNextWorkflowTools(bridge).manage_project_sessions_uxp;
expect(projects.parameters).toMatchObject({
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["list", "validate", "create", "open", "save", "save_as", "branch_copies", "close"] },
paths: { maxItems: 16, uniqueItems: true },
confirm_external_write: { type: "boolean" },
confirm_discard_unsaved: { type: "boolean" },
},
});
const growing = getUxpNextWorkflowTools(bridge).manage_growing_media_uxp;
expect(growing.parameters).toMatchObject({
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["status", "pause", "resume"] },
lease_ms: { minimum: 1000, maximum: 600000 },
confirm_pause: { type: "boolean" },
},
});
const checkpoints = getUxpNextWorkflowTools(bridge).manage_workflow_checkpoints_uxp;
expect(checkpoints.parameters).toMatchObject({
additionalProperties: false,
required: ["action", "name"],
properties: {
action: { enum: ["has", "get", "set", "clear"] },
owner: { enum: ["project", "sequence"] },
value_type: { enum: ["string", "int", "float", "bool"] },
persistence: { enum: ["session", "persistent"] },
},
});
const media = getUxpNextWorkflowTools(bridge).maintain_media_health_uxp;
expect(media.parameters).toMatchObject({
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["inspect", "refresh", "set_offline", "find_by_media_path"] },
project_item_ids: { maxItems: 64, uniqueItems: true },
confirm_set_offline: { type: "boolean" },
include_paths: { type: "boolean" },
include_media_timing: { type: "boolean" },
},
});
const sourceMediaTiming = getUxpNextWorkflowTools(bridge).manage_source_media_timing_uxp;
expect(sourceMediaTiming.parameters).toMatchObject({
additionalProperties: false,
required: ["action", "project_item_id"],
properties: {
action: { enum: ["inspect", "set_start"] },
project_item_id: { maxLength: 512 },
expected_timing: {
additionalProperties: false,
required: ["start_seconds", "duration_seconds"],
},
start_seconds: { maximum: 86400000 },
confirm_set_start: { type: "boolean" },
},
});
const sourceMediaOverrides = getUxpNextWorkflowTools(bridge).manage_source_media_overrides_uxp;
expect(sourceMediaOverrides.parameters).toMatchObject({
additionalProperties: false,
required: ["action", "project_item_id"],
properties: {
action: { enum: ["inspect", "update"] },
project_item_id: { maxLength: 512 },
expected_overrides: {
additionalProperties: false,
required: ["project_guid", "frame_rate", "pixel_aspect_ratio"],
},
frame_rate: { minimum: 1, maximum: 240 },
pixel_aspect_ratio: {
additionalProperties: false,
required: ["numerator", "denominator"],
},
confirm_media_interpretation: { type: "boolean" },
operation_id: { pattern: "^[A-Za-z0-9._:-]{1,128}$" },
},
});
const tracks = getUxpNextWorkflowTools(bridge).manage_track_state_uxp;
expect(tracks.parameters).toMatchObject({
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["inspect", "set_mute"] },
media_type: { enum: ["all", "video", "audio", "caption"] },
track_indices: { maxItems: 64, uniqueItems: true },
muted: { type: "boolean" },
},
});
const source = getUxpNextWorkflowTools(bridge).manage_source_clip_uxp;
expect(source.parameters).toMatchObject({
additionalProperties: false,
required: ["action", "items"],
properties: {
action: { enum: ["inspect", "update"] },
items: {
maxItems: 64,
items: {
additionalProperties: false,
required: ["project_item_id"],
properties: {
media_type: { enum: ["video", "audio"] },
scale_to_frame: { enum: [true] },
},
},
},
},
});
});
it("maps snake-case event queries to the exact bridge commands", async () => {
const request = vi.fn().mockResolvedValue({ events: [] });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).inspect_premiere_events_uxp;
await tool.handler({
action: "wait", after_revision: 7, categories: ["encoder"],
event_names: ["encoder.complete"], limit: 4, timeout_ms: 5000,
});
expect(request).toHaveBeenCalledWith("events.wait", {
afterRevision: 7,
categories: ["encoder"],
eventNames: ["encoder.complete"],
limit: 4,
timeoutMs: 5000,
}, { minimumTimeoutMs: 10000 });
const readiness = getUxpNextWorkflowTools(bridge).wait_for_host_readiness_uxp;
await readiness.handler({
action: "analysis", sequence_id: "sequence-1", expected_sequence_id: "sequence-1",
timeout_ms: 10000, poll_min_ms: 100, poll_max_ms: 1000,
});
expect(request).toHaveBeenLastCalledWith("readiness.analysis.wait", {
sequenceId: "sequence-1", expectedSequenceId: "sequence-1",
timeoutMs: 10000, pollMinMs: 100, pollMaxMs: 1000,
}, { minimumTimeoutMs: 15000 });
await readiness.handler({
action: "operation", operation_type: "effect_drop", after_revision: 9, timeout_ms: 5000,
});
expect(request).toHaveBeenLastCalledWith("readiness.operation.wait", {
operationType: "effectDrop", afterRevision: 9, timeoutMs: 5000,
}, { minimumTimeoutMs: 10000 });
const projects = getUxpNextWorkflowTools(bridge).manage_project_sessions_uxp;
await projects.handler({
action: "branch_copies", project_id: "project-1", expected_path: "C:/work/source.prproj",
paths: ["C:/work/a.prproj", "C:/work/b.prproj"], confirm_external_write: true,
confirm_overwrite: false, operation_id: "branches-1",
});
expect(request).toHaveBeenLastCalledWith("project.sessions.branchCopies", {
projectId: "project-1", expectedPath: "C:/work/source.prproj", operationId: "branches-1",
paths: ["C:/work/a.prproj", "C:/work/b.prproj"],
confirmExternalWrite: true, confirmOverwrite: false,
});
const growing = getUxpNextWorkflowTools(bridge).manage_growing_media_uxp;
await growing.handler({
action: "pause", project_id: "project-1", expected_path: "C:/work/source.prproj",
lease_ms: 30000, confirm_pause: true, operation_id: "pause-1",
});
expect(request).toHaveBeenLastCalledWith("growing.pause", {
projectId: "project-1", expectedPath: "C:/work/source.prproj",
leaseMs: 30000, confirmPause: true, operationId: "pause-1",
});
const checkpoints = getUxpNextWorkflowTools(bridge).manage_workflow_checkpoints_uxp;
await checkpoints.handler({
action: "set", owner: "sequence", sequence_id: "sequence-1", expected_owner_id: "sequence-1",
name: "render.pass", value_type: "int", value: 3, persistence: "persistent", operation_id: "checkpoint-1",
});
expect(request).toHaveBeenLastCalledWith("checkpoint.set", {
owner: "sequence", sequenceId: "sequence-1", expectedOwnerId: "sequence-1",
name: "render.pass", valueType: "int", value: 3, persistence: "persistent", operationId: "checkpoint-1",
});
const media = getUxpNextWorkflowTools(bridge).maintain_media_health_uxp;
await media.handler({
action: "set_offline", project_item_ids: ["clip-1", "clip-2"],
expected_offline: false, confirm_set_offline: true, operation_id: "offline-1",
});
expect(request).toHaveBeenLastCalledWith("media.health.setOffline", {
projectItemIds: ["clip-1", "clip-2"], expectedOffline: false,
confirmSetOffline: true, operationId: "offline-1",
});
const sourceMediaTiming = getUxpNextWorkflowTools(bridge).manage_source_media_timing_uxp;
await sourceMediaTiming.handler({ action: "set_start", project_item_id: "clip-1", start_seconds: 12,
expected_timing: { start_seconds: 10, duration_seconds: 60 }, confirm_set_start: true, operation_id: "source-time-1" });
expect(request).toHaveBeenLastCalledWith("source.mediaTiming.setStart", {
projectItemId: "clip-1", expectedTiming: { startSeconds: 10, durationSeconds: 60 },
startSeconds: 12, confirmSetStart: true, operationId: "source-time-1",
});
const sourceMediaOverrides = getUxpNextWorkflowTools(bridge).manage_source_media_overrides_uxp;
await sourceMediaOverrides.handler({
action: "update", project_item_id: "clip-1",
expected_overrides: { project_guid: "project-1", frame_rate: 23.976, pixel_aspect_ratio: 1 },
frame_rate: 25, pixel_aspect_ratio: { numerator: 4, denominator: 3 },
confirm_media_interpretation: true, operation_id: "source-override-1",
});
expect(request).toHaveBeenLastCalledWith("source.mediaOverrides.update", {
projectItemId: "clip-1",
expectedOverrides: { projectGuid: "project-1", frameRate: 23.976, pixelAspectRatio: 1 },
frameRate: 25, pixelAspectRatio: { numerator: 4, denominator: 3 },
confirmMediaInterpretation: true, operationId: "source-override-1",
});
const tracks = getUxpNextWorkflowTools(bridge).manage_track_state_uxp;
await tracks.handler({
action: "set_mute", sequence_id: "sequence-1", expected_sequence_id: "sequence-1",
media_type: "caption", track_indices: [0], muted: true, expected_muted: false, operation_id: "mute-1",
});
expect(request).toHaveBeenLastCalledWith("track.state.set", {
sequenceId: "sequence-1", expectedSequenceId: "sequence-1",
mediaType: "caption", trackIndices: [0], muted: true, expectedMuted: false, operationId: "mute-1",
});
const source = getUxpNextWorkflowTools(bridge).manage_source_clip_uxp;
await source.handler({
action: "update",
items: [{
project_item_id: "clip-1", media_type: "video", expected_in_seconds: 1,
in_seconds: 2, out_seconds: 8, scale_to_frame: true,
}],
operation_id: "source-1",
});
expect(request).toHaveBeenLastCalledWith("source.clip.update", {
items: [{
projectItemId: "clip-1", mediaType: "video", expectedInSeconds: 1,
inSeconds: 2, outSeconds: 8, scaleToFrame: true,
}],
operationId: "source-1",
});
});
it("covers bounded event and readiness fallbacks without hiding bridge errors", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpNextWorkflowTools(bridge);
await tools.inspect_premiere_events_uxp.handler({ action: "list" });
expect(request).toHaveBeenLastCalledWith("events.list", {});
await tools.inspect_premiere_events_uxp.handler({
action: "list",
after_revision: 0,
categories: [],
event_names: [],
limit: 1,
timeout_ms: 10,
});
expect(request).toHaveBeenLastCalledWith("events.list", {
afterRevision: 0,
categories: [],
eventNames: [],
limit: 1,
});
await expect(tools.inspect_premiere_events_uxp.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported event action: unsupported" });
request.mockRejectedValueOnce(new Error("bridge unavailable"));
await expect(tools.inspect_premiere_events_uxp.handler({ action: "wait" }))
.resolves.toEqual({ success: false, error: "bridge unavailable" });
request.mockRejectedValueOnce("plain failure");
await expect(tools.inspect_premiere_events_uxp.handler({ action: "wait", timeout_ms: 0 }))
.resolves.toEqual({ success: false, error: "plain failure" });
await tools.wait_for_host_readiness_uxp.handler({ action: "snapshot" });
expect(request).toHaveBeenLastCalledWith("readiness.snapshot", {});
await tools.wait_for_host_readiness_uxp.handler({ action: "snapshot", sequence_id: "sequence-1" });
expect(request).toHaveBeenLastCalledWith("readiness.snapshot", { sequenceId: "sequence-1" });
await tools.wait_for_host_readiness_uxp.handler({ action: "analysis" });
expect(request).toHaveBeenLastCalledWith(
"readiness.analysis.wait", {}, { minimumTimeoutMs: 35000 },
);
await tools.wait_for_host_readiness_uxp.handler({ action: "operation" });
expect(request).toHaveBeenLastCalledWith(
"readiness.operation.wait", {}, { minimumTimeoutMs: 35000 },
);
await expect(tools.wait_for_host_readiness_uxp.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported readiness action: unsupported" });
});
it("covers every project-session route and its optional argument mappings", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_project_sessions_uxp;
await tool.handler({ action: "list" });
expect(request).toHaveBeenLastCalledWith("project.sessions.list", {});
await tool.handler({ action: "list", include_paths: false });
expect(request).toHaveBeenLastCalledWith("project.sessions.list", { includePaths: false });
await tool.handler({ action: "validate", path: "C:/work/source.prproj" });
expect(request).toHaveBeenLastCalledWith("project.sessions.validate", { path: "C:/work/source.prproj" });
await tool.handler({ action: "create", path: "C:/work/new.prproj" });
expect(request).toHaveBeenLastCalledWith("project.sessions.create", { path: "C:/work/new.prproj" });
await tool.handler({
action: "create", path: "C:/work/new.prproj", confirm_external_write: true,
confirm_overwrite: false, operation_id: "create-1",
});
expect(request).toHaveBeenLastCalledWith("project.sessions.create", {
path: "C:/work/new.prproj", confirmExternalWrite: true,
confirmOverwrite: false, operationId: "create-1",
});
await tool.handler({ action: "open", path: "C:/work/open.prproj" });
expect(request).toHaveBeenLastCalledWith("project.sessions.open", { path: "C:/work/open.prproj" });
await tool.handler({
action: "open", path: "C:/work/open.prproj", show_dialogs: false,
add_to_mru: true, operation_id: "open-1",
});
expect(request).toHaveBeenLastCalledWith("project.sessions.open", {
path: "C:/work/open.prproj", showDialogs: false, addToMru: true, operationId: "open-1",
});
await tool.handler({ action: "save" });
expect(request).toHaveBeenLastCalledWith("project.sessions.save", {});
await tool.handler({ action: "save_as", path: "C:/work/copy.prproj" });
expect(request).toHaveBeenLastCalledWith("project.sessions.saveAs", { path: "C:/work/copy.prproj" });
await tool.handler({
action: "save_as", project_id: "project-1", expected_path: "C:/work/source.prproj",
path: "C:/work/copy.prproj", confirm_external_write: true,
confirm_overwrite: true, operation_id: "save-as-1",
});
expect(request).toHaveBeenLastCalledWith("project.sessions.saveAs", {
projectId: "project-1", expectedPath: "C:/work/source.prproj", operationId: "save-as-1",
path: "C:/work/copy.prproj", confirmExternalWrite: true, confirmOverwrite: true,
});
await tool.handler({ action: "branch_copies", paths: ["C:/work/branch.prproj"] });
expect(request).toHaveBeenLastCalledWith("project.sessions.branchCopies", {
paths: ["C:/work/branch.prproj"],
});
await tool.handler({ action: "close" });
expect(request).toHaveBeenLastCalledWith("project.sessions.close", {});
await tool.handler({
action: "close", project_id: "project-1", expected_path: "C:/work/source.prproj",
save_before_close: false, confirm_close: true, confirm_discard_unsaved: true,
operation_id: "close-1",
});
expect(request).toHaveBeenLastCalledWith("project.sessions.close", {
projectId: "project-1", expectedPath: "C:/work/source.prproj", operationId: "close-1",
saveBeforeClose: false, confirmClose: true, confirmDiscardUnsaved: true,
});
await expect(tool.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported project-session action: unsupported" });
});
it("covers growing-media status, lease defaults, resume targeting, and rejection", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_growing_media_uxp;
await tool.handler({ action: "status" });
expect(request).toHaveBeenLastCalledWith("growing.status", {});
await tool.handler({ action: "pause" });
expect(request).toHaveBeenLastCalledWith("growing.pause", {});
await tool.handler({ action: "resume" });
expect(request).toHaveBeenLastCalledWith("growing.resume", {});
await tool.handler({ action: "resume", project_id: "project-1", operation_id: "resume-1" });
expect(request).toHaveBeenLastCalledWith("growing.resume", {
projectId: "project-1", operationId: "resume-1",
});
await expect(tool.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported growing-media action: unsupported" });
});
it("covers checkpoint reads, typed writes, clears, and optional owner targeting", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_workflow_checkpoints_uxp;
await tool.handler({ action: "has", name: "render.pass" });
expect(request).toHaveBeenLastCalledWith("checkpoint.has", { name: "render.pass" });
await tool.handler({ action: "get", name: "render.pass" });
expect(request).toHaveBeenLastCalledWith("checkpoint.get", { name: "render.pass" });
await tool.handler({
action: "get", owner: "project", sequence_id: "sequence-1",
expected_owner_id: "project-1", name: "render.pass", value_type: "int",
});
expect(request).toHaveBeenLastCalledWith("checkpoint.get", {
owner: "project", sequenceId: "sequence-1", expectedOwnerId: "project-1",
name: "render.pass", valueType: "int",
});
await tool.handler({ action: "set", name: "render.pass" });
expect(request).toHaveBeenLastCalledWith("checkpoint.set", { name: "render.pass" });
await tool.handler({ action: "clear", name: "render.pass" });
expect(request).toHaveBeenLastCalledWith("checkpoint.clear", { name: "render.pass" });
await tool.handler({ action: "clear", name: "render.pass", operation_id: "clear-1" });
expect(request).toHaveBeenLastCalledWith("checkpoint.clear", {
name: "render.pass", operationId: "clear-1",
});
await expect(tool.handler({ action: "unsupported", name: "render.pass" }))
.resolves.toEqual({ success: false, error: "Unsupported checkpoint action: unsupported" });
});
it("covers every media-health route with redacted defaults and explicit options", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).maintain_media_health_uxp;
await tool.handler({ action: "inspect" });
expect(request).toHaveBeenLastCalledWith("media.health.inspect", {});
await tool.handler({ action: "inspect", project_item_ids: ["clip-1"], include_paths: false, include_media_timing: true });
expect(request).toHaveBeenLastCalledWith("media.health.inspect", {
projectItemIds: ["clip-1"], includePaths: false, includeMediaTiming: true,
});
await tool.handler({ action: "refresh" });
expect(request).toHaveBeenLastCalledWith("media.health.refresh", {});
await tool.handler({
action: "refresh", project_item_ids: ["clip-1"], expected_offline: true,
operation_id: "refresh-1",
});
expect(request).toHaveBeenLastCalledWith("media.health.refresh", {
projectItemIds: ["clip-1"], expectedOffline: true, operationId: "refresh-1",
});
await tool.handler({ action: "set_offline" });
expect(request).toHaveBeenLastCalledWith("media.health.setOffline", {});
await tool.handler({ action: "find_by_media_path" });
expect(request).toHaveBeenLastCalledWith("media.health.findByPath", {});
await tool.handler({
action: "find_by_media_path", project_item_id: "clip-1", match_path: "C:/media/a.mov",
ignore_subclips: true, include_paths: true,
});
expect(request).toHaveBeenLastCalledWith("media.health.findByPath", {
projectItemId: "clip-1", matchPath: "C:/media/a.mov",
ignoreSubclips: true, includePaths: true,
});
await expect(tool.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported media-health action: unsupported" });
});
it("covers source-media timing inspection, guarded start mapping, and rejection", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_source_media_timing_uxp;
await tool.handler({ action: "inspect", project_item_id: "clip-1" });
expect(request).toHaveBeenLastCalledWith("source.mediaTiming.inspect", { projectItemId: "clip-1" });
await tool.handler({ action: "set_start", project_item_id: "clip-1" });
expect(request).toHaveBeenLastCalledWith("source.mediaTiming.setStart", { projectItemId: "clip-1" });
await tool.handler({
action: "set_start", project_item_id: "clip-1", start_seconds: 12,
expected_timing: { start_seconds: 10, duration_seconds: 60 }, confirm_set_start: false,
});
expect(request).toHaveBeenLastCalledWith("source.mediaTiming.setStart", {
projectItemId: "clip-1", expectedTiming: { startSeconds: 10, durationSeconds: 60 },
startSeconds: 12, confirmSetStart: false,
});
await expect(tool.handler({ action: "unsupported", project_item_id: "clip-1" }))
.resolves.toEqual({ success: false, error: "Unsupported source-media timing action: unsupported" });
});
it("covers source-media override inspection, exact snapshot mapping, and rejection", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_source_media_overrides_uxp;
await tool.handler({ action: "inspect", project_item_id: "clip-1" });
expect(request).toHaveBeenLastCalledWith("source.mediaOverrides.inspect", { projectItemId: "clip-1" });
await tool.handler({ action: "update", project_item_id: "clip-1" });
expect(request).toHaveBeenLastCalledWith("source.mediaOverrides.update", { projectItemId: "clip-1" });
await tool.handler({
action: "update", project_item_id: "clip-1",
expected_overrides: { project_guid: "project-1", frame_rate: 23.976, pixel_aspect_ratio: 1 },
pixel_aspect_ratio: { numerator: 4, denominator: 3 }, confirm_media_interpretation: false,
operation_id: "source-override-1",
});
expect(request).toHaveBeenLastCalledWith("source.mediaOverrides.update", {
projectItemId: "clip-1",
expectedOverrides: { projectGuid: "project-1", frameRate: 23.976, pixelAspectRatio: 1 },
pixelAspectRatio: { numerator: 4, denominator: 3 },
confirmMediaInterpretation: false, operationId: "source-override-1",
});
await expect(tool.handler({ action: "unsupported", project_item_id: "clip-1" }))
.resolves.toEqual({ success: false, error: "Unsupported source-media override action: unsupported" });
});
it("covers track-state inspection, mute defaults, and unsupported actions", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_track_state_uxp;
await tool.handler({ action: "inspect" });
expect(request).toHaveBeenLastCalledWith("track.state.inspect", {});
await tool.handler({
action: "inspect", sequence_id: "sequence-1", expected_sequence_id: "sequence-1",
media_type: "all", track_indices: [0, 1],
});
expect(request).toHaveBeenLastCalledWith("track.state.inspect", {
sequenceId: "sequence-1", expectedSequenceId: "sequence-1",
mediaType: "all", trackIndices: [0, 1],
});
await tool.handler({ action: "set_mute" });
expect(request).toHaveBeenLastCalledWith("track.state.set", {});
await expect(tool.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported track-state action: unsupported" });
});
it("covers source-clip inspection, complete mappings, absent items, and rejection", async () => {
const request = vi.fn().mockResolvedValue({ ok: true });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpNextWorkflowTools(bridge).manage_source_clip_uxp;
await tool.handler({ action: "inspect" });
expect(request).toHaveBeenLastCalledWith("source.clip.inspect", { items: undefined });
await tool.handler({ action: "inspect", items: [{}] });
expect(request).toHaveBeenLastCalledWith("source.clip.inspect", { items: [{}] });
await tool.handler({
action: "inspect",
items: [{
project_item_id: "clip-1", media_type: "audio",
expected_in_seconds: 1, expected_out_seconds: 10,
in_seconds: 2, out_seconds: 9, clear_in_out: false, scale_to_frame: true,
}],
});
expect(request).toHaveBeenLastCalledWith("source.clip.inspect", {
items: [{
projectItemId: "clip-1", mediaType: "audio",
expectedInSeconds: 1, expectedOutSeconds: 10,
inSeconds: 2, outSeconds: 9, clearInOut: false, scaleToFrame: true,
}],
});
await tool.handler({ action: "update" });
expect(request).toHaveBeenLastCalledWith("source.clip.update", { items: undefined });
await expect(tool.handler({ action: "unsupported" }))
.resolves.toEqual({ success: false, error: "Unsupported source-clip action: unsupported" });
});
});
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpObjectMaskAuditWorkflowTools } from "../../src/tools/uxp-object-mask-audit-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public documented UXP object-mask audit tool", () => {
it("publishes a bounded read-only schema and maps only explicit audit selectors", async () => {
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1" });
const tool = getUxpObjectMaskAuditWorkflowTools({ request } as unknown as UxpWebSocketBridge).audit_object_masks_uxp;
expect(tool.parameters).toMatchObject({
type: "object", additionalProperties: false,
properties: {
expected_project_guid: { minLength: 1, maxLength: 512 },
sequence_ids: { minItems: 1, maxItems: 64, uniqueItems: true, items: { minLength: 1, maxLength: 512 } },
},
});
expect(tool.description).toContain("not mask count");
expect(tool.operationalCapability).toMatchObject({ backend: "UXP", minimumPremiereVersion: "26.3", hostVerificationRequired: true });
await tool.handler({ expected_project_guid: "project-1", sequence_ids: ["sequence-b", "sequence-a"] });
await tool.handler({});
expect(request).toHaveBeenNthCalledWith(1, "objectMask.audit", {
expectedProjectGuid: "project-1", sequenceIds: ["sequence-b", "sequence-a"],
});
expect(request).toHaveBeenNthCalledWith(2, "objectMask.audit", {});
});
});
+126
View File
@@ -0,0 +1,126 @@
import { describe, expect, it, vi } from "vitest";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
import { getUxpTools } from "../../src/tools/uxp.js";
function createTools(result: unknown = { accepted: true }) {
const request = vi.fn().mockResolvedValue(result);
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
return { request, tools: getUxpTools(bridge) };
}
describe("UXP tool optional argument mappings", () => {
it("keeps an explicitly empty AAF options object distinct from omitted options", async () => {
const { request, tools } = createTools({ path: "/exports/mix.aaf" });
await expect(tools.export_aaf_uxp.handler({
output_file_path: "/exports/mix.aaf",
options: {},
})).resolves.toEqual({
success: true,
data: { backend: "uxp", result: { path: "/exports/mix.aaf" } },
});
expect(request).toHaveBeenCalledWith("interchange.aaf.export", {
outputFilePath: "/exports/mix.aaf",
options: {},
});
});
it("maps explicitly false AAF toggles and the optional operation id", async () => {
const { request, tools } = createTools();
await tools.export_aaf_uxp.handler({
output_file_path: "/exports/final.aaf",
operation_id: "aaf-optional-1",
options: {
mixdown_video: false,
explode_to_mono: false,
sample_rate: 48_000,
bits_per_sample: 24,
embed_audio: false,
audio_file_format: "wav",
trim_sources: false,
handle_frames: 0,
video_mixdown_preset_path: "/presets/none.epr",
render_audio_effects: false,
interleave_without_effects: false,
preserve_parent_folder: false,
},
});
expect(request).toHaveBeenCalledWith("interchange.aaf.export", {
outputFilePath: "/exports/final.aaf",
operationId: "aaf-optional-1",
options: {
mixdownVideo: false,
explodeToMono: false,
sampleRate: 48_000,
bitsPerSample: 24,
embedAudio: false,
audioFileFormat: "wav",
trimSources: false,
handleFrames: 0,
videoMixdownPresetPath: "/presets/none.epr",
renderAudioEffects: false,
interleaveWithoutEffects: false,
preserveParentFolder: false,
},
});
});
it("normalizes a non-Error transcript export failure while mapping name selection", async () => {
const { request, tools } = createTools();
request.mockRejectedValueOnce("host disconnected");
await expect(tools.get_clip_transcript_uxp.handler({
project_item_name: "Interview A",
})).resolves.toEqual({ success: false, error: "host disconnected" });
expect(request).toHaveBeenCalledWith("transcript.export", {
projectItemName: "Interview A",
});
});
it("rejects an invalid transcript before generating a preview token", async () => {
const { request, tools } = createTools({ projectItemId: "clip-9" });
await expect(tools.preview_transcript_edit_uxp.handler({
project_item_id: "clip-9",
transcript_revision: `sha256:${"a".repeat(64)}`,
deletions: [{ start_seconds: 1, end_seconds: 2 }],
})).resolves.toEqual({
success: false,
error: "Premiere returned an empty transcript",
});
expect(request).toHaveBeenCalledWith("transcript.export", { projectItemId: "clip-9" });
});
it("normalizes a non-Error transcript preview export failure", async () => {
const { request, tools } = createTools();
request.mockRejectedValueOnce(null);
await expect(tools.preview_transcript_edit_uxp.handler({
transcript_revision: `sha256:${"b".repeat(64)}`,
deletions: [{ start_seconds: 1, end_seconds: 2 }],
})).resolves.toEqual({ success: false, error: "null" });
});
it("maps the explicit sequence marker scope to the wire protocol spelling", async () => {
const { request, tools } = createTools();
await tools.list_markers_uxp.handler({ scope: "sequence" });
expect(request).toHaveBeenCalledWith("marker.list", { scope: "sequence" });
});
it("maps an explicit marker web-link read opt-in to the protocol", async () => {
const { request, tools } = createTools();
await tools.list_markers_uxp.handler({ scope: "sequence", include_web_links: true });
expect(request).toHaveBeenCalledWith("marker.list", { scope: "sequence", includeWebLinks: true });
});
it("maps an explicit marker raw-color read opt-in to the protocol", async () => {
const { request, tools } = createTools();
await tools.list_markers_uxp.handler({ scope: "sequence", include_color_values: true });
expect(request).toHaveBeenCalledWith("marker.list", { scope: "sequence", includeColorValues: true });
});
});
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpRippleDeleteWorkflowTools } from "../../src/tools/uxp-ripple-delete-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const target = { project_item_id: "target-1", start_seconds: 0, end_seconds: 10, in_seconds: 20, out_seconds: 30, duration_seconds: 10, speed: 1, reversed: false };
const following = { project_item_id: "following-1", start_seconds: 10, end_seconds: 20, in_seconds: 40, out_seconds: 50, duration_seconds: 10, speed: 1, reversed: false };
describe("public guarded contiguous track-item ripple-delete MCP tool", () => {
it("uses closed target and successor snapshot schemas and translates snake_case apply arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const tool = getUxpRippleDeleteWorkflowTools({ request } as unknown as UxpWebSocketBridge).ripple_delete_track_item_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["action", "media_type", "track_index", "clip_index"],
properties: { action: { enum: ["inspect", "apply"] }, expected_snapshot: { additionalProperties: false, required: ["project_guid", "sequence_id", "media_type", "track_index", "clip_index", "track_item_count", "target", "following"] } },
});
await tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 0, confirm_ripple_delete: true, operation_id: "ripple-tool-1",
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 0, track_item_count: 2, target, following },
});
expect(request).toHaveBeenCalledWith("trackItem.rippleDelete", {
mediaType: "video", trackIndex: 0, clipIndex: 0, confirmRippleDelete: true, operationId: "ripple-tool-1",
expectedSnapshot: {
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 0, trackItemCount: 2,
target: { projectItemId: "target-1", startSeconds: 0, endSeconds: 10, inSeconds: 20, outSeconds: 30, durationSeconds: 10, speed: 1, reversed: false },
following: { projectItemId: "following-1", startSeconds: 10, endSeconds: 20, inSeconds: 40, outSeconds: 50, durationSeconds: 10, speed: 1, reversed: false },
},
});
});
it("does not silently drop unknown fields from a reviewed successor snapshot", async () => {
const request = vi.fn();
const tool = getUxpRippleDeleteWorkflowTools({ request } as unknown as UxpWebSocketBridge).ripple_delete_track_item_uxp;
await expect(tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 0,
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 0, track_item_count: 2, target, following: { ...following, unexpected: true } },
})).rejects.toThrow("expected_snapshot.following has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpSequencePreviewFrameWorkflowTools } from "../../src/tools/uxp-sequence-preview-frame-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const snapshot = {
project_guid: "project-1", sequence_id: "sequence-1", preview_width: 640, preview_height: 360,
};
describe("public guarded sequence preview-frame MCP tool", () => {
it("uses a closed complete snapshot and translates the update arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const tool = getUxpSequencePreviewFrameWorkflowTools({ request } as unknown as UxpWebSocketBridge).manage_sequence_preview_frame_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["action", "sequence_id"],
properties: { action: { enum: ["inspect", "update"] }, expected_snapshot: { additionalProperties: false, required: Object.keys(snapshot) } },
});
await tool.handler({
action: "update", sequence_id: "sequence-1", preview_width: 1920, preview_height: 1080,
expected_snapshot: snapshot, confirm_set_preview_frame: true, operation_id: "preview-frame-tool-1",
});
expect(request).toHaveBeenCalledWith("sequence.previewFrame.update", {
sequenceId: "sequence-1", previewWidth: 1920, previewHeight: 1080, confirmSetPreviewFrame: true, operationId: "preview-frame-tool-1",
expectedSnapshot: { projectGuid: "project-1", sequenceId: "sequence-1", previewWidth: 640, previewHeight: 360 },
});
});
it("does not silently drop unknown reviewed fields", async () => {
const request = vi.fn();
const tool = getUxpSequencePreviewFrameWorkflowTools({ request } as unknown as UxpWebSocketBridge).manage_sequence_preview_frame_uxp;
await expect(tool.handler({
action: "update", sequence_id: "sequence-1", preview_width: 1920, preview_height: 1080,
expected_snapshot: { ...snapshot, unexpected: true }, confirm_set_preview_frame: true, operation_id: "preview-frame-invalid",
})).rejects.toThrow("expected_snapshot has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpSlideWorkflowTools } from "../../src/tools/uxp-slide-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const item = { start_seconds: 0, end_seconds: 10, in_seconds: 0, out_seconds: 10, duration_seconds: 10, speed: 1, reversed: false };
describe("public guarded track-item slide MCP tool", () => {
it("uses a closed complete three-item snapshot schema and translates snake_case apply arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const tool = getUxpSlideWorkflowTools({ request } as unknown as UxpWebSocketBridge).slide_track_item_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["action", "media_type", "track_index", "clip_index"],
properties: { action: { enum: ["inspect", "apply"] }, slide_by_seconds: { minimum: -60, maximum: 60 }, expected_snapshot: { additionalProperties: false, required: ["project_guid", "sequence_id", "media_type", "track_index", "clip_index", "previous", "target", "following"] } },
});
await tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 1, slide_by_seconds: 2, confirm_slide: true, operation_id: "slide-tool-1",
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 1, previous: item, target: { ...item, start_seconds: 10, end_seconds: 20, in_seconds: 20, out_seconds: 30 }, following: { ...item, start_seconds: 20, end_seconds: 30, in_seconds: 30, out_seconds: 40 } },
});
expect(request).toHaveBeenCalledWith("trackItem.slide", {
mediaType: "video", trackIndex: 0, clipIndex: 1, slideBySeconds: 2, confirmSlide: true, operationId: "slide-tool-1",
expectedSnapshot: {
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 1,
previous: { startSeconds: 0, endSeconds: 10, inSeconds: 0, outSeconds: 10, durationSeconds: 10, speed: 1, reversed: false },
target: { startSeconds: 10, endSeconds: 20, inSeconds: 20, outSeconds: 30, durationSeconds: 10, speed: 1, reversed: false },
following: { startSeconds: 20, endSeconds: 30, inSeconds: 30, outSeconds: 40, durationSeconds: 10, speed: 1, reversed: false },
},
});
});
it("does not silently drop unknown fields from a reviewed neighbour snapshot", async () => {
const request = vi.fn();
const tool = getUxpSlideWorkflowTools({ request } as unknown as UxpWebSocketBridge).slide_track_item_uxp;
await expect(tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 1,
expected_snapshot: { project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 1, previous: { ...item, unexpected: true }, target: item, following: item },
})).rejects.toThrow("expected_snapshot.previous has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpSlipWorkflowTools } from "../../src/tools/uxp-slip-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public guarded track-item slip MCP tool", () => {
it("uses a closed complete snapshot schema and translates snake_case apply arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request } as unknown as UxpWebSocketBridge;
const tool = getUxpSlipWorkflowTools(bridge).slip_track_item_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false,
required: ["action", "media_type", "track_index", "clip_index"],
properties: {
action: { enum: ["inspect", "apply"] },
slip_by_seconds: { minimum: -60, maximum: 60 },
expected_snapshot: {
additionalProperties: false,
required: ["project_guid", "sequence_id", "media_type", "track_index", "clip_index", "start_seconds", "end_seconds", "in_seconds", "out_seconds", "duration_seconds", "speed", "reversed"],
},
},
});
await tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 1,
expected_snapshot: {
project_guid: "project-1", sequence_id: "sequence-1", media_type: "video", track_index: 0, clip_index: 1,
start_seconds: 10, end_seconds: 20, in_seconds: 30, out_seconds: 40, duration_seconds: 10, speed: 1, reversed: false,
},
slip_by_seconds: -2, confirm_slip: true, operation_id: "slip-tool-1",
});
expect(request).toHaveBeenCalledWith("trackItem.slip", {
mediaType: "video", trackIndex: 0, clipIndex: 1,
expectedSnapshot: {
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 1,
startSeconds: 10, endSeconds: 20, inSeconds: 30, outSeconds: 40, durationSeconds: 10, speed: 1, reversed: false,
},
slipBySeconds: -2, confirmSlip: true, operationId: "slip-tool-1",
});
});
it("does not silently drop malformed or unknown reviewed snapshot fields", async () => {
const request = vi.fn();
const bridge = { request } as unknown as UxpWebSocketBridge;
const tool = getUxpSlipWorkflowTools(bridge).slip_track_item_uxp;
await expect(tool.handler({
action: "apply", media_type: "video", track_index: 0, clip_index: 0,
expected_snapshot: { project_guid: "project-1", unexpected: true },
})).rejects.toThrow("expected_snapshot has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpSourceMediaProvenanceWorkflowTools } from "../../src/tools/uxp-source-media-provenance-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public bounded source-media provenance MCP tool", () => {
it("requires an explicit path opt-in schema and translates only supplied flags", async () => {
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1" });
const tool = getUxpSourceMediaProvenanceWorkflowTools({
request,
} as unknown as UxpWebSocketBridge).inspect_source_media_provenance_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false,
required: ["project_item_id"],
properties: {
project_item_id: { minLength: 1, maxLength: 512 },
include_media_file_path: { type: "boolean" },
include_originating_project_path: { type: "boolean" },
},
});
await tool.handler({
project_item_id: "clip-1", include_media_file_path: true,
});
expect(request).toHaveBeenCalledWith("source.provenance.inspect", {
projectItemId: "clip-1", includeMediaFilePath: true,
});
});
it("preserves an explicit false flag so the host can enforce the paired opt-in", async () => {
const request = vi.fn().mockResolvedValue({});
const tool = getUxpSourceMediaProvenanceWorkflowTools({
request,
} as unknown as UxpWebSocketBridge).inspect_source_media_provenance_uxp;
await tool.handler({
project_item_id: "clip-1", include_media_file_path: false, include_originating_project_path: true,
});
expect(request).toHaveBeenCalledWith("source.provenance.inspect", {
projectItemId: "clip-1", includeMediaFilePath: false, includeOriginatingProjectPath: true,
});
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpSourceProxyWorkflowTools } from "../../src/tools/uxp-source-proxy-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public bounded source-proxy MCP tool", () => {
it("documents a read-only explicit target and translates only a supplied proxy-path opt-in", async () => {
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1" });
const tool = getUxpSourceProxyWorkflowTools({ request } as unknown as UxpWebSocketBridge).inspect_source_proxy_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false,
required: ["project_item_id"],
properties: {
project_item_id: { minLength: 1, maxLength: 512 },
include_proxy_path: { type: "boolean" },
},
});
expect(tool.annotations).toEqual({ readOnlyHint: true, destructiveHint: false });
await expect(tool.handler({ project_item_id: "clip-1" })).resolves.toMatchObject({ success: true });
expect(request).toHaveBeenLastCalledWith("source.proxy.inspect", { projectItemId: "clip-1" });
await expect(tool.handler({ project_item_id: "clip-1", include_proxy_path: true })).resolves.toMatchObject({ success: true });
expect(request).toHaveBeenLastCalledWith("source.proxy.inspect", { projectItemId: "clip-1", includeProxyPath: true });
expect(tool.description).toContain("does not enumerate folders");
expect(tool.description).toContain("does not access the filesystem");
expect(tool.description).toContain("licensed-host behavior");
});
it("returns a truthful bridge error without a fallback backend", async () => {
const tool = getUxpSourceProxyWorkflowTools({
request: vi.fn().mockRejectedValue(new Error("source.proxy.inspect unavailable")),
} as unknown as UxpWebSocketBridge).inspect_source_proxy_uxp;
await expect(tool.handler({ project_item_id: "clip-1" })).resolves.toEqual({
success: false, error: "source.proxy.inspect unavailable",
});
});
});
@@ -0,0 +1,19 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpTickTimeArithmeticWorkflowTools } from "../../src/tools/uxp-tick-time-arithmetic-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public native TickTime arithmetic MCP tool", () => {
it("uses a closed read-only schema and preserves canonical tick inputs", async () => {
const request = vi.fn().mockResolvedValue({ result: { ticks: "3", seconds: 0 } });
const tool = getUxpTickTimeArithmeticWorkflowTools({ request } as unknown as UxpWebSocketBridge).calculate_tick_time_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["operation", "base_ticks"],
properties: { operation: { enum: ["add", "subtract", "multiply", "divide"] }, base_ticks: { pattern: "^(?:0|-?[1-9][0-9]{0,17})$" } },
});
expect(tool.annotations).toEqual({ readOnlyHint: true, destructiveHint: false });
await tool.handler({ operation: "add", base_ticks: "1", operand_ticks: "2" });
await tool.handler({ operation: "multiply", base_ticks: "3", factor: 4 });
expect(request).toHaveBeenNthCalledWith(1, "time.tickArithmetic.inspect", { operation: "add", baseTicks: "1", operandTicks: "2" });
expect(request).toHaveBeenNthCalledWith(2, "time.tickArithmetic.inspect", { operation: "multiply", baseTicks: "3", factor: 4 });
});
});
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpTimelineSourceLabelWorkflowTools } from "../../src/tools/uxp-timeline-source-label-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const snapshot = {
project_guid: "project-1", sequence_id: "sequence-1", media_type: "video" as const, track_index: 0, clip_index: 0,
track_item_count: 1, source_project_item_id: "source-1", source_color_label_index: 3, start_seconds: 12, end_seconds: 20,
};
describe("public guarded timeline source-label MCP tool", () => {
it("uses a closed complete source snapshot and translates update arguments", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const tool = getUxpTimelineSourceLabelWorkflowTools({ request } as unknown as UxpWebSocketBridge).manage_timeline_source_label_uxp;
expect(tool.parameters).toMatchObject({
additionalProperties: false, required: ["action", "media_type", "track_index", "clip_index"],
properties: { action: { enum: ["inspect", "update"] }, expected_snapshot: { additionalProperties: false, required: Object.keys(snapshot) } },
});
await tool.handler({
action: "update", media_type: "video", track_index: 0, clip_index: 0, color_index: 9,
expected_snapshot: snapshot, confirm_set_label: true, operation_id: "source-label-tool-1",
});
expect(request).toHaveBeenCalledWith("timeline.sourceLabel.update", {
mediaType: "video", trackIndex: 0, clipIndex: 0, colorIndex: 9, confirmSetLabel: true, operationId: "source-label-tool-1",
expectedSnapshot: {
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 0,
trackItemCount: 1, sourceProjectItemId: "source-1", sourceColorLabelIndex: 3, startSeconds: 12, endSeconds: 20,
},
});
});
it("does not silently drop unknown reviewed source fields", async () => {
const request = vi.fn();
const tool = getUxpTimelineSourceLabelWorkflowTools({ request } as unknown as UxpWebSocketBridge).manage_timeline_source_label_uxp;
await expect(tool.handler({
action: "update", media_type: "video", track_index: 0, clip_index: 0, color_index: 9,
expected_snapshot: { ...snapshot, unexpected: true }, confirm_set_label: true, operation_id: "source-label-tool-invalid",
})).rejects.toThrow("expected_snapshot has an unknown field: unexpected");
expect(request).not.toHaveBeenCalled();
});
});
+474
View File
@@ -0,0 +1,474 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpTools } from "../../src/tools/uxp.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
import { createServer } from "../../src/server.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
describe("UXP MCP tools", () => {
it("maps MCP arguments to the supported frame export command", async () => {
const request = vi.fn().mockResolvedValue({ path: "/tmp/frame.png" });
const bridge = {
request,
getState: vi.fn(),
} as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const result = await tools.export_frame_uxp.handler({
output_directory: "/tmp",
filename: "frame.png",
seconds: 2.5,
width: 1920,
height: 1080,
});
expect(request).toHaveBeenCalledWith("frame.export", {
outputDirectory: "/tmp",
filename: "frame.png",
seconds: 2.5,
width: 1920,
height: 1080,
});
expect(result).toEqual({
success: true,
data: { backend: "uxp", result: { path: "/tmp/frame.png" } },
});
});
it("exposes connection discovery without requiring a host request", async () => {
const state = { status: "listening", connected: false } as const;
const bridge = {
request: vi.fn(),
getState: vi.fn(() => state),
} as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).get_uxp_capabilities.handler();
expect(result).toEqual({ success: true, data: state });
expect(bridge.request).not.toHaveBeenCalled();
});
it("maps verified project and interchange workflows to versioned UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.create_sequence_with_preset_uxp.handler({
name: "Delivery",
preset_path: "/presets/hd.sqpreset",
operation_id: "sequence-1",
});
await tools.export_interchange_uxp.handler({
format: "otio",
output_file_path: "/exports/edit.otio",
operation_id: "export-1",
});
expect(request).toHaveBeenNthCalledWith(1, "sequence.createPreset", {
name: "Delivery", presetPath: "/presets/hd.sqpreset", operationId: "sequence-1",
});
expect(request).toHaveBeenNthCalledWith(2, "interchange.export", {
format: "otio", outputFilePath: "/exports/edit.otio", operationId: "export-1",
});
});
it("maps guarded empty sequence creation only after confirmation and with a replay key", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).create_empty_sequence_uxp;
await expect(tool.handler({
name: "Empty Assembly", confirm_non_undoable: false, operation_id: "empty-1",
})).resolves.toEqual({ success: false, error: "create_empty_sequence_uxp requires confirm_non_undoable: true" });
await expect(tool.handler({
name: "Empty Assembly", confirm_non_undoable: true, operation_id: "",
})).resolves.toEqual({ success: false, error: "create_empty_sequence_uxp requires operation_id for safe replay" });
expect(request).not.toHaveBeenCalled();
await expect(tool.handler({
name: "Empty Assembly", confirm_non_undoable: true, operation_id: "empty-1",
})).resolves.toEqual({ success: true, data: { backend: "uxp", result: { outcome: "verified" } } });
expect(request).toHaveBeenCalledWith("sequences.createEmpty", {
name: "Empty Assembly", confirmNonUndoable: true, operationId: "empty-1",
});
expect(tool.parameters).toMatchObject({
additionalProperties: false,
required: ["name", "confirm_non_undoable", "operation_id"],
});
});
it("maps native caption-track inspection to its documented UXP command", async () => {
const request = vi.fn().mockResolvedValue({ trackCount: 0, tracks: [] });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
await expect(getUxpTools(bridge).inspect_caption_tracks_uxp.handler()).resolves.toEqual({
success: true,
data: { backend: "uxp", result: { trackCount: 0, tracks: [] } },
});
expect(request).toHaveBeenCalledWith("captions.inspect", {});
});
it("maps guarded native Project-panel insertion-bin inspection to its documented UXP command", async () => {
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).inspect_project_insertion_bin_uxp;
await expect(tool.handler()).resolves.toEqual({
success: true,
data: { backend: "uxp", result: { projectGuid: "project-1" } },
});
expect(request).toHaveBeenCalledWith("project.insertionBin.inspect", {});
expect(tool.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: {},
});
});
it("maps opt-in installed MOGRT-directory disclosure to its documented UXP command", async () => {
const request = vi.fn().mockResolvedValue({ pathDisclosure: "requested" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).inspect_installed_mogrt_directory_uxp;
await expect(tool.handler()).resolves.toEqual({
success: true,
data: { backend: "uxp", result: { pathDisclosure: "requested" } },
});
await tool.handler({ include_path: true });
expect(request).toHaveBeenNthCalledWith(1, "graphics.mogrtPath.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "graphics.mogrtPath.inspect", { includePath: true });
expect(tool.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: { include_path: { type: "boolean" } },
});
});
it("maps guarded sequence-range inspection and updates to documented UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.manage_sequence_range_uxp.handler({ action: "inspect" });
await tools.manage_sequence_range_uxp.handler({
action: "update",
expected_sequence_guid: "sequence-1",
expected_range: { in_seconds: 1, out_seconds: 10, zero_point_seconds: 3600, end_seconds: 120 },
updates: { in_seconds: 2, zero_point_seconds: 7200 },
operation_id: "range-1",
});
expect(request).toHaveBeenNthCalledWith(1, "sequence.range.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "sequence.range.update", {
expectedSequenceGuid: "sequence-1",
expectedRange: { inSeconds: 1, outSeconds: 10, zeroPointSeconds: 3600, endSeconds: 120 },
updates: { inSeconds: 2, zeroPointSeconds: 7200 },
operationId: "range-1",
});
expect(tools.manage_sequence_range_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { type: "string", enum: ["inspect", "update"] },
expected_sequence_guid: { type: "string" },
expected_range: {
type: "object",
additionalProperties: false,
required: ["in_seconds", "out_seconds", "zero_point_seconds", "end_seconds"],
},
updates: { type: "object", additionalProperties: false },
operation_id: { type: "string" },
},
});
});
it("maps guarded sequence-playhead inspection and settings to documented UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.manage_sequence_playhead_uxp.handler({ action: "inspect" });
await tools.manage_sequence_playhead_uxp.handler({
action: "set",
expected_sequence_guid: "sequence-1",
expected_position_seconds: 3,
position_seconds: 8,
operation_id: "playhead-1",
});
expect(request).toHaveBeenNthCalledWith(1, "sequence.playhead.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "sequence.playhead.set", {
expectedSequenceGuid: "sequence-1",
expectedPositionSeconds: 3,
positionSeconds: 8,
operationId: "playhead-1",
});
expect(tools.manage_sequence_playhead_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { type: "string", enum: ["inspect", "set"] },
expected_sequence_guid: { type: "string" },
expected_position_seconds: { type: "number", minimum: 0, maximum: 86400 },
position_seconds: { type: "number", minimum: 0, maximum: 86400 },
operation_id: { type: "string" },
},
});
});
it("maps bounded guarded app-preference inspection and direct writes to documented UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.manage_app_preferences_uxp.handler({ action: "inspect" });
await tools.manage_app_preferences_uxp.handler({
action: "set",
preference: "auto_peak_generation",
expected_value: "0",
value: "1",
persistence: "persistent",
confirm_preference_change: true,
operation_id: "app-preference-1",
});
expect(request).toHaveBeenNthCalledWith(1, "preferences.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "preferences.set", {
preference: "auto_peak_generation",
expectedValue: "0",
value: "1",
persistence: "persistent",
confirmPreferenceChange: true,
operationId: "app-preference-1",
});
expect(tools.manage_app_preferences_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { type: "string", enum: ["inspect", "set"] },
preference: { enum: ["auto_peak_generation", "import_workspace", "show_quickstart_dialog"] },
expected_value: { type: "string", maxLength: 1024 },
value: { type: "string", maxLength: 1024 },
persistence: { enum: ["persistent", "non_persistent"] },
confirm_preference_change: { type: "boolean" },
operation_id: { type: "string", minLength: 1, maxLength: 128 },
},
});
});
it("rejects incomplete or unconfirmed app-preference writes before bridge access", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).manage_app_preferences_uxp;
await expect(tool.handler({ action: "set", preference: "import_workspace" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("requires preference") });
await expect(tool.handler({
action: "set", preference: "import_workspace", expected_value: "1", value: "0", persistence: "persistent", operation_id: "app-preference-unconfirmed",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("confirm_preference_change") });
expect(request).not.toHaveBeenCalled();
});
it("maps bounded native sequence-timing inspection to its documented UXP command", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.inspect_sequence_timing_uxp.handler({});
expect(request).toHaveBeenCalledWith("sequence.timing.inspect", {});
expect(tools.inspect_sequence_timing_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: {},
});
});
it("maps native frame alignment with a closed action-specific argument contract", async () => {
const request = vi.fn().mockResolvedValue({ verificationBoundary: "native_ticktime_value_readback" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.inspect_frame_alignment_uxp.handler({ action: "align", frame_rate: 30000 / 1001, seconds: 1.03 });
await tools.inspect_frame_alignment_uxp.handler({ action: "frame", frame_rate: 24, frame_count: 42 });
expect(request).toHaveBeenNthCalledWith(1, "time.frameAlignment.inspect", { action: "align", frameRate: 30000 / 1001, seconds: 1.03 });
expect(request).toHaveBeenNthCalledWith(2, "time.frameAlignment.inspect", { action: "frame", frameRate: 24, frameCount: 42 });
expect(tools.inspect_frame_alignment_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["action", "frame_rate"],
properties: {
action: { enum: ["align", "frame"] },
frame_rate: { minimum: 1, maximum: 240 },
seconds: { minimum: 0, maximum: 86400 },
frame_count: { minimum: 0, maximum: 20736000 },
},
});
});
it("maps non-active native sequence timing inspection to its guarded GUID command", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.inspect_sequence_timing_by_guid_uxp.handler({ sequence_guid: "sequence-2" });
expect(request).toHaveBeenCalledWith("sequence.timingByGuid.inspect", { sequenceGuid: "sequence-2" });
expect(tools.inspect_sequence_timing_by_guid_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false, required: ["sequence_guid"],
properties: { sequence_guid: { minLength: 1, maxLength: 512 } },
});
});
it("maps bounded native timeline-structure inspection and documents scoped track counts", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.inspect_sequence_structure_uxp.handler({
sequence_id: "sequence-1", expected_sequence_id: "sequence-1", media_type: "video",
track_indices: [1, 0], include_empty_tracks: true, include_source_project_items: true,
include_source_project_item_classification: true, include_source_project_item_content_type: true,
include_source_nested_sequence_identity: true, max_items: 8,
});
expect(request).toHaveBeenCalledWith("timeline.structure.inspect", {
sequenceId: "sequence-1", expectedSequenceId: "sequence-1", mediaType: "video",
trackIndices: [1, 0], includeEmptyTracks: true, includeSourceProjectItems: true,
includeSourceProjectItemClassification: true, includeSourceProjectItemContentType: true,
includeSourceNestedSequenceIdentity: true, maxItems: 8,
});
expect(tools.inspect_sequence_structure_uxp.description).toContain(
"track_counts contains only the requested media types",
);
expect(tools.inspect_sequence_structure_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: {
sequence_id: { type: "string", minLength: 1, maxLength: 128 },
expected_sequence_id: { type: "string", minLength: 1, maxLength: 128 },
media_type: { type: "string", enum: ["all", "video", "audio"] },
track_indices: { type: "array", minItems: 1, maxItems: 64, uniqueItems: true },
include_empty_tracks: { type: "boolean" },
include_source_project_items: { type: "boolean" },
include_source_project_item_classification: { type: "boolean" },
include_source_project_item_content_type: { type: "boolean" },
include_source_nested_sequence_identity: { type: "boolean" },
max_items: { type: "integer", minimum: 1, maximum: 512 },
},
});
});
it("maps guarded native transition inspection and mutation arguments to documented UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "committed_unverified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const expectedTarget = {
sequence_guid: "sequence-1", video_track_index: 2, clip_index: 3, project_item_id: "project-item-1",
start_seconds: 10, end_seconds: 20, position: "end" as const, transition_present: false,
};
await tools.lift_selection_uxp.handler({ expected_sequence_guid: "sequence-1", operation_id: "lift-1" });
await tools.list_video_transitions_uxp.handler();
await tools.inspect_video_transition_uxp.handler({ video_track_index: 2, clip_index: 3, position: "end" });
await tools.add_video_transition_uxp.handler({
video_track_index: 2, clip_index: 3, match_name: "CrossDissolve", position: "end",
duration_seconds: 0.5, force_single_sided: true, transition_alignment: 1, expected_target: expectedTarget, operation_id: "transition-add-1",
});
await tools.remove_video_transition_uxp.handler({
video_track_index: 2, clip_index: 3, position: "end", expected_target: { ...expectedTarget, transition_present: true }, operation_id: "transition-remove-1",
});
expect(request).toHaveBeenNthCalledWith(1, "timeline.selection.lift", { expectedSequenceGuid: "sequence-1", operationId: "lift-1" });
expect(request).toHaveBeenNthCalledWith(2, "transition.video.list", {});
expect(request).toHaveBeenNthCalledWith(3, "transition.video.inspect", {
videoTrackIndex: 2, clipIndex: 3, position: "end",
});
expect(request).toHaveBeenNthCalledWith(4, "transition.video.add", {
videoTrackIndex: 2, clipIndex: 3, matchName: "CrossDissolve", position: "end", durationSeconds: 0.5,
forceSingleSided: true, transitionAlignment: 1,
expectedTarget: {
sequenceGuid: "sequence-1", videoTrackIndex: 2, clipIndex: 3, projectItemId: "project-item-1",
startSeconds: 10, endSeconds: 20, position: "end", transitionPresent: false,
},
operationId: "transition-add-1",
});
expect(request).toHaveBeenNthCalledWith(5, "transition.video.remove", {
videoTrackIndex: 2, clipIndex: 3, position: "end", operationId: "transition-remove-1",
expectedTarget: {
sequenceGuid: "sequence-1", videoTrackIndex: 2, clipIndex: 3, projectItemId: "project-item-1",
startSeconds: 10, endSeconds: 20, position: "end", transitionPresent: true,
},
});
expect(tools.add_video_transition_uxp.parameters).toMatchObject({ required: expect.arrayContaining(["expected_target"]) });
expect(tools.remove_video_transition_uxp.parameters).toMatchObject({ required: expect.arrayContaining(["expected_target"]) });
});
it("returns transport errors through the normal tool envelope", async () => {
const bridge = {
request: vi.fn().mockRejectedValue(new Error("UXP bridge is not connected")),
getState: vi.fn(),
} as unknown as UxpWebSocketBridge;
const result = await getUxpTools(bridge).get_uxp_state.handler();
expect(result).toEqual({
success: false,
error: "UXP bridge is not connected",
});
});
it("registers UXP tools only when an adapter is supplied", async () => {
const bridge = {
request: vi.fn(),
getState: vi.fn(() => ({ status: "listening", connected: false })),
} as unknown as UxpWebSocketBridge;
const server = createServer({}, { uxpBridge: bridge });
const client = new Client({ name: "uxp-tool-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
const tools = await client.listTools();
expect(tools.tools.map((tool) => tool.name)).toEqual(
expect.arrayContaining([
"get_uxp_capabilities",
"get_uxp_state",
"inspect_project_uxp",
"inspect_project_insertion_bin_uxp",
"inspect_installed_mogrt_directory_uxp",
"inspect_sequence_timing_uxp",
"inspect_frame_alignment_uxp",
"calculate_tick_time_uxp",
"inspect_sequence_timing_by_guid_uxp",
"inspect_caption_tracks_uxp",
"manage_sequence_display_format_uxp",
"manage_sequence_range_uxp",
"manage_sequence_playhead_uxp",
"manage_app_preferences_uxp",
"save_project_uxp",
"create_sequence_with_preset_uxp",
"create_empty_sequence_uxp",
"export_interchange_uxp",
"get_transcript_languages_uxp",
"get_clip_transcript_uxp",
"search_clip_transcript_uxp",
"preview_transcript_edit_uxp",
"plan_transcript_rough_cut_uxp",
"detect_object_masks_uxp",
"audit_object_masks_uxp",
"configure_encoder_uxp",
"rename_track_uxp",
"create_subclip_uxp",
"list_markers_uxp",
"set_source_monitor_position_uxp",
"has_transcript_uxp",
"export_aaf_uxp",
"export_frame_uxp",
"lift_selection_uxp",
"list_video_transitions_uxp",
"inspect_video_transition_uxp",
"add_video_transition_uxp",
"remove_video_transition_uxp",
"inspect_project_panel_metadata_uxp",
"slide_track_item_uxp",
"duplicate_track_item_uxp",
"ripple_delete_track_item_uxp",
"manage_timeline_source_label_uxp",
"apply_editorial_organization_plan",
]),
);
// The default profile excludes two unsafe-script tools. The native
// transcript workflow and documented Premiere 26.3 tools add nineteen. The
// two stable workflow expansions, confirmed organization application, four
// bounded native migration adapters, beat-grid marker application, and
// guarded sequence-playhead control, native sequence-timing inspection, and
// guarded sequence-display-format updates and guarded Project-panel
// insertion-bin inspection, guarded empty-sequence creation, marker web-link
// inspection, Project-panel metadata inspection and guarded replacement,
// guarded app-preference control, source-media interpretation overrides,
// direct track-item identity inspection, targeted sequence timing, guarded
// three-item slides, append-only timeline duplication, guarded Project metadata
// schema-field creation, guarded contiguous ripple deletes, and guarded
// source-label updates, opt-in installed-MOGRT directory inspection, and
// bounded Object Mask audits, native TickTime arithmetic, and guarded
// explicit-sequence preview-frame updates plus unique serializable identity
// inspection add fifty consolidated UXP tools;
// connection verification and delivery conformance add two default-profile core tools.
expect(tools.tools).toHaveLength(440);
} finally {
await client.close();
await server.close();
}
});
});
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpUniqueIdentityWorkflowTools } from "../../src/tools/uxp-unique-identity-workflows.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
describe("public documented UXP unique serializable identity tool", () => {
it("publishes an exclusive bounded locator schema and maps only explicit selectors", async () => {
const request = vi.fn().mockResolvedValue({ projectGuid: "project-1" });
const tool = getUxpUniqueIdentityWorkflowTools({ request } as unknown as UxpWebSocketBridge)
.inspect_unique_object_identity_uxp;
expect(tool.parameters).toMatchObject({
type: "object",
additionalProperties: false,
properties: {
project_item_id: { minLength: 1, maxLength: 512 },
sequence_guid: { minLength: 1, maxLength: 512 },
expected_project_guid: { minLength: 1, maxLength: 512 },
expected_unique_id: { minLength: 1, maxLength: 512 },
},
oneOf: [{ required: ["project_item_id"] }, { required: ["sequence_guid"] }],
});
expect(tool.description).toContain("does not expose paths");
expect(tool.operationalCapability).toMatchObject({
backend: "UXP", minimumPremiereVersion: "26.3", hostVerificationRequired: true,
});
await tool.handler({ project_item_id: "item-1", expected_project_guid: "project-1", expected_unique_id: "unique-1" });
await tool.handler({ sequence_guid: "sequence-1" });
expect(request).toHaveBeenNthCalledWith(1, "object.uniqueIdentity.inspect", {
projectItemId: "item-1", expectedProjectGuid: "project-1", expectedUniqueId: "unique-1",
});
expect(request).toHaveBeenNthCalledWith(2, "object.uniqueIdentity.inspect", { sequenceGuid: "sequence-1" });
});
});
+368
View File
@@ -0,0 +1,368 @@
import { describe, expect, it, vi } from "vitest";
import { getUxpTools } from "../../src/tools/uxp.js";
import type { UxpWebSocketBridge } from "../../src/bridge/uxp-websocket-bridge.js";
const WORKFLOW_TOOLS = [
"manage_clip_effects_uxp",
"inspect_track_item_identity_uxp",
"batch_selected_clips_uxp",
"manage_timeline_selection_uxp",
"detect_scene_edits_uxp",
"manage_proxy_ingest_uxp",
"relink_offline_media_uxp",
"manage_metadata_uxp",
"inspect_project_panel_metadata_uxp",
"manage_project_panel_metadata_uxp",
"create_project_metadata_field_uxp",
"manage_color_conformance_uxp",
"audition_source_monitor_uxp",
"preflight_production_storage_uxp",
"inspect_premiere_environment_uxp",
"get_uxp_workspace_access",
] as const;
describe("stable UXP workflow MCP catalog", () => {
it("publishes the researched workflow entrypoints with bounded schemas", () => {
const bridge = { request: vi.fn(), getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge) as Record<string, { parameters: Record<string, unknown> }>;
expect(Object.keys(tools)).toEqual(expect.arrayContaining(WORKFLOW_TOOLS));
expect(tools.manage_clip_effects_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["catalog", "inspect", "add", "remove"] },
effect_id: { maxLength: 256 },
operation_id: { pattern: expect.any(String) },
},
});
expect(tools.inspect_track_item_identity_uxp.parameters).toMatchObject({
type: "object", additionalProperties: false,
required: ["media_type", "track_index", "clip_index"],
properties: { media_type: { enum: ["video", "audio"] }, expected_sequence_guid: { maxLength: 512 } },
});
expect(tools.manage_metadata_uxp.parameters).toMatchObject({
properties: {
project_metadata: { maxLength: 350000, description: expect.stringContaining("900,000-byte") },
xmp_metadata: { maxLength: 350000, description: expect.stringContaining("900,000-byte") },
updated_fields: { maxItems: 128 },
},
});
expect(tools.inspect_project_panel_metadata_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["panel", "item_columns"] },
project_item_id: { minLength: 1, maxLength: 512 },
project_item_name: { minLength: 1, maxLength: 255 },
},
});
expect(tools.manage_project_panel_metadata_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["inspect", "update"] },
expected_project_guid: { minLength: 1, maxLength: 512 },
expected_project_panel_metadata: { maxLength: 12288 },
project_panel_metadata: { maxLength: 12288 },
confirm_update: { type: "boolean" },
operation_id: { pattern: expect.any(String) },
},
});
expect(tools.create_project_metadata_field_uxp.parameters).toMatchObject({
type: "object",
additionalProperties: false,
required: ["action"],
properties: {
action: { enum: ["inspect", "create"] },
expected_project_guid: { minLength: 1, maxLength: 512 },
expected_project_panel_metadata: { maxLength: 12288 },
field_name: { minLength: 1, maxLength: 128, pattern: expect.any(String) },
field_label: { minLength: 1, maxLength: 255 },
schema_field_type: { enum: ["integer", "real", "text", "boolean"] },
confirm_create: { type: "boolean" },
operation_id: { pattern: expect.any(String) },
},
});
expect(tools.relink_offline_media_uxp.parameters).toMatchObject({
required: ["new_path", "confirm_non_undoable"],
properties: { new_path: { maxLength: 4096 }, confirm_non_undoable: { type: "boolean" } },
});
expect(tools.manage_proxy_ingest_uxp.parameters).toMatchObject({
properties: {
project_item_id: { minLength: 1, maxLength: 512 },
project_item_name: { minLength: 1, maxLength: 255 },
},
});
expect(tools.manage_timeline_selection_uxp.parameters).toMatchObject({
required: ["action"],
properties: {
action: { enum: ["inspect", "inspect_targets", "replace", "add", "remove", "clear"] },
selection_targets: {
minItems: 1, maxItems: 64,
items: { required: ["media_type", "track_index", "clip_index"] },
},
expected_sequence_guid: { minLength: 1, maxLength: 512 },
selection_items: {
minItems: 1, maxItems: 64,
items: {
additionalProperties: false,
required: [
"media_type", "track_index", "clip_index", "expected_project_item_id",
"expected_start_seconds", "expected_end_seconds",
],
},
},
},
});
});
it("maps timeline selection inspection and guarded mutations", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.manage_timeline_selection_uxp.handler({ action: "inspect" });
await tools.manage_timeline_selection_uxp.handler({
action: "inspect_targets",
selection_targets: [{ media_type: "audio", track_index: 0, clip_index: 4 }],
});
await tools.manage_timeline_selection_uxp.handler({
action: "replace", expected_sequence_guid: "sequence-1", operation_id: "selection-1",
selection_items: [{
media_type: "video", track_index: 1, clip_index: 2,
expected_project_item_id: "clip-17", expected_start_seconds: 3.25, expected_end_seconds: 7.5,
}],
});
await tools.manage_timeline_selection_uxp.handler({
action: "clear", expected_sequence_guid: "sequence-1", operation_id: "selection-2",
});
expect(request).toHaveBeenNthCalledWith(1, "selection.fingerprints.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "selection.targets.inspect", {
items: [{ mediaType: "audio", trackIndex: 0, clipIndex: 4 }],
});
expect(request).toHaveBeenNthCalledWith(3, "selection.update", {
mode: "replace", expectedSequenceGuid: "sequence-1", operationId: "selection-1",
items: [{
mediaType: "video", trackIndex: 1, clipIndex: 2,
expectedProjectItemId: "clip-17", expectedStartSeconds: 3.25, expectedEndSeconds: 7.5,
}],
});
expect(request).toHaveBeenNthCalledWith(4, "selection.update", {
mode: "clear", expectedSequenceGuid: "sequence-1", operationId: "selection-2",
});
});
it("maps bounded track-item identity inspection without granting an edit route", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
await getUxpTools(bridge).inspect_track_item_identity_uxp.handler({
media_type: "audio", track_index: 2, clip_index: 4, expected_sequence_guid: "sequence-1",
});
expect(request).toHaveBeenCalledWith("trackItem.identity.inspect", {
mediaType: "audio", trackIndex: 2, clipIndex: 4, expectedSequenceGuid: "sequence-1",
});
});
it("rejects incomplete timeline selection mutations before bridge access", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await expect(tools.manage_timeline_selection_uxp.handler({ action: "clear" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("expected_sequence_guid") });
await expect(tools.manage_timeline_selection_uxp.handler({ action: "inspect_targets" }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("selection_targets") });
await expect(tools.manage_timeline_selection_uxp.handler({
action: "replace", expected_sequence_guid: "sequence-1",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("selection_items") });
await expect(tools.manage_timeline_selection_uxp.handler({
action: "clear", expected_sequence_guid: "sequence-1", selection_items: [],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("omitted") });
expect(request).not.toHaveBeenCalled();
});
it("rejects incomplete storage configuration and preserves explicit empty selectors", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await expect(tools.preflight_production_storage_uxp.handler({
action: "configure_project", folder_types: ["capture"],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("destination") });
expect(request).not.toHaveBeenCalled();
await tools.manage_metadata_uxp.handler({ action: "get", project_item_id: "" });
expect(request).toHaveBeenCalledWith("metadata.get", { projectItemId: "" });
});
it("maps consolidated public actions to exact capability-gated UXP commands", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await tools.manage_clip_effects_uxp.handler({
action: "add", media_type: "video", track_index: 1, clip_index: 2,
effect_id: "PR.Test", insertion_index: 3, operation_id: "effect-1",
});
await tools.batch_selected_clips_uxp.handler({
action: "remove_effect", media_type: "audio", component_index: 4,
expected_effect_id: "Dynamics", operation_id: "batch-1",
});
await tools.detect_scene_edits_uxp.handler({ mode: "create_markers", operation_id: "scene-1" });
await tools.manage_proxy_ingest_uxp.handler({ action: "set_ingest", enabled: true, operation_id: "ingest-1" });
await tools.relink_offline_media_uxp.handler({
project_item_id: "clip-1", new_path: "D:/Approved/online.mov", expected_current_path: "D:/Approved/missing.mov",
confirm_non_undoable: true, operation_id: "relink-1",
});
await tools.manage_metadata_uxp.handler({
action: "update", project_item_id: "clip-1", project_metadata: "metadata", updated_fields: ["LogNote"], operation_id: "meta-1",
});
await tools.manage_project_panel_metadata_uxp.handler({
action: "update", expected_project_guid: "project-1", expected_project_panel_metadata: "<before/>",
project_panel_metadata: "<after/>", confirm_update: true, operation_id: "panel-1",
});
await tools.manage_color_conformance_uxp.handler({
action: "update", project_item_id: "clip-1", frame_rate: 24, input_lut_id: "lut-guid", operation_id: "color-1",
});
await tools.audition_source_monitor_uxp.handler({ action: "open_file", file_path: "D:/Approved/take.mov", operation_id: "monitor-1" });
await tools.preflight_production_storage_uxp.handler({
action: "configure_project", folder_types: ["capture", "auto_save"], destination: "same_as_project", operation_id: "scratch-1",
});
await tools.inspect_premiere_environment_uxp.handler();
await tools.get_uxp_workspace_access.handler();
expect(request).toHaveBeenNthCalledWith(1, "effects.chain.add", {
mediaType: "video", trackIndex: 1, clipIndex: 2, effectId: "PR.Test", insertionIndex: 3, operationId: "effect-1",
});
expect(request).toHaveBeenNthCalledWith(2, "effects.selection.remove", {
mediaType: "audio", componentIndex: 4, expectedEffectId: "Dynamics", operationId: "batch-1",
});
expect(request).toHaveBeenNthCalledWith(3, "sceneEdit.detect", { mode: "createMarkers", operationId: "scene-1" });
expect(request).toHaveBeenNthCalledWith(4, "ingest.configure", { enabled: true, operationId: "ingest-1" });
expect(request).toHaveBeenNthCalledWith(5, "media.relink", {
projectItemId: "clip-1", newPath: "D:/Approved/online.mov", expectedCurrentPath: "D:/Approved/missing.mov",
confirmNonUndoable: true, operationId: "relink-1",
});
expect(request).toHaveBeenNthCalledWith(6, "metadata.update", {
projectItemId: "clip-1", projectMetadata: "metadata", updatedFields: ["LogNote"], operationId: "meta-1",
});
expect(request).toHaveBeenNthCalledWith(7, "metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "<before/>", projectPanelMetadata: "<after/>",
confirmUpdate: true, operationId: "panel-1",
});
expect(request).toHaveBeenNthCalledWith(8, "footage.conform", {
projectItemId: "clip-1", frameRate: 24, inputLutId: "lut-guid", operationId: "color-1",
});
expect(request).toHaveBeenNthCalledWith(9, "sourceMonitor.open", { filePath: "D:/Approved/take.mov", operationId: "monitor-1" });
expect(request).toHaveBeenNthCalledWith(10, "scratch.configure", {
folderTypes: ["capture", "autoSave"], destination: "sameAsProject", operationId: "scratch-1",
});
expect(request).toHaveBeenNthCalledWith(11, "environment.inspect", {});
expect(request).toHaveBeenNthCalledWith(12, "workspace.status", {});
});
it("maps Project-panel metadata reads and guarded writes to separate authority routes", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).inspect_project_panel_metadata_uxp;
await tool.handler({ action: "panel" });
await tool.handler({ action: "item_columns", project_item_id: "clip-1" });
await getUxpTools(bridge).manage_project_panel_metadata_uxp.handler({
action: "inspect",
});
await getUxpTools(bridge).manage_project_panel_metadata_uxp.handler({
action: "update", expected_project_guid: "project-1", expected_project_panel_metadata: "<before/>",
project_panel_metadata: "<after/>", confirm_update: true, operation_id: "panel-1",
});
expect(request).toHaveBeenNthCalledWith(1, "metadata.projectPanel.get", {});
expect(request).toHaveBeenNthCalledWith(2, "metadata.columns.get", { projectItemId: "clip-1" });
expect(request).toHaveBeenNthCalledWith(3, "metadata.projectPanel.get", {});
expect(request).toHaveBeenNthCalledWith(4, "metadata.projectPanel.update", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "<before/>", projectPanelMetadata: "<after/>",
confirmUpdate: true, operationId: "panel-1",
});
});
it("maps Project metadata schema inspection and creation to separate authority routes", async () => {
const request = vi.fn().mockResolvedValue({ outcome: "committed_unverified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tool = getUxpTools(bridge).create_project_metadata_field_uxp;
await tool.handler({ action: "inspect" });
await tool.handler({
action: "create", expected_project_guid: "project-1", expected_project_panel_metadata: "<before/>",
field_name: "McpReviewState", field_label: "MCP Review State", schema_field_type: "text",
confirm_create: true, operation_id: "schema-1",
});
expect(request).toHaveBeenNthCalledWith(1, "metadata.projectSchema.inspect", {});
expect(request).toHaveBeenNthCalledWith(2, "metadata.projectSchema.create", {
expectedProjectGuid: "project-1", expectedProjectPanelMetadata: "<before/>",
fieldName: "McpReviewState", fieldLabel: "MCP Review State", fieldType: "text",
confirmCreate: true, operationId: "schema-1",
});
});
it("rejects unsupported dispatcher actions before bridge access", async () => {
const request = vi.fn();
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
const results = await Promise.all([
tools.manage_clip_effects_uxp.handler({ action: "unsupported" }),
tools.batch_selected_clips_uxp.handler({ action: "unsupported" }),
tools.manage_timeline_selection_uxp.handler({ action: "unsupported" }),
tools.manage_proxy_ingest_uxp.handler({ action: "unsupported" }),
tools.manage_metadata_uxp.handler({ action: "unsupported" }),
tools.inspect_project_panel_metadata_uxp.handler({ action: "unsupported" }),
tools.manage_project_panel_metadata_uxp.handler({ action: "unsupported" }),
tools.create_project_metadata_field_uxp.handler({ action: "unsupported" }),
tools.manage_color_conformance_uxp.handler({ action: "unsupported" }),
tools.audition_source_monitor_uxp.handler({ action: "unsupported" }),
]);
expect(results).toEqual(Array.from({ length: 10 }, () => ({
success: false,
error: "Unsupported workflow action: unsupported",
})));
expect(request).not.toHaveBeenCalled();
});
it("normalizes bridge failures and forwards optional proxy controls", async () => {
const request = vi.fn()
.mockRejectedValueOnce(new Error("host disconnected"))
.mockResolvedValueOnce({ outcome: "verified" });
const bridge = { request, getState: vi.fn() } as unknown as UxpWebSocketBridge;
const tools = getUxpTools(bridge);
await expect(tools.manage_clip_effects_uxp.handler({ action: "catalog" })).resolves.toEqual({
success: false,
error: "host disconnected",
});
await tools.manage_proxy_ingest_uxp.handler({
action: "attach_proxy",
project_item_name: "Interview A",
media_path: "D:/Approved/Interview A Proxy.mov",
is_high_resolution: false,
make_alternate_link_in_team_projects: true,
replace_existing_proxy: true,
confirm_non_undoable: true,
});
expect(request).toHaveBeenNthCalledWith(2, "proxy.attach", {
projectItemName: "Interview A",
mediaPath: "D:/Approved/Interview A Proxy.mov",
isHiRes: false,
makeAlternateLinkInTeamProjects: true,
replaceExistingProxy: true,
confirmNonUndoable: true,
});
});
});
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { getWorkflowRecipeTools, validateWorkflowRecipe } from "../../src/tools/workflow-recipes.js";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
describe("workflow recipes", () => {
it("expands only audited declarative routes", async () => {
const tools = getWorkflowRecipeTools();
const result = await tools.preview_workflow_recipe.handler({ recipe_id: "talking-head-cleanup", provided_inputs: ["source_project_item_id"] });
expect(result.success).toBe(true);
expect((result.data as any).applied).toBe(false);
expect((result.data as any).execution_manifest[0].routes).toEqual(["verify_premiere_connection"]);
});
it("rejects arbitrary steps", () => expect(() => validateWorkflowRecipe({ schema_version: 1, id: "x", title: "X", description: "X", tags: [], required_inputs: [], steps: ["execute_extendscript"] })).toThrow(/unsupported/));
it("loads a contained custom recipe and rejects conflicts", async () => {
const root = mkdtempSync(path.join(tmpdir(), "recipes-")), file = path.join(root, "recipes.json");
const recipe = { schema_version: 1, id: "custom", title: "Custom", description: "Custom", tags: ["one"], required_inputs: [], steps: ["verify_connection"] };
writeFileSync(file, JSON.stringify([recipe]));
const tools = getWorkflowRecipeTools();
expect((await tools.search_workflow_recipes.handler({ query: "custom", approved_workspace_path: root, recipe_file: file })).success).toBe(true);
writeFileSync(file, JSON.stringify([{ ...recipe, id: "talking-head-cleanup" }]));
expect((await tools.search_workflow_recipes.handler({ approved_workspace_path: root, recipe_file: file })).success).toBe(false);
});
it.each([null, [], { schema_version: 2 }, { schema_version: 1, id: "x", title: "X", description: "X", steps: [] }, { schema_version: 1, id: "x", title: "X", description: "X", tags: ["a", "a"], required_inputs: [], steps: ["verify_connection"] }])("rejects invalid recipe %#", (recipe) => expect(() => validateWorkflowRecipe(recipe)).toThrow());
it("fails closed for malformed and escaped custom files", async () => {
const root = mkdtempSync(path.join(tmpdir(), "recipes-")), outside = mkdtempSync(path.join(tmpdir(), "recipes-out-"));
const tools = getWorkflowRecipeTools();
writeFileSync(path.join(outside, "r.json"), "[]");
expect((await tools.search_workflow_recipes.handler({ approved_workspace_path: root, recipe_file: path.join(outside, "r.json") })).success).toBe(false);
const file = path.join(root, "r.json"); writeFileSync(file, "not json");
expect((await tools.search_workflow_recipes.handler({ approved_workspace_path: root, recipe_file: file })).success).toBe(false);
writeFileSync(file, "{}"); expect((await tools.search_workflow_recipes.handler({ approved_workspace_path: root, recipe_file: file })).success).toBe(false);
expect((await tools.search_workflow_recipes.handler({ approved_workspace_path: root })).success).toBe(false);
expect((await tools.preview_workflow_recipe.handler({ recipe_id: "missing" })).success).toBe(false);
expect((await tools.preview_workflow_recipe.handler({ recipe_id: "talking-head-cleanup", provided_inputs: [1] })).success).toBe(false);
});
it("reports ready built-ins and custom recipe sources", async () => {
const root = mkdtempSync(path.join(tmpdir(), "recipes-")), file = path.join(root, "r.json");
writeFileSync(file, JSON.stringify([{ schema_version: 1, id: "custom-ready", title: "Custom", description: "Custom", tags: [], required_inputs: [], steps: ["verify_connection"] }]));
const tools = getWorkflowRecipeTools();
const custom = await tools.search_workflow_recipes.handler({ approved_workspace_path: root, recipe_file: file }) as any;
expect(custom.data.recipes.find((item: any) => item.id === "custom-ready").source).toBe("workspace_file");
const provided_inputs = ["source_project_item_id", "transcript_revision", "approved_segments", "sequence_name"];
expect(((await tools.preview_workflow_recipe.handler({ recipe_id: "talking-head-cleanup", provided_inputs })) as any).data.ready).toBe(true);
});
});