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

475 lines
23 KiB
TypeScript
Executable File

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();
}
});
});