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
File diff suppressed because it is too large Load Diff
+577
View File
@@ -0,0 +1,577 @@
import { describe, expect, it } from "vitest";
import {
ADOBE_UXP_COVERAGE_MANIFEST,
buildAdobeUxpCoverageReport,
buildPlatformCapabilityReport,
} from "../src/platform-capabilities.js";
import { resolveCapabilities } from "../src/security/capabilities.js";
describe("Adobe Premiere Pro 26.3 UXP coverage", () => {
it("pins the official stable 26.3 type baseline", () => {
expect(ADOBE_UXP_COVERAGE_MANIFEST.source).toEqual({
apiPackage: "@adobe/premierepro",
apiVersion: "26.3.0",
changelogUrl: "https://developer.adobe.com/premiere-pro/uxp/changelog/",
});
});
it("has unique, official, complete capability metadata", () => {
const entries = ADOBE_UXP_COVERAGE_MANIFEST.entries;
expect(new Set(entries.map((entry) => entry.id)).size).toBe(entries.length);
expect(entries.map((entry) => entry.id)).toEqual(expect.arrayContaining([
"action-transaction-discipline",
"project-sequence-create-preset",
"native-ticktime-frame-alignment",
"native-project-insertion-bin-snapshot",
"native-installed-mogrt-directory-inspection",
"transactional-sequence-range",
"guarded-sequence-playhead-control",
"guarded-app-preferences",
"native-sequence-timing-snapshot",
"native-caption-track-inventory",
"track-rename",
"subclip-create",
"single-source-silence-stringout",
"marker-list-with-guid",
"source-monitor-set-position",
"aaf-export",
"media-encoder-launch",
"native-effects-pipeline",
"bounded-effect-parameter-catalog",
"selection-compound-effect-batch",
"deterministic-timeline-selection",
"native-scene-edit-detection",
"proxy-ingest-controller",
"offline-media-relink-repair",
"transactional-metadata-xmp",
"color-footage-conformance",
"source-monitor-audition",
"productions-storage-preflight",
"least-privilege-uxp-workspace",
"project-view-selection-resolver",
"native-marker-crud",
"native-beat-grid-markers",
"native-marker-batch-removal",
"transactional-bin-organizer",
"bounded-native-project-tree",
"guarded-empty-sequence-creation",
"sequence-settings-profiles",
"guarded-sequence-display-format",
"workspace-gated-project-import",
"typed-parameter-keyframe-automation",
"native-track-item-identity",
"native-tick-time-arithmetic",
"track-item-transformations",
"bounded-native-timeline-structure",
"sequence-editor-timeline-layer",
"sequence-lifecycle-derivatives",
"ame-encode-controller",
"guarded-source-media-timing",
"guarded-source-media-overrides",
"guarded-transcript-json-import",
"guarded-track-item-append-duplicate",
"guarded-project-metadata-schema-field-creation",
"guarded-track-item-contiguous-ripple-delete",
"guarded-timeline-source-color-label",
"bounded-unique-serializable-identity",
"bounded-source-media-provenance",
"bounded-source-proxy-inspection",
]));
expect(entries.find((entry) => entry.id === "native-marker-crud")?.mcpTools).toEqual(["manage_markers_uxp"]);
expect(entries.find((entry) => entry.id === "native-ticktime-frame-alignment")).toMatchObject({
uxpCommand: "time.frameAlignment.inspect",
mcpTools: ["inspect_frame_alignment_uxp"],
verificationBoundary: "native_ticktime_value_readback",
liveHostVerificationStatus: "not_run",
mutatesProject: false,
});
expect(entries.find((entry) => entry.id === "native-ticktime-frame-alignment")?.adobeApi).toEqual(expect.arrayContaining([
"FrameRate.createWithValue",
"TickTime.createWithSeconds",
"TickTime.createWithFrameAndFrameRate",
"TickTime.alignToFrame",
"TickTime.alignToNearestFrame",
]));
expect(entries.find((entry) => entry.id === "marker-list-with-guid")?.adobeApi).toEqual(expect.arrayContaining([
"Marker.guid", "Marker.getColor", "Color.red", "Color.green", "Color.blue", "Color.alpha",
]));
expect(entries.find((entry) => entry.id === "native-beat-grid-markers")).toMatchObject({
uxpCommand: "markers.addBeatGrid",
mcpTools: ["apply_beat_markers_uxp"],
verificationBoundary: "beat_marker_guid_and_time_readback",
});
expect(entries.find((entry) => entry.id === "bounded-media-health-maintenance")?.adobeApi).toEqual(expect.arrayContaining([
"ClipProjectItem.getMedia",
"Media.start",
"Media.duration",
]));
expect(entries.find((entry) => entry.id === "bounded-media-health-maintenance")?.adobeApi).not.toEqual(expect.arrayContaining([
"Media.getStart",
"Media.getDuration",
]));
expect(entries.find((entry) => entry.id === "guarded-source-media-timing")).toMatchObject({
uxpCommand: "source.mediaTiming.setStart",
mcpTools: ["manage_source_media_timing_uxp"],
verificationBoundary: "guarded_source_media_timing_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-source-media-timing")?.adobeApi).toEqual(expect.arrayContaining([
"ClipProjectItem.getMedia",
"Media.start",
"Media.duration",
"Media.createSetStartAction",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-source-media-overrides")).toMatchObject({
uxpCommand: "source.mediaOverrides.update",
mcpTools: ["manage_source_media_overrides_uxp"],
verificationBoundary: "guarded_source_media_effective_interpretation_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-source-media-overrides")?.adobeApi).toEqual(expect.arrayContaining([
"ClipProjectItem.getFootageInterpretation",
"FootageInterpretation.getFrameRate",
"FootageInterpretation.getPixelAspectRatio",
"ClipProjectItem.createSetOverrideFrameRateAction",
"ClipProjectItem.createSetOverridePixelAspectRatioAction",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-transcript-json-import")).toMatchObject({
uxpCommand: "transcript.import",
mcpTools: ["import_transcript_uxp"],
verificationBoundary: "guarded_transcript_export_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-transcript-json-import")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getActiveProject",
"Project.getRootItem",
"Project.lockedAccess",
"Project.executeTransaction",
"FolderItem.getItems",
"ProjectItem.getId",
"ClipProjectItem.cast",
"Transcript.hasTranscript",
"Transcript.exportToJSON",
"Transcript.importFromJSON",
"Transcript.createImportTextSegmentsAction",
]));
expect(entries.find((entry) => entry.id === "typed-parameter-keyframe-automation")).toMatchObject({
uxpCommand: "parameters.keyframeAdd",
mcpTools: ["automate_effect_parameters_uxp"],
verificationBoundary: "parameter_value_keyframe_animation_mode_or_lookup_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "typed-parameter-keyframe-automation")?.adobeApi).toEqual(expect.arrayContaining([
"ComponentParam.areKeyframesSupported",
"ComponentParam.isTimeVarying",
"ComponentParam.getKeyframeListAsTickTimes",
"ComponentParam.createSetTimeVaryingAction",
"ComponentParam.getKeyframePtr",
"ComponentParam.findNearestKeyframe",
"ComponentParam.findNextKeyframe",
"ComponentParam.findPreviousKeyframe",
"Keyframe.position",
"Keyframe.getTemporalInterpolationMode",
]));
expect(entries.find((entry) => entry.id === "bounded-point-effect-parameter-displacement")).toMatchObject({
uxpCommand: "parameters.point.displacement.inspect",
mcpTools: ["automate_effect_parameters_uxp"],
mutatesProject: false,
undoable: false,
verificationBoundary: "native_point_distance_complete_endpoint_double_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "bounded-point-effect-parameter-displacement")?.adobeApi).toEqual(expect.arrayContaining([
"ComponentParam.getValueAtTime",
"ComponentParam.isTimeVarying",
"TickTime.createWithSeconds",
"PointF.distanceTo",
"PointF.x",
"PointF.y",
]));
expect(entries.find((entry) => entry.id === "bounded-effect-parameter-catalog")).toMatchObject({
uxpCommand: "parameters.catalog.inspect",
mcpTools: ["inspect_effect_parameter_catalog_uxp"],
verificationBoundary: "bounded_effect_parameter_catalog_double_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "bounded-effect-parameter-catalog")?.adobeApi).toEqual(expect.arrayContaining([
"Component.getParamCount",
"Component.getParam",
"ComponentParam.displayName",
"ComponentParam.areKeyframesSupported",
"ComponentParam.isTimeVarying",
]));
expect(entries.find((entry) => entry.id === "bounded-unique-serializable-identity")).toMatchObject({
uxpCommand: "object.uniqueIdentity.inspect",
mcpTools: ["inspect_unique_object_identity_uxp"],
verificationBoundary: "bounded_unique_serializable_double_readback",
liveHostVerificationStatus: "not_run",
mutatesProject: false,
});
expect(entries.find((entry) => entry.id === "bounded-unique-serializable-identity")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getActiveProject",
"Project.getRootItem",
"Project.getSequence",
"ProjectItem.getId",
"FolderItem.getItems",
"UniqueSerializeable.cast",
"UniqueSerializeable.getUniqueID",
]));
expect(entries.find((entry) => entry.id === "native-track-item-identity")).toMatchObject({
uxpCommand: "trackItem.identity.inspect",
mcpTools: ["inspect_track_item_identity_uxp"],
verificationBoundary: "active_sequence_identity_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-track-item-identity")?.adobeApi).toEqual(expect.arrayContaining([
"VideoClipTrackItem.getMatchName",
"VideoClipTrackItem.getType",
"VideoClipTrackItem.getMediaType",
"VideoClipTrackItem.getTrackIndex",
"VideoClipTrackItem.getIsSelected",
"AudioClipTrackItem.getMatchName",
"AudioClipTrackItem.getType",
"AudioClipTrackItem.getMediaType",
"AudioClipTrackItem.getTrackIndex",
"AudioClipTrackItem.getIsSelected",
]));
expect(entries.find((entry) => entry.id === "guarded-app-preferences")).toMatchObject({
uxpCommand: "preferences.set",
mcpTools: ["manage_app_preferences_uxp"],
verificationBoundary: "app_preference_native_string_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-app-preferences")?.adobeApi).toEqual(expect.arrayContaining([
"AppPreference.getValue",
"AppPreference.setValue",
"AppPreference.KEY_AUTO_PEAK_GENERATION",
"AppPreference.KEY_IMPORT_WORKSPACE",
"AppPreference.KEY_SHOW_QUICKSTART_DIALOG",
"AppPreference.PROPERTY_PERSISTENT",
"AppPreference.PROPERTY_NON_PERSISTENT",
]));
expect(entries.find((entry) => entry.id === "native-marker-batch-removal")).toMatchObject({
uxpCommand: "markers.removeMany",
mcpTools: ["manage_markers_uxp"],
verificationBoundary: "marker_guid_absence_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-marker-batch-removal")?.adobeApi).toEqual(expect.arrayContaining([
"Marker.getName",
"Marker.getStart",
"Marker.getDuration",
]));
expect(entries.find((entry) => entry.id === "native-sequence-timing-snapshot")).toMatchObject({
uxpCommand: "sequence.timing.inspect",
mcpTools: ["inspect_sequence_timing_uxp"],
verificationBoundary: "sequence_timing_readback_or_targeted_sequence_timing_double_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-sequence-timing-snapshot")?.adobeApi).toEqual(expect.arrayContaining([
"Sequence.getFrameSize",
"Sequence.getTimebase",
"Sequence.getSequenceAudioTimeDisplayFormat",
"Sequence.getSequenceVideoTimeDisplayFormat",
"Sequence.getProjectItem",
"Project.getSequence",
"Guid.fromString",
"Sequence.guid",
"Sequence.name",
"ProjectItem.getId",
"ProjectItem.name",
"RectF",
"RectF.width",
"RectF.height",
"TimeDisplay",
"TimeDisplay.type",
]));
expect(entries.find((entry) => entry.id === "native-project-insertion-bin-snapshot")).toMatchObject({
uxpCommand: "project.insertionBin.inspect",
mcpTools: ["inspect_project_insertion_bin_uxp"],
verificationBoundary: "project_insertion_bin_identity_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-project-insertion-bin-snapshot")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getInsertionBin",
"ProjectItem.getId",
"ProjectItem.name",
"ProjectItem.type",
]));
expect(entries.find((entry) => entry.id === "native-installed-mogrt-directory-inspection")).toMatchObject({
uxpCommand: "graphics.mogrtPath.inspect",
mcpTools: ["inspect_installed_mogrt_directory_uxp"],
verificationBoundary: "documented_sequence_editor_installation_directory_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-installed-mogrt-directory-inspection")?.adobeApi).toEqual([
"SequenceEditor.getInstalledMogrtPath",
]);
expect(entries.find((entry) => entry.id === "guarded-sequence-display-format")).toMatchObject({
uxpCommand: "sequence.displayFormat.update",
mcpTools: ["manage_sequence_display_format_uxp"],
verificationBoundary: "sequence_display_format_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-sequence-display-format")?.adobeApi).toEqual(expect.arrayContaining([
"Sequence.getSettings",
"Sequence.createSetSettingsAction",
"SequenceSettings.getAudioDisplayFormat",
"SequenceSettings.setAudioDisplayFormat",
"SequenceSettings.getVideoDisplayFormat",
"SequenceSettings.setVideoDisplayFormat",
]));
expect(entries.find((entry) => entry.id === "bounded-native-project-tree")).toMatchObject({
uxpCommand: "projectTree.inspect",
mcpTools: ["inspect_project_tree_uxp"],
verificationBoundary: "bounded_project_tree_item_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "bounded-native-project-tree")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getRootItem",
"FolderItem.getItems",
"ProjectItem.getId",
]));
expect(entries.find((entry) => entry.id === "bounded-native-timeline-structure")?.adobeApi).toEqual(expect.arrayContaining([
"VideoClipTrackItem.getProjectItem",
"AudioClipTrackItem.getProjectItem",
"ProjectItem.getId",
"ClipProjectItem.cast",
"ClipProjectItem.isSequence",
"ClipProjectItem.isMergedClip",
"ClipProjectItem.isMulticamClip",
"ClipProjectItem.isOffline",
]));
expect(entries.find((entry) => entry.id === "native-tick-time-arithmetic")).toMatchObject({
uxpCommand: "time.tickArithmetic.inspect",
mcpTools: ["calculate_tick_time_uxp"],
verificationBoundary: "native_tick_time_value_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "native-tick-time-arithmetic")?.adobeApi).toEqual(expect.arrayContaining([
"TickTime.createWithTicks",
"TickTime.add",
"TickTime.subtract",
"TickTime.multiply",
"TickTime.divide",
"TickTime.ticks",
"TickTime.seconds",
]));
expect(entries.find((entry) => entry.id === "guarded-empty-sequence-creation")).toMatchObject({
uxpCommand: "sequences.createEmpty",
mcpTools: ["create_empty_sequence_uxp"],
verificationBoundary: "sequence_collection_identity_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-empty-sequence-creation")?.adobeApi).toEqual(expect.arrayContaining([
"Project.createSequence",
"Project.getSequences",
"Sequence.guid",
]));
expect(entries.find((entry) => entry.id === "bounded-project-columns-metadata-inspection")).toMatchObject({
uxpCommand: "metadata.columns.get",
mcpTools: ["inspect_project_panel_metadata_uxp"],
verificationBoundary: "bounded_project_columns_metadata_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "bounded-project-panel-metadata-inspection")).toMatchObject({
uxpCommand: "metadata.projectPanel.get",
mcpTools: ["inspect_project_panel_metadata_uxp"],
verificationBoundary: "bounded_project_panel_metadata_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-project-panel-metadata-update")).toMatchObject({
uxpCommand: "metadata.projectPanel.update",
mcpTools: ["manage_project_panel_metadata_uxp"],
mutatesProject: true,
undoable: false,
idempotency: "operation_id",
verificationBoundary: "project_panel_metadata_exact_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-project-metadata-schema-field-creation")).toMatchObject({
uxpCommand: "metadata.projectSchema.create",
mcpTools: ["create_project_metadata_field_uxp"],
mutatesProject: true,
undoable: false,
idempotency: "operation_id",
verificationStatus: "committed_unverified",
verificationBoundary: "project_panel_metadata_change_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-project-metadata-schema-field-creation")?.adobeApi).toEqual(expect.arrayContaining([
"Metadata.addPropertyToProjectMetadataSchema",
"Metadata.METADATA_TYPE_INTEGER",
"Metadata.METADATA_TYPE_REAL",
"Metadata.METADATA_TYPE_TEXT",
"Metadata.METADATA_TYPE_BOOLEAN",
"Project.lockedAccess",
]));
expect(entries.find((entry) => entry.id === "guarded-track-item-slip")).toMatchObject({
uxpCommand: "trackItem.slip",
mcpTools: ["slip_track_item_uxp"],
verificationBoundary: "guarded_track_item_source_and_timeline_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-track-item-slide")).toMatchObject({
uxpCommand: "trackItem.slide",
mcpTools: ["slide_track_item_uxp"],
verificationBoundary: "guarded_three_track_item_source_and_timeline_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-track-item-slide")?.adobeApi).toEqual(expect.arrayContaining([
"AudioClipTrackItem.createMoveAction",
"VideoClipTrackItem.createSetEndAction",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-track-item-append-duplicate")).toMatchObject({
uxpCommand: "trackItem.clone",
mcpTools: ["duplicate_track_item_uxp"],
verificationBoundary: "guarded_source_and_appended_track_item_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-track-item-append-duplicate")?.adobeApi).toEqual(expect.arrayContaining([
"SequenceEditor.getEditor",
"SequenceEditor.createCloneTrackItemAction",
"TickTime.createWithSeconds",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-track-item-contiguous-ripple-delete")).toMatchObject({
uxpCommand: "trackItem.rippleDelete",
mcpTools: ["ripple_delete_track_item_uxp"],
verificationBoundary: "guarded_contiguous_successor_track_item_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-track-item-contiguous-ripple-delete")?.adobeApi).toEqual(expect.arrayContaining([
"TrackItemSelection.createEmptySelection",
"TrackItemSelection.addItem",
"Constants.MediaType.VIDEO",
"SequenceEditor.getEditor",
"SequenceEditor.createRemoveItemsAction",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-timeline-source-color-label")).toMatchObject({
uxpCommand: "timeline.sourceLabel.update",
mcpTools: ["manage_timeline_source_label_uxp"],
verificationBoundary: "timeline_coordinate_source_color_label_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-timeline-source-color-label")?.adobeApi).toEqual(expect.arrayContaining([
"VideoClipTrackItem.getProjectItem",
"AudioClipTrackItem.getProjectItem",
"ClipProjectItem.cast",
"ProjectItem.getColorLabelIndex",
"ProjectItem.createSetColorLabelAction",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "guarded-sequence-preview-frame")).toMatchObject({
uxpCommand: "sequence.previewFrame.update",
mcpTools: ["manage_sequence_preview_frame_uxp"],
verificationBoundary: "sequence_preview_frame_readback",
liveHostVerificationStatus: "not_run",
});
expect(entries.find((entry) => entry.id === "guarded-sequence-preview-frame")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getSequences",
"Sequence.getSettings",
"Sequence.createSetSettingsAction",
"SequenceSettings.getPreviewFrameRect",
"SequenceSettings.setPreviewFrameRect",
"RectF.[[construct]]",
"Project.lockedAccess",
"Project.executeTransaction",
]));
expect(entries.find((entry) => entry.id === "bounded-source-media-provenance")).toMatchObject({
uxpCommand: "source.provenance.inspect",
mcpTools: ["inspect_source_media_provenance_uxp"],
verificationBoundary: "bounded_opt_in_source_path_double_readback",
liveHostVerificationStatus: "not_run",
mutatesProject: false,
});
expect(entries.find((entry) => entry.id === "bounded-source-media-provenance")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getRootItem",
"ProjectItem.getId",
"FolderItem.getItems",
"ClipProjectItem.getMediaFilePath",
"ClipProjectItem.getOriginatingProjectPath",
]));
expect(entries.find((entry) => entry.id === "bounded-source-proxy-inspection")).toMatchObject({
uxpCommand: "source.proxy.inspect",
mcpTools: ["inspect_source_proxy_uxp"],
verificationBoundary: "bounded_source_proxy_state_double_readback",
liveHostVerificationStatus: "not_run",
mutatesProject: false,
});
expect(entries.find((entry) => entry.id === "bounded-source-proxy-inspection")?.adobeApi).toEqual(expect.arrayContaining([
"Project.getRootItem",
"ProjectItem.getId",
"FolderItem.getItems",
"ClipProjectItem.canChangeMediaPath",
"ClipProjectItem.isOffline",
"ClipProjectItem.canProxy",
"ClipProjectItem.hasProxy",
"ClipProjectItem.getProxyPath",
]));
for (const entry of entries) {
expect(entry.minimumPremiereVersion).toMatch(/^\d+\.\d+\.\d+$/);
expect(entry.backend).toBe("uxp");
expect(entry.adobeApi.length).toBeGreaterThan(0);
expect(new Set(entry.adobeApi).size).toBe(entry.adobeApi.length);
expect(entry.mcpTools.length).toBeGreaterThan(0);
expect(entry.documentationUrls.every((url) =>
url.startsWith("https://developer.adobe.com/premiere-pro/uxp/"),
)).toBe(true);
}
});
it("keeps unimplemented 26.3 work visibly planned", () => {
const report = buildAdobeUxpCoverageReport();
expect(report.summary).toEqual({
total: 87,
current: 84,
planned: 3,
implemented: 84,
committedUnverified: 9,
automatedContractVerified: 75,
liveHostVerified: 0,
});
expect(report.entries.find((entry) => entry.id === "aaf-export")).toMatchObject({
availability: "current",
implementationStatus: "implemented",
verificationStatus: "automated_contract_verified",
liveHostVerificationStatus: "not_run",
uxpCommand: "interchange.aaf.export",
});
expect(report.entries.find((entry) => entry.id === "bounded-operation-boundary-event-journal")).toMatchObject({
availability: "current",
implementationStatus: "implemented",
verificationStatus: "automated_contract_verified",
liveHostVerificationStatus: "not_run",
uxpCommand: "events.list",
mcpTools: ["inspect_premiere_events_uxp"],
verificationBoundary: "bounded_redacted_operation_boundary_event_receipt",
});
expect(report.entries.every((entry) => entry.liveHostVerificationStatus === "not_run")).toBe(true);
});
it("returns detached report data for safe capability reporting", () => {
const first = buildAdobeUxpCoverageReport();
first.entries[0].mcpTools.push("should-not-persist");
const second = buildAdobeUxpCoverageReport();
expect(second.entries[0].mcpTools).not.toContain("should-not-persist");
});
it("surfaces the baseline in the platform capability report", () => {
const report = buildPlatformCapabilityReport(resolveCapabilities("inspect"), "win32");
expect(report.backends.uxp.apiCoverage.summary).toMatchObject({
current: 84,
planned: 3,
committedUnverified: 9,
});
});
});
+108
View File
@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest";
import { buildAdvancedFeatureSupport } from "../src/advanced-feature-support.js";
import { getHealthTools } from "../src/tools/health.js";
describe("advanced collaboration and AI feature support", () => {
it("defaults to the production CEP transport and fails closed", () => {
const report = buildAdvancedFeatureSupport();
expect(report.context.backend).toBe("cep");
expect(report.policy).toMatchObject({
publicApisOnly: true,
uiAutomation: false,
privateApis: false,
reportTool: {
transport: "local",
callableThroughCurrentMcpTransport: true,
contactsPremiereHost: false,
},
featureOperations: {
currentMcpTransport: "cep",
uxpOperationsRoutedByCurrentMcpTransport: false,
liveHostCapabilityNegotiationRequired: true,
},
});
expect(report.features.productions).toMatchObject({
status: "uxp-read-only",
staticEligibility: {
backendEligible: false,
eligible: false,
},
liveHostVerificationRequired: true,
callableThroughCurrentMcpTransport: false,
});
});
it("marks documented Productions inspection available only for eligible UXP hosts", () => {
expect(buildAdvancedFeatureSupport({ backend: "uxp", premiereVersion: "25.6.0" }).features.productions.staticEligibility.eligible).toBe(true);
expect(buildAdvancedFeatureSupport({ backend: "uxp", premiereVersion: "25.5.9" }).features.productions.staticEligibility.eligible).toBe(false);
expect(buildAdvancedFeatureSupport({ backend: "uxp" }).features.productions.staticEligibility.eligible).toBeNull();
});
it("keeps entitlements separate from API availability", () => {
const report = buildAdvancedFeatureSupport({
backend: "uxp",
premiereVersion: "26.3",
frameIoEntitled: true,
generativeAiEntitled: true,
networkAvailable: true,
});
expect(report.features.frameIo).toMatchObject({
status: "external-api-required",
entitlementSatisfied: true,
callableThroughCurrentMcpTransport: false,
});
expect(report.features.generativeExtend).toMatchObject({
status: "user-assisted",
entitlementSatisfied: true,
callableThroughCurrentMcpTransport: false,
});
});
it("documents artifact detection boundaries without overstating provenance", () => {
const features = buildAdvancedFeatureSupport().features;
expect(features.generativeExtend.detection).toContain("No documented flag");
expect(features.objectMask).toMatchObject({
status: "partial",
callableThroughCurrentMcpTransport: true,
});
expect(features.objectMask.detection).toContain("ObjectMaskUtils.hasObjectMask");
expect(features.objectMask.unsupportedOperations).toContain("create an Object Mask");
expect(features.captionTranslation.detection).toContain("No documented metadata");
expect(features.remix.detection).toContain("not proof");
expect(features.editorialPlans).toMatchObject({
status: "local-planning",
access: "direct",
callableThroughCurrentMcpTransport: true,
});
expect(features.premiereAiAssistant).toMatchObject({
status: "user-assisted",
access: "user-assisted",
callableThroughCurrentMcpTransport: false,
});
expect(features.localSemanticIndex).toMatchObject({
status: "planned-local",
access: "planned",
callableThroughCurrentMcpTransport: false,
});
});
it("rejects non-numeric host versions", () => {
expect(() => buildAdvancedFeatureSupport({ premiereVersion: "26.3-beta" })).toThrow("numeric version");
});
it("exposes the report through the health tool without contacting Premiere", async () => {
const tool = getHealthTools({}).get_advanced_feature_support;
const result = await tool.handler({ backend: "uxp", premiere_version: "26.3.0" });
expect(result).toMatchObject({
success: true,
data: {
context: { backend: "uxp", premiereVersion: "26.3.0" },
features: {
speechToText: {
status: "user-assisted",
},
},
},
});
});
});
+126
View File
@@ -0,0 +1,126 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createServer } from "../src/server.js";
import { getHealthTools } from "../src/tools/health.js";
import { resolveCapabilities } from "../src/security/capabilities.js";
import { resolveToolPacks } from "../src/workflows/tool-packs.js";
afterEach(() => vi.unstubAllEnvs());
const catalog = {
get_transcript: { description: "Read speech evidence" },
get_notes: { description: "Read transcript review notes" },
delete_clip: { description: "Remove a transcript clip" },
execute_extendscript: { description: "Run transcript scripts" },
};
describe("bounded agent tool discovery", () => {
it("ranks exact names first and intersects query with explicit names", async () => {
const tool = getHealthTools({}, resolveCapabilities("inspect"), () => catalog).get_capabilities;
const exact = await tool.handler({ tool_query: " GET_TRANSCRIPT " });
expect(exact.data?.tools.tools[0]).toMatchObject({ name: "get_transcript", registered: true });
const result = await tool.handler({ tool_query: "transcript", tool_names: ["get_notes"] });
expect(result.data?.tools.tools).toEqual([
expect.objectContaining({ name: "get_notes", description: "Read transcript review notes", registered: true }),
]);
});
it("withholds unauthorized matches by default and labels diagnostic matches", async () => {
const tool = getHealthTools({}, resolveCapabilities("inspect"), () => catalog).get_capabilities;
const result = await tool.handler({ tool_query: "transcript" });
expect(result.data?.tools.tools.map((tool) => tool.name)).toEqual(["get_transcript", "get_notes"]);
const diagnostic = await tool.handler({ tool_query: "transcript", available_only: false });
expect(diagnostic.data?.tools.tools).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "delete_clip", registered: false }),
expect.objectContaining({ name: "execute_extendscript", registered: false }),
]));
});
it("respects pack membership independently of authority", async () => {
const tool = getHealthTools({}, resolveCapabilities("inspect,edit"), () => catalog,
{ toolPacks: resolveToolPacks("inspection") }).get_capabilities;
expect((await tool.handler({ tool_query: "transcript" })).data?.tools.tools).toEqual([]);
expect((await tool.handler({ tool_query: "delete_clip", available_only: false })).data?.tools.tools[0])
.toMatchObject({ name: "delete_clip", registered: false, authority: { enabled: true } });
});
it("defaults search to 20 results and pages deterministically without duplicates", async () => {
const many = Object.fromEntries(Array.from({ length: 25 }, (_, i) =>
[`get_evidence_${String(i).padStart(2, "0")}`, { description: "Read evidence" }]));
const tool = getHealthTools({}, resolveCapabilities("inspect"), () => many).get_capabilities;
const first = await tool.handler({ tool_query: "evidence" });
const second = await tool.handler({ tool_query: "evidence", tool_offset: 20 });
expect(first.data?.tools.pagination).toMatchObject({ returned: 20, hasMore: true, nextOffset: 20 });
expect(second.data?.tools.pagination).toMatchObject({ returned: 5, hasMore: false, nextOffset: null });
const names = [...first.data!.tools.tools, ...second.data!.tools.tools].map((tool) => tool.name);
expect(new Set(names).size).toBe(25);
expect(names).toEqual(Object.keys(many));
expect((await tool.handler({ tool_query: "no-match" })).data?.tools.pagination)
.toMatchObject({ totalMatching: 0, hasMore: false, nextOffset: null });
});
it("preserves unfiltered legacy shape and rejects invalid search input", async () => {
const tool = getHealthTools({}, resolveCapabilities("inspect"), () => catalog).get_capabilities;
const legacy = await tool.handler({});
expect(legacy.data?.tools.tools).toHaveLength(4);
expect(legacy.data?.tools).not.toHaveProperty("pagination");
expect(legacy.data?.tools.tools[0]).not.toHaveProperty("registered");
for (const query of ["", " ", "x".repeat(257)]) {
expect(await tool.handler({ tool_query: query })).toMatchObject({ success: false });
}
});
});
describe("agent guidance over MCP", () => {
it.each(["full", "inspection"])("matches discovery to tools/list in the %s pack", async (pack) => {
vi.stubEnv("PREMIERE_MCP_CAPABILITIES", "inspect,edit,export,filesystem");
vi.stubEnv("PREMIERE_MCP_TELEMETRY", "off");
const server = createServer({}, { toolPacks: pack });
const client = new Client({ name: "astra-discovery-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
const listed = new Set((await client.listTools()).tools.map((tool) => tool.name));
const result = await client.callTool({ name: "get_capabilities", arguments: {
tool_query: "sequence", tool_limit: 128,
} });
const data = (result.structuredContent as any).data;
expect(data.tools.tools.length).toBeGreaterThan(0);
expect(data.discovery).toMatchObject({ detail: "summary" });
expect(data.backends.uxp).not.toHaveProperty("apiCoverage");
for (const tool of data.tools.tools) {
expect(tool.registered).toBe(true);
expect(listed.has(tool.name)).toBe(true);
}
const instructions = client.getInstructions()!;
const resource = await client.readResource({ uri: "config://premiere-instructions" });
expect(resource.contents[0]).toMatchObject({ text: instructions });
expect(instructions).toContain("tool_query");
expect(instructions).toContain("Serialize operations");
expect(instructions).toContain("timeout can leave host outcome unknown");
const routes = instructions.split("AVAILABLE WORKFLOW ROUTES:\n")[1].split("\n\nRECOVER")[0];
for (const line of routes.split("\n")) {
if (!line.includes(": ")) continue;
for (const name of line.slice(2).split(": ")[0].split(" -> ")) {
expect(listed.has(name), `${name} must be registered`).toBe(true);
}
}
if (pack === "inspection") expect(routes).not.toContain("apply_edit_plan");
else expect(routes).toContain("create_editorial_context_pack");
if (pack === "full") {
const legacy = await client.callTool({ name: "get_capabilities", arguments: {} });
const search = await client.callTool({ name: "get_capabilities", arguments: {
tool_query: "transcript", tool_limit: 10,
} });
expect(JSON.stringify(search).length).toBeLessThan(JSON.stringify(legacy).length / 4);
}
const invalid = await client.callTool({ name: "get_capabilities", arguments: { tool_query: " " } });
expect(invalid.isError).toBe(true);
await expect(client.callTool({ name: "execute_extendscript", arguments: { script: "1" } }))
.rejects.toThrow(/not found/);
} finally {
await client.close();
await server.close();
}
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it, vi } from "vitest";
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 {
AFTER_EFFECTS_BRIDGE_HELPERS,
AFTER_EFFECTS_DEFAULT_TEMP_DIR_NAME,
getAfterEffectsTempDir,
sendAfterEffectsCommand,
} from "../../src/bridge/after-effects-bridge.js";
describe("After Effects bridge", () => {
it("uses an independent bridge directory and AE-only helpers", async () => {
expect(getAfterEffectsTempDir(undefined, "D:/Temp")).toBe(join("D:/Temp", AFTER_EFFECTS_DEFAULT_TEMP_DIR_NAME));
await sendAfterEffectsCommand("return __aeResult({});", { tempDir: "D:/PremiereOnly" });
expect(sendCommand).toHaveBeenCalledWith("return __aeResult({});", expect.objectContaining({
tempDir: getAfterEffectsTempDir(),
helpers: AFTER_EFFECTS_BRIDGE_HELPERS,
}));
expect(AFTER_EFFECTS_BRIDGE_HELPERS.source).toContain("__aeResult");
expect(AFTER_EFFECTS_BRIDGE_HELPERS.source).not.toContain("__findSequence");
});
});
+349
View File
@@ -0,0 +1,349 @@
import { once } from "node:events";
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
statSync,
unlinkSync,
watch,
writeFileSync,
} from "node:fs";
import { WebSocket } from "ws";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { sendCommand } from "../../src/bridge/file-bridge.js";
import {
UxpWebSocketBridge,
type UxpHello,
} from "../../src/bridge/uxp-websocket-bridge.js";
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
readFileSync: vi.fn(),
unlinkSync: vi.fn(),
readdirSync: vi.fn(),
renameSync: vi.fn(),
statSync: vi.fn(),
chmodSync: vi.fn(),
watch: vi.fn(),
}));
const fs = {
exists: vi.mocked(existsSync),
mkdir: vi.mocked(mkdirSync),
write: vi.mocked(writeFileSync),
read: vi.mocked(readFileSync),
unlink: vi.mocked(unlinkSync),
readdir: vi.mocked(readdirSync),
rename: vi.mocked(renameSync),
stat: vi.mocked(statSync),
chmod: vi.mocked(chmodSync),
watch: vi.mocked(watch),
};
const originalGetuid = Object.getOwnPropertyDescriptor(process, "getuid");
function ownedStat(mode = 0o700): ReturnType<typeof statSync> {
return {
uid: 4242,
mode,
mtimeMs: Date.now(),
} as unknown as ReturnType<typeof statSync>;
}
function usePosixProcess(uid = 4242): void {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
Object.defineProperty(process, "getuid", {
configurable: true,
value: () => uid,
});
}
function restoreGetuid(): void {
if (originalGetuid) {
Object.defineProperty(process, "getuid", originalGetuid);
} else {
delete (process as { getuid?: () => number }).getuid;
}
}
describe("file bridge uncovered security and fallback paths", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
fs.watch.mockImplementation(() => {
throw new Error("watch unavailable");
});
fs.stat.mockReturnValue(ownedStat());
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
restoreGetuid();
});
it("rejects a pre-existing POSIX directory owned by another user", async () => {
usePosixProcess();
fs.exists.mockReturnValue(true);
fs.stat.mockReturnValue(ownedStat());
fs.stat.mockReturnValueOnce({
uid: 31337,
mode: 0o700,
mtimeMs: Date.now(),
} as unknown as ReturnType<typeof statSync>);
await expect(sendCommand("var unsafe = true;", {
tempDir: "/tmp/untrusted-bridge",
})).rejects.toThrow(/owned by uid 31337/);
});
it("restores private POSIX permissions on an owned directory", async () => {
usePosixProcess();
fs.exists.mockReturnValue(true);
fs.stat.mockReturnValueOnce(ownedStat(0o755));
fs.read.mockReturnValue('{"success":true,"data":{"repaired":true}}');
await expect(sendCommand("var repair = true;", {
tempDir: "/tmp/owned-bridge",
})).resolves.toEqual({ success: true, data: { repaired: true } });
expect(fs.chmod).toHaveBeenCalledWith("/tmp/owned-bridge", 0o700);
});
it("reports a busy Premiere operation when its heartbeat cannot be statted", async () => {
usePosixProcess();
fs.exists.mockImplementation((path) => {
const value = String(path);
if (value.includes("res_")) return false;
if (value.includes("busy_")) return true;
return true;
});
fs.stat.mockImplementation(((path) => {
if (String(path).includes("busy_")) throw new Error("heartbeat locked");
return ownedStat();
}) as typeof statSync);
const response = sendCommand("var waiting = true;", {
tempDir: "/tmp/busy-stat-error",
timeoutMs: 25,
});
await vi.advanceTimersByTimeAsync(110);
await expect(response).resolves.toMatchObject({
success: false,
error: expect.stringContaining("modal dialog"),
});
});
it("normalizes non-Error response read failures into command results", async () => {
usePosixProcess();
fs.exists.mockImplementation((path) => !String(path).includes("busy_"));
fs.read.mockImplementation(() => {
throw "response file is locked";
});
const response = sendCommand("var parse = true;", {
tempDir: "/tmp/non-error-read-failure",
timeoutMs: 25,
});
await vi.advanceTimersByTimeAsync(110);
await expect(response).resolves.toEqual({
success: false,
error: "Failed to parse response: response file is locked",
});
});
});
const TOKEN = "bridge-edge-token-at-least-16-characters";
const liveBridges: UxpWebSocketBridge[] = [];
async function startBridge(options: {
port?: number;
handshakeTimeoutMs?: number;
requestTimeoutMs?: number;
} = {}): Promise<UxpWebSocketBridge> {
const bridge = new UxpWebSocketBridge({ token: TOKEN, port: 0, ...options });
liveBridges.push(bridge);
await bridge.start();
return bridge;
}
function bridgeUrl(bridge: UxpWebSocketBridge, token = TOKEN): string {
const address = bridge.address();
return `ws://${address.host}:${address.port}${address.path}?token=${encodeURIComponent(token)}`;
}
async function connectHost(
bridge: UxpWebSocketBridge,
protocolVersion = 1,
commands: Record<string, { supported: boolean }> = { "state.get": { supported: true } },
): Promise<WebSocket> {
const client = new WebSocket(bridgeUrl(bridge));
await once(client, "open");
const connected = once(bridge, "connected");
client.send(JSON.stringify({
protocolVersion,
type: "hello",
payload: {
backend: "uxp",
protocolVersion,
commands,
},
}));
await connected;
return client;
}
afterEach(async () => {
await Promise.all(liveBridges.splice(0).map((bridge) => bridge.stop()));
});
describe("UXP WebSocket bridge rejected-message and cleanup paths", () => {
it("serves a 404 for regular HTTP traffic and ignores duplicate starts", async () => {
const bridge = await startBridge();
const listening = vi.fn();
bridge.on("listening", listening);
await bridge.start();
const address = bridge.address();
const response = await fetch(`http://${address.host}:${address.port}/not-a-websocket`);
expect(listening).not.toHaveBeenCalled();
expect(response.status).toBe(404);
await expect(response.text()).resolves.toBe("Not found");
});
it("uses configured address data before it has started", () => {
const bridge = new UxpWebSocketBridge({ token: TOKEN, port: 7911, path: "/panel" });
expect(bridge.address()).toEqual({ host: "127.0.0.1", port: 7911, path: "/panel" });
expect(bridge.getState()).toEqual({ status: "stopped", connected: false });
});
it("uses a timing-safe comparison for equal-length incorrect tokens", async () => {
const bridge = await startBridge();
const client = new WebSocket(bridgeUrl(bridge, "x".repeat(TOKEN.length)));
client.on("error", () => {});
const [, response] = await once(client, "unexpected-response");
expect(response.statusCode).toBe(401);
response.resume();
});
it("closes a host that never sends the versioned hello", async () => {
const bridge = await startBridge({ handshakeTimeoutMs: 20 });
const client = new WebSocket(bridgeUrl(bridge));
await once(client, "open");
const [code, reason] = await once(client, "close");
expect(code).toBe(1008);
expect(reason.toString()).toContain("Versioned hello required");
});
it("rejects each malformed hello shape before accepting capabilities", async () => {
const bridge = await startBridge();
const malformedMessages = [
{ protocolVersion: 1, type: "event", payload: {} },
{ protocolVersion: 1, type: "hello", payload: { backend: "cep", protocolVersion: 1, commands: {} } },
{ protocolVersion: 3, type: "hello", payload: { backend: "uxp", protocolVersion: 3, commands: {} } },
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 2, commands: {} } },
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1 } },
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1, commands: "invalid" } },
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1, commands: [] } },
];
for (const message of malformedMessages) {
const client = new WebSocket(bridgeUrl(bridge));
await once(client, "open");
const closed = once(client, "close");
client.send(JSON.stringify(message));
const [code] = await closed;
expect(code).toBe(1008);
}
});
it("accepts protocol 2 and closes the connection when its protocol changes", async () => {
const bridge = await startBridge();
const client = await connectHost(bridge, 2);
expect(bridge.getState()).toMatchObject({ connected: true, protocolVersion: 2 });
const closed = once(client, "close");
client.send(JSON.stringify({ protocolVersion: 1, type: "event", payload: { kind: "wrong-version" } }));
const [code, reason] = await closed;
expect(code).toBe(1008);
expect(reason.toString()).toContain("Protocol version changed");
});
it("ignores unknown messages and reports default host command failures", async () => {
const bridge = await startBridge();
const client = await connectHost(bridge);
client.send(JSON.stringify({ protocolVersion: 1, type: "notice", payload: {} }));
client.send(JSON.stringify({
protocolVersion: 1,
type: "result",
requestId: "unknown-request",
payload: { ok: true, result: "ignored" },
}));
client.once("message", (data) => {
const command = JSON.parse(data.toString());
client.send(JSON.stringify({
protocolVersion: 1,
type: "result",
requestId: command.requestId,
payload: { ok: false },
}));
});
await expect(bridge.request("state.get")).rejects.toMatchObject({
code: "UXP_COMMAND_FAILED",
message: "UXP command 'state.get' failed",
});
expect(bridge.getState()).toMatchObject({ connected: true });
});
it("forwards client errors from an accepted connection", async () => {
const bridge = await startBridge();
await connectHost(bridge);
const emitted = once(bridge, "clientError");
const expected = new Error("client transport error");
const serverClient = (bridge as unknown as { socket: WebSocket }).socket;
serverClient.emit("error", expected);
await expect(emitted).resolves.toEqual([expected]);
});
it("rejects a request if the socket reports a send failure exactly once", async () => {
const bridge = new UxpWebSocketBridge({ token: TOKEN });
const internal = bridge as unknown as {
socket: {
readyState: number;
send: (message: string, callback: (error?: Error) => void) => void;
close: () => void;
} | null;
hello: UxpHello | null;
};
internal.socket = {
readyState: WebSocket.OPEN,
send: (_message, callback) => {
callback(new Error("socket write failed"));
callback(new Error("duplicate completion"));
},
close: vi.fn(),
};
internal.hello = {
backend: "uxp",
protocolVersion: 1,
commands: { "state.get": { supported: true } },
};
await expect(bridge.request("state.get")).rejects.toMatchObject({
code: "UXP_SEND_FAILED",
message: "socket write failed",
});
});
});
+142
View File
@@ -0,0 +1,142 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
statSync,
unlinkSync,
watch,
writeFileSync,
} from "node:fs";
import { cleanupTempDir, sendCommand } from "../../src/bridge/file-bridge.js";
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
readFileSync: vi.fn(),
unlinkSync: vi.fn(),
readdirSync: vi.fn(),
renameSync: vi.fn(),
statSync: vi.fn(),
chmodSync: vi.fn(),
watch: vi.fn(),
}));
const fs = {
exists: vi.mocked(existsSync),
mkdir: vi.mocked(mkdirSync),
write: vi.mocked(writeFileSync),
read: vi.mocked(readFileSync),
unlink: vi.mocked(unlinkSync),
readdir: vi.mocked(readdirSync),
rename: vi.mocked(renameSync),
stat: vi.mocked(statSync),
chmod: vi.mocked(chmodSync),
watch: vi.mocked(watch),
};
describe("file bridge fallback and cleanup branches", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
fs.stat.mockReturnValue({
uid: typeof process.getuid === "function" ? process.getuid() : 0,
mode: 0o700,
mtimeMs: Date.now(),
} as ReturnType<typeof statSync>);
fs.watch.mockImplementation(() => { throw new Error("watch unavailable"); });
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("extends a busy operation, then reports a likely modal dialog", async () => {
let busy = true;
fs.exists.mockImplementation((path) => {
const value = String(path);
if (value.includes("res_")) return false;
if (value.includes("busy_")) return busy;
return true;
});
fs.stat.mockImplementation(() => ({
uid: typeof process.getuid === "function" ? process.getuid() : 0,
mode: 0o700,
mtimeMs: Date.now(),
} as ReturnType<typeof statSync>));
const response = sendCommand("var operation = true;", {
tempDir: "/tmp/busy-bridge",
timeoutMs: 100,
});
await vi.advanceTimersByTimeAsync(500);
busy = false;
await vi.advanceTimersByTimeAsync(300);
await expect(response).resolves.toMatchObject({
success: false,
error: expect.stringContaining("modal dialog"),
});
});
it("ignores unrelated watch events and disables a failed watcher", async () => {
let responseExists = false;
let change: ((event: string, filename: string | Buffer | null) => void) | undefined;
let watcherError: (() => void) | undefined;
const watcher = {
close: vi.fn(),
on: vi.fn((event: string, callback: () => void) => {
if (event === "error") watcherError = callback;
return watcher;
}),
};
fs.watch.mockImplementation(((_path, _options, callback) => {
change = callback;
return watcher;
}) as typeof watch);
fs.exists.mockImplementation((path) => String(path).includes("res_") ? responseExists : true);
fs.read.mockReturnValue('{"success":true,"data":{"watched":true}}');
const response = sendCommand("var watched = true;", { tempDir: "/tmp/watch-bridge" });
change?.("rename", null);
change?.("rename", "unrelated.json");
watcherError?.();
responseExists = true;
await vi.advanceTimersByTimeAsync(100);
await expect(response).resolves.toEqual({ success: true, data: { watched: true } });
expect(watcher.close).toHaveBeenCalled();
});
it("tolerates unlink failures and removes busy protocol files", async () => {
fs.exists.mockReturnValue(true);
fs.read.mockReturnValue('{"success":true}');
fs.unlink.mockImplementation(() => { throw new Error("locked"); });
const response = sendCommand("var cleanup = true;", { tempDir: "/tmp/cleanup-bridge" });
await expect(response).resolves.toEqual({ success: true });
fs.readdir.mockReturnValue([
"busy_1.json", "cmd_1.jsx", "res_1.json", "helpers.jsx",
] as ReturnType<typeof readdirSync>);
expect(() => cleanupTempDir({ tempDir: "/tmp/cleanup-bridge" })).not.toThrow();
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("busy_1.json"));
});
it("skips POSIX ownership changes on Windows while checking response bounds", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
fs.exists.mockReturnValue(true);
fs.read.mockReturnValue('{"success":true}');
await expect(sendCommand("var windows = true;", {
tempDir: "C:\\Temp\\premiere-bridge",
})).resolves.toEqual({ success: true });
expect(fs.stat).toHaveBeenCalledWith(expect.stringContaining("res_"));
expect(fs.chmod).not.toHaveBeenCalled();
});
});
+612
View File
@@ -0,0 +1,612 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
existsSync,
mkdirSync,
writeFileSync,
readFileSync,
unlinkSync,
readdirSync,
renameSync,
statSync,
chmodSync,
watch,
} from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { execFileSync } from "node:child_process";
import {
getDarwinUserTempDirectory,
getTempDir,
getDefaultBridgeTempDir,
getBridgeLiveness,
MAX_BRIDGE_RESPONSE_BYTES,
sendCommand,
sendRawCommand,
cleanupTempDir,
} from "../../src/bridge/file-bridge.js";
// Mock fs module
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
readFileSync: vi.fn(),
unlinkSync: vi.fn(),
readdirSync: vi.fn(),
renameSync: vi.fn(),
statSync: vi.fn(),
chmodSync: vi.fn(),
watch: vi.fn(() => {
throw new Error("watch unavailable in unit-test fallback");
}),
}));
vi.mock("node:child_process", () => ({
execFileSync: vi.fn(),
}));
const mockedExistsSync = vi.mocked(existsSync);
const mockedMkdirSync = vi.mocked(mkdirSync);
const mockedWriteFileSync = vi.mocked(writeFileSync);
const mockedReadFileSync = vi.mocked(readFileSync);
const mockedUnlinkSync = vi.mocked(unlinkSync);
const mockedReaddirSync = vi.mocked(readdirSync);
const mockedRenameSync = vi.mocked(renameSync);
const mockedStatSync = vi.mocked(statSync);
const mockedChmodSync = vi.mocked(chmodSync);
const mockedWatch = vi.mocked(watch);
const mockedExecFileSync = vi.mocked(execFileSync);
// ensureDir on an existing dir stat-checks ownership; default to a dir owned by us
// with safe perms so the existing tests exercise the happy path.
const myUid = typeof process.getuid === "function" ? process.getuid() : 0;
mockedStatSync.mockReturnValue({ uid: myUid, mode: 0o700 } as unknown as ReturnType<typeof statSync>);
describe("getTempDir", () => {
const originalEnv = process.env.PREMIERE_TEMP_DIR;
afterEach(() => {
if (originalEnv !== undefined) {
process.env.PREMIERE_TEMP_DIR = originalEnv;
} else {
delete process.env.PREMIERE_TEMP_DIR;
}
});
it("returns custom dir from options", () => {
expect(getTempDir({ tempDir: "/custom/dir" })).toBe("/custom/dir");
});
it("returns env var when no options", () => {
process.env.PREMIERE_TEMP_DIR = "/env/dir";
expect(getTempDir()).toBe("/env/dir");
});
it("returns env var when options have no tempDir", () => {
process.env.PREMIERE_TEMP_DIR = "/env/dir";
expect(getTempDir({})).toBe("/env/dir");
});
it("returns default when no options or env", () => {
delete process.env.PREMIERE_TEMP_DIR;
const result = getTempDir();
expect(result).toBe(join(tmpdir(), "premiere-mcp-bridge"));
});
it("uses the Darwin per-user temporary root when a GUI client omits TMPDIR", () => {
expect(getDefaultBridgeTempDir(
"darwin",
"/tmp",
() => "/var/folders/example/T",
{},
).replaceAll("\\", "/")).toBe("/var/folders/example/T/premiere-mcp-bridge");
});
it("falls back safely when the Darwin temporary-root lookup is unavailable", () => {
expect(getDefaultBridgeTempDir("darwin", "/tmp", () => null, {}).replaceAll("\\", "/"))
.toBe("/tmp/premiere-mcp-bridge");
});
it("preserves a configured Darwin temporary root", () => {
expect(getDefaultBridgeTempDir(
"darwin",
"/configured/T",
() => "/var/folders/example/T",
{ TMPDIR: "/configured/T" },
).replaceAll("\\", "/")).toBe("/configured/T/premiere-mcp-bridge");
});
it("prefers options.tempDir over env var", () => {
process.env.PREMIERE_TEMP_DIR = "/env/dir";
expect(getTempDir({ tempDir: "/custom/dir" })).toBe("/custom/dir");
});
});
describe("getDarwinUserTempDirectory", () => {
it("uses the per-user macOS temporary root reported by getconf", () => {
mockedExecFileSync.mockReturnValue("/var/folders/example/T/\n" as never);
expect(getDarwinUserTempDirectory()).toBe("/var/folders/example/T/");
expect(mockedExecFileSync).toHaveBeenCalledWith("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
});
});
it("rejects a non-absolute getconf result", () => {
mockedExecFileSync.mockReturnValue("relative-temp\n" as never);
expect(getDarwinUserTempDirectory()).toBeNull();
});
it("falls back when the getconf lookup fails", () => {
mockedExecFileSync.mockImplementation(() => { throw new Error("unavailable"); });
expect(getDarwinUserTempDirectory()).toBeNull();
});
});
describe("sendCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockedStatSync.mockReturnValue({ uid: myUid, mode: 0o700 } as unknown as ReturnType<typeof statSync>);
});
afterEach(() => {
vi.useRealTimers();
});
it("creates temp directory if it does not exist", async () => {
let dirCreated = false;
mockedExistsSync.mockImplementation((path) => {
const p = String(path);
// The temp dir itself does not exist (until mkdirSync is called)
if (p === "/tmp/test-bridge" && !dirCreated) return false;
if (p.includes("res_")) return true; // response exists immediately
return true;
});
mockedMkdirSync.mockImplementation(() => {
dirCreated = true;
return undefined;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"ok":true}}');
const promise = sendCommand("test script", { tempDir: "/tmp/test-bridge" });
await vi.advanceTimersByTimeAsync(200);
await promise;
expect(mockedMkdirSync).toHaveBeenCalledWith("/tmp/test-bridge", {
recursive: true,
mode: 0o700,
});
});
// Security: the bridge temp dir sits at a predictable, world-accessible path, and the
// CEP panel executes any cmd_*.jsx it finds there. On shared machines that dir must be
// ours and private. See the ensureDir hardening.
it("refuses to use an existing temp dir owned by another user", async () => {
if (typeof process.getuid !== "function") return; // POSIX-only guard
mockedExistsSync.mockReturnValue(true); // dir already exists
mockedStatSync.mockReturnValueOnce({
uid: process.getuid!() + 1, // someone else owns it
mode: 0o700,
} as unknown as ReturnType<typeof statSync>);
await expect(sendCommand("var x = 1;", { tempDir: "/tmp/evil-bridge" })).rejects.toThrow(
/owned by uid .* not this user/
);
});
it("clamps a group/world-accessible existing temp dir back to 0700", async () => {
if (typeof process.getuid !== "function") return;
mockedExistsSync.mockImplementation((p) => (String(p).includes("res_") ? true : true));
mockedStatSync.mockReturnValueOnce({
uid: process.getuid!(),
mode: 0o755, // ours, but world-readable
} as unknown as ReturnType<typeof statSync>);
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
const promise = sendCommand("var x = 1;", { tempDir: "/tmp/test-bridge" });
await vi.advanceTimersByTimeAsync(200);
await promise;
expect(mockedChmodSync).toHaveBeenCalledWith("/tmp/test-bridge", 0o700);
});
it("atomically publishes a complete command file as .jsx", async () => {
mockedExistsSync.mockImplementation((path) => {
if (String(path).includes("res_")) return true;
return true;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
const promise = sendCommand("var x = 1;", { tempDir: "/tmp/test-bridge" });
await vi.advanceTimersByTimeAsync(200);
await promise;
const writeCall = mockedWriteFileSync.mock.calls.find(([path]) => String(path).endsWith(".jsx.staged"));
expect(writeCall).toBeDefined();
expect(String(writeCall?.[0])).toMatch(/cmd_.*\.jsx\.staged$/);
const publishedPath = mockedRenameSync.mock.calls[0]?.[1];
expect(String(publishedPath)).toMatch(/cmd_.*\.jsx$/);
// command = one-line helpers bootstrap, then the script itself
const content = String(writeCall?.[1]);
expect(content.endsWith("\nvar x = 1;")).toBe(true);
expect(content.replaceAll("\\\\", "/")).toContain('$.evalFile("/tmp/test-bridge/helpers_');
expect(writeCall?.[2]).toBe("utf-8");
});
it("returns parsed JSON response", async () => {
mockedExistsSync.mockImplementation((path) => {
if (String(path).includes("res_")) return true;
return true;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"version":"24.0"}}');
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
await vi.advanceTimersByTimeAsync(200);
const result = await promise;
expect(result).toEqual({ success: true, data: { version: "24.0" } });
});
it("rejects an oversized bridge response before reading it into memory", async () => {
mockedExistsSync.mockReturnValue(true);
mockedStatSync.mockReturnValue({
uid: myUid,
mode: 0o700,
size: MAX_BRIDGE_RESPONSE_BYTES + 1,
} as unknown as ReturnType<typeof statSync>);
await expect(sendCommand("test", { tempDir: "/tmp/test-bridge" })).resolves.toEqual({
success: false,
error: `Bridge response exceeds the ${MAX_BRIDGE_RESPONSE_BYTES}-byte limit`,
});
expect(mockedReadFileSync).not.toHaveBeenCalled();
});
it("uses distinct cryptographic IDs for concurrently-created command files", async () => {
mockedExistsSync.mockReturnValue(true);
mockedReadFileSync.mockReturnValue('{"success":true}');
await Promise.all([
sendCommand("first", { tempDir: "/tmp/test-bridge" }),
sendCommand("second", { tempDir: "/tmp/test-bridge" }),
]);
const publishedPaths = mockedRenameSync.mock.calls.map(([, target]) => String(target));
expect(publishedPaths).toHaveLength(2);
expect(new Set(publishedPaths).size).toBe(2);
expect(publishedPaths.every((path) => /cmd_[0-9a-f-]{36}\.jsx$/.test(path))).toBe(true);
});
it("attempts event-driven response watching before using the polling fallback", async () => {
mockedExistsSync.mockImplementation((path) => String(path).includes("res_"));
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
await vi.advanceTimersByTimeAsync(100);
await promise;
expect(mockedWatch).toHaveBeenCalledWith(
dirname(join("/tmp/test-bridge", "response.json")),
{ persistent: false },
expect.any(Function),
);
});
it("resolves immediately when the watcher reports the response file", async () => {
let responseExists = false;
let onChange: ((event: string, filename: string) => void) | undefined;
const fakeWatcher = { on: vi.fn().mockReturnThis(), close: vi.fn() };
mockedWatch.mockImplementationOnce(((_path, _options, listener) => {
onChange = listener as (event: string, filename: string) => void;
return fakeWatcher;
}) as typeof watch);
mockedExistsSync.mockImplementation((path) =>
String(path).includes("res_") ? responseExists : true,
);
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"eventDriven":true}}');
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
const responsePath = mockedWatch.mock.calls[0]?.[0];
expect(responsePath).toBeDefined();
responseExists = true;
const commandPath = String(mockedRenameSync.mock.calls[0]?.[1]);
const responseName = commandPath.replace(/.*[\\/]+cmd_/, "res_").replace(/\.jsx$/, ".json");
onChange?.("rename", responseName);
await expect(promise).resolves.toEqual({ success: true, data: { eventDriven: true } });
expect(fakeWatcher.close).toHaveBeenCalled();
});
it("serializes concurrent bridge commands before they reach CEP", async () => {
const readyResponses = new Set<string>();
let onChange: ((event: string, filename: string) => void) | undefined;
const fakeWatcher = { on: vi.fn().mockReturnThis(), close: vi.fn() };
mockedWatch.mockImplementation(((_path, _options, listener) => {
onChange = listener as (event: string, filename: string) => void;
return fakeWatcher;
}) as typeof watch);
mockedExistsSync.mockImplementation((path) => {
const value = String(path);
return value.includes("res_") ? readyResponses.has(value) : true;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"shared":true}}');
const pending = [
sendCommand("first", { tempDir: "/tmp/shared-watch-bridge" }),
sendCommand("second", { tempDir: "/tmp/shared-watch-bridge" }),
];
expect(mockedWatch).toHaveBeenCalledTimes(1);
expect(mockedRenameSync).toHaveBeenCalledTimes(1);
const signalResponse = (commandPath: unknown) => {
const responsePath = String(commandPath)
.replace(/cmd_/, "res_")
.replace(/\.jsx$/, ".json");
readyResponses.add(responsePath);
onChange?.("rename", responsePath.split(/[\\/]/).pop()!);
};
signalResponse(mockedRenameSync.mock.calls[0]?.[1]);
await expect(pending[0]).resolves.toEqual({ success: true, data: { shared: true } });
await vi.advanceTimersByTimeAsync(0);
expect(mockedRenameSync).toHaveBeenCalledTimes(2);
expect(mockedWatch).toHaveBeenCalledTimes(2);
signalResponse(mockedRenameSync.mock.calls[1]?.[1]);
await expect(pending[1]).resolves.toEqual({ success: true, data: { shared: true } });
expect(fakeWatcher.close).toHaveBeenCalledTimes(2);
});
it("fails fast when a bridge directory already has the bounded command backlog", async () => {
mockedExistsSync.mockImplementation((path) => !String(path).includes("res_"));
const commands = Array.from({ length: 34 }, (_, index) => sendCommand(`queued-${index}`, {
tempDir: "/tmp/queue-capacity-bridge",
}));
await expect(commands[33]).resolves.toEqual({
success: false,
error: "Bridge command queue is full (32 waiting); retry after an active command finishes",
});
});
it("keeps polling a malformed response without resending the command", async () => {
mockedExistsSync.mockImplementation((path) => {
if (String(path).includes("res_")) return true;
if (String(path).includes("busy_")) return false;
return true;
});
mockedReadFileSync.mockReturnValue("not valid json{{{");
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge", timeoutMs: 500 });
await vi.advanceTimersByTimeAsync(700);
const result = await promise;
expect(result.success).toBe(false);
expect(result.error).toContain("Failed to parse response");
expect(mockedRenameSync).toHaveBeenCalledTimes(1);
});
it("returns timeout error when response file never appears", async () => {
mockedExistsSync.mockReturnValue(false);
const promise = sendCommand("test", {
tempDir: "/tmp/test-bridge",
timeoutMs: 500,
});
await vi.advanceTimersByTimeAsync(600);
const result = await promise;
expect(result.success).toBe(false);
expect(result.error).toContain("timed out");
expect(result.error).toContain("CEP plugin");
});
it("fails health-style commands before publication when a current connector is waiting", async () => {
vi.setSystemTime(new Date(10_000));
mockedExistsSync.mockImplementation((path) => String(path).includes("bridge-heartbeat"));
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"waiting"}');
mockedStatSync.mockReturnValue({
uid: myUid,
mode: 0o700,
mtimeMs: 9_500,
} as unknown as ReturnType<typeof statSync>);
await expect(sendCommand("var health = true;", {
tempDir: "/tmp/test-bridge",
failFastOnUnreadyHeartbeat: true,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("not running") });
expect(mockedRenameSync).not.toHaveBeenCalled();
});
it("fails health-style commands before publication when a known connector heartbeat is stale", async () => {
vi.setSystemTime(new Date(10_000));
mockedExistsSync.mockImplementation((path) => String(path).includes("bridge-heartbeat"));
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"running"}');
mockedStatSync.mockReturnValue({
uid: myUid,
mode: 0o700,
mtimeMs: 1_000,
} as unknown as ReturnType<typeof statSync>);
await expect(sendCommand("var health = true;", {
tempDir: "/tmp/test-bridge",
failFastOnUnreadyHeartbeat: true,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("heartbeat is stale") });
expect(mockedRenameSync).not.toHaveBeenCalled();
});
it("falls back to normal command delivery when no heartbeat exists", async () => {
mockedExistsSync.mockImplementation((path) => String(path).includes("res_"));
mockedReadFileSync.mockReturnValue('{"success":true}');
await expect(sendCommand("var legacy = true;", {
tempDir: "/tmp/test-bridge",
failFastOnUnreadyHeartbeat: true,
})).resolves.toEqual({ success: true });
expect(mockedRenameSync).toHaveBeenCalled();
});
it("cleans up command and response files after success", async () => {
let responseExists = false;
mockedExistsSync.mockImplementation((path) => {
if (String(path).includes("res_")) return responseExists;
return true;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
responseExists = true;
await vi.advanceTimersByTimeAsync(200);
await promise;
// unlinkSync should be called for both cmd and res files
expect(mockedUnlinkSync).toHaveBeenCalled();
});
it("rejects scripts containing eval()", async () => {
await expect(
sendCommand('eval("dangerous")', { tempDir: "/tmp/test-bridge" })
).rejects.toThrow("blocked pattern");
});
it("rejects scripts containing new Function()", async () => {
await expect(
sendCommand('new Function("code")', { tempDir: "/tmp/test-bridge" })
).rejects.toThrow("blocked pattern");
});
it("rejects scripts containing System.callSystem()", async () => {
await expect(
sendCommand('System.callSystem("rm -rf /")', {
tempDir: "/tmp/test-bridge",
})
).rejects.toThrow("blocked pattern");
});
it("rejects scripts exceeding 500KB", async () => {
const largeScript = "x".repeat(501 * 1024);
await expect(
sendCommand(largeScript, { tempDir: "/tmp/test-bridge" })
).rejects.toThrow("500KB size limit");
});
});
describe("getBridgeLiveness", () => {
it("reports fresh, stale, and unknown states without exposing heartbeat contents", () => {
mockedExistsSync.mockReturnValue(true);
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"running"}');
mockedStatSync.mockReturnValue({ mtimeMs: 9_000 } as ReturnType<typeof statSync>);
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
state: "running",
ageMs: 1_000,
});
mockedStatSync.mockReturnValue({ mtimeMs: 1_000 } as ReturnType<typeof statSync>);
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
state: "stale",
ageMs: 9_000,
});
mockedReadFileSync.mockReturnValue('{"state":"running"}');
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
state: "unknown",
ageMs: null,
});
// This module-level fs mock is shared with the following raw-command
// suite, which exercises the POSIX ownership guard.
mockedStatSync.mockReturnValue({
uid: myUid,
mode: 0o700,
mtimeMs: Date.now(),
} as unknown as ReturnType<typeof statSync>);
});
});
describe("sendRawCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("allows eval() in raw commands", async () => {
mockedExistsSync.mockImplementation((path) => {
if (String(path).includes("res_")) return true;
return true;
});
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
const promise = sendRawCommand('eval("1+1")', {
tempDir: "/tmp/test-bridge",
});
await vi.advanceTimersByTimeAsync(200);
const result = await promise;
expect(result.success).toBe(true);
});
it("still enforces size limit on raw commands", async () => {
const largeScript = "x".repeat(501 * 1024);
await expect(
sendRawCommand(largeScript, { tempDir: "/tmp/test-bridge" })
).rejects.toThrow("500KB size limit");
});
});
describe("cleanupTempDir", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("removes cmd_ and res_ files", () => {
mockedExistsSync.mockReturnValue(true);
mockedReaddirSync.mockReturnValue([
"cmd_123.jsx" as any,
"cmd_124.jsx.staged" as any,
"res_123.json" as any,
"other_file.txt" as any,
]);
cleanupTempDir({ tempDir: "/tmp/test-bridge" });
// Should unlink cmd_ and res_ files but not other_file.txt
const unlinkCalls = mockedUnlinkSync.mock.calls.map((c) => String(c[0]));
expect(unlinkCalls).toContainEqual(
join("/tmp/test-bridge", "cmd_123.jsx")
);
expect(unlinkCalls).toContainEqual(
join("/tmp/test-bridge", "res_123.json")
);
expect(unlinkCalls).toContainEqual(
join("/tmp/test-bridge", "cmd_124.jsx.staged")
);
expect(unlinkCalls).not.toContainEqual(
join("/tmp/test-bridge", "other_file.txt")
);
});
it("does nothing if temp dir does not exist", () => {
mockedExistsSync.mockReturnValue(false);
cleanupTempDir({ tempDir: "/tmp/nonexistent" });
expect(mockedReaddirSync).not.toHaveBeenCalled();
});
it("handles errors gracefully", () => {
mockedExistsSync.mockReturnValue(true);
mockedReaddirSync.mockImplementation(() => {
throw new Error("permission denied");
});
// Should not throw
expect(() => cleanupTempDir({ tempDir: "/tmp/test-bridge" })).not.toThrow();
});
});
+176
View File
@@ -0,0 +1,176 @@
import { describe, it, expect } from "vitest";
import { buildScript, escapeForExtendScript, buildToolScript, getHelpersSource, buildBootstrap, helpersFileName, HELPERS_VERSION } from "../../src/bridge/script-builder.js";
import { runInNewContext } from "node:vm";
describe("buildScript", () => {
it("wraps code in an IIFE with try/catch", () => {
const result = buildScript("return __result({ ok: true });");
expect(result).toContain("(function() {");
expect(result).toContain("return __result({ ok: true });");
expect(result).toContain("} catch(e) {");
expect(result).toContain("return __error(e.toString());");
expect(result).toContain("})();");
});
it("defines helper functions in the helpers source", () => {
const result = getHelpersSource();
expect(result).toContain("var TICKS_PER_SECOND = 254016000000;");
expect(result).toContain("function __ticksToSeconds(ticks)");
expect(result).toContain("function __secondsToTicks(seconds)");
expect(result).toContain("function __ticksToTimecode(ticks, fps)");
expect(result).toContain("function __pad(n)");
expect(result).toContain("function __findSequence(idOrName)");
expect(result).toContain("function __findProjectItem(nodeIdOrName, rootItem)");
expect(result).toContain("function __findClip(nodeId)");
expect(result).toContain("function __getAllClips(seq)");
expect(result).toContain("function __jsonStringify(obj)");
expect(result).toContain("function __result(data)");
expect(result).toContain("function __error(msg)");
});
it("preserves multi-line code blocks", () => {
const code = `var x = 1;
var y = 2;
return __result({ sum: x + y });`;
const result = buildScript(code);
expect(result).toContain("var x = 1;");
expect(result).toContain("var y = 2;");
expect(result).toContain("return __result({ sum: x + y });");
});
it("handles empty code", () => {
const result = buildScript("");
expect(result).toContain("(function() {");
expect(result).toContain("})();");
});
it("returns a string", () => {
expect(typeof buildScript("")).toBe("string");
});
});
describe("buildToolScript (alias)", () => {
it("is the same function as buildScript", () => {
expect(buildToolScript).toBe(buildScript);
});
it("produces identical output to buildScript", () => {
const code = "return __result({ test: true });";
expect(buildToolScript(code)).toBe(buildScript(code));
});
});
describe("escapeForExtendScript", () => {
it("escapes backslashes", () => {
expect(escapeForExtendScript("C:\\Users\\test")).toBe("C:\\\\Users\\\\test");
});
it("escapes double quotes", () => {
expect(escapeForExtendScript('say "hello"')).toBe('say \\"hello\\"');
});
it("escapes single quotes", () => {
expect(escapeForExtendScript("it's")).toBe("it\\'s");
});
it("escapes newlines", () => {
expect(escapeForExtendScript("line1\nline2")).toBe("line1\\nline2");
});
it("escapes carriage returns", () => {
expect(escapeForExtendScript("line1\rline2")).toBe("line1\\rline2");
});
it("escapes tabs", () => {
expect(escapeForExtendScript("col1\tcol2")).toBe("col1\\tcol2");
});
it("handles empty strings", () => {
expect(escapeForExtendScript("")).toBe("");
});
it("handles strings with no special characters", () => {
expect(escapeForExtendScript("hello world")).toBe("hello world");
});
it("handles multiple escape characters in one string", () => {
const input = 'C:\\path\\to\n"file"\t\'test\'';
const result = escapeForExtendScript(input);
expect(result).toBe('C:\\\\path\\\\to\\n\\"file\\"\\t\\\'test\\\'');
});
it("handles unicode characters (passes through)", () => {
expect(escapeForExtendScript("日本語")).toBe("日本語");
});
});
describe("generated script structure", () => {
it("bootstrap loads this exact helpers version via $.evalFile", () => {
const bootstrap = buildBootstrap("/tmp/x/" + helpersFileName());
expect(bootstrap).toContain(`__HELPERS_V !== "${HELPERS_VERSION}"`);
expect(bootstrap).toContain(`$.evalFile("/tmp/x/helpers_${HELPERS_VERSION}.jsx")`);
expect(getHelpersSource()).toContain(`var __HELPERS_V = "${HELPERS_VERSION}";`);
});
it("bootstrap escapes quotes and backslashes in the helpers path", () => {
const bootstrap = buildBootstrap('C:\\temp\\he"rs.jsx');
expect(bootstrap).toContain('$.evalFile("C:\\\\temp\\\\he\\"rs.jsx")');
});
it("__findProjectItem recursively searches bins", () => {
const result = getHelpersSource();
expect(result).toContain("if (item.type === 2)");
expect(result).toContain("var found = __findProjectItem(nodeIdOrName, item);");
});
it("normalizes numeric host IDs without changing exact name matching", () => {
const result = getHelpersSource();
expect(result).toContain("var wantedId = String(idOrName);");
expect(result).toContain("String(seq.sequenceID) === wantedId || seq.name === idOrName");
expect(result).toContain("var wantedId = String(nodeIdOrName);");
expect(result).toContain("String(item.nodeId) === wantedId || item.name === nodeIdOrName");
expect(result).toContain("var wantedId = String(nodeId);");
expect(result).toContain("String(clip.nodeId) === wantedId");
});
it("__findClip searches both video and audio tracks", () => {
const result = getHelpersSource();
expect(result).toContain("seq.videoTracks.numTracks");
expect(result).toContain("seq.audioTracks.numTracks");
expect(result).toContain('trackType: "video"');
expect(result).toContain('trackType: "audio"');
});
it("__jsonStringify handles all types", () => {
const result = getHelpersSource();
expect(result).toContain('if (obj === null) return "null"');
expect(result).toContain('if (typeof obj === "string")');
expect(result).toContain('if (typeof obj === "number"');
expect(result).toContain("if (obj instanceof Array)");
expect(result).toContain('if (typeof obj === "object")');
});
});
describe("helpers execute correctly in an ES3-like engine", () => {
it("__result works when JSON is undefined (polyfill must not recurse into itself)", () => {
// Regression: __jsonStringify once delegated to JSON.stringify while the JSON
// polyfill delegated back to __jsonStringify — infinite mutual recursion that
// stack-overran the shared ExtendScript engine on every tool response.
const sandbox: Record<string, unknown> = { JSON: undefined };
const out = runInNewContext(
getHelpersSource() + '\n__result({ connected: true, nested: { n: 1, arr: [1, "a", false, null] } });',
sandbox
);
expect(out).toBe('{"success":true,"data":{"connected":true,"nested":{"n":1,"arr":[1,"a",false,null]}}}');
});
it("a stale wrapper from an older helpers version gets replaced", () => {
// Simulate a polluted long-lived engine: JSON.stringify is our old-style wrapper.
const stale = { stringify: function badWrapper(o: unknown) { return "__jsonStringify" + String(o); } };
const sandbox: Record<string, unknown> = { JSON: stale };
runInNewContext(getHelpersSource(), sandbox);
const json = sandbox.JSON as { stringify: (o: unknown) => string; __mcpPolyfill?: boolean };
expect(json.__mcpPolyfill).toBe(true);
expect(json.stringify({ ok: 1 })).toBe('{"ok":1}');
});
});
+155
View File
@@ -0,0 +1,155 @@
import { describe, expect, it, vi } from "vitest";
import { buildCaptionTimingPlan, parseCaptionArtifact } from "../src/ai/caption-timing.js";
vi.mock("../src/bridge/file-bridge.js", () => ({
sendCommand: vi.fn(),
}));
import { getCaptionTools } from "../src/tools/captions.js";
const SRT = `1
00:00:00,000 --> 00:00:02,000
Opening
2
00:00:05,000 --> 00:00:07,000
Middle
3
00:00:08,000 --> 00:00:10,000
Ending`;
describe("caption timing plans", () => {
it("parses bounded SRT and VTT artifacts without exposing cue text in the plan", () => {
const srt = parseCaptionArtifact(SRT, "srt");
const vtt = parseCaptionArtifact(`WEBVTT
00:00:00.000 --> 00:00:02.000 align:start
Opening`, "vtt");
expect(srt.cues).toHaveLength(3);
expect(vtt.cues).toEqual([{ startSeconds: 0, endSeconds: 2 }]);
const plan = buildCaptionTimingPlan(SRT, "srt", { targetDurationSeconds: 10 });
expect(plan).toMatchObject({
status: "aligned",
applied: false,
cueCount: 3,
correction: { proposed: false, kind: "none" },
});
expect(JSON.stringify(plan)).not.toContain("Opening");
});
it("proposes only a safe constant-offset correction", () => {
const plan = buildCaptionTimingPlan(SRT, "srt", {
observedOffsetSeconds: -1,
targetDurationSeconds: 11,
});
expect(plan).toMatchObject({
status: "constant_offset",
correction: { kind: "shift", shiftSeconds: 1, scale: 1, proposed: true },
});
expect(plan.samples.at(-1)).toMatchObject({
before: { endSeconds: 10 },
after: { endSeconds: 11 },
});
});
it("requires explicit authorization before proposing proportional drift correction", () => {
const withheld = buildCaptionTimingPlan(SRT, "srt", { targetDurationSeconds: 12 });
expect(withheld).toMatchObject({ status: "review_required", correction: { proposed: false, kind: "none" } });
const planned = buildCaptionTimingPlan(SRT, "srt", {
targetDurationSeconds: 12,
allowProportionalScaling: true,
});
expect(planned).toMatchObject({
status: "proportional_drift",
correction: { kind: "scale", proposed: true, scale: 1.2 },
});
expect(planned.samples.at(-1)).toMatchObject({ after: { endSeconds: 12 } });
});
it("rejects invalid timing, overlap, and unsafe leading negative correction", () => {
expect(() => parseCaptionArtifact("1\n00:00:02,000 --> 00:00:01,000\nBad", "srt"))
.toThrow("must end after");
expect(() => parseCaptionArtifact("1\n00:00:00,000 --> 00:00:02,000\nA\n\n2\n00:00:01,000 --> 00:00:03,000\nB", "srt"))
.toThrow("overlaps");
expect(() => parseCaptionArtifact("00:00:00,000 --> later\nBad", "srt"))
.toThrow("invalid timecode");
const unsafe = buildCaptionTimingPlan(SRT, "srt", { observedOffsetSeconds: 1 });
expect(unsafe).toMatchObject({ status: "review_required", correction: { proposed: false, shiftSeconds: 0 } });
});
it("rejects malformed caller input and withholds ambiguous duration scaling", () => {
expect(() => parseCaptionArtifact("", "srt")).toThrow("caption_content is required");
expect(() => parseCaptionArtifact(SRT, "txt")).toThrow("artifact_format");
expect(() => parseCaptionArtifact("00:00:00,000 --> 00:00:01,000\nNo header", "vtt"))
.toThrow("begin with WEBVTT");
expect(() => parseCaptionArtifact("NOTE an ignored note", "srt")).toThrow("no timed cues");
expect(() => buildCaptionTimingPlan(SRT, "srt", { allowProportionalScaling: "yes" as unknown as boolean }))
.toThrow("allow_proportional_scaling");
const leadIn = "1\n00:00:02,000 --> 00:00:04,000\nOpening";
expect(buildCaptionTimingPlan(leadIn, "srt", {
targetDurationSeconds: 8,
allowProportionalScaling: true,
})).toMatchObject({ status: "review_required", correction: { proposed: false, kind: "none" } });
expect(buildCaptionTimingPlan(SRT, "srt", {
targetDurationSeconds: 30,
allowProportionalScaling: true,
})).toMatchObject({ status: "review_required", correction: { proposed: false, kind: "none" } });
});
it("keeps a caller-owned one-cue artifact aligned without optional timing inputs", () => {
const plan = buildCaptionTimingPlan("1\n00:00:01,000 --> 00:00:02,000\nOnly cue", "srt");
expect(plan).toMatchObject({
status: "aligned",
samples: [{ position: "beginning", cueNumber: 1 }],
});
expect(plan).not.toHaveProperty("targetDurationSeconds");
expect(plan).not.toHaveProperty("observedOffsetSeconds");
expect(plan.samples[0]).not.toHaveProperty("after");
});
});
describe("guided lecture-caption action", () => {
it("returns a local review plan with separate structural, playback, and rendered-output states", async () => {
const tool = getCaptionTools({}).create_caption_track as any;
const result = await tool.handler({
action: "plan_lecture_workflow",
caption_content: SRT,
artifact_format: "srt",
target_duration_seconds: 10,
});
expect(result).toMatchObject({
success: true,
data: {
applied: false,
workflow: {
id: "guided_lecture_caption_workflow",
appliesCaptionArtifact: false,
verification: {
structuralReadback: "not_run",
playbackReview: "not_run",
renderedOutputReview: "not_run",
},
},
},
});
expect(result.data.workflow.steps.map((step: { route: string }) => step.route)).toEqual([
"manage_sequences_uxp",
"create_caption_track",
"create_caption_track",
"read_sequence_captions",
"export_sequence_review_frames",
]);
});
it("preserves import behavior and rejects an import without its project item", async () => {
const tool = getCaptionTools({}).create_caption_track as any;
await expect(tool.handler({})).resolves.toMatchObject({ success: false, error: expect.stringContaining("item_id") });
});
});
+121
View File
@@ -0,0 +1,121 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const root = process.cwd();
describe("CEP installation metadata", () => {
it("keeps the CEP bundle and extension versions aligned with package.json", () => {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const manifest = readFileSync(join(root, "cep-plugin", "CSXS", "manifest.xml"), "utf8");
const updater = readFileSync(join(root, "cep-plugin", "updater.cjs"), "utf8");
expect(manifest).toContain(`ExtensionBundleVersion="${pkg.version}"`);
expect(manifest.match(new RegExp(`Version="${pkg.version.replaceAll(".", "\\.")}"`, "g"))).toHaveLength(3);
expect(updater).toContain(`CURRENT_VERSION = "${pkg.version}"`);
});
it("declares Premiere compatibility with a Marketplace-safe minimum only", () => {
const manifest = readFileSync(join(root, "cep-plugin", "CSXS", "manifest.xml"), "utf8");
expect(manifest).toContain('<Host Name="PPRO" Version="14.0"/>');
expect(manifest).not.toMatch(/<Host Name="PPRO" Version="[[(]/);
});
it("ships a separately addressed After Effects authoring connector", () => {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const manifest = readFileSync(join(root, "after-effects-cep-plugin", "CSXS", "manifest.xml"), "utf8");
const panel = readFileSync(join(root, "after-effects-cep-plugin", "main.js"), "utf8");
const cli = readFileSync(join(root, "src", "index.ts"), "utf8");
expect(pkg.files).toContain("after-effects-cep-plugin");
expect(manifest).toContain(`ExtensionBundleVersion="${pkg.version}"`);
expect(manifest).toContain('<Host Name="AEFT" Version="15.0"/>');
expect(panel).toContain("after-effects-mcp-bridge");
expect(panel).toContain("AFTER_EFFECTS_MCP_TEMP_DIR");
expect(cli).toContain("--install-after-effects-cep");
expect(cli).toContain('powershellArgs.push("-ConnectorHost", "AfterEffects")');
});
it("documents the Windows unsigned-extension value as REG_SZ", () => {
const readme = readFileSync(join(root, "README.md"), "utf8");
const installer = readFileSync(join(root, "scripts", "install-cep.ps1"), "utf8");
expect(readme).toContain("String (`REG_SZ`)");
expect(readme).not.toContain("set these DWORD values");
expect(installer).toContain('-PropertyType String -Value "1"');
expect(installer).toContain("artifacts\\MCPBridgeCEP.zxp");
expect(installer).toContain('ExtensionBundleVersion="');
expect(installer).toContain("signedPackageMatchesRelease");
expect(installer).toContain("embedded connector version does not match package version");
expect(installer).toContain("Signature verification failed");
});
it("builds and verifies a signed Windows CEP release package", () => {
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const signer = readFileSync(join(root, "scripts", "build-signed-cep.ps1"), "utf8");
const workflow = readFileSync(join(root, ".github", "workflows", "npm-publish.yml"), "utf8");
expect(packageJson.files).toContain("artifacts/MCPBridgeCEP.zxp");
expect(signer).toContain("-selfSignedCert");
expect(signer).toContain("-verify");
expect(workflow).toContain("build-signed-cep");
expect(workflow).toContain("signed-cep");
});
it("publishes connector updates and exposes the updater in the panel", () => {
const workflow = readFileSync(join(root, ".github", "workflows", "cep-release.yml"), "utf8");
const panel = readFileSync(join(root, "cep-plugin", "index.html"), "utf8");
const panelLogic = readFileSync(join(root, "cep-plugin", "main.js"), "utf8");
expect(workflow).toContain("release:");
expect(workflow).toContain("artifacts/MCPBridgeCEP.zxp");
expect(panel).toContain('src="updater.cjs"');
expect(panel).toContain("MCP updates");
expect(panel).toContain('aria-describedby="updateDetail"');
expect(panelLogic).toContain("Update after quit");
expect(panelLogic).toContain("scheduleWindowsGlobalUpdate");
expect(panelLogic).toContain("window.confirm");
expect(panelLogic).toContain("getPerUserGlobalInstall");
expect(panelLogic).toContain("LATEST_PACKAGE_API");
expect(panelLogic).toContain("This panel will not modify a source checkout.");
expect(panelLogic).toContain("writeResponseFile(resFilePath, response)");
expect(panelLogic).toContain("fs.renameSync(stagedPath, filePath)");
expect(panelLogic).toContain('"bridge-heartbeat.json"');
expect(panelLogic).toContain("protocolVersion: 1");
expect(panelLogic).toContain("startBridgeHeartbeat()");
expect(panelLogic).toContain("PREMIERE_TEMP_DIR");
expect(panelLogic).toContain('nodeRequire("process")');
expect(panelLogic).toContain('return "mcpstate:"');
expect(panelLogic).toContain("/^mcpstate:([01]),([01])$/");
expect(panelLogic).not.toContain("return JSON.stringify({projectOpen:");
});
it("copies the macOS plugin for npm installs and supports diagnostics", () => {
const cli = readFileSync(join(root, "src", "index.ts"), "utf8");
const installer = readFileSync(join(root, "scripts", "install-cep.sh"), "utf8");
expect(cli).toContain('diagnose ? "--diagnose" : "--copy"');
expect(installer).toContain('HOST="Premiere"');
expect(installer).toContain('for arg in "$@"; do');
expect(installer).toContain("--after-effects");
expect(installer).toContain('if [ "$MODE" = "--diagnose" ]');
expect(installer).toContain("Installation verified");
});
it("rejects CEP installation on unsupported host operating systems", () => {
const cli = readFileSync(join(root, "src", "index.ts"), "utf8");
expect(cli).toContain("supported only on Windows and macOS");
});
it("offers an idempotent connector-only uninstall path on both supported operating systems", () => {
const cli = readFileSync(join(root, "src", "index.ts"), "utf8");
const windows = readFileSync(join(root, "scripts", "uninstall-cep.ps1"), "utf8");
const macos = readFileSync(join(root, "scripts", "uninstall-cep.sh"), "utf8");
expect(cli).toContain("--uninstall-cep");
expect(windows).toContain("Refusing to uninstall outside the CEP extensions directory");
expect(windows).toContain("PlayerDebugMode settings were left unchanged");
expect(macos).toContain("--uninstall-system");
expect(macos).toContain("PlayerDebugMode setting was left unchanged");
});
});
+112
View File
@@ -0,0 +1,112 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, it } from "vitest";
const inventory = JSON.parse(readFileSync("src/resources/cep-reference-inventory.json", "utf8"));
const fixtureNames = [
"Adobe-CEP__CEP-Resources.json",
"Adobe-CEP__Samples.json",
"docsforadobe__premiere-scripting-guide.json",
];
function runWithFixtures(overrides: Record<string, string>) {
const directory = mkdtempSync(join(tmpdir(), "cep-reference-inventory-"));
const output = join(directory, "inventory.json");
const validTrees = [
{ tree: [{ type: "blob", path: "CSInterface.js", sha: "a".repeat(40), size: 1 }] },
{ tree: [{ type: "blob", path: "PProPanel/ReadMe.md", sha: "b".repeat(40), size: 2 }] },
{ tree: [{ type: "blob", path: "docs/index.md", sha: "c".repeat(40), size: 3 }] },
];
fixtureNames.forEach((name, index) => writeFileSync(join(directory, name), JSON.stringify(validTrees[index])));
for (const [name, contents] of Object.entries(overrides)) writeFileSync(join(directory, name), contents);
const result = spawnSync(process.execPath, ["scripts/generate-cep-reference-inventory.mjs"], {
encoding: "utf8",
env: { ...process.env, CEP_INVENTORY_FIXTURE_DIRECTORY: directory, CEP_INVENTORY_OUTPUT_PATH: output },
});
return { directory, output, result };
}
describe("CEP and Premiere scripting reference inventory", () => {
it("pins and accounts for every file without mixing authority classes", () => {
expect(inventory.schemaVersion).toBe(1);
expect(inventory.sources).toEqual([
expect.objectContaining({
repository: "Adobe-CEP/CEP-Resources",
commit: "ab5e4e3e53a42fad08e1225a22a991bb1ffe73f6",
authority: "adobe",
}),
expect.objectContaining({
repository: "Adobe-CEP/Samples",
commit: "e4946b73ac1e566dced8e95dba10811c31036927",
authority: "adobe",
pathPrefix: "PProPanel/",
}),
expect.objectContaining({
repository: "docsforadobe/premiere-scripting-guide",
commit: "4253cea094e84d43590b77012b33bd1c140f72ea",
authority: "community",
}),
]);
expect(inventory.stats.total).toBe(inventory.entries.length);
expect(Object.values(inventory.stats.byRepository).reduce((sum: number, count) => sum + Number(count), 0))
.toBe(inventory.stats.total);
expect(new Set(inventory.entries.map((entry: { repository: string; path: string }) => `${entry.repository}:${entry.path}`)).size)
.toBe(inventory.entries.length);
});
it("retains exact blob identities and bounded categories", () => {
const categories = new Set([
"asset", "cep-runtime-library", "configuration", "documentation", "documentation-tooling",
"sample", "signing-tool", "site-asset", "source",
]);
for (const entry of inventory.entries) {
expect(entry.commit).toMatch(/^[0-9a-f]{40}$/);
expect(entry.blobSha).toMatch(/^[0-9a-f]{40}$/);
expect(entry.size).toBeGreaterThanOrEqual(0);
expect(categories.has(entry.category)).toBe(true);
expect(["adobe", "community"]).toContain(entry.authority);
if (entry.repository === "Adobe-CEP/Samples") expect(entry.path).toMatch(/^PProPanel\//);
}
expect(inventory.entries).toEqual(expect.arrayContaining([
expect.objectContaining({ repository: "Adobe-CEP/Samples", path: "PProPanel/ReadMe.md", category: "sample" }),
expect.objectContaining({ repository: "docsforadobe/premiere-scripting-guide", authority: "community", category: "documentation" }),
]));
});
it.each([
["malformed", "Adobe-CEP__CEP-Resources.json", "{", "SyntaxError"],
["empty", "Adobe-CEP__CEP-Resources.json", JSON.stringify({ tree: [] }), "No files matched"],
["truncated", "Adobe-CEP__CEP-Resources.json", JSON.stringify({ truncated: true, tree: [] }), "truncated tree"],
["invalid metadata", "Adobe-CEP__CEP-Resources.json", JSON.stringify({ tree: [{ type: "blob", path: "x.js", sha: "bad", size: 1 }] }), "Invalid Git blob metadata"],
["duplicate", "Adobe-CEP__CEP-Resources.json", JSON.stringify({ tree: [
{ type: "blob", path: "x.js", sha: "d".repeat(40), size: 1 },
{ type: "blob", path: "x.js", sha: "e".repeat(40), size: 2 },
] }), "duplicate repository paths"],
])("fails closed for a %s fixture tree", (_label, name, contents, expectedError) => {
const { directory, result } = runWithFixtures({ [name]: contents });
try {
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(expectedError);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("executes the generator against fixtures and preserves classification boundaries", () => {
const { directory, output, result } = runWithFixtures({});
try {
expect(result.status).toBe(0);
const generated = JSON.parse(readFileSync(output, "utf8"));
expect(generated.entries).toEqual(expect.arrayContaining([
expect.objectContaining({ path: "CSInterface.js", category: "cep-runtime-library", authority: "adobe" }),
expect.objectContaining({ path: "PProPanel/ReadMe.md", category: "sample", authority: "adobe" }),
expect.objectContaining({ path: "docs/index.md", category: "documentation", authority: "community" }),
]));
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
});
+109
View File
@@ -0,0 +1,109 @@
import { createRequire } from "node:module";
import { join, win32 } from "node:path";
import { describe, expect, it, vi } from "vitest";
const require = createRequire(import.meta.url);
const updater = require(join(process.cwd(), "cep-plugin", "updater.cjs"));
describe("CEP connector updater", () => {
it("compares release versions numerically", () => {
expect(updater.compareVersions("v1.10.0", "1.9.9")).toBe(1);
expect(updater.compareVersions("1.3.1", "1.3.1")).toBe(0);
expect(updater.compareVersions("1.3.0", "1.3.1")).toBe(-1);
});
it("prefers the signed connector package", () => {
const release = {
html_url: "https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.4.0",
assets: [
{
name: "source.zip",
browser_download_url: "https://github.com/example/source.zip",
},
{
name: "MCPBridgeCEP.zxp",
browser_download_url:
"https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.4.0/MCPBridgeCEP.zxp",
},
],
};
expect(updater.chooseDownloadUrl(release)).toContain("MCPBridgeCEP.zxp");
});
it("rejects untrusted download hosts", () => {
expect(updater.isTrustedDownloadUrl("https://github.com/example/file.zxp")).toBe(true);
expect(updater.isTrustedDownloadUrl("https://evil.example/file.zxp")).toBe(false);
});
it("reads the latest global package version from the npm registry record", () => {
const update = updater.updateStateFromPackageRecord("1.14.7", {
"dist-tags": { latest: "v1.14.8" },
});
expect(updater.LATEST_PACKAGE_API).toBe("https://registry.npmjs.org/premiere-pro-mcp");
expect(update).toEqual({
currentVersion: "1.14.7",
latestVersion: "1.14.8",
updateAvailable: true,
});
expect(() => updater.latestPackageVersion({ "dist-tags": { latest: "nightly" } }))
.toThrow("valid latest version");
});
it("creates a detached Windows updater that waits for Premiere instead of closing it", () => {
const scriptPath = "C:\\Temp\\premiere-pro-mcp-update-abc.ps1";
const statusPath = "C:\\Temp\\premiere-pro-mcp-update-abc.json";
const cliPath = "C:\\Users\\editor\\AppData\\Roaming\\npm\\premiere-pro-mcp.cmd";
const script = updater.buildWindowsGlobalUpdateScript(cliPath, statusPath, scriptPath);
expect(script).toContain("while (Get-Process -Name $premiereProcesses");
expect(script).toContain("Start-Sleep -Seconds 2");
expect(script).toContain("Get-Command npm.cmd");
expect(script).toContain("& $npmCommand install --global 'premiere-pro-mcp@latest'");
expect(script).toContain("& $cliPath --install-cep");
expect(script).toContain("premiere-pro-mcp.desktop-update.v1");
expect(script).not.toContain("Stop-Process");
});
it("schedules only an existing absolute per-user command and cleans up if launch fails", () => {
const writes = new Map<string, string>();
const unref = vi.fn();
const spawn = vi.fn(() => ({ unref }));
const runtime = {
fs: {
existsSync: vi.fn(() => true),
mkdirSync: vi.fn(),
writeFileSync: vi.fn((file: string, value: string) => writes.set(file, value)),
unlinkSync: vi.fn(),
},
path: win32,
os: { tmpdir: () => "C:\\Temp" },
childProcess: { spawn },
crypto: { randomBytes: () => Buffer.from("abcdef", "hex") },
};
const cliPath = "C:\\Users\\editor\\AppData\\Roaming\\npm\\premiere-pro-mcp.cmd";
const scheduled = updater.scheduleWindowsGlobalUpdate({ cliPath, runtime });
expect(scheduled.statusPath).toBe("C:\\Temp\\premiere-pro-mcp-update-abcdef.json");
expect(writes.get("C:\\Temp\\premiere-pro-mcp-update-abcdef.ps1"))
.toContain("& $npmCommand install --global 'premiere-pro-mcp@latest'");
expect(spawn).toHaveBeenCalledWith(
"powershell.exe",
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Temp\\premiere-pro-mcp-update-abcdef.ps1"],
{ detached: true, windowsHide: true, stdio: "ignore" },
);
expect(unref).toHaveBeenCalledOnce();
expect(() => updater.scheduleWindowsGlobalUpdate({ cliPath: "relative.cmd", runtime }))
.toThrow("could not be resolved");
const failingRuntime = {
...runtime,
childProcess: { spawn: vi.fn(() => { throw new Error("launch failed"); }) },
};
expect(() => updater.scheduleWindowsGlobalUpdate({ cliPath, runtime: failingRuntime }))
.toThrow("launch failed");
expect(runtime.fs.unlinkSync).toHaveBeenCalled();
});
});
+84
View File
@@ -0,0 +1,84 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
const readJson = (path: string) => JSON.parse(read(path)) as Record<string, unknown>;
const release = readJson("release-metadata.json");
const registry = readJson("docs/claims-registry.json") as {
schemaVersion: number;
authoritativeReleaseMetadata: string;
claims: Array<{
id: string;
status: string;
claim: string;
fields?: string[];
boundary: string;
}>;
prohibitedUntilEvidenceExists: string[];
};
const renderReleaseClaim = (claim: string) =>
claim.replaceAll(/\{([^}]+)\}/g, (_match, field: string) => String(release[field]));
describe("product claims registry", () => {
it("is a unique, versioned registry with explicit boundaries", () => {
expect(registry.schemaVersion).toBe(1);
expect(registry.authoritativeReleaseMetadata).toBe("release-metadata.json");
expect(registry.claims.map((claim) => claim.id)).toHaveLength(
new Set(registry.claims.map((claim) => claim.id)).size,
);
expect(registry.claims.every((claim) => claim.boundary.trim().length > 0)).toBe(true);
expect(registry.prohibitedUntilEvidenceExists.length).toBeGreaterThan(0);
});
it("derives every release-backed claim from canonical release metadata", () => {
const releaseClaims = registry.claims.filter((claim) => claim.status === "release_metadata");
expect(releaseClaims).toHaveLength(2);
for (const claim of releaseClaims) {
expect(claim.fields?.length).toBeGreaterThan(0);
for (const field of claim.fields ?? []) {
expect(release).toHaveProperty(field);
expect(claim.claim).toContain(`{${field}}`);
}
}
});
it("keeps the product-marketing context aligned with the rendered release facts", () => {
const marketingContext = read(".agents/product-marketing.md");
const releaseSurface = registry.claims.find((claim) => claim.id === "release-capability-surface");
const compatibility = registry.claims.find((claim) => claim.id === "release-compatibility");
expect(releaseSurface).toBeDefined();
expect(compatibility).toBeDefined();
expect(marketingContext).toContain(renderReleaseClaim(releaseSurface!.claim));
expect(marketingContext).toContain(renderReleaseClaim(compatibility!.claim));
});
it("labels commercial pricing as a hypothesis and records evidence-gated boundaries", () => {
const pricing = registry.claims.find((claim) => claim.id === "commercial-companion-pricing");
const marketingContext = read(".agents/product-marketing.md");
expect(pricing?.status).toBe("hypothesis");
expect(pricing?.claim.toLowerCase()).toContain("hypotheses");
expect(marketingContext).toContain("unvalidated pricing hypotheses");
expect(marketingContext).toContain("No approved customer-logo claims, adoption claims, case studies, or public testimonials are currently documented.");
expect(marketingContext).toContain("Current production activation, retention, support, conversion, and revenue metrics have not been queried");
expect(marketingContext).toContain("Do not claim current Adobe Marketplace approval");
});
it("rejects known stale or unsupported marketing phrases on governed public surfaces", () => {
const governedSurfaces = [
".agents/product-marketing.md",
"README.md",
"landing/lib/articles.ts",
].map(read);
const governedContent = governedSurfaces.join("\n");
expect(governedContent).not.toMatch(/49 (?:documented, )?capability-gated tools/i);
expect(read("landing/components/sections/hero.tsx")).not.toMatch(/editor approved/i);
});
});
+59
View File
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import {
dbToPremiereLevel,
premiereLevelToDb,
PREMIERE_MAX_LEVEL_DB,
} from "../src/tools/track-targeting.js";
/**
* Premiere's `Volume > Level` is a normalised 0..1 value, not decibels.
* Measured against Premiere Pro 26.3.2: a clip left untouched (0 dB in the
* UI) reads back 0.17782794, which is exactly 10^(-15/20). That fixes the
* scale - 1.0 is +15 dB.
*/
describe("Premiere audio level conversion", () => {
it("maps unity gain to Premiere's observed 0 dB value", () => {
expect(dbToPremiereLevel(0)).toBeCloseTo(0.17782794, 7);
});
it("maps the top of the range to 1.0", () => {
expect(dbToPremiereLevel(PREMIERE_MAX_LEVEL_DB)).toBeCloseTo(1.0, 10);
});
it("keeps every level inside the range Premiere accepts", () => {
for (const db of [-60, -18, -11, -5.8, 0, 6, 15]) {
const level = dbToPremiereLevel(db);
expect(level).toBeGreaterThanOrEqual(0);
expect(level).toBeLessThanOrEqual(1);
}
});
it("clamps above +15 dB rather than exceeding 1.0", () => {
expect(dbToPremiereLevel(40)).toBeCloseTo(1.0, 10);
});
it("round-trips dB -> level -> dB", () => {
for (const db of [-24, -17.7, -11, -5.8, 0, 7.5]) {
const back = premiereLevelToDb(dbToPremiereLevel(db));
expect(back).not.toBeNull();
expect(back as number).toBeCloseTo(db, 6);
}
});
it("reports silence as null rather than -Infinity", () => {
expect(premiereLevelToDb(0)).toBeNull();
expect(premiereLevelToDb(-1)).toBeNull();
});
/**
* Regression guard. Before the fix, volume_db was passed straight to
* setValue(): -11 dB fell outside 0..1 and clamped to 0 (silence), while
* +7 dB clamped to 1.0 (+15 dB). Both failed without raising an error.
*/
it("does not pass raw decibels through as a level", () => {
expect(dbToPremiereLevel(-11)).not.toBe(-11);
expect(dbToPremiereLevel(-11)).toBeGreaterThan(0);
expect(dbToPremiereLevel(7)).not.toBe(7);
expect(dbToPremiereLevel(7)).toBeLessThan(1);
});
});
+141
View File
@@ -0,0 +1,141 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const readJson = (path: string) =>
JSON.parse(readFileSync(join(root, path), "utf8"));
describe("Codex plugin package", () => {
it("keeps the plugin and MCP package versions aligned", () => {
const pkg = readJson("package.json");
const plugin = readJson("plugins/premiere-pro/.codex-plugin/plugin.json");
const mcp = readJson("plugins/premiere-pro/.mcp.json");
expect(plugin.name).toBe("premiere-pro");
expect(plugin.version).toBe(pkg.version);
expect(plugin.skills).toBe("./skills/");
expect(plugin.mcpServers).toBe("./.mcp.json");
expect(mcp.mcpServers["premiere-pro"].args).toContain(
`premiere-pro-mcp@${pkg.version}`,
);
});
it("publishes the plugin through the repository marketplace", () => {
const marketplace = readJson(".agents/plugins/marketplace.json");
const entry = marketplace.plugins.find(
(plugin: { name: string }) => plugin.name === "premiere-pro",
);
expect(marketplace.name).toBe("premiere-pro-mcp");
expect(entry).toMatchObject({
source: { source: "local", path: "./plugins/premiere-pro" },
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
category: "Creativity",
});
});
it("ships a complete Premiere editing skill without placeholders", () => {
const skill = readFileSync(
join(
root,
"plugins",
"premiere-pro",
"skills",
"edit-premiere-project",
"SKILL.md",
),
"utf8",
);
expect(skill).toContain("name: edit-premiere-project");
expect(skill).toContain("Call `ping`");
expect(skill).toContain("preview_edit_plan");
expect(skill).toContain("export_sequence");
expect(skill).not.toContain("TODO");
});
});
describe("Claude distributions", () => {
it("keeps Claude Code metadata aligned with the npm package", () => {
const pkg = readJson("package.json");
const marketplace = readJson(".claude-plugin/marketplace.json");
const plugin = readJson(
"claude-plugins/premiere-pro/.claude-plugin/plugin.json",
);
const mcp = readJson("claude-plugins/premiere-pro/.mcp.json");
expect(marketplace.name).toBe("premiere-pro-mcp");
expect(marketplace.plugins[0]).toMatchObject({
name: "premiere-pro",
source: "./claude-plugins/premiere-pro",
version: pkg.version,
});
expect(plugin.version).toBe(pkg.version);
expect(plugin.mcpServers).toBe("./.mcp.json");
expect(mcp.mcpServers["premiere-pro"].args).toContain(
`premiere-pro-mcp@${pkg.version}`,
);
});
it("keeps the Claude and Codex editing skills identical", () => {
const codexSkill = readFileSync(
join(
root,
"plugins",
"premiere-pro",
"skills",
"edit-premiere-project",
"SKILL.md",
),
"utf8",
);
const claudeSkill = readFileSync(
join(
root,
"claude-plugins",
"premiere-pro",
"skills",
"edit-premiere-project",
"SKILL.md",
),
"utf8",
);
expect(claudeSkill).toBe(codexSkill);
});
it("defines a self-contained Claude Desktop MCP bundle", () => {
const pkg = readJson("package.json");
const manifest = readJson("claude-desktop/manifest.json");
expect(manifest).toMatchObject({
manifest_version: "0.4",
name: "premiere-pro-mcp",
version: pkg.version,
server: {
type: "node",
entry_point: "server/dist/index.js",
},
compatibility: {
platforms: ["darwin", "win32"],
},
});
expect(manifest.server.mcp_config.args).toContain(
"${__dirname}/server/dist/index.js",
);
expect(manifest.user_config.premiere_uxp_token).toMatchObject({
type: "string",
sensitive: true,
required: true,
});
expect(manifest.user_config.premiere_mcp_protocol_mode).toMatchObject({
type: "string",
required: false,
});
expect(manifest.server.mcp_config.env).toEqual({
PREMIERE_UXP_TOKEN: "${user_config.premiere_uxp_token}",
PREMIERE_MCP_PROTOCOL_MODE: "${user_config.premiere_mcp_protocol_mode}",
});
});
});
+55
View File
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
describe("native connector installers", () => {
it("keeps Windows installation inside the current user's CEP directory", () => {
const source = read("installer/windows/Program.cs");
expect(source).toContain("Environment.SpecialFolder.ApplicationData");
expect(source).toContain("EnsureInsideCepRoot");
expect(source).toContain("RegistryValueKind.String");
expect(source).toContain("GetManifestResourceStream");
});
it("rejects unsafe ZIP paths and an invalid connector payload", () => {
const source = read("installer/windows/Program.cs");
expect(source).toContain("contains an unsafe path");
expect(source).toContain("CSXS");
expect(source).toContain("manifest.xml");
});
it("verifies the shipped Windows installer payload without installing it", () => {
const source = read("installer/windows/Program.cs");
const workflow = read(".github/workflows/connector-installers.yml");
expect(source).toContain('"--verify-only"');
expect(source).toContain("VerifyEmbeddedPackage");
expect(source).toContain("premiere-connector-validate");
expect(source).toContain("GetManifestResourceStream");
expect(workflow).toContain("--verify-only");
expect(workflow).toContain("Start-Process");
expect(workflow).toContain("without installing it");
});
it("makes production signing an explicit fail-closed gate", () => {
const windows = read("scripts/build-connector-installer.ps1");
const macos = read("scripts/build-connector-installer.sh");
const workflow = read(".github/workflows/connector-installers.yml");
expect(windows).toContain("RequireSigning");
expect(windows).toContain("Authenticode verification failed");
expect(macos).toContain("REQUIRE_SIGNING");
expect(workflow).toContain("require_production_signing");
});
it("ships a guarded macOS companion uninstaller and blocks Windows removal while Premiere is open", () => {
const windows = read("installer/windows/Program.cs");
const macos = read("scripts/build-connector-installer.sh");
const workflow = read(".github/workflows/connector-installers.yml");
expect(windows).toContain("Close it before removing the Connector");
expect(macos).toContain("Premiere-Connector-Uninstall-$VERSION-macos.command");
expect(macos).toContain("uninstall-cep.sh");
expect(workflow).toContain("*.command");
});
});
+145
View File
@@ -0,0 +1,145 @@
import { describe, expect, it } from "vitest";
import {
buildFirstRunReport,
collectLocalDoctor,
createDoctorRepairPlan,
createSupportBundle,
renderDoctorHuman,
} from "../src/diagnostics.js";
describe("non-technical diagnostics", () => {
it("keeps local doctor states separate from a live Premiere verification", () => {
const report = collectLocalDoctor({
platform: "win32",
nodeVersion: "v22.12.0",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
exists: (file) => file.endsWith("manifest.xml"),
now: () => new Date("2026-08-01T12:00:00.000Z"),
});
expect(report.overall).toBe("ready");
expect(report.runtime).toEqual({ platform: "win32", nodeMajor: 22 });
expect(report.components).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "premiere_connector", code: "CEP_CONNECTOR_READY", boundary: "installed", state: "ready" }),
expect.objectContaining({ id: "premiere_host", code: "PREMIERE_HOST_NOT_CHECKED", boundary: "live_verified", state: "not_checked" }),
]));
});
it("creates a privacy-safe, no-write repair plan with stable diagnostic codes", () => {
const report = collectLocalDoctor({
platform: "win32",
nodeVersion: "v22.12.0",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
exists: () => false,
now: () => new Date("2026-09-04T00:00:00.000Z"),
});
const plan = createDoctorRepairPlan(report);
expect(plan).toMatchObject({
schemaVersion: "premiere-pro-mcp.doctor-repair-plan.v1",
actions: expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", diagnosticCode: "CEP_CONNECTOR_MISSING", canApplyLocally: true, createsBackup: true }),
expect.objectContaining({ id: "verify_live_connection", diagnosticCode: "PREMIERE_HOST_NOT_CHECKED", canApplyLocally: false }),
]),
});
const serialized = JSON.stringify(plan);
expect(serialized).not.toContain("C:\\Users\\Example");
expect(serialized).not.toContain("APPDATA");
});
it("proves the complete first-run lane with booleans only", () => {
const report = buildFirstRunReport("cep", {
reachable: true,
projectOpen: true,
sequenceOpen: true,
});
expect(report).toMatchObject({
overall: "ready",
safeCheck: { readOnly: true, mutatesProject: false },
});
expect(report.components).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "mcp_process", boundary: "connected", state: "ready" }),
expect.objectContaining({ id: "premiere_connector", boundary: "connected", state: "ready" }),
expect.objectContaining({ id: "active_project", boundary: "live_verified", state: "ready" }),
expect.objectContaining({ id: "active_sequence", boundary: "live_verified", state: "ready" }),
]));
});
it("gives a repairable outcome when the connector cannot answer", () => {
const report = buildFirstRunReport("uxp", { reachable: false });
expect(report.overall).toBe("needs_attention");
expect(report.safeCheck.mutatesProject).toBe(false);
expect(report.components.find((component) => component.id === "premiere_connector"))
.toMatchObject({ boundary: "connected", state: "needs_attention" });
expect(report.repair).toContain("diagnose-cep");
});
it("creates a support snapshot without credentials, paths, or project data", () => {
const bundle = createSupportBundle({
version: "1.7.0",
platform: "win32",
architecture: "x64",
nodeVersion: "v22.12.0",
environment: {
APPDATA: "C:\\Users\\Private\\AppData",
PREMIERE_UXP_TOKEN: "very-secret-token-value",
POSTHOG_API_KEY: "another-secret",
},
exists: () => true,
now: () => new Date("2026-08-01T12:00:00.000Z"),
});
const serialized = JSON.stringify(bundle);
expect(serialized).not.toContain("very-secret-token-value");
expect(serialized).not.toContain("another-secret");
expect(serialized).not.toContain("C:\\Users\\Private");
expect(serialized).not.toMatch(/"(?:projectName|mediaName|outputDirectory|arguments|results)"\s*:/);
});
it("reports unsupported local layouts and malformed Node versions conservatively", () => {
const report = collectLocalDoctor({
platform: "linux", nodeVersion: "not-a-version", environment: {},
exists: () => { throw new Error("must not inspect an unknown path"); },
now: () => new Date("2026-08-02T00:00:00.000Z"),
});
expect(report.overall).toBe("needs_attention");
expect(report.runtime.nodeMajor).toBeNull();
expect(report.components).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "node_runtime", code: "NODE_RUNTIME_UNSUPPORTED", state: "needs_attention" }),
expect.objectContaining({ id: "premiere_connector", state: "needs_attention", repair: expect.any(String) }),
expect.objectContaining({ id: "uxp_bridge", state: "not_checked" }),
]));
const human = renderDoctorHuman(report);
expect(human).toContain("Premiere MCP local check: needs attention");
expect(human).toContain("Next: Install the Connector");
expect(human).toContain("Not checked: UXP connection");
});
it("describes each reachable but incomplete first-run state", () => {
const noProject = buildFirstRunReport("cep", { reachable: true });
expect(noProject.overall).toBe("needs_attention");
expect(noProject.components.find((item) => item.id === "active_project"))
.toMatchObject({ state: "needs_attention", repair: expect.any(String) });
expect(noProject.components.find((item) => item.id === "active_sequence"))
.toMatchObject({ state: "needs_attention", repair: expect.any(String) });
const noSequence = buildFirstRunReport("uxp", { reachable: true, projectOpen: true });
expect(noSequence.components.find((item) => item.id === "active_project"))
.toMatchObject({ state: "ready" });
expect(noSequence.components.find((item) => item.id === "active_sequence"))
.toMatchObject({ state: "needs_attention" });
});
it("recognizes the macOS connector path and configured UXP without exposing the token", () => {
let inspected = "";
const report = collectLocalDoctor({
platform: "darwin", environment: { HOME: "/Users/example", PREMIERE_UXP_TOKEN: "secret" },
exists: (file) => { inspected = file; return true; },
});
expect(inspected).toContain("Library");
expect(report.components.find((item) => item.id === "uxp_bridge")).toMatchObject({ state: "ready" });
expect(JSON.stringify(report)).not.toContain("secret");
expect(renderDoctorHuman(report)).toContain("Premiere MCP local check: ready");
});
});
+25
View File
@@ -0,0 +1,25 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
describe("Docker release build context", () => {
it("copies every repository script required by the package build", () => {
const dockerfile = readFileSync("Dockerfile", "utf8");
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;
};
const buildScripts = [...packageJson.scripts.build.matchAll(/scripts\/[\w-]+\.mjs/g)]
.map((match) => match[0]);
expect(buildScripts).toEqual([
"scripts/generate-adobe-api-inventory.mjs",
"scripts/generate-uxp-js-api-inventory.mjs",
"scripts/copy-adobe-uxp-coverage.mjs",
]);
for (const script of buildScripts) {
expect(dockerfile).toContain(`COPY ${script} ./${script}`);
}
for (const generator of buildScripts.filter((script) => script.includes("/generate-"))) {
expect(packageJson.scripts.build).toContain(`${generator} --check`);
}
});
});
+140
View File
@@ -0,0 +1,140 @@
import { describe, expect, it, vi } from "vitest";
import { applyDoctorRepairPlan } from "../src/doctor-repairs.js";
import { collectLocalDoctor, createDoctorRepairPlan } from "../src/diagnostics.js";
function missingConnectorReport() {
return collectLocalDoctor({
platform: "win32",
nodeVersion: "v22.12.0",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
exists: () => false,
now: () => new Date("2026-09-04T00:00:00.000Z"),
});
}
describe("doctor repair application", () => {
it("withholds a connector write until Premiere closure is explicitly confirmed", () => {
const runInstaller = vi.fn();
const result = applyDoctorRepairPlan(createDoctorRepairPlan(missingConnectorReport()), {
projectRoot: "D:\\fixture",
platform: "win32",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
runInstaller,
collect: missingConnectorReport,
});
expect(runInstaller).not.toHaveBeenCalled();
expect(result.actions).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", status: "withheld", backupCreated: false }),
]));
expect(JSON.stringify(result)).not.toContain("C:\\Users\\Example");
});
it("backs up an incomplete connector before an explicit installer run and rechecks local readiness", () => {
const rename = vi.fn();
const runInstaller = vi.fn();
const readyReport = collectLocalDoctor({
platform: "win32",
nodeVersion: "v22.12.0",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
exists: () => true,
now: () => new Date("2026-09-04T00:00:01.000Z"),
});
const result = applyDoctorRepairPlan(createDoctorRepairPlan(missingConnectorReport()), {
projectRoot: "D:\\fixture",
platform: "win32",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
confirmPremiereClosed: true,
exists: () => true,
rename,
runInstaller,
collect: () => readyReport,
now: () => new Date("2026-09-04T00:00:01.000Z"),
});
expect(rename).toHaveBeenCalledOnce();
expect(runInstaller).toHaveBeenCalledWith("win32", "D:\\fixture");
expect(result).toMatchObject({
applied: true,
actions: expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", status: "applied", backupCreated: true }),
]),
});
expect(JSON.stringify(result)).not.toContain("C:\\Users\\Example");
});
it("keeps non-connector and non-applicable repair guidance manual", () => {
const plan = createDoctorRepairPlan(missingConnectorReport());
const connector = plan.actions.find((action) => action.id === "install_cep_connector")!;
const runInstaller = vi.fn();
const result = applyDoctorRepairPlan({
...plan,
actions: [
{ ...connector, id: "upgrade_node_runtime", canApplyLocally: false },
{ ...connector, canApplyLocally: false },
],
}, {
projectRoot: "D:\\fixture",
platform: "win32",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
confirmPremiereClosed: true,
runInstaller,
collect: missingConnectorReport,
});
expect(runInstaller).not.toHaveBeenCalled();
expect(result.applied).toBe(false);
expect(result.actions).toHaveLength(2);
expect(result.actions.every((action) => action.status === "manual_required")).toBe(true);
});
it("fails closed when no supported connector directory is available or a post-install recheck is not ready", () => {
const plan = createDoctorRepairPlan(missingConnectorReport());
const unavailable = applyDoctorRepairPlan(plan, {
projectRoot: "D:\\fixture",
platform: "linux",
environment: {},
confirmPremiereClosed: true,
runInstaller: vi.fn(),
collect: missingConnectorReport,
});
expect(unavailable.actions).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", status: "manual_required" }),
]));
const runInstaller = vi.fn();
const incomplete = applyDoctorRepairPlan(plan, {
projectRoot: "D:\\fixture",
platform: "win32",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
confirmPremiereClosed: true,
exists: () => false,
runInstaller,
collect: missingConnectorReport,
});
expect(runInstaller).toHaveBeenCalledOnce();
expect(incomplete.actions).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", status: "failed", backupCreated: false }),
]));
});
it("retains a backup and reports a failed result when connector installation throws", () => {
const rename = vi.fn();
const result = applyDoctorRepairPlan(createDoctorRepairPlan(missingConnectorReport()), {
projectRoot: "D:\\fixture",
platform: "win32",
environment: { APPDATA: "C:\\Users\\Example\\AppData" },
confirmPremiereClosed: true,
exists: () => true,
rename,
runInstaller: () => { throw new Error("fixture installer failure"); },
collect: missingConnectorReport,
now: () => new Date("2026-09-04T00:00:01.000Z"),
});
expect(rename).toHaveBeenCalledOnce();
expect(result.actions).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "install_cep_connector", status: "failed", backupCreated: true }),
]));
});
});
+43
View File
@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../src/bridge/file-bridge.js", async (importOriginal) => {
const original = await importOriginal<typeof import("../src/bridge/file-bridge.js")>();
return { ...original, sendCommand: vi.fn(async () => ({ success: true, data: { applied: true } })) };
});
import { sendCommand } from "../src/bridge/file-bridge.js";
import { confirmationToken, getEditPlanTools, validateEditPlan } from "../src/tools/edit-plans.js";
const plan = { operations: [{ type: "insert_clip" as const, item_id: "clip-1", start_seconds: 2 }] };
describe("edit plans", () => {
beforeEach(() => vi.clearAllMocks());
it("previews without contacting Premiere", async () => {
const tools = getEditPlanTools({}, { capabilities: { capabilities: new Set(["inspect"]), source: "explicit" }, operationIdFactory: () => "preview-1" });
const result = await tools.preview_edit_plan.handler({ plan });
expect(result.data).toMatchObject({ operationId: "preview-1", applied: false, confirmationToken: confirmationToken(plan) });
expect(sendCommand).not.toHaveBeenCalled();
});
it("requires the edit capability before apply", async () => {
const tools = getEditPlanTools({}, { capabilities: { capabilities: new Set(["inspect"]), source: "explicit" }, auditSink: vi.fn(), operationIdFactory: () => "apply-1" });
await expect(tools.apply_edit_plan.handler({ plan, confirmation_token: confirmationToken(plan) })).rejects.toMatchObject({ code: "CAPABILITY_DENIED" });
expect(sendCommand).not.toHaveBeenCalled();
});
it("rejects changed plans and applies an exact previewed plan in one command", async () => {
const auditSink = vi.fn();
const tools = getEditPlanTools({}, { capabilities: { capabilities: new Set(["inspect", "edit"]), source: "explicit" }, auditSink, operationIdFactory: () => "apply-2" });
await expect(tools.apply_edit_plan.handler({ plan: { ...plan, sequence_id: "different" }, confirmation_token: confirmationToken(plan) })).rejects.toThrow("does not match");
const result = await tools.apply_edit_plan.handler({ plan, confirmation_token: confirmationToken(plan) });
expect(result).toMatchObject({ success: true, data: { applied: true, operationId: "apply-2" } });
expect(sendCommand).toHaveBeenCalledOnce();
expect(auditSink).toHaveBeenCalledTimes(3);
});
it("rejects unsupported and unsafe operations", () => {
expect(() => validateEditPlan({ operations: [{ type: "raw_script", code: "app.quit()" }] })).toThrow("unsupported type");
expect(() => validateEditPlan({ operations: [{ type: "insert_clip", item_id: "x", start_seconds: -1 }] })).toThrow("non-negative");
});
});
+255
View File
@@ -0,0 +1,255 @@
import { describe, expect, it } from "vitest";
import {
buildEditorialContextPack,
MAX_EDITORIAL_CONTEXT_PACK_CHARACTERS,
MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS,
} from "../src/ai/editorial-context-pack.js";
import {
ProjectContextRepository,
searchProjectContext,
type ProjectContextDocument,
} from "../src/context/project-context-store.js";
import { getEditorialContextPackTools } from "../src/tools/editorial-context-pack.js";
import { createServer } from "../src/server.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
function document(): ProjectContextDocument {
return {
schemaVersion: 1,
projectId: "project-1",
projectName: "Launch interview",
revision: "context-r1",
sourceRevision: "source-r1",
timelineRevision: "timeline-r1",
updatedAt: "2026-09-02T00:00:00.000Z",
records: [
{
id: "source-1",
kind: "source",
name: "Founder interview",
text: "Source Founder interview. Media type mov.",
keywords: ["interview", "mov"],
sourceId: "source-item-1",
sourceRevision: "source-r1",
indexedAt: "2026-09-02T00:00:00.000Z",
},
{
id: "transcript-1",
kind: "transcript",
name: "Founder on the launch budget",
text: "We made the launch budget work by simplifying the onboarding flow.",
keywords: ["launch", "budget", "onboarding"],
sequenceId: "sequence-1",
sourceId: "source-item-1",
startSeconds: 12.5,
endSeconds: 17.25,
sourceRevision: "source-r1",
timelineRevision: "timeline-r1",
metadata: { secret: "must-not-appear" },
indexedAt: "2026-09-02T00:00:00.000Z",
},
{
id: "shot-1",
kind: "shot",
name: "Product close-up",
text: "Product close-up during the onboarding explanation.",
keywords: ["product", "onboarding"],
sequenceId: "sequence-1",
sourceId: "source-item-2",
startSeconds: 30,
endSeconds: 35,
sourceRevision: "source-r1",
indexedAt: "2026-09-02T00:00:00.000Z",
},
],
};
}
describe("editorial context pack", () => {
it("renders stable, compact evidence without leaking metadata", () => {
const source = document();
const results = searchProjectContext(source, { query: "launch budget onboarding", limit: 12 });
const first = buildEditorialContextPack(source, { intent: "launch budget onboarding", results });
const second = buildEditorialContextPack(source, { intent: "launch budget onboarding", results });
expect(first).toMatchObject({
schemaVersion: 1,
projectId: "project-1",
expectedContextRevision: "context-r1",
expectedSourceRevision: "source-r1",
expectedTimelineRevision: "timeline-r1",
applied: false,
truncated: false,
});
expect(first.markdown).toContain("# Premiere editorial context pack");
expect(first.markdown).toContain("evidence transcript-1");
expect(first.markdown).toContain("12.500s–17.250s");
expect(first.markdown).toContain("We made the launch budget work");
expect(first.markdown).not.toContain("must-not-appear");
expect(first.evidence[0]).toMatchObject({
evidenceId: "transcript-1",
kind: "transcript",
sourceId: "source-item-1",
startSeconds: 12.5,
endSeconds: 17.25,
textTruncated: false,
});
expect(first).toEqual(second);
});
it("enforces a strict Markdown cap and labels partial evidence", () => {
const source = document();
source.records[1].text = `Budget evidence ${"with supporting detail ".repeat(3_000)}`;
const results = searchProjectContext(source, { query: "budget", limit: 12 });
const pack = buildEditorialContextPack(source, {
intent: "budget",
results,
maxCharacters: MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS,
});
expect(pack.markdown.length).toBeLessThanOrEqual(MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS);
expect(pack.truncated).toBe(true);
expect(pack.evidence[0]).toMatchObject({ evidenceId: "transcript-1", textTruncated: true });
expect(pack.markdown).toContain("[excerpt truncated]");
});
it("preserves the Markdown cap when project labels and revisions are long", () => {
const source = document();
source.projectName = "Project ".repeat(80);
source.revision = "context-".repeat(100);
source.sourceRevision = "source-".repeat(100);
source.timelineRevision = "timeline-".repeat(100);
const intent = `budget ${"review ".repeat(140)}`;
const results = searchProjectContext(source, { query: "budget", limit: 12 });
const pack = buildEditorialContextPack(source, {
intent,
results,
maxCharacters: MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS,
});
expect(pack.intent).toBe(intent.trim());
expect(pack.markdown.length).toBeLessThanOrEqual(MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS);
});
it("validates pack bounds before producing output", () => {
const source = document();
const results = searchProjectContext(source, { query: "budget" });
expect(() => buildEditorialContextPack(source, { intent: " ", results })).toThrow("intent must not be empty");
expect(() => buildEditorialContextPack(source, { intent: "budget", results, maxEntries: 0 })).toThrow("max_entries");
expect(() => buildEditorialContextPack(source, { intent: "budget", results, maxCharacters: MIN_EDITORIAL_CONTEXT_PACK_CHARACTERS - 1 })).toThrow("max_characters");
expect(() => buildEditorialContextPack(source, { intent: "budget", results, maxCharacters: MAX_EDITORIAL_CONTEXT_PACK_CHARACTERS + 1 })).toThrow("max_characters");
});
it("omits unavailable record metadata from compact evidence", () => {
const source = document();
const record = source.records[0];
delete record.sourceId;
delete record.sourceRevision;
const results = searchProjectContext(source, { query: "interview" }).filter((result) => result.record.id === record.id);
const pack = buildEditorialContextPack(source, { intent: "interview", results });
expect(pack.evidence).toHaveLength(1);
expect(pack.evidence[0]).not.toHaveProperty("sequenceId");
expect(pack.evidence[0]).not.toHaveProperty("sourceId");
expect(pack.evidence[0]).not.toHaveProperty("timelineItemId");
expect(pack.evidence[0]).not.toHaveProperty("startSeconds");
expect(pack.evidence[0]).not.toHaveProperty("endSeconds");
expect(pack.evidence[0]).not.toHaveProperty("sourceRevision");
expect(pack.evidence[0]).not.toHaveProperty("timelineRevision");
expect(pack.markdown).not.toContain("source source-item-1");
});
it("reads only stored local context and rejects invalid tool input", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const tools = getEditorialContextPackTools({ repository });
await repository.put(document());
await expect(tools.create_editorial_context_pack.handler({
project_id: "project-1",
intent: "launch budget",
kinds: ["transcript"],
max_entries: 4,
max_characters: 2_000,
})).resolves.toMatchObject({
success: true,
data: {
applied: false,
evidence: [expect.objectContaining({ kind: "transcript" })],
},
});
await expect(tools.create_editorial_context_pack.handler({
project_id: "missing",
intent: "launch budget",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("not found") });
await expect(tools.create_editorial_context_pack.handler({
project_id: "project-1",
intent: "launch budget",
kinds: ["transcript", "transcript"],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("duplicates") });
await expect(tools.create_editorial_context_pack.handler({
project_id: "project-1",
intent: "launch budget",
max_characters: 1,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("max_characters") });
await expect(tools.create_editorial_context_pack.handler({
project_id: "p".repeat(513),
intent: "launch budget",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("at most 512") });
});
it("rejects malformed optional filters before searching context", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const tool = getEditorialContextPackTools({ repository }).create_editorial_context_pack;
await repository.put(document());
await expect(tool.handler({ project_id: "project-1", intent: "budget", sequence_id: " " }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("sequence_id") });
await expect(tool.handler({ project_id: "project-1", intent: "budget", kinds: [] }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("between 1 and 8") });
await expect(tool.handler({ project_id: "project-1", intent: "budget", kinds: ["unsupported"] as any }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("supported project-context kind") });
await expect(tool.handler({ project_id: "project-1", intent: "budget", kinds: [1] as any }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("supported project-context kind") });
});
it("reports entry overflow and excludes records with no matched intent term", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const source = document();
for (let index = 2; index <= 100; index++) {
source.records.push({
...source.records[1], id: `transcript-${index}`, name: `Budget answer ${index}`,
text: `The launch budget answer ${index}.`,
});
}
await repository.put(source);
const tool = getEditorialContextPackTools({ repository }).create_editorial_context_pack;
await expect(tool.handler({ project_id: "project-1", intent: "budget", max_entries: 12 }))
.resolves.toMatchObject({
success: true,
data: { evidence: expect.any(Array), omittedEvidenceCount: 88, truncated: true },
});
await expect(tool.handler({ project_id: "project-1", intent: "unrelated", kinds: ["transcript"] }))
.resolves.toMatchObject({ success: true, data: { evidence: [], omittedEvidenceCount: 0, truncated: false } });
});
it("shares the injected server context repository with the reading tool", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document());
const server = createServer({}, { contextRepository: repository });
const client = new Client({ name: "editorial-context-pack-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
const result = await client.callTool({
name: "create_editorial_context_pack",
arguments: { project_id: "project-1", intent: "launch budget" },
});
expect((result.structuredContent as any).data).toMatchObject({ projectId: "project-1", applied: false });
} finally {
await client.close();
await server.close();
}
});
});
+808
View File
@@ -0,0 +1,808 @@
import { describe, expect, it, vi } from "vitest";
import {
buildEditorialPlan,
validateEditorialPlan,
} from "../src/ai/editorial-plan.js";
import { ProjectContextRepository } from "../src/context/project-context-store.js";
import { buildContextDocumentFromSnapshot, enrichContextDocument, type PremiereContextSnapshot } from "../src/tools/project-context.js";
import { getEditorialPlanTools } from "../src/tools/editorial-plans.js";
import type { UxpWebSocketBridge } from "../src/bridge/uxp-websocket-bridge.js";
async function context() {
const snapshot: PremiereContextSnapshot = {
projectName: "Documentary",
projectPath: "D:/Projects/documentary.prproj",
sequence: {
id: "sequence-1",
name: "Assembly",
durationSeconds: 120,
truncated: false,
clips: [{
nodeId: "timeline-1",
name: "Interview A",
startSeconds: 10,
endSeconds: 20,
inPointSeconds: 30,
outPointSeconds: 40,
speed: 1,
trackType: "video",
trackIndex: 0,
sourceId: "source-1",
sourceName: "Interview A.mov",
mediaPath: "D:/Media/interview-a.mov",
offline: false,
}],
},
};
const built = await buildContextDocumentFromSnapshot(snapshot, undefined, async () => ({ mediaPathHash: "source-hash" }));
const source = built.document.records.find((record) => record.kind === "source")!;
return enrichContextDocument(built.document, [{
kind: "transcript",
source_id: "source-1",
source_revision: source.sourceRevision,
start_seconds: 31,
end_seconds: 35,
text: "The budget needs approval by Friday.",
keywords: ["budget", "approval", "interview"],
metadata: { confidence: 0.97 },
}]).document;
}
function verifiedUxpReadback(boundary: string, result: Record<string, unknown>) {
return {
...result,
outcome: "verified",
verified: true,
verificationBoundary: boundary,
operation: {
verification: {
status: "verified",
boundary,
evidence: [{ type: boundary, verified: true }],
},
},
};
}
async function createAndPreviewOrganizationPlan(
tools: any,
document: Awaited<ReturnType<typeof context>>,
organizationRules: Array<{ name: string; keywords: string[]; colorIndex?: number }>,
) {
const created = await tools.create_editorial_plan.handler({
project_id: document.projectId,
workflow: "organize",
intent: "interview budget",
organization_rules: organizationRules.map((rule) => ({
name: rule.name,
keywords: rule.keywords,
...(rule.colorIndex === undefined ? {} : { color_index: rule.colorIndex }),
})),
});
expect(created).toMatchObject({ success: true, data: { workflow: "organize", applied: false } });
const plan = created.data;
const preview = await tools.preview_editorial_plan.handler({ plan });
expect(preview).toMatchObject({ success: true, data: { applied: false, workflow: "organize" } });
return { plan, confirmationToken: preview.data.confirmationToken as string };
}
describe("editorial workflow plans", () => {
it("creates a review-only organization plan from explicit rules", async () => {
const document = await context();
const plan = buildEditorialPlan(document, {
workflow: "organize",
intent: "interview budget approval",
organizationRules: [{ name: "Interviews", keywords: ["interview"], colorIndex: 3 }],
});
expect(plan).toMatchObject({
schemaVersion: 1,
workflow: "organize",
applied: false,
expectedContextRevision: document.revision,
expectedTimelineRevision: document.timelineRevision,
recommendations: [expect.objectContaining({
route: "apply_editorial_organization_plan",
mutatesProject: false,
requiresReview: true,
})],
});
expect(plan.limitations.join(" ")).toContain("does not call an LLM");
});
it("routes rough cuts and caption review without claiming automatic application", async () => {
const document = await context();
const roughCut = buildEditorialPlan(document, { workflow: "rough_cut", intent: "budget approval" });
const caption = buildEditorialPlan(document, { workflow: "caption_review", intent: "budget approval" });
expect(roughCut.recommendations[0]).toMatchObject({ route: "preview_transcript_edit_uxp", mutatesProject: false });
expect(roughCut.limitations.join(" ")).toContain("licensed Premiere host");
expect(caption.recommendations[0]).toMatchObject({ route: "create_caption_track", mutatesProject: false });
expect(caption.limitations.join(" ")).toContain("paid-provider request");
});
it("plans bounded platform cutdowns without creating, reframing, or exporting a sequence", async () => {
const document = await context();
const plan = buildEditorialPlan(document, {
workflow: "platform_cutdown",
intent: "interview budget",
platformTargets: [{ name: "Vertical short", width: 1080, height: 1920, includeCaptions: true }],
});
expect(plan).toMatchObject({ workflow: "platform_cutdown", applied: false });
expect(plan.recommendations[0]).toMatchObject({
kind: "create_platform_cutdown",
route: "manage_sequences_uxp",
details: expect.objectContaining({
sourceSequenceId: "sequence-1",
target: { width: 1080, height: 1920 },
includeCaptions: true,
}),
});
expect((plan.recommendations[0].details.nextRoutes as Array<{ tool: string }>).map((route) => route.tool))
.toEqual(["manage_sequences_uxp", "auto_reframe_sequence", "create_caption_track", "get_sequence_structure", "export_sequence"]);
expect(plan.limitations.join(" ")).toContain("does not create a sequence");
expect(() => buildEditorialPlan(document, { workflow: "platform_cutdown", intent: "cutdown" }))
.toThrow("platform_targets are required");
expect(() => buildEditorialPlan(document, {
workflow: "platform_cutdown",
intent: "cutdown",
sequenceId: "missing-sequence",
platformTargets: [{ name: "Square", width: 1080, height: 1080 }],
})).toThrow("matching sequence_id");
expect(() => buildEditorialPlan(document, {
workflow: "platform_cutdown",
intent: "cutdown",
platformTargets: [{ name: "Bad dimensions", width: 0, height: 1080 }],
})).toThrow("width must be an integer");
});
it("requires explicit categories and rejects plans that claim they were applied", async () => {
const document = await context();
expect(() => buildEditorialPlan(document, { workflow: "organize", intent: "interviews" }))
.toThrow("organization_rules are required");
const plan = buildEditorialPlan(document, { workflow: "stringout", intent: "interview" });
expect(() => validateEditorialPlan({ ...plan, applied: true })).toThrow("preview-only");
expect(() => validateEditorialPlan({ ...plan, recommendations: [] })).toThrow("recommendations must contain");
expect(() => buildEditorialPlan(document, { workflow: "stringout", intent: "interview", maxCandidates: Number.POSITIVE_INFINITY }))
.toThrow("max_candidates must be a finite number");
});
it("bounds malformed organization, cutdown, and serialized-plan inputs", async () => {
const document = await context();
for (const rules of [
{} as any,
Array.from({ length: 17 }, () => ({ name: "Bin", keywords: ["interview"] })),
[null],
[{ name: "", keywords: ["interview"] }],
[{ name: "Bin", keywords: [] }],
[{ name: "Bin", keywords: ["interview"] }, { name: "bin", keywords: ["budget"] }],
[{ name: "Bin", keywords: ["interview"], color_index: 15 }],
]) {
expect(() => buildEditorialPlan(document, { workflow: "organize", intent: "interview", organizationRules: rules }))
.toThrow();
}
for (const targets of [
[],
[null],
[{ name: "", width: 1080, height: 1920 }],
[{ name: "Square", width: 1080, height: 1080 }, { name: "square", width: 1080, height: 1080 }],
[{ name: "Missing width", height: 1080 }],
[{ name: "Bad height", width: 1080, height: 0 }],
[{ name: "Bad captions", width: 1080, height: 1920, include_captions: "yes" }],
]) {
expect(() => buildEditorialPlan(document, { workflow: "platform_cutdown", intent: "cutdown", platformTargets: targets as any }))
.toThrow();
}
const plan = buildEditorialPlan(document, { workflow: "stringout", intent: "interview" });
for (const malformed of [
null,
{ ...plan, schemaVersion: 2 },
{ ...plan, workflow: "unknown" },
{ ...plan, projectId: "" },
{ ...plan, candidates: null },
{ ...plan, candidates: [{}] },
{ ...plan, recommendations: [] },
{ ...plan, recommendations: [null] },
{ ...plan, recommendations: [{ ...plan.recommendations[0], route: "" }] },
{ ...plan, recommendations: [{ ...plan.recommendations[0], mutatesProject: true }] },
{ ...plan, recommendations: [{ ...plan.recommendations[0], candidateEvidenceIds: ["unknown-evidence"] }] },
{ ...plan, limitations: [] },
]) {
expect(() => validateEditorialPlan(malformed)).toThrow();
}
});
it("creates and previews plans only while the captured revision remains current", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const tools = getEditorialPlanTools({ repository });
const created = await tools.create_editorial_plan.handler({
project_id: document.projectId,
workflow: "stringout",
intent: "interview budget",
});
expect(created).toMatchObject({ success: true, data: { applied: false, workflow: "stringout" } });
const plan = created.data as any;
const preview = await tools.preview_editorial_plan.handler({ plan });
expect(preview).toMatchObject({ success: true, data: { applied: false, workflow: "stringout" } });
expect((preview as any).data.confirmationToken).toMatch(/^[A-Za-z0-9_-]{32,}$/);
await repository.put({
...document,
revision: "changed-context-revision",
timelineRevision: "changed-timeline-revision",
});
await expect(tools.preview_editorial_plan.handler({ plan }))
.resolves.toEqual(expect.objectContaining({ success: false, error: expect.stringContaining("stale") }));
});
it("rejects client-modified server-issued plans during preview and apply", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn();
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
const sourceIndex = plan.candidates.findIndex((candidate: any) => candidate.kind === "source");
const source = plan.candidates[sourceIndex];
const recommendation = plan.recommendations[0];
const sourceGuards = [{ evidence_id: source.evidenceId, project_item_id: source.sourceId, expected_parent_id: "root-bin" }];
const forgedPlans = [
{
...plan,
candidates: plan.candidates.map((candidate: any, index: number) => index === sourceIndex
? { ...candidate, sourceId: "attacker-source" }
: candidate),
},
{
...plan,
recommendations: [{ ...recommendation, details: { ...recommendation.details, proposedBinName: "Attacker Bin" } }],
},
{
...plan,
recommendations: [{ ...recommendation, route: "organize_project_items_uxp" }],
},
{
...plan,
recommendations: [{ ...recommendation, candidateEvidenceIds: [] }],
},
];
for (const forgedPlan of forgedPlans) {
await expect(tools.preview_editorial_plan.handler({ plan: forgedPlan }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("was not issued") });
await expect(tools.apply_editorial_organization_plan.handler({
plan: forgedPlan,
confirmation_token: confirmationToken,
operation_id: "forged-plan",
organization_operations: [{ recommendation_id: recommendation.id, source_guards: sourceGuards }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("was not issued") });
}
expect(request).not.toHaveBeenCalled();
});
it("rejects duplicate source guards across the complete organization batch before UXP mutation", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn();
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[
{ name: "Interviews", keywords: ["interview"] },
{ name: "Interviews again", keywords: ["interview"] },
],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
const sourceGuard = { evidence_id: source.evidenceId, project_item_id: source.sourceId, expected_parent_id: "root-bin" };
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "duplicate-evidence",
organization_operations: plan.recommendations.map((recommendation: any) => ({
recommendation_id: recommendation.id,
source_guards: [sourceGuard],
})),
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("duplicate evidence_id") });
expect(request).not.toHaveBeenCalled();
const duplicateSourceDocument = structuredClone(document);
const originalSource = duplicateSourceDocument.records.find((record) => record.kind === "source")!;
duplicateSourceDocument.records.push({
...originalSource,
id: "duplicate-source-evidence",
name: "Interview alternate record.mov",
text: "Interview alternate record.",
keywords: ["interview"],
});
await repository.put(duplicateSourceDocument);
const duplicateSourceTools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const duplicateSourcePlan = await createAndPreviewOrganizationPlan(
duplicateSourceTools,
duplicateSourceDocument,
[
{ name: "Interviews", keywords: ["interview"] },
{ name: "Interview copies", keywords: ["interview"] },
],
);
const [firstEvidence, secondEvidence] = duplicateSourcePlan.plan.candidates
.filter((candidate: any) => candidate.kind === "source")
.map((candidate: any) => candidate.evidenceId);
await expect(duplicateSourceTools.apply_editorial_organization_plan.handler({
plan: duplicateSourcePlan.plan,
confirmation_token: duplicateSourcePlan.confirmationToken,
operation_id: "duplicate-project-item",
organization_operations: duplicateSourcePlan.plan.recommendations.map((recommendation: any, index: number) => ({
recommendation_id: recommendation.id,
source_guards: [{
evidence_id: index === 0 ? firstEvidence : secondEvidence,
project_item_id: "source-1",
expected_parent_id: "root-bin",
}],
})),
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("duplicate project_item_id") });
expect(request).not.toHaveBeenCalled();
});
it("keeps platform-cutdown creation and preview local even when a UXP bridge is available", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn().mockRejectedValue(new Error("A plan must not call the host"));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge });
const created = await tools.create_editorial_plan.handler({
project_id: document.projectId,
workflow: "platform_cutdown",
intent: "interview budget",
platform_targets: [{ name: "Vertical", width: 1080, height: 1920, include_captions: true }],
});
expect(created).toMatchObject({
success: true,
data: { workflow: "platform_cutdown", applied: false },
});
await expect(tools.preview_editorial_plan.handler({ plan: created.data }))
.resolves.toMatchObject({ success: true, data: { applied: false, workflow: "platform_cutdown" } });
expect(request).not.toHaveBeenCalled();
});
it("returns clear errors when a project context is missing", async () => {
const tools = getEditorialPlanTools({ repository: new ProjectContextRepository({ backend: "memory" }) });
await expect(tools.create_editorial_plan.handler({
project_id: "missing",
workflow: "stringout",
intent: "interview",
})).resolves.toEqual({ success: false, error: "Project context not found; capture it before creating an editorial plan" });
});
it("registers organization application only with the UXP bridge and applies each guarded host transaction", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn()
.mockResolvedValueOnce(verifiedUxpReadback("bin_child_id_readback", { created: true, item: { id: "bin-interviews" } }))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_parent_readback", {
moved: true,
destinationBinId: "bin-interviews",
after: { parentId: "bin-interviews" },
}))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_color_readback", {
updated: true,
after: { colorLabelIndex: 3 },
}));
const bridge = { request } as unknown as UxpWebSocketBridge;
const withoutUxp = getEditorialPlanTools({ repository });
expect(withoutUxp).not.toHaveProperty("apply_editorial_organization_plan");
const tools = getEditorialPlanTools({ repository, uxpBridge: bridge }) as any;
expect(tools.apply_editorial_organization_plan.operationalCapability).toMatchObject({
verificationBoundary: "structured_uxp_readback",
hostVerificationRequired: true,
});
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"], colorIndex: 3 }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
expect(source).toMatchObject({ sourceId: "source-1" });
const result = await tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-2026-08-22",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
});
expect(result).toMatchObject({
success: true,
data: { applied: true, outcome: "verified", verified: true, committedActions: [
{ action: "create_bin", verified: true },
{ action: "move", projectItemId: "source-1", verified: true },
{ action: "set_color", projectItemId: "source-1", verified: true },
] },
});
expect(request.mock.calls).toEqual([
["bins.create", expect.objectContaining({ name: "Interviews", makeUnique: true, operationId: expect.stringMatching(/^organize-2026-08-22:create:/) })],
["bins.move", expect.objectContaining({ projectItemId: "source-1", destinationBinId: "bin-interviews", expectedParentId: "root-bin", operationId: expect.stringMatching(/^organize-2026-08-22:move:/) })],
["bins.color", expect.objectContaining({ projectItemId: "source-1", colorIndex: 3, operationId: expect.stringMatching(/^organize-2026-08-22:color:/) })],
]);
});
it("accepts an unchanged editorial plan after an MCP JSON round trip", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn()
.mockResolvedValueOnce(verifiedUxpReadback("bin_child_id_readback", { created: true, item: { id: "bin-interviews" } }))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_parent_readback", {
moved: true,
destinationBinId: "bin-interviews",
after: { parentId: "bin-interviews" },
}));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
const roundTrippedPlan = JSON.parse(JSON.stringify(plan));
const source = roundTrippedPlan.candidates.find((candidate: any) => candidate.kind === "source")!;
await expect(tools.apply_editorial_organization_plan.handler({
plan: roundTrippedPlan,
confirmation_token: confirmationToken,
operation_id: "organize-json-roundtrip",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({
success: true,
data: { applied: true, outcome: "verified", verified: true },
});
expect(request).toHaveBeenCalledTimes(2);
});
it("fails closed before UXP mutation for stale, unconfirmed, or unplanned organization input", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn();
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
const unpreviewed = await tools.create_editorial_plan.handler({
project_id: document.projectId,
workflow: "organize",
intent: "interview budget",
organization_rules: [{ name: "Unpreviewed", keywords: ["interview"] }],
});
const unpreviewedPlan = unpreviewed.data;
const unpreviewedSource = unpreviewedPlan.candidates.find((candidate: any) => candidate.kind === "source")!;
const validOperations = [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}];
await expect(tools.apply_editorial_organization_plan.handler({
plan: unpreviewedPlan,
confirmation_token: "not-previewed",
operation_id: "organize-unpreviewed",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: unpreviewedSource.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("opaque token") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: "wrong-token",
operation_id: "organize-invalid",
organization_operations: validOperations,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("confirmation_token") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-invalid",
organization_operations: [{ ...validOperations[0], source_guards: [{ ...validOperations[0].source_guards[0], project_item_id: "unplanned-source" }] }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("must match the planned source ID") });
await repository.put({ ...document, revision: "new-revision", timelineRevision: "new-timeline-revision" });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-stale",
organization_operations: validOperations,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("stale") });
expect(request).not.toHaveBeenCalled();
});
it("reports completed UXP transactions as partial without claiming rollback or verification", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn()
.mockResolvedValueOnce(verifiedUxpReadback("bin_child_id_readback", { created: true, item: { id: "bin-interviews" } }))
.mockRejectedValueOnce(new Error("expected parent no longer matches"));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"], colorIndex: 3 }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-partial",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({
success: true,
data: {
applied: true,
outcome: "partial",
verified: false,
committedActions: [{ action: "create_bin" }],
unverifiedAttempts: [{ action: "move", verified: false }],
nextSteps: expect.arrayContaining([expect.stringContaining("undo the verified completed host transactions")]),
},
});
expect(request).toHaveBeenCalledTimes(2);
});
it("halts before a later mutation when a verified UXP reply contradicts its readback", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn()
.mockResolvedValueOnce(verifiedUxpReadback("bin_child_id_readback", { created: true, item: { id: "bin-interviews" } }))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_parent_readback", {
moved: true,
destinationBinId: "bin-interviews",
after: { parentId: "wrong-bin" },
}))
.mockRejectedValueOnce(new Error("Color should not be attempted after a contradictory move readback"));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"], colorIndex: 3 }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-contradictory-readback",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({
success: true,
data: {
outcome: "partial",
verified: false,
committedActions: [
{ action: "create_bin", verified: true },
],
unverifiedAttempts: [{ action: "move", verified: false }],
error: expect.stringContaining("destination-parent readback"),
},
});
expect(request).toHaveBeenCalledTimes(2);
});
it("reports a color response with the wrong observed label as partial", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn()
.mockResolvedValueOnce(verifiedUxpReadback("bin_child_id_readback", { created: true, item: { id: "bin-interviews" } }))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_parent_readback", {
moved: true,
destinationBinId: "bin-interviews",
after: { parentId: "bin-interviews" },
}))
.mockResolvedValueOnce(verifiedUxpReadback("project_item_color_readback", {
updated: true,
after: { colorLabelIndex: 2 },
}));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"], colorIndex: 3 }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-color-readback",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({
success: true,
data: {
outcome: "partial",
verified: false,
committedActions: [
{ action: "create_bin", verified: true },
{ action: "move", verified: true },
],
unverifiedAttempts: [{ action: "set_color", verified: false }],
error: expect.stringContaining("color-label readback"),
},
});
expect(request).toHaveBeenCalledTimes(3);
});
it("fails a malformed first UXP response without claiming an applied plan", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const existingBinRequest = vi.fn().mockResolvedValue({ moved: true });
const existingBinTools = getEditorialPlanTools({ repository, uxpBridge: { request: existingBinRequest } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
existingBinTools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
const malformedResult = await existingBinTools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-existing-bin",
organization_operations: [{
recommendation_id: "organization-1",
destination_bin_id: "existing-bin",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
});
expect(malformedResult).toMatchObject({
success: false,
error: expect.stringContaining("destination-parent readback"),
});
expect(malformedResult.error).toContain("inspect Premiere before retrying");
expect(existingBinRequest).toHaveBeenCalledTimes(1);
expect(existingBinRequest).toHaveBeenCalledWith("bins.move", expect.objectContaining({ destinationBinId: "existing-bin" }));
const incompleteCreateRequest = vi.fn().mockResolvedValue({ item: {}, verified: false });
const incompleteCreateTools = getEditorialPlanTools({ repository, uxpBridge: { request: incompleteCreateRequest } as unknown as UxpWebSocketBridge }) as any;
const incomplete = await createAndPreviewOrganizationPlan(
incompleteCreateTools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
await expect(incompleteCreateTools.apply_editorial_organization_plan.handler({
plan: incomplete.plan,
confirmation_token: incomplete.confirmationToken,
operation_id: "organize-incomplete-create",
organization_operations: [{
recommendation_id: "organization-1",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
})).resolves.toMatchObject({
success: false,
error: expect.stringContaining("stable-bin readback"),
});
expect(incompleteCreateRequest).toHaveBeenCalledTimes(1);
});
it("fails a first UXP request rejection and warns that host state is unknown", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn().mockRejectedValue(new Error("UXP bridge disconnected"));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
const source = plan.candidates.find((candidate: any) => candidate.kind === "source")!;
const rejectedResult = await tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-rejected-first-request",
organization_operations: [{
recommendation_id: "organization-1",
destination_bin_id: "existing-bin",
source_guards: [{ evidence_id: source.evidenceId, project_item_id: "source-1", expected_parent_id: "root-bin" }],
}],
});
expect(rejectedResult).toMatchObject({
success: false,
error: expect.stringContaining("UXP bridge disconnected"),
});
expect(rejectedResult.error).toContain("may have reached the host");
expect(request).toHaveBeenCalledTimes(1);
});
it("returns local validation failures without starting a UXP transaction", async () => {
const document = await context();
const repository = new ProjectContextRepository({ backend: "memory" });
await repository.put(document);
const request = vi.fn().mockRejectedValue(new Error("host unavailable"));
const tools = getEditorialPlanTools({ repository, uxpBridge: { request } as unknown as UxpWebSocketBridge }) as any;
const { plan, confirmationToken } = await createAndPreviewOrganizationPlan(
tools,
document,
[{ name: "Interviews", keywords: ["interview"] }],
);
await expect(tools.create_editorial_plan.handler({
project_id: document.projectId,
workflow: "platform_cutdown",
intent: "cutdown",
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("platform_targets are required") });
await expect(tools.preview_editorial_plan.handler({ plan: null })).resolves.toMatchObject({ success: false, error: expect.stringContaining("plan must be an object") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("organization_operations") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [{ recommendation_id: "unknown", source_guards: [{ evidence_id: "missing", project_item_id: "source-1", expected_parent_id: "root" }] }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("organize_source recommendation") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [{ recommendation_id: "organization-1", source_guards: [] }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("source_guards") });
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [{ recommendation_id: "organization-1", source_guards: [null] }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("must be an object") });
const transcript = plan.candidates.find((candidate) => candidate.kind === "transcript")!;
await expect(tools.apply_editorial_organization_plan.handler({
plan,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [{ recommendation_id: "organization-1", source_guards: [{ evidence_id: transcript.evidenceId, project_item_id: "source-1", expected_parent_id: "root" }] }],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("must reference a source") });
const stringout = buildEditorialPlan(document, { workflow: "stringout", intent: "interview" });
await expect(tools.apply_editorial_organization_plan.handler({
plan: stringout,
confirmation_token: confirmationToken,
operation_id: "organize-host-failure",
organization_operations: [],
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("only accepts an organize") });
expect(request).not.toHaveBeenCalled();
});
});
+771
View File
@@ -0,0 +1,771 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { dirname } from "node:path";
import { RequestBodyTooLargeError } from "../src/http-admission.js";
const mocks = vi.hoisted(() => ({
requestHandler: undefined as undefined | ((req: any, res: any) => Promise<void>),
listen: vi.fn((_port: number, _host: string, callback: () => void) => callback()),
closeHttp: vi.fn(),
capture: vi.fn(),
shutdown: vi.fn(async () => {}),
cleanup: vi.fn(),
connect: vi.fn(async () => {}),
serveStdio: vi.fn(),
stdioServerTransport: vi.fn(),
closeMcp: vi.fn(async () => {}),
handleRequest: vi.fn(async (_req: any, res: any) => { res.statusCode = 204; }),
closeTransport: vi.fn(async () => {}),
uxpStart: vi.fn(async () => {}),
uxpStop: vi.fn(async () => {}),
execFileSync: vi.fn(),
spawnSync: vi.fn(),
fetchLatestNpmVersion: vi.fn(async () => "1.14.8"),
fsExists: vi.fn(() => false),
fsStat: vi.fn(() => ({ isDirectory: () => false, isFile: () => true })),
fsCreateReadStream: vi.fn(),
fsReadFileSync: vi.fn(),
streamOnce: vi.fn(),
pipe: vi.fn(),
readBoundedBody: vi.fn(async () => Buffer.from("{}")),
oauthAuthenticate: vi.fn(async () => ({
authenticated: true,
principal: { subject: "user-1", scopes: ["premiere:mcp"], rateLimitKey: "opaque-user-key" },
})),
}));
vi.mock("node:http", () => ({
default: {
createServer: vi.fn((handler: any) => {
mocks.requestHandler = handler;
return { listen: mocks.listen, close: mocks.closeHttp };
}),
},
}));
vi.mock("../src/server.js", () => ({
createServer: vi.fn(() => ({ connect: mocks.connect, close: mocks.closeMcp })),
}));
vi.mock("../src/bridge/file-bridge.js", () => ({
cleanupTempDir: mocks.cleanup,
getTempDir: vi.fn(() => "C:\\temp\\premiere"),
}));
vi.mock("../src/telemetry.js", async (original) => {
const actual = await original<typeof import("../src/telemetry.js")>();
return { ...actual, getTelemetry: () => ({ enabled: true, capture: mocks.capture, shutdown: mocks.shutdown }) };
});
vi.mock("@modelcontextprotocol/server", () => ({
createMcpHandler: vi.fn((factory: any) => ({
factory,
close: mocks.closeTransport,
})),
}));
vi.mock("@modelcontextprotocol/node", () => ({
toNodeHandler: vi.fn((handler: any) => async (req: any, res: any, body: unknown) => {
handler.factory();
await mocks.connect();
res.on?.("close", () => {
void mocks.closeMcp();
void mocks.closeTransport();
});
await mocks.handleRequest(req, res, body);
}),
}));
vi.mock("@modelcontextprotocol/server/stdio", () => ({
serveStdio: (...args: any[]) => mocks.serveStdio(...args),
StdioServerTransport: class {
constructor() { mocks.stdioServerTransport(); }
close = mocks.closeTransport;
},
}));
vi.mock("../src/http-security.js", () => ({ applyHttpSecurityHeaders: vi.fn() }));
vi.mock("../src/http-admission.js", async (original) => {
const actual = await original<typeof import("../src/http-admission.js")>();
return { ...actual, readBoundedRequestBody: mocks.readBoundedBody };
});
vi.mock("../src/oauth-resource-server.js", () => ({
OAuthResourceServer: class {
authenticate = mocks.oauthAuthenticate;
metadata() {
return {
resource: "https://premiere.example.com/mcp",
authorization_servers: ["https://identity.example.com"],
bearer_methods_supported: ["header"],
scopes_supported: ["premiere:mcp"],
};
}
challenge(error?: string) {
return `Bearer resource_metadata="https://premiere.example.com/.well-known/oauth-protected-resource/mcp", scope="premiere:mcp"${error && error !== "missing_token" ? `, error="${error}"` : ""}`;
}
},
}));
vi.mock("../src/bridge/uxp-websocket-bridge.js", () => ({
UxpWebSocketBridge: class {
start = mocks.uxpStart;
stop = mocks.uxpStop;
address() { return { host: "127.0.0.1", port: 7788, path: "/premiere-uxp" }; }
},
}));
vi.mock("../src/update.js", () => ({
compareVersions: (left: string, right: string) => left.localeCompare(right),
fetchLatestNpmVersion: mocks.fetchLatestNpmVersion,
}));
vi.mock("node:child_process", () => ({ execFileSync: mocks.execFileSync, spawnSync: mocks.spawnSync }));
vi.mock("node:fs", async (original) => {
const actual = await original<typeof import("node:fs")>();
return {
...actual,
default: {
...actual,
existsSync: mocks.fsExists,
statSync: mocks.fsStat,
createReadStream: mocks.fsCreateReadStream,
readFileSync: mocks.fsReadFileSync,
},
};
});
const originalArgv = process.argv;
const env = { ...process.env };
const originalSignalListeners = {
SIGINT: new Set(process.listeners("SIGINT")),
SIGTERM: new Set(process.listeners("SIGTERM")),
};
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mocks.requestHandler = undefined;
mocks.fetchLatestNpmVersion.mockResolvedValue("1.14.8");
mocks.spawnSync.mockReturnValue({ status: 1, stdout: "" });
mocks.fsExists.mockReturnValue(false);
mocks.fsStat.mockReturnValue({ isDirectory: () => false, isFile: () => true });
mocks.fsCreateReadStream.mockReturnValue({ once: mocks.streamOnce, pipe: mocks.pipe });
mocks.fsReadFileSync.mockReturnValue("<html><head><script>bootstrap()</script></head></html>");
mocks.readBoundedBody.mockResolvedValue(Buffer.from("{}"));
mocks.serveStdio.mockImplementation((factory: () => unknown) => {
factory();
void mocks.connect();
return { close: mocks.closeMcp };
});
process.env = { ...env };
});
afterEach(() => {
process.argv = originalArgv;
process.env = { ...env };
vi.restoreAllMocks();
for (const signal of ["SIGINT", "SIGTERM"] as const) {
for (const listener of process.listeners(signal)) {
if (!originalSignalListeners[signal].has(listener)) process.removeListener(signal, listener);
}
}
});
function response() {
const res: any = {
statusCode: 200, headersSent: false, body: "", closeHandler: undefined,
writeHead: vi.fn((status: number) => { res.statusCode = status; res.headersSent = true; }),
end: vi.fn((body = "") => { res.body = body; }),
destroy: vi.fn(),
on: vi.fn((name: string, handler: () => void) => { if (name === "close") res.closeHandler = handler; }),
};
return res;
}
async function importCli(args: string[]) {
process.argv = [process.execPath, "index.js", ...args];
const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`EXIT:${code}`);
}) as never);
return { promise: import("../src/index.js"), exit };
}
describe("stdio CLI entry point", () => {
it("prints help and exits successfully", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const { promise, exit } = await importCli(["--help"]);
await expect(promise).rejects.toThrow("EXIT:0");
expect(log).toHaveBeenCalledWith(expect.stringContaining("Usage:"));
expect(exit).toHaveBeenCalledWith(0);
});
it("prints version and machine-readable doctor output", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
let loaded = await importCli(["--version"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(log).toHaveBeenCalledWith(expect.stringMatching(/^\d+\.\d+\.\d+$/));
vi.resetModules();
log.mockClear();
loaded = await importCli(["--doctor", "--json"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({ schemaVersion: "premiere-pro-mcp.doctor.v1" });
});
it("prints human doctor and privacy-safe support bundle output", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
let loaded = await importCli(["--doctor"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(log).toHaveBeenCalledWith(expect.stringContaining("Premiere MCP local check"));
vi.resetModules();
log.mockClear();
loaded = await importCli(["--support-bundle"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({
schemaVersion: "premiere-pro-mcp.support-bundle.v1",
});
});
it("prints a no-write doctor repair plan and applies no writes without the closure confirmation", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
let loaded = await importCli(["--doctor", "--plan-fixes"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({
schemaVersion: "premiere-pro-mcp.doctor-repair-plan.v1",
});
vi.resetModules();
log.mockClear();
loaded = await importCli(["--doctor", "--apply-fixes"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(JSON.parse(String(log.mock.calls[0][0]))).toMatchObject({
schemaVersion: "premiere-pro-mcp.doctor-repair-result.v1",
});
expect(JSON.parse(String(log.mock.calls[0][0])).applied).toBe(false);
});
it("checks for a newer release and updates a global npm installation", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
mocks.fetchLatestNpmVersion.mockResolvedValueOnce("1.15.0");
let loaded = await importCli(["--check-update"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(log).toHaveBeenCalledWith(expect.stringContaining("1.14.9 → 1.15.0"));
vi.resetModules();
vi.clearAllMocks();
log.mockClear();
mocks.fetchLatestNpmVersion.mockResolvedValueOnce("1.15.0");
mocks.spawnSync.mockReturnValue({ status: 0, stdout: `${dirname(process.cwd())}\n` });
loaded = await importCli(["--update"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync).toHaveBeenCalledWith(
expect.any(String),
expect.arrayContaining(["install", "--global", "premiere-pro-mcp@latest"]),
expect.objectContaining({ stdio: "inherit" }),
);
expect(mocks.execFileSync).toHaveBeenCalledWith(
process.execPath,
expect.arrayContaining(["--install-cep"]),
expect.objectContaining({ cwd: process.cwd() }),
);
});
it("rejects conflicting update actions and leaves a current installation untouched", async () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
let loaded = await importCli(["--check-update", "--update"]);
await expect(loaded.promise).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("only one update action"));
vi.resetModules();
vi.clearAllMocks();
const log = vi.spyOn(console, "log").mockImplementation(() => {});
mocks.fetchLatestNpmVersion.mockResolvedValueOnce("1.14.9");
loaded = await importCli(["--update"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(log).toHaveBeenCalledWith(expect.stringContaining("is current"));
expect(mocks.execFileSync).not.toHaveBeenCalled();
});
it("rejects CEP installation on unsupported platforms", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const { promise, exit } = await importCli(["--install-cep"]);
await expect(promise).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("supported only"));
expect(exit).toHaveBeenCalledWith(1);
});
it("starts the stdio server with configured bridge options", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_TEMP_DIR = "C:\\custom-temp";
process.env.PREMIERE_TIMEOUT_MS = "4321";
delete process.env.PREMIERE_UXP_TOKEN;
await import("../src/index.js");
await vi.waitFor(() => expect(mocks.connect).toHaveBeenCalledOnce());
expect(mocks.cleanup).toHaveBeenCalledWith({ tempDir: "C:\\custom-temp", timeoutMs: 4321 });
expect(process.env.PREMIERE_MCP_TRANSPORT).toBe("stdio");
});
it("offers an explicit legacy stdio fallback for clients that cannot negotiate server/discover", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_MCP_PROTOCOL_MODE = "legacy";
await import("../src/index.js");
await vi.waitFor(() => expect(mocks.connect).toHaveBeenCalledOnce());
expect(mocks.stdioServerTransport).toHaveBeenCalledOnce();
expect(mocks.serveStdio).not.toHaveBeenCalled();
});
it("rejects an unknown MCP protocol mode before opening stdio", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_MCP_PROTOCOL_MODE = "unsupported";
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
await import("../src/index.js");
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
expect(mocks.serveStdio).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
"[premiere-pro-mcp] Fatal error:",
expect.objectContaining({ message: "PREMIERE_MCP_PROTOCOL_MODE must be either auto or legacy." }),
);
});
it("starts the authenticated UXP bridge and emits debug readiness details", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_UXP_TOKEN = "a-secure-token-with-length";
process.env.PREMIERE_UXP_PORT = "7788";
process.env.PREMIERE_MCP_DEBUG = "true";
const error = vi.spyOn(console, "error").mockImplementation(() => {});
await import("../src/index.js");
await vi.waitFor(() => expect(mocks.uxpStart).toHaveBeenCalledOnce());
expect(error).toHaveBeenCalledWith(expect.stringContaining("UXP bridge listening"));
});
it("continues with CEP-only tools when another MCP instance owns the UXP loopback port", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_UXP_TOKEN = "a-secure-token-with-length";
mocks.uxpStart.mockRejectedValueOnce(Object.assign(new Error("address already in use"), { code: "EADDRINUSE" }));
const error = vi.spyOn(console, "error").mockImplementation(() => {});
await import("../src/index.js");
await vi.waitFor(() => expect(mocks.serveStdio).toHaveBeenCalledOnce());
expect(mocks.connect).toHaveBeenCalledOnce();
expect(error).toHaveBeenCalledWith(expect.stringContaining("continuing with CEP-only tools"));
});
it("keeps non-port UXP startup failures fatal", async () => {
process.argv = [process.execPath, "index.js"];
process.env.PREMIERE_UXP_TOKEN = "a-secure-token-with-length";
mocks.uxpStart.mockRejectedValueOnce(Object.assign(new Error("permission denied"), { code: "EACCES" }));
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
await import("../src/index.js");
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
expect(mocks.serveStdio).not.toHaveBeenCalled();
expect(error).toHaveBeenCalledWith(
"[premiere-pro-mcp] Fatal error:",
expect.objectContaining({ message: "permission denied" }),
);
});
it("reports a fatal stdio startup failure", async () => {
process.argv = [process.execPath, "index.js"];
mocks.serveStdio.mockImplementationOnce(() => { throw new Error("connect failed"); });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
await import("../src/index.js");
await vi.waitFor(() => expect(error).toHaveBeenCalledWith(
"[premiere-pro-mcp] Fatal error:",
expect.objectContaining({ message: "connect failed" }),
));
expect(exit).toHaveBeenCalledWith(1);
});
it("runs and diagnoses the Windows CEP installer", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const log = vi.spyOn(console, "log").mockImplementation(() => {});
let loaded = await importCli(["--install-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync).toHaveBeenCalledWith(
"powershell.exe",
expect.arrayContaining(["-File"]),
expect.objectContaining({ stdio: "inherit" }),
);
vi.resetModules();
mocks.execFileSync.mockClear();
log.mockClear();
loaded = await importCli(["--diagnose-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync.mock.calls[0][1]).toContain("-Diagnose");
vi.resetModules();
mocks.execFileSync.mockClear();
loaded = await importCli(["--uninstall-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync.mock.calls[0][1]).toEqual(expect.arrayContaining([
expect.stringMatching(/uninstall-cep\.ps1$/),
]));
});
it("routes After Effects connector actions to the separate host target", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
vi.spyOn(console, "log").mockImplementation(() => {});
const loaded = await importCli(["--install-after-effects-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync.mock.calls[0][1]).toEqual(expect.arrayContaining([
"-ConnectorHost",
"AfterEffects",
]));
});
it("runs the macOS CEP diagnostic script", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
vi.spyOn(console, "log").mockImplementation(() => {});
const loaded = await importCli(["--diagnose-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync).toHaveBeenCalledWith(
"bash",
[expect.stringMatching(/install-cep\.sh$/), "--diagnose"],
expect.objectContaining({ stdio: "inherit" }),
);
expect(loaded.exit).toHaveBeenCalledWith(0);
});
it("runs the macOS After Effects diagnostic script with an isolated host flag", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
vi.spyOn(console, "log").mockImplementation(() => {});
const loaded = await importCli(["--diagnose-after-effects-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync).toHaveBeenCalledWith(
"bash",
[expect.stringMatching(/install-cep\.sh$/), "--diagnose", "--after-effects"],
expect.objectContaining({ stdio: "inherit" }),
);
});
it("runs the macOS CEP uninstaller and rejects conflicting CEP actions", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
vi.spyOn(console, "log").mockImplementation(() => {});
let loaded = await importCli(["--uninstall-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:0");
expect(mocks.execFileSync).toHaveBeenCalledWith(
"bash",
[expect.stringMatching(/uninstall-cep\.sh$/), "--user"],
expect.objectContaining({ stdio: "inherit" }),
);
vi.resetModules();
const error = vi.spyOn(console, "error").mockImplementation(() => {});
loaded = await importCli(["--install-cep", "--uninstall-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("only one CEP action"));
});
it("reports a failed CEP installer command", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
mocks.execFileSync.mockImplementationOnce(() => { throw new Error("installer failed"); });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const loaded = await importCli(["--install-cep"]);
await expect(loaded.promise).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("installation failed"));
});
});
describe("HTTP entry point", () => {
async function loadHttp(auth = "strong-test-token") {
process.env.MCP_AUTH_TOKEN = auth;
delete process.env.ALLOW_UNAUTHENTICATED;
process.env.NODE_ENV = "test";
await import("../src/http-server.js");
return mocks.requestHandler!;
}
async function loadOAuth() {
delete process.env.MCP_AUTH_TOKEN;
delete process.env.ALLOW_UNAUTHENTICATED;
process.env.NODE_ENV = "production";
process.env.MCP_OAUTH_ISSUER = "https://identity.example.com";
process.env.MCP_OAUTH_AUDIENCE = "https://premiere.example.com/mcp";
process.env.MCP_OAUTH_JWKS_URI = "https://identity.example.com/.well-known/jwks.json";
process.env.MCP_PUBLIC_URL = "https://premiere.example.com";
process.env.MCP_OAUTH_REQUIRED_SCOPES = "premiere:mcp";
process.env.MCP_OAUTH_ALLOWED_SUBJECTS = "user-1";
await import("../src/http-server.js");
return mocks.requestHandler!;
}
it("serves health and rejects missing bearer credentials", async () => {
const handler = await loadHttp();
const health = response();
await handler({ method: "GET", url: "/health", headers: {} }, health);
expect(health.statusCode).toBe(200);
expect(JSON.parse(health.body)).toMatchObject({ status: "ok" });
const denied = response();
await handler({ method: "POST", url: "/mcp", headers: {} }, denied);
expect(denied.statusCode).toBe(401);
expect(mocks.capture).toHaveBeenCalledWith("mcp_connection_attempt", expect.objectContaining({ outcome: "unauthorized" }));
});
it("rejects malformed and incorrect bearer credentials", async () => {
const handler = await loadHttp();
for (const authorization of ["Basic strong-test-token", "Bearer short", "Bearer xxxxxxxxxxxxxxxxx"]) {
const denied = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization } }, denied);
expect(denied.statusCode).toBe(401);
}
});
it("publishes OAuth protected-resource metadata without authentication", async () => {
const handler = await loadOAuth();
const res = response();
await handler({ method: "GET", url: "/.well-known/oauth-protected-resource/mcp", headers: {} }, res);
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toMatchObject({
resource: "https://premiere.example.com/mcp",
authorization_servers: ["https://identity.example.com"],
});
expect(mocks.oauthAuthenticate).not.toHaveBeenCalled();
});
it("returns discoverable OAuth challenges and separates invalid token from insufficient scope", async () => {
let handler = await loadOAuth();
mocks.oauthAuthenticate.mockResolvedValueOnce({ authenticated: false, error: "invalid_token" });
const invalid = response();
await handler({ method: "POST", url: "/mcp", headers: {} }, invalid);
expect(invalid.statusCode).toBe(401);
expect(invalid.writeHead).toHaveBeenCalledWith(401, expect.objectContaining({
"WWW-Authenticate": expect.stringContaining("/.well-known/oauth-protected-resource/mcp"),
"Cache-Control": "no-store",
}));
vi.resetModules();
handler = await loadOAuth();
mocks.oauthAuthenticate.mockResolvedValueOnce({ authenticated: false, error: "insufficient_scope" });
const insufficient = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer redacted" } }, insufficient);
expect(insufficient.statusCode).toBe(403);
expect(insufficient.writeHead).toHaveBeenCalledWith(403, expect.objectContaining({
"WWW-Authenticate": expect.stringContaining('error="insufficient_scope"'),
}));
expect(mocks.handleRequest).not.toHaveBeenCalled();
});
it("rate-limits before OAuth verification work", async () => {
process.env.MCP_RATE_LIMIT_PER_MINUTE = "1";
process.env.MCP_RATE_LIMIT_BURST = "1";
const handler = await loadOAuth();
const request = { method: "POST", url: "/mcp", headers: {}, socket: { remoteAddress: "203.0.113.9" } };
const first = response();
await handler(request, first);
const second = response();
await handler(request, second);
expect(mocks.oauthAuthenticate).toHaveBeenCalledOnce();
expect(second.statusCode).toBe(429);
});
it("allows an explicitly unauthenticated deployment", async () => {
process.env.ALLOW_UNAUTHENTICATED = "1";
delete process.env.MCP_AUTH_TOKEN;
process.env.NODE_ENV = "test";
await import("../src/http-server.js");
const res = response();
await mocks.requestHandler!({ method: "POST", url: "/mcp", headers: {} }, res);
expect(mocks.handleRequest).toHaveBeenCalledOnce();
});
it("refuses to start without authentication or an explicit override", async () => {
delete process.env.ALLOW_UNAUTHENTICATED;
delete process.env.MCP_AUTH_TOKEN;
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`EXIT:${code}`);
}) as never);
await expect(import("../src/http-server.js")).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("Refusing to start"), expect.stringContaining("MCP_AUTH_TOKEN"));
expect(exit).toHaveBeenCalledWith(1);
});
it("refuses an unauthenticated production HTTP deployment", async () => {
process.env.ALLOW_UNAUTHENTICATED = "1";
delete process.env.MCP_AUTH_TOKEN;
process.env.NODE_ENV = "production";
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`EXIT:${code}`);
}) as never);
await expect(import("../src/http-server.js")).rejects.toThrow("EXIT:1");
expect(error).toHaveBeenCalledWith(expect.stringContaining("Refusing to start"), expect.stringContaining("MCP_AUTH_TOKEN"));
expect(exit).toHaveBeenCalledWith(1);
});
it("rejects non-exact MCP paths and unsupported methods before server construction", async () => {
const handler = await loadHttp();
const incorrectPath = response();
await handler({ method: "POST", url: "/mcp-typo", headers: {} }, incorrectPath);
expect(incorrectPath.statusCode).toBe(404);
expect(mocks.connect).not.toHaveBeenCalled();
const wrongMethod = response();
await handler({ method: "PUT", url: "/mcp?client=test", headers: {} }, wrongMethod);
expect(wrongMethod.statusCode).toBe(405);
expect(wrongMethod.writeHead).toHaveBeenCalledWith(405, expect.objectContaining({ Allow: "GET, POST, DELETE" }));
expect(mocks.connect).not.toHaveBeenCalled();
});
it("rejects chunked over-limit and malformed POST bodies before transport construction", async () => {
const handler = await loadHttp();
mocks.readBoundedBody.mockRejectedValueOnce(new RequestBodyTooLargeError());
const tooLarge = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, tooLarge);
expect(tooLarge.statusCode).toBe(413);
expect(tooLarge.writeHead).toHaveBeenCalledWith(413, expect.objectContaining({ Connection: "close" }));
expect(mocks.connect).not.toHaveBeenCalled();
mocks.readBoundedBody.mockResolvedValueOnce(Buffer.from("not-json"));
const malformed = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, malformed);
expect(malformed.statusCode).toBe(400);
expect(JSON.parse(malformed.body)).toMatchObject({ jsonrpc: "2.0", error: { code: -32700 }, id: null });
expect(mocks.connect).not.toHaveBeenCalled();
});
it("handles authorized MCP requests and closes request resources", async () => {
const handler = await loadHttp();
const res = response();
const req = {
method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" },
};
await handler(req, res);
expect(mocks.connect).toHaveBeenCalledOnce();
expect(mocks.handleRequest).toHaveBeenCalledOnce();
expect(mocks.capture).toHaveBeenCalledWith("mcp_request", expect.objectContaining({ outcome: "succeeded", status_code: 204 }));
res.closeHandler();
expect(mocks.closeTransport).toHaveBeenCalled();
expect(mocks.closeMcp).toHaveBeenCalled();
});
it("bounds DELETE request bodies before the MCP transport handles them", async () => {
const handler = await loadHttp();
const res = response();
await handler({ method: "DELETE", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, res);
expect(mocks.readBoundedBody).toHaveBeenCalledOnce();
expect(mocks.handleRequest).toHaveBeenCalledOnce();
});
it("returns 500 when MCP request handling fails", async () => {
const handler = await loadHttp();
mocks.handleRequest.mockRejectedValueOnce(new TypeError("broken"));
const res = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, res);
expect(res.statusCode).toBe(500);
expect(JSON.parse(res.body)).toEqual({ error: "Internal server error" });
expect(mocks.capture).toHaveBeenCalledWith("mcp_request", expect.objectContaining({ outcome: "failed", error_type: "TypeError" }));
});
it("records an MCP response status failure and preserves an already-started response", async () => {
let handler = await loadHttp();
mocks.handleRequest.mockImplementationOnce(async (_req, res) => { res.statusCode = 422; });
const failedStatus = response();
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, failedStatus);
expect(mocks.capture).toHaveBeenCalledWith("mcp_request", expect.objectContaining({
outcome: "failed", method: "POST", status_code: 422,
}));
vi.resetModules();
handler = await loadHttp();
mocks.handleRequest.mockRejectedValueOnce("non-error failure");
const started = response();
started.headersSent = true;
await handler({ method: "POST", url: "/mcp", headers: { authorization: "Bearer strong-test-token" } }, started);
expect(started.writeHead).not.toHaveBeenCalled();
expect(mocks.capture).toHaveBeenCalledWith("mcp_request", expect.objectContaining({ error_type: "UnknownError" }));
});
it("serves a landing asset with its MIME type", async () => {
mocks.fsExists.mockReturnValue(true);
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/docs/", headers: {} }, res);
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache, must-revalidate",
}));
expect(res.body).toContain("<script nonce=");
});
it("serves landing document headers without a body for HEAD", async () => {
mocks.fsExists.mockReturnValue(true);
const handler = await loadHttp();
const res = response();
await handler({ method: "HEAD", url: "/docs/", headers: {} }, res);
expect(res.statusCode).toBe(200);
expect(res.body).toBe("");
expect(mocks.fsReadFileSync).toHaveBeenCalledOnce();
});
it("serves extensionless landing routes from their index file", async () => {
mocks.fsExists.mockReturnValue(true);
mocks.fsStat
.mockReturnValueOnce({ isDirectory: () => true, isFile: () => false })
.mockReturnValueOnce({ isDirectory: () => false, isFile: () => true });
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/changelog", headers: {} }, res);
expect(mocks.fsReadFileSync).toHaveBeenCalledWith(
expect.stringMatching(/[\\/]changelog[\\/]index\.html$/),
"utf8",
);
expect(res.statusCode).toBe(200);
});
it("marks hashed Next static assets immutable", async () => {
mocks.fsExists.mockReturnValue(true);
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/_next/static/chunks/app.js", headers: {} }, res);
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
"Cache-Control": "public, max-age=31536000, immutable",
}));
});
it("serves nested static-export Flight payloads requested with Next's flattened path", async () => {
mocks.fsExists.mockImplementation((candidate) => (
String(candidate).endsWith("landing-dist") ||
/[\\/]blog[\\/]guide[\\/]__next\.blog[\\/]\$d\$slug[\\/]__PAGE__\.txt$/.test(String(candidate))
));
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/blog/guide/__next.blog.$d$slug.__PAGE__.txt?_rsc=test", headers: {} }, res);
expect(mocks.fsCreateReadStream).toHaveBeenCalledWith(
expect.stringMatching(/[\\/]blog[\\/]guide[\\/]__next\.blog[\\/]\$d\$slug[\\/]__PAGE__\.txt$/),
);
expect(res.statusCode).toBe(200);
});
it("does not crash when a landing asset read fails after validation", async () => {
mocks.fsExists.mockReturnValue(true);
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/analytics.js", headers: {} }, res);
const streamError = mocks.streamOnce.mock.calls.find(([event]) => event === "error")?.[1];
expect(streamError).toBeTypeOf("function");
streamError(new Error("read failed"));
expect(res.destroy).toHaveBeenCalledOnce();
expect(error).toHaveBeenCalledWith(
"[premiere-pro-mcp] Landing asset read failed:",
expect.any(Error),
);
});
it("rejects landing paths that escape the static root", async () => {
mocks.fsExists.mockReturnValue(true);
const handler = await loadHttp();
for (const url of ["/../outside.txt", "/%2e%2e/outside.txt", "/..\\outside.txt"]) {
const res = response();
await handler({ method: "GET", url, headers: {} }, res);
expect(res.statusCode).toBe(404);
}
expect(mocks.fsCreateReadStream).not.toHaveBeenCalled();
});
it("returns 404 when no landing asset matches", async () => {
const handler = await loadHttp();
const res = response();
await handler({ method: "GET", url: "/missing", headers: {} }, res);
expect(res.statusCode).toBe(404);
expect(res.body).toBe("Not found");
});
});
+115
View File
@@ -0,0 +1,115 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, it } from "vitest";
const inventory = JSON.parse(readFileSync("src/resources/extendscript-api-inventory.json", "utf8"));
function runFixture(markdown: string, referenceOverrides: Record<string, string> = {}) {
const directory = mkdtempSync(join(tmpdir(), "extendscript-api-inventory-"));
const sourcePath = "docs/example/example.md";
const fixturePath = join(directory, sourcePath);
mkdirSync(join(directory, "docs/example"), { recursive: true });
writeFileSync(fixturePath, markdown);
const referencePath = join(directory, "reference.json");
writeFileSync(referencePath, JSON.stringify({ entries: [{
repository: "docsforadobe/premiere-scripting-guide",
commit: "4253cea094e84d43590b77012b33bd1c140f72ea",
scope: "premiere-extendscript-guide",
path: sourcePath,
...referenceOverrides,
}] }));
const outputPath = join(directory, "output.json");
const result = spawnSync(process.execPath, ["scripts/generate-extendscript-api-inventory.mjs"], {
encoding: "utf8",
env: {
...process.env,
EXTENDSCRIPT_INVENTORY_FIXTURE_DIRECTORY: directory,
EXTENDSCRIPT_REFERENCE_PATH: referencePath,
EXTENDSCRIPT_INVENTORY_OUTPUT_PATH: outputPath,
},
});
return { directory, outputPath, result };
}
describe("Premiere ExtendScript API inventory", () => {
it("pins a community source without inflating its authority", () => {
expect(inventory.source).toMatchObject({
repository: "docsforadobe/premiere-scripting-guide",
commit: "4253cea094e84d43590b77012b33bd1c140f72ea",
authority: "community",
});
expect(inventory.source.authorityNote).toContain("not Adobe API authority");
});
it("accounts for unique object members and representative APIs", () => {
expect(inventory.stats).toEqual({ total: 328, objects: 26, attributes: 108, methods: 220 });
expect(inventory.stats.total).toBe(inventory.symbols.length);
expect(inventory.stats.attributes + inventory.stats.methods).toBe(inventory.stats.total);
expect(inventory.stats.objects).toBe(inventory.objects.length);
expect(new Set(inventory.symbols.map((symbol: { object: string; kind: string; name: string }) =>
`${symbol.object}:${symbol.kind}:${symbol.name}`)).size).toBe(inventory.symbols.length);
expect(inventory.symbols).toEqual(expect.arrayContaining([
expect.objectContaining({ object: "Application", name: "app.project", kind: "attribute", signature: "app.project" }),
expect.objectContaining({ object: "Application", name: "app.openDocument()", kind: "method" }),
expect.objectContaining({ object: "Sequence", name: "Sequence.importMGT()", kind: "method" }),
]));
});
it("executes the generator against a fixture page", () => {
const fixture = runFixture("# Example object\n\n## Attributes\n\n### Example.value\n\n`app.value`\n\n## Methods\n\n### Example.run()\n\n`app.run(arg)`\n");
try {
expect(fixture.result.status).toBe(0);
expect(JSON.parse(readFileSync(fixture.outputPath, "utf8")).symbols).toEqual([
expect.objectContaining({ name: "Example.value", kind: "attribute", signature: "app.value" }),
expect.objectContaining({ name: "Example.run()", kind: "method", signature: "app.run(arg)" }),
]);
} finally {
rmSync(fixture.directory, { recursive: true, force: true });
}
});
it("parses table-defined collection members without treating parameter tables as APIs", () => {
const fixture = runFixture("# Collection object\n\n## Attributes\n\n| Attribute | Type |\n| --- | --- |\n| `length` | Integer |\n\n## Methods\n\n| Method | Return Type |\n| --- | --- |\n| `[]` | Object |\n\n### Collection.find()\n\n`collection.find(index)`\n\n#### Parameters\n\n| Parameter | Type |\n| --- | --- |\n| `index` | Integer |\n");
try {
expect(fixture.result.status).toBe(0);
expect(JSON.parse(readFileSync(fixture.outputPath, "utf8")).symbols).toEqual([
expect.objectContaining({ name: "Collection.length", kind: "attribute", signature: "length" }),
expect.objectContaining({ name: "Collection.[]", kind: "method", signature: "[]" }),
expect.objectContaining({ name: "Collection.find()", kind: "method", signature: "collection.find(index)" }),
]);
expect(JSON.parse(readFileSync(fixture.outputPath, "utf8")).symbols)
.not.toEqual(expect.arrayContaining([expect.objectContaining({ name: "Collection.index" })]));
} finally {
rmSync(fixture.directory, { recursive: true, force: true });
}
});
it.each([
["missing signature", "# Example object\n\n## Methods\n\n### Example.run()\n\nNo signature\n", "Missing inline signature"],
["duplicate member", "# Example object\n\n## Methods\n\n### Example.run()\n\n`app.run()`\n\n### Example.run()\n\n`app.run()`\n", "duplicate object members"],
["no symbols", "# Introduction\n\nNo object members.\n", "No ExtendScript symbols"],
])("fails closed for %s", (_label, markdown, expectedError) => {
const fixture = runFixture(markdown);
try {
expect(fixture.result.status).not.toBe(0);
expect(fixture.result.stderr).toContain(expectedError);
} finally {
rmSync(fixture.directory, { recursive: true, force: true });
}
});
it.each([
["commit", { commit: "0".repeat(40) }],
["scope", { scope: "wrong-scope" }],
])("fails closed for mismatched reference %s", (_label, overrides) => {
const fixture = runFixture("# Example object\n\n## Attributes\n\n### Example.value\n\n`app.value`\n", overrides);
try {
expect(fixture.result.status).not.toBe(0);
expect(fixture.result.stderr).toContain("do not match the pinned commit and scope");
} finally {
rmSync(fixture.directory, { recursive: true, force: true });
}
});
});
+222
View File
@@ -0,0 +1,222 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import {
HttpAdmissionController,
RequestBodyTooLargeError,
exceedsRequestBodyLimit,
getRequestPathname,
isAuthorizedBearer,
isSupportedMcpMethod,
rateLimitIdentity,
readBoundedRequestBody,
readHttpAdmissionSettings,
readHttpAuthConfiguration,
requestContentLength,
} from "../src/http-admission.js";
describe("HTTP admission settings", () => {
it("uses bounded secure defaults and rejects invalid limits", () => {
expect(readHttpAdmissionSettings({})).toMatchObject({
maxRequestBytes: 1_048_576,
maxConcurrentRequests: 8,
maxConcurrentStreams: 32,
rateLimitPerMinute: 120,
rateLimitBurst: 30,
});
expect(() => readHttpAdmissionSettings({ MCP_MAX_REQUEST_BYTES: "unlimited" })).toThrow("MCP_MAX_REQUEST_BYTES");
expect(() => readHttpAdmissionSettings({ MCP_RATE_LIMIT_PER_MINUTE: "10", MCP_RATE_LIMIT_BURST: "11" })).toThrow("MCP_RATE_LIMIT_BURST");
});
it("never permits unauthenticated HTTP in production", () => {
expect(readHttpAuthConfiguration({ MCP_AUTH_TOKEN: "secret", NODE_ENV: "production" })).toEqual({
mode: "shared-token", authToken: "secret", allowUnauthenticated: false,
});
expect(readHttpAuthConfiguration({ ALLOW_UNAUTHENTICATED: "1", NODE_ENV: "test" })).toEqual({
mode: "unauthenticated", allowUnauthenticated: true,
});
expect(() => readHttpAuthConfiguration({ ALLOW_UNAUTHENTICATED: "1", NODE_ENV: "production" })).toThrow("MCP_AUTH_TOKEN");
});
it("requires complete HTTPS OAuth configuration and rejects ambiguous auth modes", () => {
const oauth = readHttpAuthConfiguration({
NODE_ENV: "production",
MCP_OAUTH_ISSUER: "https://identity.example.com",
MCP_OAUTH_AUDIENCE: "https://premiere.example.com/mcp",
MCP_OAUTH_JWKS_URI: "https://identity.example.com/.well-known/jwks.json",
MCP_PUBLIC_URL: "https://premiere.example.com",
MCP_OAUTH_REQUIRED_SCOPES: "premiere:mcp premiere:read",
MCP_OAUTH_ALLOWED_SUBJECTS: "user-1,user-2",
});
expect(oauth).toEqual({
mode: "oauth",
allowUnauthenticated: false,
oauth: {
issuer: "https://identity.example.com",
audience: "https://premiere.example.com/mcp",
jwksUri: "https://identity.example.com/.well-known/jwks.json",
publicUrl: "https://premiere.example.com",
requiredScopes: ["premiere:mcp", "premiere:read"],
allowedSubjects: ["user-1", "user-2"],
},
});
expect(() => readHttpAuthConfiguration({
NODE_ENV: "production", MCP_OAUTH_ISSUER: "https://identity.example.com",
})).toThrow("all required");
expect(() => readHttpAuthConfiguration({
NODE_ENV: "production",
MCP_OAUTH_ISSUER: "http://identity.example.com",
MCP_OAUTH_AUDIENCE: "https://premiere.example.com/mcp",
MCP_OAUTH_JWKS_URI: "https://identity.example.com/jwks.json",
MCP_PUBLIC_URL: "https://premiere.example.com",
MCP_OAUTH_ALLOWED_SUBJECTS: "user-1",
})).toThrow("HTTPS");
expect(() => readHttpAuthConfiguration({
NODE_ENV: "production",
MCP_AUTH_TOKEN: "legacy",
MCP_OAUTH_ISSUER: "https://identity.example.com",
})).toThrow("either MCP_AUTH_TOKEN or MCP_OAUTH_ISSUER");
expect(() => readHttpAuthConfiguration({
NODE_ENV: "production",
MCP_AUTH_TOKEN: "legacy",
MCP_OAUTH_REQUIRED_SCOPES: "premiere:mcp",
})).toThrow("either MCP_AUTH_TOKEN or MCP_OAUTH_ISSUER");
expect(() => readHttpAuthConfiguration({
NODE_ENV: "production",
MCP_OAUTH_ISSUER: "https://identity.example.com",
MCP_OAUTH_AUDIENCE: "https://other.example.com/mcp",
MCP_OAUTH_JWKS_URI: "https://identity.example.com/jwks.json",
MCP_PUBLIC_URL: "https://premiere.example.com",
MCP_OAUTH_ALLOWED_SUBJECTS: "user-1",
})).toThrow("exactly equal");
});
});
describe("HTTP request classification", () => {
it("accepts only the exact MCP pathname and supported methods", () => {
expect(getRequestPathname("/mcp?client=desktop")).toBe("/mcp");
expect(getRequestPathname("/mcp-typo")).not.toBe("/mcp");
expect(getRequestPathname("/mcp%2Fprivate")).not.toBe("/mcp");
expect(getRequestPathname(undefined)).toBeUndefined();
expect(getRequestPathname("http://[")).toBeUndefined();
expect(isSupportedMcpMethod("POST")).toBe(true);
expect(isSupportedMcpMethod("PUT")).toBe(false);
expect(isSupportedMcpMethod(undefined)).toBe(false);
});
it("rejects invalid or over-limit declared body sizes before parsing", () => {
expect(exceedsRequestBodyLimit({ headers: { "content-length": "1024" } } as never, 1024)).toBe(false);
expect(exceedsRequestBodyLimit({ headers: { "content-length": "1025" } } as never, 1024)).toBe(true);
expect(exceedsRequestBodyLimit({ headers: { "content-length": "unknown" } } as never, 1024)).toBe(true);
expect(requestContentLength({ headers: {} } as never)).toBeUndefined();
expect(requestContentLength({ headers: { "content-length": ["1024", "2048"] } } as never)).toBe(1024);
expect(Number.isNaN(requestContentLength({ headers: { "content-length": "9007199254740992" } } as never))).toBe(true);
});
it("bounds chunked bodies before transport parsing", async () => {
const request = Object.assign(new EventEmitter(), {
resume: vi.fn(),
});
const body = readBoundedRequestBody(request as never, 4);
request.emit("data", Buffer.from("four"));
request.emit("data", Buffer.from("!"));
await expect(body).rejects.toBeInstanceOf(RequestBodyTooLargeError);
expect(request.resume).toHaveBeenCalledOnce();
const complete = Object.assign(new EventEmitter(), { resume: vi.fn() });
const completeBody = readBoundedRequestBody(complete as never, 4);
complete.emit("data", Buffer.from("ok"));
complete.emit("data", "!");
complete.emit("end");
await expect(completeBody).resolves.toEqual(Buffer.from("ok!"));
});
it("compares bearer credentials without treating malformed headers as authorized", () => {
const request = (authorization?: string) => ({ headers: { authorization } } as never);
expect(isAuthorizedBearer(request("Bearer a-strong-token"), "a-strong-token")).toBe(true);
expect(isAuthorizedBearer(request("Bearer wrong"), "a-strong-token")).toBe(false);
expect(isAuthorizedBearer(request("Basic a-strong-token"), "a-strong-token")).toBe(false);
expect(isAuthorizedBearer(request(), "a-strong-token")).toBe(false);
expect(isAuthorizedBearer({ headers: { authorization: ["Bearer a-strong-token"] } } as never, "a-strong-token")).toBe(true);
expect(isAuthorizedBearer(request(), undefined)).toBe(true);
});
it("does not trust forwarded addresses unless configured", () => {
const request = {
headers: { "x-forwarded-for": "198.51.100.4, 10.0.0.1" },
socket: { remoteAddress: "10.0.0.1" },
} as never;
expect(rateLimitIdentity(request, false)).not.toBe(rateLimitIdentity(request, true));
expect(rateLimitIdentity({ headers: { "x-forwarded-for": ["203.0.113.8"] }, socket: { remoteAddress: "10.0.0.1" } } as never, true)).toMatch(/^ip:/);
expect(rateLimitIdentity({ headers: {}, socket: { remoteAddress: "" } } as never, false)).toMatch(/^ip:/);
expect(rateLimitIdentity({ headers: {}, socket: {} } as never, true)).toMatch(/^ip:/);
});
});
describe("HTTP admission controller", () => {
it("bounds concurrent requests and makes release idempotent", () => {
const controller = new HttpAdmissionController({
maxConcurrentRequests: 1, maxConcurrentStreams: 2, rateLimitPerMinute: 100, rateLimitBurst: 10, maxRateLimitKeys: 16,
});
const first = controller.acquire("credential:a");
expect(first.accepted).toBe(true);
const second = controller.acquire("credential:b");
expect(second).toMatchObject({ accepted: false, reason: "at_capacity", statusCode: 503 });
if (first.accepted) {
first.release();
first.release();
}
expect(controller.metrics().activeRequests).toBe(0);
expect(controller.acquire("credential:b").accepted).toBe(true);
});
it("enforces a bounded token bucket with Retry-After", () => {
let now = 0;
const controller = new HttpAdmissionController({
maxConcurrentRequests: 5, maxConcurrentStreams: 5, rateLimitPerMinute: 60, rateLimitBurst: 2, maxRateLimitKeys: 16,
}, () => now);
const first = controller.acquire("credential:a");
const second = controller.acquire("credential:a");
if (first.accepted) first.release();
if (second.accepted) second.release();
expect(controller.acquire("credential:a")).toMatchObject({ accepted: false, statusCode: 429, retryAfterSeconds: 1 });
now = 1_000;
expect(controller.acquire("credential:a").accepted).toBe(true);
});
it("bounds tracked identities and prunes stale buckets", () => {
let now = 0;
const controller = new HttpAdmissionController({
maxConcurrentRequests: 2, maxConcurrentStreams: 2, rateLimitPerMinute: 60, rateLimitBurst: 1, maxRateLimitKeys: 1,
}, () => now);
const first = controller.acquire("credential:a");
if (first.accepted) first.release();
expect(controller.acquire("credential:b")).toMatchObject({ accepted: false, reason: "rate_limited" });
now = 120_001;
const second = controller.acquire("credential:b");
expect(second.accepted).toBe(true);
expect(controller.metrics().trackedRateLimitKeys).toBe(1);
if (second.accepted) second.release();
});
it("reserves SSE capacity without consuming operation capacity", () => {
const controller = new HttpAdmissionController({
maxConcurrentRequests: 1, maxConcurrentStreams: 2, rateLimitPerMinute: 100, rateLimitBurst: 10, maxRateLimitKeys: 16,
});
const operation = controller.acquire("credential:a", "operation");
const firstStream = controller.acquire("credential:a", "stream");
const secondStream = controller.acquire("credential:b", "stream");
expect(operation.accepted).toBe(true);
expect(firstStream.accepted).toBe(true);
expect(secondStream.accepted).toBe(true);
expect(controller.acquire("credential:b", "operation")).toMatchObject({ accepted: false, reason: "at_capacity" });
expect(controller.acquire("credential:c", "stream")).toMatchObject({ accepted: false, reason: "at_capacity" });
expect(controller.metrics()).toMatchObject({
activeRequests: 3,
activeOperationRequests: 1,
activeStreamRequests: 2,
});
if (operation.accepted) operation.release();
if (firstStream.accepted) firstStream.release();
if (secondStream.accepted) secondStream.release();
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { applyHttpSecurityHeaders, buildContentSecurityPolicy, HTTP_SECURITY_HEADERS } from "../src/http-security.js";
describe("HTTP security headers", () => {
it("sets a restrictive baseline on every response", () => {
const setHeader = vi.fn();
applyHttpSecurityHeaders({ setHeader } as never);
expect(setHeader).toHaveBeenCalledWith("X-Content-Type-Options", "nosniff");
expect(setHeader).toHaveBeenCalledWith("X-Frame-Options", "DENY");
expect(setHeader).toHaveBeenCalledWith(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
);
expect(setHeader).toHaveBeenCalledWith(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()",
);
});
it("blocks framing, plugins, and unlisted network destinations", () => {
const policy = HTTP_SECURITY_HEADERS["Content-Security-Policy"];
expect(policy).toContain("frame-ancestors 'none'");
expect(policy).toContain("object-src 'none'");
expect(policy).toContain("connect-src 'self'");
expect(policy).toContain("https://www.google.com");
expect(policy).not.toContain("connect-src *");
expect(policy).not.toContain("script-src 'self' 'unsafe-inline'");
});
it("uses a per-response nonce instead of allowing inline scripts", () => {
const setHeader = vi.fn();
applyHttpSecurityHeaders({ setHeader } as never, { scriptNonce: "nonce-value" });
expect(setHeader).toHaveBeenCalledWith(
"Content-Security-Policy",
expect.stringContaining("script-src 'self' 'nonce-nonce-value' https://www.googletagmanager.com"),
);
expect(buildContentSecurityPolicy()).not.toContain("'unsafe-inline' https://www.googletagmanager.com");
});
});
+346
View File
@@ -0,0 +1,346 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, it } from "vitest";
import {
REQUIRED_TARGETS,
verifyHybridBenchmarkEvidence,
verifyHybridBenchmarkEvidenceWithLocalCcx,
} from "../scripts/verify-uxp-hybrid-benchmark.mjs";
import { NATIVE_SDK_HEADER_INVENTORY_SEMANTICS } from "../scripts/native-sdk-header-inventory-contract.mjs";
import { canonicalNativeSdkHeaderInventorySha256 } from "../scripts/verify-native-sdk-header-inventory.mjs";
import {
UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
} from "../scripts/uxp-hybrid-addon-receipt-contract.mjs";
import {
canonicalUxpHybridAddonReceiptSha256,
} from "../scripts/verify-uxp-hybrid-addon-receipt.mjs";
import {
UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS,
} from "../scripts/uxp-hybrid-ccx-receipt-contract.mjs";
import {
canonicalUxpHybridCcxReceiptSha256,
} from "../scripts/uxp-hybrid-ccx-receipt-core.mjs";
const SHA = "a".repeat(40);
const ADDON_SHA = "b".repeat(64);
const ADDON_SHA_BY_TARGET = Object.freeze({
"mac-x64": "b".repeat(64),
"mac-arm64": "c".repeat(64),
"win-x64": "d".repeat(64),
});
function sdkHeaderReceipt(sdkVersion = "UXP Hybrid SDK test fixture") {
return {
schemaVersion: 1,
source: {
sdk: "uxp-hybrid",
sdkVersion,
authorityUrl: "https://developer.adobe.com/premiere-pro/uxp/plugins/hybrid-plugins/",
archiveSha256: "c".repeat(64),
inventoryScope: "header_files_only",
includeDirectories: ["src/api", "src/utilities"],
},
semantics: NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
stats: { headers: 3, bytes: 24 },
headers: [
{ path: "src/api/UxpAddonShared.h", bytes: 7, sha256: "d".repeat(64) },
{ path: "src/api/UxpAddonTypes.h", bytes: 8, sha256: "e".repeat(64) },
{ path: "src/utilities/UxpAddon.h", bytes: 9, sha256: "f".repeat(64) },
],
};
}
function prsdkHeaderReceipt() {
return {
schemaVersion: 1,
source: {
sdk: "premiere-prsdk",
sdkVersion: "UXP Hybrid SDK test fixture",
authorityUrl: "https://developer.adobe.com/premiere-pro/",
archiveSha256: "c".repeat(64),
inventoryScope: "header_files_only",
includeDirectories: ["Headers"],
},
semantics: NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
stats: { headers: 1, bytes: 7 },
headers: [
{ path: "Headers/PrSDKFixture.h", bytes: 7, sha256: "d".repeat(64) },
],
};
}
function evidence(headerReceipt = sdkHeaderReceipt()) {
return {
schemaVersion: 2,
workloadId: "weighted-energy-v1",
configuration: { sampleCount: 30, warmupCount: 3, iterations: 4, inputLength: 131072, seed: 1337 },
memoryMeasurement: "fixture process monitor",
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(headerReceipt),
runs: REQUIRED_TARGETS.map((target) => {
const [platform, arch] = target.split("-");
return {
platform,
arch,
hostVersion: "26.3.0",
sdkVersion: "UXP Hybrid SDK test fixture",
buildMode: "Release",
addonLoaded: true,
addonSha256: ADDON_SHA,
sourceCommit: SHA,
checksumMatch: true,
codeSigned: platform === "mac",
notarized: platform === "mac",
javascript: { sampleCount: 30, p50Ms: 100, p95Ms: 140, peakWorkingSetBytes: 1000 },
native: { sampleCount: 30, p50Ms: 60, p95Ms: 80, peakWorkingSetBytes: 1050 },
};
}),
};
}
function legacyEvidence() {
const { sdkHeaderReceiptSha256, ...v1 } = evidence();
return { ...v1, schemaVersion: 1 };
}
function addonReceipt(headerReceipt = sdkHeaderReceipt()) {
return {
schemaVersion: 2,
source: {
sdk: "uxp-hybrid",
sdkVersion: headerReceipt.source.sdkVersion,
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(headerReceipt),
authorityUrl: "https://developer.adobe.com/premiere-pro/uxp/plugins/hybrid-plugins/build",
},
manifest: {
manifestVersion: 6,
hostApp: "premierepro",
hostMinVersion: "26.2.0",
addonName: "fixture-addon.uxpaddon",
enableAddon: true,
},
entrypoint: { path: "main.js", bytes: 3, sha256: "e".repeat(64) },
semantics: UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
stats: { artifacts: 3, addonBytes: 12, entrypoints: 1, entrypointBytes: 3 },
artifacts: [
{ target: "mac-x64", path: "mac/x64/fixture-addon.uxpaddon", bytes: 3, sha256: ADDON_SHA_BY_TARGET["mac-x64"] },
{ target: "mac-arm64", path: "mac/arm64/fixture-addon.uxpaddon", bytes: 4, sha256: ADDON_SHA_BY_TARGET["mac-arm64"] },
{ target: "win-x64", path: "win/x64/fixture-addon.uxpaddon", bytes: 5, sha256: ADDON_SHA_BY_TARGET["win-x64"] },
],
};
}
function ccxReceipt(headerReceipt = sdkHeaderReceipt(), addon = addonReceipt(headerReceipt)) {
return {
schemaVersion: 1,
source: {
sdk: "uxp-hybrid",
sdkVersion: headerReceipt.source.sdkVersion,
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(headerReceipt),
addonReceiptSha256: canonicalUxpHybridAddonReceiptSha256(addon),
authorityUrl: "https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/package/",
},
archive: { format: "zip", bytes: 64, sha256: "f".repeat(64) },
manifest: {
bytes: 32,
sha256: "1".repeat(64),
idSha256: "2".repeat(64),
idLength: 7,
manifestVersion: 6,
hostApp: "premierepro",
hostMinVersion: "26.2.0",
addonName: "fixture-addon.uxpaddon",
enableAddon: true,
},
semantics: UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS,
stats: { artifacts: 3, addonBytes: 12, entrypoints: 1, entrypointBytes: 3 },
};
}
function packageEvidence(headerReceipt = sdkHeaderReceipt(), addon = addonReceipt(headerReceipt), ccx = ccxReceipt(headerReceipt, addon)) {
return {
...evidence(headerReceipt),
schemaVersion: 3,
addonReceiptSha256: canonicalUxpHybridAddonReceiptSha256(addon),
ccxReceiptSha256: canonicalUxpHybridCcxReceiptSha256(ccx),
runs: REQUIRED_TARGETS.map((target) => {
const [platform, arch] = target.split("-");
return {
platform,
arch,
hostVersion: "26.3.0",
sdkVersion: headerReceipt.source.sdkVersion,
buildMode: "Release",
addonLoaded: true,
addonSha256: ADDON_SHA_BY_TARGET[target],
sourceCommit: SHA,
checksumMatch: true,
codeSigned: platform === "mac",
notarized: platform === "mac",
javascript: { sampleCount: 30, p50Ms: 100, p95Ms: 140, peakWorkingSetBytes: 1000 },
native: { sampleCount: 30, p50Ms: 60, p95Ms: 80, peakWorkingSetBytes: 1050 },
};
}),
};
}
describe("UXP hybrid benchmark evidence verifier", () => {
it("keeps frozen v1/v2 schemas and verifier paths available while making package binding v3", () => {
const v1Schema = JSON.parse(readFileSync("benchmarks/uxp-hybrid/evidence.v1.schema.json", "utf8"));
const v2Schema = JSON.parse(readFileSync("benchmarks/uxp-hybrid/evidence.v2.schema.json", "utf8"));
const v3Schema = JSON.parse(readFileSync("benchmarks/uxp-hybrid/evidence.schema.json", "utf8"));
expect(v1Schema.$id).toBe("https://premiere-pro-mcp.com/schemas/uxp-hybrid-benchmark-evidence-v1.json");
expect(v1Schema.properties.schemaVersion.const).toBe(1);
expect(v1Schema.required).not.toContain("sdkHeaderReceiptSha256");
expect(v2Schema.$id).toBe("https://premiere-pro-mcp.com/schemas/uxp-hybrid-benchmark-evidence-v2.json");
expect(v2Schema.properties.schemaVersion.const).toBe(2);
expect(v2Schema.required).toContain("sdkHeaderReceiptSha256");
expect(v3Schema.$id).toBe("https://premiere-pro-mcp.com/schemas/uxp-hybrid-benchmark-evidence-v3.json");
expect(v3Schema.properties.schemaVersion.const).toBe(3);
expect(v3Schema.required).toEqual(expect.arrayContaining(["sdkHeaderReceiptSha256", "addonReceiptSha256", "ccxReceiptSha256"]));
expect(verifyHybridBenchmarkEvidence(legacyEvidence())).toEqual({
promotionEligible: true,
errors: [],
targets: ["mac-arm64", "mac-x64", "win-x64"],
thresholds: { minimumSpeedupPercent: 30, maximumMemoryRegressionPercent: 10 },
verificationBoundary: "submitted_cross_platform_release_build_evidence",
});
});
it("requires all release targets, matching output, speed, memory, signing, and one commit", () => {
const receipt = sdkHeaderReceipt();
expect(verifyHybridBenchmarkEvidence(evidence(receipt), { sdkHeaderReceipt: receipt })).toEqual({
promotionEligible: true,
errors: [],
targets: ["mac-arm64", "mac-x64", "win-x64"],
thresholds: { minimumSpeedupPercent: 30, maximumMemoryRegressionPercent: 10 },
verificationBoundary: "submitted_cross_platform_release_build_and_verified_sdk_receipt_evidence",
});
});
it("fails closed when a target is missing or a percentile misses the threshold", () => {
const receipt = sdkHeaderReceipt();
const value = evidence(receipt);
value.runs.pop();
value.runs[0].native.p95Ms = 110;
const result = verifyHybridBenchmarkEvidence(value, { sdkHeaderReceipt: receipt });
expect(result.promotionEligible).toBe(false);
expect(result.errors).toEqual(expect.arrayContaining([
expect.stringContaining("p95 speedup"),
expect.stringContaining("Missing required mac-arm64 evidence"),
]));
});
it("requires a structurally verified matching Hybrid receipt and rejects undocumented fields", () => {
const receipt = sdkHeaderReceipt();
const value = evidence(receipt);
expect(verifyHybridBenchmarkEvidence(value).errors).toContain("A verified UXP Hybrid SDK header receipt is required.");
const mismatchedDigest = { ...value, sdkHeaderReceiptSha256: "0".repeat(64) };
expect(verifyHybridBenchmarkEvidence(mismatchedDigest, { sdkHeaderReceipt: receipt }).errors)
.toContain("sdkHeaderReceiptSha256 does not match the verified SDK header receipt.");
const mismatchedVersionReceipt = sdkHeaderReceipt("UXP Hybrid SDK other fixture");
expect(verifyHybridBenchmarkEvidence(value, { sdkHeaderReceipt: mismatchedVersionReceipt }).errors)
.toEqual(expect.arrayContaining([
"SDK header receipt sdkVersion must match all benchmark runs.",
"sdkHeaderReceiptSha256 does not match the verified SDK header receipt.",
]));
const wrongSdkReceipt = prsdkHeaderReceipt();
expect(verifyHybridBenchmarkEvidence(value, { sdkHeaderReceipt: wrongSdkReceipt }).errors)
.toEqual(expect.arrayContaining([
"SDK header receipt must identify uxp-hybrid.",
"sdkHeaderReceiptSha256 does not match the verified SDK header receipt.",
]));
const withExtraField = { ...value, privateSdkPath: "C:Headers" };
expect(verifyHybridBenchmarkEvidence(withExtraField, { sdkHeaderReceipt: receipt }).errors)
.toContain("Evidence must contain only the documented benchmark receipt fields.");
const withExtraRunField = {
...value,
runs: [{ ...value.runs[0], localAddonPath: "C:Headers" }, ...value.runs.slice(1)],
};
expect(verifyHybridBenchmarkEvidence(withExtraRunField, { sdkHeaderReceipt: receipt }).errors)
.toContain("runs[0] must contain only the documented benchmark fields.");
const withExtraMetricField = {
...value,
runs: [{ ...value.runs[0], native: { ...value.runs[0].native, rawSamples: [60] } }, ...value.runs.slice(1)],
};
expect(verifyHybridBenchmarkEvidence(withExtraMetricField, { sdkHeaderReceipt: receipt }).errors)
.toContain("runs[0].native must contain only the documented metric fields.");
});
it("binds each target run to a verified current addon-layout and CCX receipt chain", async () => {
const headerReceipt = sdkHeaderReceipt();
const addon = addonReceipt(headerReceipt);
const ccx = ccxReceipt(headerReceipt, addon);
const value = packageEvidence(headerReceipt, addon, ccx);
expect(verifyHybridBenchmarkEvidence(value, {
sdkHeaderReceipt: headerReceipt,
addonReceipt: addon,
ccxReceipt: ccx,
})).toEqual({
promotionEligible: false,
errors: ["A local CCX archive must be rechecked for schemaVersion 3."],
targets: ["mac-arm64", "mac-x64", "win-x64"],
thresholds: { minimumSpeedupPercent: 30, maximumMemoryRegressionPercent: 10 },
verificationBoundary: "submitted_cross_platform_release_build_and_verified_sdk_addon_ccx_receipt_evidence",
});
const mismatchedArtifact = packageEvidence(headerReceipt, addon, ccx);
mismatchedArtifact.runs[0].addonSha256 = "0".repeat(64);
expect(verifyHybridBenchmarkEvidence(mismatchedArtifact, {
sdkHeaderReceipt: headerReceipt,
addonReceipt: addon,
ccxReceipt: ccx,
}).errors).toContain("runs for win-x64 must match the corresponding addon-layout artifact SHA-256.");
const withoutArchive = await verifyHybridBenchmarkEvidenceWithLocalCcx(value, {
sdkHeaderReceipt: headerReceipt,
addonReceipt: addon,
ccxReceipt: ccx,
});
expect(withoutArchive).toMatchObject({
promotionEligible: false,
errors: ["A local CCX archive path is required for schemaVersion 3."],
});
});
it("requires a local receipt path for v2 without printing it while retaining v1 CLI compatibility", () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-hybrid-benchmark-"));
const input = join(directory, "evidence.json");
const receiptPath = join(directory, "receipt.json");
const receipt = sdkHeaderReceipt();
try {
writeFileSync(input, `${JSON.stringify(evidence(receipt), null, 2)}\n`);
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
const missingReceipt = spawnSync(process.execPath, ["scripts/verify-uxp-hybrid-benchmark.mjs", "--input", input], { encoding: "utf8" });
expect(missingReceipt.status).not.toBe(0);
expect(missingReceipt.stdout).toContain("A verified UXP Hybrid SDK header receipt is required.");
writeFileSync(input, `${JSON.stringify(legacyEvidence(), null, 2)}\n`);
const legacy = spawnSync(process.execPath, ["scripts/verify-uxp-hybrid-benchmark.mjs", "--input", input], { encoding: "utf8" });
expect(legacy.status).toBe(0);
expect(legacy.stdout).toContain('"verificationBoundary": "submitted_cross_platform_release_build_evidence"');
writeFileSync(input, `${JSON.stringify(evidence(receipt), null, 2)}\n`);
const verified = spawnSync(process.execPath, ["scripts/verify-uxp-hybrid-benchmark.mjs", "--input", input, "--sdk-header-receipt", receiptPath], { encoding: "utf8" });
expect(verified.status).toBe(0);
expect(verified.stdout).toContain('"promotionEligible": true');
expect(verified.stdout).not.toContain("UxpAddonShared.h");
const unreadableReceipt = join(directory, "do-not-disclose-this-receipt.json");
const unreadable = spawnSync(process.execPath, ["scripts/verify-uxp-hybrid-benchmark.mjs", "--input", input, "--sdk-header-receipt", unreadableReceipt], { encoding: "utf8" });
expect(unreadable.status).not.toBe(0);
expect(unreadable.stderr).toContain("SDK header receipt must be a readable JSON document.");
expect(unreadable.stderr).not.toContain(unreadableReceipt);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
});
+73
View File
@@ -0,0 +1,73 @@
import { describe, it, expect } from "vitest";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "path";
const BIN = join(process.cwd(), "dist", "index.js");
describe("CLI flags", () => {
it("--help prints usage and exits 0", () => {
const output = execFileSync(process.execPath, [BIN, "--help"], { encoding: "utf-8" });
expect(output).toContain("premiere-pro-mcp");
expect(output).toContain("Usage:");
expect(output).toContain("--install-cep");
expect(output).toContain("--uninstall-cep");
expect(output).toContain("--diagnose-cep");
expect(output).toContain("--install-after-effects-cep");
expect(output).toContain("--uninstall-after-effects-cep");
expect(output).toContain("--diagnose-after-effects-cep");
expect(output).toContain("PREMIERE_TEMP_DIR");
expect(output).toContain("PREMIERE_TIMEOUT_MS");
expect(output).toContain("PREMIERE_MCP_CAPABILITIES");
});
it("-h is an alias for --help", () => {
const output = execFileSync(process.execPath, [BIN, "-h"], { encoding: "utf-8" });
expect(output).toContain("Usage:");
});
it("--version prints a semver version and exits 0", () => {
const output = execFileSync(process.execPath, [BIN, "--version"], { encoding: "utf-8" }).trim();
expect(output).toMatch(/^\d+\.\d+\.\d+$/);
});
it("-v is an alias for --version", () => {
const output = execFileSync(process.execPath, [BIN, "-v"], { encoding: "utf-8" }).trim();
expect(output).toMatch(/^\d+\.\d+\.\d+$/);
});
it("--version matches package.json version", () => {
const version = execFileSync(process.execPath, [BIN, "--version"], { encoding: "utf-8" }).trim();
const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"));
expect(version).toBe(pkg.version);
});
it("--doctor --json emits a privacy-safe machine-readable readiness report", () => {
const output = execFileSync(process.execPath, [BIN, "--doctor", "--json"], {
encoding: "utf-8",
env: {
...process.env,
PREMIERE_UXP_TOKEN: "cli-test-token-that-must-not-appear",
POSTHOG_API_KEY: "cli-test-key-that-must-not-appear",
},
});
const report = JSON.parse(output);
expect(report.schemaVersion).toBe("premiere-pro-mcp.doctor.v1");
expect(report.components).toEqual(expect.arrayContaining([
expect.objectContaining({ boundary: "installed" }),
expect.objectContaining({ boundary: "live_verified", state: "not_checked" }),
]));
expect(output).not.toContain("cli-test-token-that-must-not-appear");
expect(output).not.toContain("cli-test-key-that-must-not-appear");
});
it("--support-bundle emits a status snapshot rather than logs or configuration", () => {
const output = execFileSync(process.execPath, [BIN, "--support-bundle"], { encoding: "utf-8" });
const bundle = JSON.parse(output);
expect(bundle.schemaVersion).toBe("premiere-pro-mcp.support-bundle.v1");
expect(bundle.doctor.schemaVersion).toBe("premiere-pro-mcp.doctor.v1");
expect(JSON.stringify(bundle)).not.toMatch(/"(?:projectName|mediaName|outputDirectory|arguments|results)"\s*:/);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
describe("landing AI and search discovery", () => {
it("keeps machine-readable references discoverable and unambiguous", () => {
const layout = read("landing/app/layout.tsx");
const home = read("landing/app/page.tsx");
const hero = read("landing/components/sections/hero.tsx");
const llms = read("landing/public/llms.txt");
const llmsFull = read("landing/public/llms-full.txt");
const llmAlias = read("landing/public/llm.txt");
expect(layout).toContain('href="/llms.txt"');
expect(layout).toContain('href="/llms-full.txt"');
expect(layout).toContain("MCP for Adobe Premiere Pro | Reviewable Workflow Automation");
expect(home).toContain('alternateName: ["Premiere Pro MCP", "premiere-pro-mcp"]');
expect(hero).toContain("MCP for Adobe Premiere Pro:");
expect(llms).toContain("Preferred product name: **MCP for Adobe Premiere Pro**");
expect(llms).toContain("https://premiere-pro-mcp.com/facts/");
expect(llmsFull).toContain("https://premiere-pro-mcp.com/llm.txt");
expect(llmAlias).toContain("https://premiere-pro-mcp.com/llms.txt");
});
it("allows representative AI crawlers and refreshes release-backed product pages", () => {
const robots = read("landing/app/robots.ts");
const sitemap = read("landing/app/sitemap.ts");
const facts = read("landing/app/facts/page.tsx");
const docs = read("landing/app/docs/page.tsx");
for (const userAgent of ["OAI-SearchBot", "GPTBot", "Claude-SearchBot", "PerplexityBot", "Google-Extended"]) {
expect(robots).toContain(`"${userAgent}"`);
}
expect(sitemap).toContain("productContentDate");
expect(facts).toContain("Premiere Pro MCP the same product");
expect(facts).toContain("For accurate AI and search answers");
expect(docs).toContain("dateModified: product.releaseDate");
});
});
+83
View File
@@ -0,0 +1,83 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const createScript = join(root, "scripts", "create-licensed-host-sweep.mjs");
const validateScript = join(root, "scripts", "validate-licensed-host-report.mjs");
const fixtureHash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
function createReport() {
return JSON.parse(execFileSync(process.execPath, [
createScript,
"--host-os", "Windows",
"--premiere-version", "26.3.0",
"--panel-build", "0123abcd",
"--fixture-revision", "generated-fixture-v1",
"--fixture-sha256", fixtureHash,
], { cwd: root, encoding: "utf8" }));
}
function validate(report: unknown) {
const directory = mkdtempSync(join(tmpdir(), "premiere-host-sweep-"));
const reportPath = join(directory, "report.json");
try {
writeFileSync(reportPath, `${JSON.stringify(report)}\n`, "utf8");
return execFileSync(process.execPath, [validateScript, reportPath], {
cwd: root,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
describe("licensed-host sweep reporting", () => {
it("keeps the checked-in report template aligned with the privacy-safe schema", () => {
const schema = JSON.parse(readFileSync(join(root, "docs", "licensed-host-sweep.schema.json"), "utf8"));
const matrix = JSON.parse(readFileSync(join(root, "docs", "licensed-host-sweep.matrix.json"), "utf8"));
const template = JSON.parse(readFileSync(join(root, "docs", "licensed-host-sweep.template.json"), "utf8"));
expect(schema.properties.schemaVersion.const).toBe("premiere-pro-mcp.licensed-host-sweep.v1");
expect(template.sweep.matrixId).toBe(matrix.id);
expect(template.cases.map((entry: { id: string }) => entry.id)).toEqual(matrix.cases.map((entry: { id: string }) => entry.id));
});
it("generates a not-run sweep skeleton and accepts it without private evidence", () => {
const report = createReport();
expect(report).toMatchObject({
schemaVersion: "premiere-pro-mcp.licensed-host-sweep.v1",
host: { os: "Windows", premiereVersion: "26.3.0" },
fixture: { revision: "generated-fixture-v1", sha256: fixtureHash },
});
expect(report.cases).toHaveLength(4);
expect(validate(report)).toContain('"not_run": 4');
});
it("continues to accept the focused editorial report template", () => {
const editorialTemplate = JSON.parse(readFileSync(join(root, "docs", "licensed-host-report.template.json"), "utf8"));
expect(validate(editorialTemplate)).toContain('"schemaVersion": "legacy-editorial-report"');
});
it("rejects raw paths and requires opaque evidence references", () => {
const report = createReport();
report.cases[0] = {
...report.cases[0],
status: "passed",
evidence: [
{ kind: "host_state", ref: "C:\\private\\before.png" },
{ kind: "structured_response", ref: "connection-response-001" },
],
};
expect(() => validate(report)).toThrow(/opaque non-sensitive IDs|local path/i);
});
it("rejects extra fields that could turn a report into an unredacted evidence dump", () => {
const report = createReport();
report.host.machineName = "editing-workstation";
expect(() => validate(report)).toThrow(/host must contain only/i);
});
});
+92
View File
@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { sendCommand } = vi.hoisted(() => ({ sendCommand: vi.fn() }));
vi.mock("../src/bridge/file-bridge.js", () => ({
sendCommand,
}));
import { getLiveContextResources } from "../src/resources/live-context-resources.js";
describe("live context resources", () => {
beforeEach(() => {
sendCommand.mockReset();
});
it("defines ten bounded, read-only resource endpoints", () => {
const resources = getLiveContextResources({ timeoutMs: 10 });
expect(resources.map((resource) => resource.uri)).toEqual([
"premiere://project/info",
"premiere://project/sequences",
"premiere://project/media",
"premiere://project/bins",
"premiere://timeline/active",
"premiere://effects/available",
"premiere://effects/applied",
"premiere://transitions/available",
"premiere://export/presets",
"premiere://project/metadata",
]);
expect(resources.every((resource) => {
const description = resource.description.toLowerCase();
return description.includes("read-only") || description.includes("path-redacted");
})).toBe(true);
});
it("returns a revisioned, path-safe JSON snapshot", async () => {
sendCommand.mockResolvedValueOnce({
success: true,
data: { project: { name: "Edit" }, privacy: { nativePaths: "not returned" } },
});
const resource = getLiveContextResources({ timeoutMs: 10 })[0];
const output = await resource.read(new URL(resource.uri));
const snapshot = JSON.parse(output.contents[0].text);
expect(snapshot).toMatchObject({
ok: true,
resource: "premiere://project/info",
resourceSchemaVersion: 1,
backend: "cep",
data: { project: { name: "Edit" } },
});
expect(snapshot.snapshotRevision).toMatch(/^[a-f0-9]{20}$/);
expect(output.contents[0].uri).toBe("premiere://project/info");
});
it("returns a structured resource failure without fabricating host data", async () => {
sendCommand.mockResolvedValueOnce({ success: false, error: "CEP bridge offline" });
const resource = getLiveContextResources({ timeoutMs: 10 })[2];
const output = await resource.read(new URL(resource.uri));
const snapshot = JSON.parse(output.contents[0].text);
expect(snapshot).toMatchObject({
ok: false,
resource: "premiere://project/media",
error: "CEP bridge offline",
});
expect(snapshot).not.toHaveProperty("data");
});
it("redacts unexpected path fields at the attachable-resource boundary", async () => {
sendCommand.mockResolvedValueOnce({
success: true,
data: {
presets: [{ name: "Match Source", path: "C:/Users/editor/private.epr" }],
projectPath: "C:/Projects/private.prproj",
nested: { tree_path: "Root/Private", fsName: "/tmp/private" },
},
});
const resource = getLiveContextResources({ timeoutMs: 10 }).find(
(candidate) => candidate.uri === "premiere://export/presets",
);
const output = await resource!.read(new URL(resource!.uri));
const snapshot = JSON.parse(output.contents[0].text);
expect(snapshot.data).toEqual({
presets: [{ name: "Match Source" }],
nested: {},
});
expect(output.contents[0].text).not.toContain("C:/");
});
});
+86
View File
@@ -0,0 +1,86 @@
import http from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
import { toNodeHandler } from "@modelcontextprotocol/node";
import { createMcpHandler } from "@modelcontextprotocol/server";
import { createServer } from "../src/server.js";
const closers: Array<() => Promise<void>> = [];
afterEach(async () => {
await Promise.all(closers.splice(0).map((close) => close()));
});
describe("MCP 2026-07-28 protocol entry", () => {
it("negotiates the modern era and emits routing and cache metadata", async () => {
const observed: Array<{ method?: string; name?: string; version?: string }> = [];
const handler = createMcpHandler(() => createServer({ timeoutMs: 25 }));
const nodeHandler = toNodeHandler(handler);
const server = http.createServer((req, res) => {
observed.push({
method: req.headers["mcp-method"] as string | undefined,
name: req.headers["mcp-name"] as string | undefined,
version: req.headers["mcp-protocol-version"] as string | undefined,
});
void nodeHandler(req, res);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Expected a TCP test address");
const client = new Client(
{ name: "premiere-modern-protocol-test", version: "1.0.0" },
{ versionNegotiation: { mode: "auto" } },
);
const transport = new StreamableHTTPClientTransport(
new URL(`http://127.0.0.1:${address.port}/mcp`),
);
closers.push(async () => {
await client.close().catch(() => {});
await handler.close().catch(() => {});
await new Promise<void>((resolve) => server.close(() => resolve()));
});
await client.connect(transport);
expect(client.getProtocolEra()).toBe("modern");
const capabilities = client.getServerCapabilities();
expect(capabilities?.extensions?.["io.github.leancoderkavy/premiere-pro"]).toMatchObject({
protocolRevision: "2026-07-28",
dualEra: true,
});
const tools = await client.listTools();
expect(tools.ttlMs).toBe(30_000);
expect(tools.cacheScope).toBe("private");
expect(tools.tools).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "get_capabilities" }),
]));
const result = await client.callTool({ name: "get_capabilities", arguments: {} });
expect(result.structuredContent).toMatchObject({
ok: true,
data: {
mcp: {
protocol: { modernRevision: "2026-07-28", dualEra: true },
features: {
resultTypeDiscriminator: "supported",
cancellation: "supported_by_protocol_host_limited",
jsonSchema202012: "supported",
cursorPagination: "supported_single_page_catalogs",
resourceTemplates: "not_exposed_no_parameterized_resource_family",
completions: "not_exposed_no_bounded_completion_domain",
mcpAppsExtension: "not_implemented",
skillsOverMcp: "experimental_not_advertised",
},
},
},
});
expect(observed).toEqual(expect.arrayContaining([
expect.objectContaining({ method: "tools/list", version: "2026-07-28" }),
expect.objectContaining({ method: "tools/call", name: "get_capabilities", version: "2026-07-28" }),
]));
});
});
+273
View File
@@ -0,0 +1,273 @@
import { describe, expect, it } from "vitest";
import { WORKFLOW_CATALOG, WORKFLOW_PROMPTS, WORKFLOW_RESOURCE } from "../src/workflows/catalog.js";
import { annotationsForTool, structuredToolResult } from "../src/workflows/tool-metadata.js";
import { createServer } from "../src/server.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { Telemetry, TelemetryProperties } from "../src/telemetry.js";
describe("modern MCP surface", () => {
it("publishes focused workflow prompts with inspection and verification guidance", () => {
expect(WORKFLOW_PROMPTS).toHaveLength(WORKFLOW_CATALOG.length);
expect(new Set(WORKFLOW_PROMPTS.map((prompt) => prompt.name)).size).toBe(WORKFLOW_PROMPTS.length);
const rendered = WORKFLOW_PROMPTS[0].render({ goal: "Create a 30 second teaser" });
expect(rendered.messages[0].content.text).toContain("Begin with project inspection");
expect(rendered.messages[0].content.text).toContain("inspect the result");
});
it("exposes a machine-readable workflow resource", () => {
const resource = JSON.parse(WORKFLOW_RESOURCE);
expect(resource.version).toBe(1);
expect(resource.workflows).toHaveLength(16);
expect(resource.workflows[0].recommendedTools).toContain("get_premiere_state");
const organization = resource.workflows.find((workflow: { id: string }) => workflow.id === "project-organization");
expect(organization.recommendedTools).toContain("apply_editorial_organization_plan");
expect(organization.recommendedTools).not.toContain("organize_project_items_uxp");
expect(organization.summary).toContain("advanced/manual only");
const transcriptFirst = resource.workflows.find((workflow: { id: string }) => workflow.id === "transcript-first-context");
expect(transcriptFirst.recommendedTools).toContain("create_editorial_context_pack");
});
it("marks inspection as read-only and script execution as open-world", () => {
expect(annotationsForTool("get_project_info")).toMatchObject({
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
});
expect(annotationsForTool("delete_bin")).toMatchObject({
readOnlyHint: false,
destructiveHint: true,
});
expect(annotationsForTool("execute_extendscript").openWorldHint).toBe(true);
expect(annotationsForTool("search_project_context")).toMatchObject({ readOnlyHint: true, idempotentHint: true });
expect(annotationsForTool("create_context_edit_plan")).toMatchObject({ readOnlyHint: true, idempotentHint: true });
expect(annotationsForTool("create_editorial_context_pack")).toMatchObject({ readOnlyHint: true, idempotentHint: true });
expect(annotationsForTool("create_editorial_plan")).toMatchObject({ readOnlyHint: true, idempotentHint: true });
expect(annotationsForTool("preview_editorial_plan")).toMatchObject({ readOnlyHint: true, idempotentHint: true });
expect(annotationsForTool("verify_delivery_conformance")).toMatchObject({ readOnlyHint: true, idempotentHint: true, openWorldHint: false });
expect(annotationsForTool("apply_editorial_organization_plan")).toMatchObject({
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
});
expect(annotationsForTool("manage_project_context")).toMatchObject({
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
});
for (const name of ["list_bins", "inspect_clip", "find_media", "check_project"]) {
expect(annotationsForTool(name)).toMatchObject({ readOnlyHint: true, idempotentHint: true });
}
for (const name of ["remove_clip", "ripple_delete_clip", "close_project"]) {
expect(annotationsForTool(name).destructiveHint).toBe(true);
}
expect(annotationsForTool("send_raw_script").openWorldHint).toBe(true);
expect(annotationsForTool("save_project")).toMatchObject({
title: "Save Project",
readOnlyHint: false,
destructiveHint: false,
openWorldHint: false,
});
});
it("builds stable structured result envelopes", () => {
expect(structuredToolResult("ping", true, { ready: true })).toEqual({
ok: true,
tool: "ping",
data: { ready: true },
});
expect(structuredToolResult("save_project", false, undefined, "offline")).toEqual({
ok: false,
tool: "save_project",
error: "offline",
});
expect(structuredToolResult("ping", true)).toEqual({ ok: true, tool: "ping", data: null });
expect(structuredToolResult("ping", false)).toEqual({ ok: false, tool: "ping", error: "Unknown error" });
});
it("renders optional workflow constraints", () => {
const rendered = WORKFLOW_PROMPTS[1].render({ goal: "clean dialogue", constraints: "preserve room tone" });
expect(rendered.messages[0].content.text).toContain("Constraints: preserve room tone");
});
it("advertises prompts, resources, and tool annotations over MCP", async () => {
const server = createServer({ timeoutMs: 50 });
const client = new Client({ name: "modernization-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
const prompts = await client.listPrompts();
expect(prompts.prompts.map((prompt) => prompt.name)).toContain("premiere-rough-cut");
const resources = await client.listResources();
expect(resources.resources.map((resource) => resource.uri)).toContain("config://premiere-workflows");
expect(resources.resources.map((resource) => resource.uri)).toEqual(expect.arrayContaining([
"premiere://effects/available",
"premiere://effects/applied",
"premiere://transitions/available",
"premiere://export/presets",
"premiere://project/metadata",
]));
expect(resources.resources.map((resource) => resource.uri)).toContain("config://premiere-project-context");
expect(resources.resources.map((resource) => resource.uri)).toEqual(expect.arrayContaining([
"premiere://project/info",
"premiere://project/sequences",
"premiere://project/media",
"premiere://project/bins",
"premiere://timeline/active",
]));
const tools = await client.listTools();
expect(tools.tools.find((tool) => tool.name === "get_project_info")?.annotations?.readOnlyHint).toBe(true);
expect(tools.tools.map((tool) => tool.name)).toContain("get_capabilities");
// The default profile grants inspect/edit/export/filesystem but not
// unsafe-script, so the two scripting tools are not advertised.
expect(tools.tools.map((tool) => tool.name)).not.toContain("execute_extendscript");
expect(tools.tools.map((tool) => tool.name)).not.toContain("evaluate_expression");
expect(tools.tools).toHaveLength(347);
const capabilityTool = tools.tools.find((tool) => tool.name === "get_capabilities");
expect(capabilityTool?.outputSchema).toMatchObject({
type: "object",
// MCP clients such as Claude Desktop can require the 2020-12 dialect.
// Keep this explicit: an older SDK release silently serialized every
// registered output schema as draft-07, making every tool unusable
// before its handler could run.
$schema: "https://json-schema.org/draft/2020-12/schema",
required: expect.arrayContaining(["ok", "tool"]),
properties: expect.objectContaining({
ok: expect.any(Object),
tool: expect.any(Object),
data: expect.any(Object),
error: expect.any(Object),
}),
});
expect(tools.tools.map((tool) => tool.name)).toEqual(expect.arrayContaining([
"manage_project_context",
"search_project_context",
"create_context_edit_plan",
"create_editorial_context_pack",
"create_editorial_plan",
"preview_editorial_plan",
]));
expect(tools.tools.find((tool) => tool.name === "create_editorial_context_pack")?.inputSchema)
.toMatchObject({
properties: {
max_entries: { type: "integer", minimum: 1, maximum: 50 },
max_characters: { type: "integer", minimum: 1024, maximum: 24_000 },
},
});
expect(tools.tools.find((tool) => tool.name === "search_project_context")?.inputSchema)
.toMatchObject({
properties: { max_results: { type: "integer", minimum: 1, maximum: 50 } },
});
const capabilities = await client.callTool({
name: "get_capabilities",
arguments: {},
});
const capabilityData = (capabilities.structuredContent as any).data;
expect(capabilityData.tools.generatedFrom).toBe("registered-tool-catalog");
// The catalog intentionally reports every tool that exists — including
// ones this profile disables, each flagged with its authority state — so
// it is a superset of what tools/list advertises.
expect(capabilityData.tools.total).toBeGreaterThanOrEqual(tools.tools.length);
expect(capabilityData.tools.tools.map((tool: any) => tool.name)).toEqual(
expect.arrayContaining(tools.tools.map((tool) => tool.name)),
);
expect(
capabilityData.tools.tools.find((tool: any) => tool.name === "create_editorial_context_pack"),
).toMatchObject({
backend: "local",
authority: { required: "inspect", enabled: true },
verificationBoundary: "static_metadata_only",
hostVerificationRequired: false,
});
expect(
capabilityData.tools.tools.find((tool: any) => tool.name === "execute_extendscript")
?.authority,
).toMatchObject({ required: "unsafe-script", enabled: false });
} finally {
await client.close();
await server.close();
}
});
it("narrows tools/list with an explicit workflow pack without changing the authority report", async () => {
const server = createServer({ timeoutMs: 50 }, { toolPacks: "essential" });
const client = new Client({ name: "tool-pack-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();
const names = tools.tools.map((tool) => tool.name);
expect(names).toEqual(expect.arrayContaining([
"ping",
"get_capabilities",
"get_project_info",
"inspect_sequence_review_report",
"preview_edit_plan",
"verify_delivery_file",
]));
expect(names).not.toContain("create_bin");
// Essential is a 14-tool focused path (including the two always-visible
// diagnostics), versus 342 tools in the default full catalog.
expect(names).toHaveLength(14);
const capabilities = await client.callTool({
name: "get_capabilities",
arguments: {},
});
const capabilityData = (capabilities.structuredContent as any).data;
expect(capabilityData.toolPacks).toMatchObject({
source: "explicit",
selected: ["essential"],
fullCatalog: false,
});
expect(capabilityData.authority.enabled).toEqual(expect.arrayContaining([
"inspect",
"edit",
"export",
"filesystem",
]));
} finally {
await client.close();
await server.close();
}
});
it("records bounded tool usage without arguments or result content", async () => {
const events: Array<{ event: string; properties?: TelemetryProperties }> = [];
const telemetry: Telemetry = {
enabled: true,
capture: (event, properties) => events.push({ event, properties }),
shutdown: async () => {},
};
const server = createServer({ timeoutMs: 50 }, { telemetry });
const client = new Client({ name: "telemetry-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
await client.callTool({
name: "get_capabilities",
arguments: {},
});
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
event: "mcp_tool_call",
properties: {
tool: "get_capabilities",
outcome: "succeeded",
},
});
expect(events[0].properties).not.toHaveProperty("arguments");
expect(events[0].properties).not.toHaveProperty("result");
} finally {
await client.close();
await server.close();
}
});
});
+154
View File
@@ -0,0 +1,154 @@
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { generateNativeSdkHeaderInventory } from "../scripts/generate-native-sdk-header-inventory.mjs";
async function writeFixture(root: string, relativePath: string, content: string): Promise<void> {
const path = join(root, relativePath);
await mkdir(dirname(path), { recursive: true });
writeFileSync(path, content);
}
function linkDirectory(target: string, path: string): void {
symlinkSync(target, path, process.platform === "win32" ? "junction" : "dir");
}
describe("native SDK header-inventory receipt", () => {
it("accounts for the documented UXP Hybrid headers without recording contents or absolute paths", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-native-sdk-"));
const sdkRoot = join(directory, "hybrid-sdk");
const archivePath = join(directory, "hybrid-sdk.zip");
try {
writeFileSync(archivePath, "authorized SDK fixture");
await writeFixture(sdkRoot, "src/api/UxpAddonTypes.h", "typedef void* addon_value;");
await writeFixture(sdkRoot, "src/api/UxpAddonShared.h", "void uxp_addon_example(void);");
await writeFixture(sdkRoot, "src/utilities/UxpAddon.h", "#define UXP_ADDON_INIT(x)");
await writeFixture(sdkRoot, "src/utilities/internal.hpp", "namespace fixture {}");
const inventory = await generateNativeSdkHeaderInventory({
sdk: "uxp-hybrid",
sdkVersion: "7.3.0-fixture",
archivePath,
sdkRoot,
});
expect(inventory).toMatchObject({
schemaVersion: 1,
source: {
sdk: "uxp-hybrid",
authorityUrl: "https://developer.adobe.com/premiere-pro/uxp/plugins/hybrid-plugins/",
inventoryScope: "header_files_only",
archiveSha256: expect.stringMatching(/^[a-f0-9]{64}$/),
includeDirectories: ["src/api", "src/utilities"],
},
stats: { headers: 4 },
});
expect(inventory.headers.map((header) => header.path)).toEqual([
"src/api/UxpAddonShared.h",
"src/api/UxpAddonTypes.h",
"src/utilities/UxpAddon.h",
"src/utilities/internal.hpp",
]);
expect(JSON.stringify(inventory)).not.toContain(sdkRoot);
expect(JSON.stringify(inventory)).not.toContain("typedef void*");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("requires documented Hybrid headers and explicit PrSDK include directories", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-native-sdk-invalid-"));
const sdkRoot = join(directory, "sdk");
const archivePath = join(directory, "sdk.zip");
try {
writeFileSync(archivePath, "authorized SDK fixture");
await writeFixture(sdkRoot, "src/api/UxpAddonTypes.h", "typedef void* addon_value;");
await writeFixture(sdkRoot, "src/api/UxpAddonShared.h", "void uxp_addon_example(void);");
await writeFixture(sdkRoot, "src/utilities/Other.h", "#define OTHER 1");
await expect(generateNativeSdkHeaderInventory({
sdk: "uxp-hybrid", sdkVersion: "fixture", archivePath, sdkRoot,
})).rejects.toThrow("Missing required UXP Hybrid SDK header");
await writeFixture(sdkRoot, "Headers/PrSDKFixture.h", "class PrSDKFixture {};");
await expect(generateNativeSdkHeaderInventory({
sdk: "premiere-prsdk", sdkVersion: "fixture", archivePath, sdkRoot,
})).rejects.toThrow("requires one or more explicit includeDirectories");
await expect(generateNativeSdkHeaderInventory({
sdk: "premiere-prsdk", sdkVersion: "fixture", archivePath, sdkRoot,
includeDirectories: ["Headers"],
})).resolves.toMatchObject({
source: { sdk: "premiere-prsdk", includeDirectories: ["Headers"] },
stats: { headers: 1 },
});
await expect(generateNativeSdkHeaderInventory({
sdk: "premiere-prsdk", sdkVersion: "fixture", archivePath, sdkRoot,
includeDirectories: ["Headers/"],
})).rejects.toThrow("must use a canonical relative path");
await expect(generateNativeSdkHeaderInventory({
sdk: "premiere-prsdk", sdkVersion: "fixture", archivePath, sdkRoot,
includeDirectories: ["C:Headers"],
})).rejects.toThrow("must stay relative to the SDK root");
await expect(generateNativeSdkHeaderInventory({
sdk: "constructor", sdkVersion: "fixture", archivePath, sdkRoot,
})).rejects.toThrow("sdk must be uxp-hybrid or premiere-prsdk");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("rejects include-root and intermediate links that resolve outside the SDK root", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-native-sdk-links-"));
const archivePath = join(directory, "sdk.zip");
const prsdkRoot = join(directory, "prsdk");
const hybridRoot = join(directory, "hybrid");
const outsideHeaders = join(directory, "outside-headers");
const outsideSrc = join(directory, "outside-src");
try {
writeFileSync(archivePath, "authorized SDK fixture");
await mkdir(prsdkRoot, { recursive: true });
await writeFixture(outsideHeaders, "PrSDKFixture.h", "class PrSDKFixture {};");
linkDirectory(outsideHeaders, join(prsdkRoot, "Headers"));
await expect(generateNativeSdkHeaderInventory({
sdk: "premiere-prsdk", sdkVersion: "fixture", archivePath, sdkRoot: prsdkRoot,
includeDirectories: ["Headers"],
})).rejects.toThrow("must stay inside the SDK root");
await mkdir(hybridRoot, { recursive: true });
await writeFixture(outsideSrc, "api/UxpAddonTypes.h", "typedef void* addon_value;");
await writeFixture(outsideSrc, "api/UxpAddonShared.h", "void uxp_addon_example(void);");
await writeFixture(outsideSrc, "utilities/UxpAddon.h", "#define UXP_ADDON_INIT(x)");
linkDirectory(outsideSrc, join(hybridRoot, "src"));
await expect(generateNativeSdkHeaderInventory({
sdk: "uxp-hybrid", sdkVersion: "fixture", archivePath, sdkRoot: hybridRoot,
})).rejects.toThrow("must stay inside the SDK root");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("writes a deterministic receipt and rejects a stale check", async () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-native-sdk-cli-"));
const sdkRoot = join(directory, "hybrid-sdk");
const archivePath = join(directory, "hybrid-sdk.zip");
const outputPath = join(directory, "inventory.json");
const script = "scripts/generate-native-sdk-header-inventory.mjs";
try {
writeFileSync(archivePath, "authorized SDK fixture");
await writeFixture(sdkRoot, "src/api/UxpAddonTypes.h", "typedef void* addon_value;");
await writeFixture(sdkRoot, "src/api/UxpAddonShared.h", "void uxp_addon_example(void);");
await writeFixture(sdkRoot, "src/utilities/UxpAddon.h", "#define UXP_ADDON_INIT(x)");
const args = [script, "--sdk", "uxp-hybrid", "--sdk-version", "fixture", "--archive", archivePath, "--sdk-root", sdkRoot, "--output", outputPath];
expect(spawnSync(process.execPath, args, { encoding: "utf8" }).status).toBe(0);
expect(JSON.parse(readFileSync(outputPath, "utf8"))).toMatchObject({ stats: { headers: 3 } });
expect(spawnSync(process.execPath, [...args, "--check"], { encoding: "utf8" }).status).toBe(0);
writeFileSync(join(sdkRoot, "src/api/UxpAddonShared.h"), "changed");
const stale = spawnSync(process.execPath, [...args, "--check"], { encoding: "utf8" });
expect(stale.status).not.toBe(0);
expect(stale.stderr).toContain("Native SDK header inventory is stale");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
});
+193
View File
@@ -0,0 +1,193 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createOperationId,
emitAudit,
stderrAuditSink,
} from "../src/security/audit.js";
describe("security audit helpers", () => {
afterEach(() => vi.restoreAllMocks());
it("creates UUID operation identifiers", () => {
expect(createOperationId()).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
});
it("adds an ISO timestamp before emitting an audit event", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-15T21:30:00.000Z"));
const sink = vi.fn();
emitAudit(sink, {
operationId: "operation-1",
action: "project.inspect",
outcome: "succeeded",
details: { clips: 2 },
});
expect(sink).toHaveBeenCalledWith({
operationId: "operation-1",
action: "project.inspect",
outcome: "succeeded",
details: { clips: 2 },
timestamp: "2026-08-15T21:30:00.000Z",
});
vi.useRealTimers();
});
it("writes one protocol-safe JSON line to stderr", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
stderrAuditSink({
operationId: "operation-2",
action: "media.export",
outcome: "denied",
timestamp: "2026-08-15T21:31:00.000Z",
});
expect(error).toHaveBeenCalledOnce();
expect(JSON.parse(String(error.mock.calls[0][0]))).toEqual({
type: "premiere-mcp-audit",
operationId: "operation-2",
action: "media.export",
outcome: "denied",
timestamp: "2026-08-15T21:31:00.000Z",
});
});
});
type JsonRecord = Record<string, unknown>;
function validManifest(): JsonRecord {
return {
schemaVersion: 1,
source: {
apiPackage: "@adobe/premierepro",
apiVersion: "26.3.0",
changelogUrl: "https://developer.adobe.com/premiere-pro/uxp/changelog/",
},
entries: [validEntry()],
};
}
function validEntry(): JsonRecord {
return {
id: "test-entry",
adobeApi: ["Project.test"],
documentationUrls: [
"https://developer.adobe.com/premiere-pro/uxp/ppro-reference/",
],
minimumPremiereVersion: "26.3.0",
backend: "uxp",
uxpCommand: null,
mcpTools: ["test_tool"],
availability: "current",
implementationStatus: "implemented",
verificationStatus: "automated_contract_verified",
liveHostVerificationStatus: "not_run",
mutatesProject: false,
undoable: false,
idempotency: "not_applicable",
verificationBoundary: "test_result",
};
}
async function importWithManifest(manifest: unknown): Promise<unknown> {
vi.resetModules();
vi.doMock("node:module", () => ({
createRequire: () => () => manifest,
}));
return import("../src/platform-capabilities.js");
}
describe("Adobe UXP manifest validation", () => {
afterEach(() => {
vi.doUnmock("node:module");
vi.resetModules();
});
const invalidCases: Array<[string, (manifest: JsonRecord) => unknown, RegExp]> = [
["requires an object manifest", () => null, /must be an object/],
["requires schema version one", (m) => ({ ...m, schemaVersion: 2 }), /schemaVersion/],
["requires an object source", (m) => ({ ...m, source: [] }), /source must be an object/],
["requires the Premiere API package", (m) => ({ ...m, source: { ...(m.source as JsonRecord), apiPackage: "other" } }), /source package/],
["requires a source API version", (m) => ({ ...m, source: { ...(m.source as JsonRecord), apiVersion: "" } }), /apiVersion/],
["rejects malformed URLs", (m) => ({ ...m, source: { ...(m.source as JsonRecord), changelogUrl: "not a url" } }), /valid URL/],
["rejects non-Adobe documentation", (m) => ({ ...m, source: { ...(m.source as JsonRecord), changelogUrl: "https://example.com/premiere-pro/uxp/" } }), /official Premiere Pro UXP/],
["requires entries", (m) => ({ ...m, entries: [] }), /non-empty array/],
["requires entry objects", (m) => ({ ...m, entries: [null] }), /entry 0 must be an object/],
["requires entry identifiers", (m) => ({ ...m, entries: [{ ...validEntry(), id: "" }] }), /entry 0.id/],
["requires kebab-case identifiers", (m) => ({ ...m, entries: [{ ...validEntry(), id: "Not Kebab" }] }), /unique kebab-case/],
["rejects duplicate identifiers", (m) => ({ ...m, entries: [validEntry(), validEntry()] }), /unique kebab-case/],
["requires documentation arrays", (m) => ({ ...m, entries: [{ ...validEntry(), documentationUrls: [] }] }), /documentationUrls/],
["requires current or planned availability", (m) => ({ ...m, entries: [{ ...validEntry(), availability: "future" }] }), /availability/],
["requires a supported implementation status", (m) => ({ ...m, entries: [{ ...validEntry(), implementationStatus: "partial" }] }), /implementationStatus/],
["requires a supported verification status", (m) => ({ ...m, entries: [{ ...validEntry(), verificationStatus: "maybe" }] }), /verificationStatus/],
["requires a supported live-host status", (m) => ({ ...m, entries: [{ ...validEntry(), liveHostVerificationStatus: "maybe" }] }), /liveHostVerificationStatus/],
["requires supported idempotency", (m) => ({ ...m, entries: [{ ...validEntry(), idempotency: "sometimes" }] }), /idempotency/],
["requires the UXP backend", (m) => ({ ...m, entries: [{ ...validEntry(), backend: "cep" }] }), /backend must be uxp/],
["requires a nullable string command", (m) => ({ ...m, entries: [{ ...validEntry(), uxpCommand: 42 }] }), /uxpCommand/],
["requires boolean mutation metadata", (m) => ({ ...m, entries: [{ ...validEntry(), mutatesProject: "no" }] }), /mutation metadata/],
["requires non-empty API arrays", (m) => ({ ...m, entries: [{ ...validEntry(), adobeApi: [""] }] }), /adobeApi/],
["requires a minimum version", (m) => ({ ...m, entries: [{ ...validEntry(), minimumPremiereVersion: null }] }), /minimumPremiereVersion/],
["requires MCP tool arrays", (m) => ({ ...m, entries: [{ ...validEntry(), mcpTools: "tool" }] }), /mcpTools/],
["requires a verification boundary", (m) => ({ ...m, entries: [{ ...validEntry(), verificationBoundary: "" }] }), /verificationBoundary/],
];
it.each(invalidCases)("%s", async (_name, mutate, expected) => {
await expect(importWithManifest(mutate(validManifest()))).rejects.toThrow(expected);
});
it("accepts every supported manifest enum variant", async () => {
const manifest = validManifest();
manifest.entries = [
validEntry(),
{
...validEntry(),
id: "planned-entry",
availability: "planned",
implementationStatus: "planned",
verificationStatus: "committed_unverified",
liveHostVerificationStatus: "verified",
idempotency: "operation_id",
uxpCommand: "test.command",
},
{
...validEntry(),
id: "unstarted-entry",
verificationStatus: "not_started",
idempotency: "operation_id_when_supported",
},
];
const module = await importWithManifest(manifest) as {
buildAdobeUxpCoverageReport: () => { summary: Record<string, number> };
buildPlatformCapabilityReport: (
capabilities: { source: "default"; capabilities: Set<"inspect"> },
platform: NodeJS.Platform,
tempDirectory: string,
) => { runtime: { platformName: string; supported: boolean }; authority: { enabled: string[] } };
platformName: (platform: NodeJS.Platform) => string;
};
expect(module.buildAdobeUxpCoverageReport().summary).toMatchObject({
total: 3,
current: 2,
planned: 1,
implemented: 2,
committedUnverified: 1,
automatedContractVerified: 1,
liveHostVerified: 1,
});
expect(module.platformName("darwin")).toBe("macOS");
expect(module.platformName("win32")).toBe("Windows");
expect(module.platformName("linux")).toBe("linux");
expect(module.buildPlatformCapabilityReport(
{ source: "default", capabilities: new Set(["inspect"]) },
"linux",
"/private/bridge",
)).toMatchObject({
runtime: { platformName: "linux", supported: false, tempDirectory: "/private/bridge" },
authority: { enabled: ["inspect"] },
});
});
});
+164
View File
@@ -0,0 +1,164 @@
import { describe, expect, it } from "vitest";
import http from "node:http";
import { SignJWT, exportJWK, generateKeyPair } from "jose";
import { OAuthResourceServer } from "../src/oauth-resource-server.js";
const configuration = {
issuer: "https://identity.example.com",
audience: "https://premiere.example.com/mcp",
jwksUri: "https://identity.example.com/.well-known/jwks.json",
publicUrl: "https://premiere.example.com",
requiredScopes: ["premiere:mcp", "premiere:read"],
allowedSubjects: ["user-1"],
};
const request = (authorization?: string) => ({ headers: { authorization } } as never);
describe("OAuth resource server", () => {
it("publishes RFC 9728 metadata and a discovery challenge without secrets", () => {
const resource = new OAuthResourceServer(configuration, async () => ({ sub: "user-1" }));
expect(resource.metadata()).toEqual({
resource: "https://premiere.example.com/mcp",
authorization_servers: ["https://identity.example.com"],
bearer_methods_supported: ["header"],
scopes_supported: ["premiere:mcp", "premiere:read"],
});
expect(resource.challenge()).toBe(
'Bearer resource_metadata="https://premiere.example.com/.well-known/oauth-protected-resource/mcp", scope="premiere:mcp premiere:read"',
);
expect(resource.challenge("missing_token")).not.toContain("error=");
expect(resource.challenge("insufficient_scope")).toContain('scope="premiere:mcp premiere:read"');
});
it("requires a bearer token, subject, and every configured scope", async () => {
const valid = new OAuthResourceServer(configuration, async () => ({
sub: "user-1", scope: "premiere:mcp premiere:read extra",
}));
await expect(valid.authenticate(request("Bearer signed-token"))).resolves.toMatchObject({
authenticated: true,
principal: { subject: "user-1", scopes: ["premiere:mcp", "premiere:read", "extra"] },
});
await expect(valid.authenticate(request())).resolves.toEqual({ authenticated: false, error: "missing_token" });
await expect(valid.authenticate(request("Basic signed-token"))).resolves.toEqual({ authenticated: false, error: "missing_token" });
const missingSubject = new OAuthResourceServer(configuration, async () => ({ scope: "premiere:mcp premiere:read" }));
await expect(missingSubject.authenticate(request("Bearer token"))).resolves.toEqual({ authenticated: false, error: "invalid_token" });
const missingScope = new OAuthResourceServer(configuration, async () => ({ sub: "user-1", scope: "premiere:mcp" }));
await expect(missingScope.authenticate(request("Bearer token"))).resolves.toEqual({ authenticated: false, error: "insufficient_scope" });
const wrongSubject = new OAuthResourceServer(configuration, async () => ({
sub: "user-2", scope: "premiere:mcp premiere:read",
}));
await expect(wrongSubject.authenticate(request("Bearer token"))).resolves.toEqual({ authenticated: false, error: "invalid_token" });
});
it("fails closed when verification or JWKS retrieval fails", async () => {
const resource = new OAuthResourceServer(configuration, async () => {
throw new Error("identity provider unavailable");
});
await expect(resource.authenticate(request("Bearer token"))).resolves.toEqual({
authenticated: false,
error: "invalid_token",
});
});
it("verifies signature, issuer, audience, lifetime, and required claims with remote JWKS", async () => {
const { publicKey, privateKey } = await generateKeyPair("RS256");
const jwk = await exportJWK(publicKey);
const jwksServer = http.createServer((_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ keys: [{ ...jwk, kid: "test-key", alg: "RS256", use: "sig" }] }));
});
await new Promise<void>((resolve) => jwksServer.listen(0, "127.0.0.1", resolve));
const address = jwksServer.address();
if (!address || typeof address === "string") throw new Error("Test JWKS server did not bind");
try {
const resource = new OAuthResourceServer({
...configuration,
jwksUri: `http://127.0.0.1:${address.port}/jwks.json`,
});
const now = Math.floor(Date.now() / 1000);
const validToken = await new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience(configuration.audience)
.setSubject("user-1")
.setIssuedAt(now)
.setExpirationTime(now + 300)
.sign(privateKey);
await expect(resource.authenticate(request(`Bearer ${validToken}`))).resolves.toMatchObject({
authenticated: true,
principal: { subject: "user-1", rateLimitKey: expect.stringMatching(/^[a-f0-9]{32}$/) },
});
const wrongAudience = await new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience("https://attacker.example.com/mcp")
.setSubject("user-1")
.setIssuedAt(now)
.setExpirationTime(now + 300)
.sign(privateKey);
await expect(resource.authenticate(request(`Bearer ${wrongAudience}`))).resolves.toEqual({
authenticated: false,
error: "invalid_token",
});
const rejectedClaims = [
new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer("https://wrong-issuer.example.com")
.setAudience(configuration.audience)
.setSubject("user-1")
.setIssuedAt(now)
.setExpirationTime(now + 300),
new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience(configuration.audience)
.setSubject("user-1")
.setIssuedAt(now)
.setExpirationTime(now - 60),
new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience(configuration.audience)
.setSubject("user-1")
.setIssuedAt(now)
.setNotBefore(now + 300)
.setExpirationTime(now + 600),
new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience(configuration.audience)
.setSubject("user-1")
.setExpirationTime(now + 300),
];
for (const builder of rejectedClaims) {
const token = await builder.sign(privateKey);
await expect(resource.authenticate(request(`Bearer ${token}`))).resolves.toEqual({
authenticated: false,
error: "invalid_token",
});
}
const { privateKey: attackerKey } = await generateKeyPair("RS256");
const badSignature = await new SignJWT({ scope: "premiere:mcp premiere:read" })
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
.setIssuer(configuration.issuer)
.setAudience(configuration.audience)
.setSubject("user-1")
.setIssuedAt(now)
.setExpirationTime(now + 300)
.sign(attackerKey);
await expect(resource.authenticate(request(`Bearer ${badSignature}`))).resolves.toEqual({
authenticated: false,
error: "invalid_token",
});
} finally {
await new Promise<void>((resolve, reject) => jwksServer.close((error) => error ? reject(error) : resolve()));
}
});
});
+121
View File
@@ -0,0 +1,121 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, it } from "vitest";
const inventory = JSON.parse(readFileSync("src/resources/premiere-doc-inventory.json", "utf8"));
describe("Adobe Premiere UXP documentation inventory", () => {
it("accounts for every page and every public surface family", () => {
expect(inventory.source.sitemapUrl).toBe("https://developer.adobe.com/sitemap.xml");
expect(inventory.stats.total).toBe(inventory.pages.length);
expect(Object.values(inventory.stats.bySurface).reduce((sum: number, count) => sum + Number(count), 0))
.toBe(inventory.stats.total);
expect(new Set(inventory.pages.map((page: { url: string }) => page.url)).size).toBe(inventory.pages.length);
expect(Object.keys(inventory.stats.bySurface)).toEqual([
"premiere-dom",
"premiere-uxp-supporting-docs",
"spectrum-web-components",
"uxp-css",
"uxp-html",
"uxp-javascript",
"uxp-plugin-guides",
]);
for (const page of inventory.pages) {
expect(page.url).toMatch(/^https:\/\/developer\.adobe\.com\/premiere-pro\/uxp\//);
expect(page.lastModified === null || /^\d{4}-\d{2}-\d{2}$/.test(page.lastModified)).toBe(true);
}
});
it("validates the committed snapshot without refetching the live sitemap", () => {
const result = spawnSync(process.execPath, ["scripts/generate-premiere-doc-inventory.mjs", "--check"], {
encoding: "utf8",
env: {
...process.env,
PREMIERE_DOC_SITEMAP_PATH: join(tmpdir(), "missing-premiere-doc-sitemap.xml"),
},
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("Premiere documentation inventory is current:");
});
it.each([
["empty", "<urlset></urlset>", "no URL entries"],
["missing location", "<urlset><url><lastmod>2026-09-01</lastmod></url></urlset>", "no location"],
["bad date", "<urlset><url><loc>https://developer.adobe.com/premiere-pro/uxp/plugins/</loc><lastmod>today</lastmod></url></urlset>", "Invalid Adobe sitemap lastmod"],
["impossible date", "<urlset><url><loc>https://developer.adobe.com/premiere-pro/uxp/plugins/</loc><lastmod>2026-02-31</lastmod></url></urlset>", "Invalid Adobe sitemap lastmod"],
["duplicate", "<urlset><url><loc>https://developer.adobe.com/premiere-pro/uxp/plugins/</loc></url><url><loc>https://developer.adobe.com/premiere-pro/uxp/plugins/</loc></url></urlset>", "duplicate"],
])("fails closed for a %s sitemap", (_label, sitemap, expectedError) => {
const directory = mkdtempSync(join(tmpdir(), "premiere-doc-inventory-"));
const fixture = join(directory, "sitemap.xml");
writeFileSync(fixture, sitemap);
try {
const result = spawnSync(process.execPath, ["scripts/generate-premiere-doc-inventory.mjs", "--validate-only"], {
encoding: "utf8",
env: { ...process.env, PREMIERE_DOC_SITEMAP_PATH: fixture },
});
expect(result.status).not.toBe(0);
expect(result.stderr).toContain(expectedError);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("decodes XML entities once without double-unescaping nested text", () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-doc-inventory-"));
const fixture = join(directory, "sitemap.xml");
const output = join(directory, "inventory.json");
const nestedEntityUrl = "https://developer.adobe.com/premiere-pro/uxp/plugins/?value=&amp;amp;";
writeFileSync(fixture, `<urlset><url><loc>${nestedEntityUrl}</loc></url></urlset>`);
try {
const result = spawnSync(process.execPath, ["scripts/generate-premiere-doc-inventory.mjs"], {
encoding: "utf8",
env: {
...process.env,
PREMIERE_DOC_SITEMAP_PATH: fixture,
PREMIERE_DOC_INVENTORY_OUTPUT_PATH: output,
},
});
expect(result.status).toBe(0);
const generated = JSON.parse(readFileSync(output, "utf8"));
expect(generated.pages).toHaveLength(1);
expect(generated.pages[0].url).toBe("https://developer.adobe.com/premiere-pro/uxp/plugins/?value=&amp;");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
it("classifies a representative URL from every documentation branch", () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-doc-inventory-"));
const fixture = join(directory, "sitemap.xml");
const output = join(directory, "inventory.json");
const expected = new Map([
["https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/project", "premiere-dom"],
["https://developer.adobe.com/premiere-pro/uxp/uxp-api/reference-js/Modules/uxp/", "uxp-javascript"],
["https://developer.adobe.com/premiere-pro/uxp/uxp-api/reference-html/HTML%20Elements/", "uxp-html"],
["https://developer.adobe.com/premiere-pro/uxp/uxp-api/reference-css/General/", "uxp-css"],
["https://developer.adobe.com/premiere-pro/uxp/uxp-api/reference-spectrum/spectrum-to-swc-mapping/", "spectrum-web-components"],
["https://developer.adobe.com/premiere-pro/uxp/plugins/concepts/manifest/", "uxp-plugin-guides"],
["https://developer.adobe.com/premiere-pro/uxp/resources/migration-guides/", "premiere-uxp-supporting-docs"],
]);
const sitemap = `<urlset>${[...expected.keys()].map((url) => `<url><loc>${url}</loc></url>`).join("")}</urlset>`;
writeFileSync(fixture, sitemap);
try {
const result = spawnSync(process.execPath, ["scripts/generate-premiere-doc-inventory.mjs"], {
encoding: "utf8",
env: {
...process.env,
PREMIERE_DOC_SITEMAP_PATH: fixture,
PREMIERE_DOC_INVENTORY_OUTPUT_PATH: output,
},
});
expect(result.status).toBe(0);
const generated = JSON.parse(readFileSync(output, "utf8"));
expect(new Map(generated.pages.map((page: { url: string; surface: string }) => [page.url, page.surface])))
.toEqual(expected);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
});
+273
View File
@@ -0,0 +1,273 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
type Surface = {
id: string;
kind: string;
authorityUrls: string[];
communityReferenceUrls?: string[];
versionSource: string;
inventoryArtifact: string | null;
inventoryState: string;
implementationState: string;
inventoryCommand?: string;
inventoryVerificationCommand?: string;
inventoryDocumentation?: string;
benchmarkEvidenceCommand?: string;
benchmarkEvidenceSchema?: string;
addonReceiptCommand?: string;
addonReceiptVerificationCommand?: string;
addonReceiptDocumentation?: string;
ccxReceiptCommand?: string;
ccxReceiptVerificationCommand?: string;
ccxReceiptDocumentation?: string;
ccxReceiptSchemaVersion?: number;
betaAafExportOptionsDriftArtifact?: string;
betaAafExportOptionsDriftCommand?: string;
betaAafExportOptionsDriftVerificationCommand?: string;
betaAafExportOptionsDriftDocumentation?: string;
betaProjectOptionsDriftArtifact?: string;
betaProjectOptionsDriftCommand?: string;
betaProjectOptionsDriftVerificationCommand?: string;
betaProjectOptionsDriftDocumentation?: string;
betaTransitionOptionsDriftArtifact?: string;
betaTransitionOptionsDriftCommand?: string;
betaTransitionOptionsDriftVerificationCommand?: string;
betaTransitionOptionsDriftDocumentation?: string;
betaRectFDriftArtifact?: string;
betaRectFDriftCommand?: string;
betaRectFDriftVerificationCommand?: string;
betaRectFDriftDocumentation?: string;
betaColorDriftArtifact?: string;
betaColorDriftCommand?: string;
betaColorDriftVerificationCommand?: string;
betaColorDriftDocumentation?: string;
betaPointFDriftArtifact?: string;
betaPointFDriftCommand?: string;
betaPointFDriftVerificationCommand?: string;
betaPointFDriftDocumentation?: string;
betaGuidDriftArtifact?: string;
betaGuidDriftCommand?: string;
betaGuidDriftVerificationCommand?: string;
betaGuidDriftDocumentation?: string;
betaFrameRateDriftArtifact?: string;
betaFrameRateDriftCommand?: string;
betaFrameRateDriftVerificationCommand?: string;
betaFrameRateDriftDocumentation?: string;
betaTickTimeDriftArtifact?: string;
betaTickTimeDriftCommand?: string;
betaTickTimeDriftVerificationCommand?: string;
betaTickTimeDriftDocumentation?: string;
betaC2paDriftArtifact?: string;
betaC2paDriftCommand?: string;
betaC2paDriftVerificationCommand?: string;
betaC2paDriftDocumentation?: string;
betaMediaDriftArtifact?: string;
betaMediaDriftCommand?: string;
betaMediaDriftVerificationCommand?: string;
betaMediaDriftDocumentation?: string;
betaMediaManagerDriftArtifact?: string;
betaMediaManagerDriftCommand?: string;
betaMediaManagerDriftVerificationCommand?: string;
betaMediaManagerDriftDocumentation?: string;
betaTranscriptDriftArtifact?: string;
betaTranscriptDriftCommand?: string;
betaTranscriptDriftVerificationCommand?: string;
betaTranscriptDriftDocumentation?: string;
betaWorkAreaDriftArtifact?: string;
betaWorkAreaDriftCommand?: string;
betaWorkAreaDriftVerificationCommand?: string;
betaWorkAreaDriftDocumentation?: string;
notes: string;
};
type Competitor = {
repository: string;
commit: string;
observedAt: string;
featureFamilies: string[];
adoptionBoundary: string;
};
const registry = JSON.parse(readFileSync("src/resources/premiere-surface-registry.json", "utf8")) as {
schemaVersion: number;
researchedAt: string;
completionPolicy: string;
integrationSurfaces: Surface[];
competitorSources: Competitor[];
};
describe("Premiere API and competitor surface registry", () => {
it("enumerates every official surface family without overstating completion", () => {
expect(registry.schemaVersion).toBe(1);
expect(registry.researchedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(registry.completionPolicy).toContain("every item is classified");
expect(registry.integrationSurfaces.map((surface) => surface.id)).toEqual([
"premiere-dom",
"uxp-javascript",
"uxp-html",
"uxp-css",
"spectrum-web-components",
"uxp-plugin-guides",
"uxp-hybrid-cpp",
"premiere-cpp-sdk",
"cep-extendscript",
"cep-platform",
"qe-dom",
]);
expect(new Set(registry.integrationSurfaces.map((surface) => surface.id)).size)
.toBe(registry.integrationSurfaces.length);
const inventoryStates = new Set([
"complete",
"partial",
"not_started",
"blocked_external_artifact",
"unavailable_authoritative_source",
]);
const implementationStates = new Set(["partial", "gated", "not_started", "experimental"]);
for (const surface of registry.integrationSurfaces) {
if (surface.kind === "undocumented_api") {
expect(surface.authorityUrls).toEqual([]);
} else {
expect(surface.authorityUrls.length).toBeGreaterThan(0);
for (const url of surface.authorityUrls) expect(url).toMatch(/^https:\/\//);
}
for (const url of surface.communityReferenceUrls ?? []) expect(url).toMatch(/^https:\/\//);
expect(surface.versionSource.length).toBeGreaterThan(0);
expect(surface.notes.length).toBeGreaterThan(20);
expect(inventoryStates.has(surface.inventoryState)).toBe(true);
expect(implementationStates.has(surface.implementationState)).toBe(true);
if (surface.inventoryState === "complete") {
expect(surface.inventoryArtifact).toBeTruthy();
}
}
expect(registry.integrationSurfaces.find((surface) => surface.id === "premiere-dom"))
.toMatchObject({
inventoryState: "complete",
implementationState: "partial",
betaAafExportOptionsDriftArtifact: "dist/resources/adobe-beta-aaf-export-options-drift.json",
betaAafExportOptionsDriftCommand: "npm run adobe:beta-aaf-export-options-drift",
betaAafExportOptionsDriftVerificationCommand: "npm run adobe:beta-aaf-export-options-drift:check",
betaAafExportOptionsDriftDocumentation: "docs/adobe-beta-aaf-export-options-drift.md",
betaProjectOptionsDriftArtifact: "dist/resources/adobe-beta-project-options-drift.json",
betaProjectOptionsDriftCommand: "npm run adobe:beta-project-options-drift",
betaProjectOptionsDriftVerificationCommand: "npm run adobe:beta-project-options-drift:check",
betaProjectOptionsDriftDocumentation: "docs/adobe-beta-project-options-drift.md",
betaTransitionOptionsDriftArtifact: "dist/resources/adobe-beta-transition-options-drift.json",
betaTransitionOptionsDriftCommand: "npm run adobe:beta-transition-options-drift",
betaTransitionOptionsDriftVerificationCommand: "npm run adobe:beta-transition-options-drift:check",
betaTransitionOptionsDriftDocumentation: "docs/adobe-beta-transition-options-drift.md",
betaRectFDriftArtifact: "dist/resources/adobe-beta-rectf-drift.json",
betaRectFDriftCommand: "npm run adobe:beta-rectf-drift",
betaRectFDriftVerificationCommand: "npm run adobe:beta-rectf-drift:check",
betaRectFDriftDocumentation: "docs/adobe-beta-rectf-drift.md",
betaColorDriftArtifact: "dist/resources/adobe-beta-color-drift.json",
betaColorDriftCommand: "npm run adobe:beta-color-drift",
betaColorDriftVerificationCommand: "npm run adobe:beta-color-drift:check",
betaColorDriftDocumentation: "docs/adobe-beta-color-drift.md",
betaPointFDriftArtifact: "dist/resources/adobe-beta-pointf-drift.json",
betaPointFDriftCommand: "npm run adobe:beta-pointf-drift",
betaPointFDriftVerificationCommand: "npm run adobe:beta-pointf-drift:check",
betaPointFDriftDocumentation: "docs/adobe-beta-pointf-drift.md",
betaGuidDriftArtifact: "dist/resources/adobe-beta-guid-drift.json",
betaGuidDriftCommand: "npm run adobe:beta-guid-drift",
betaGuidDriftVerificationCommand: "npm run adobe:beta-guid-drift:check",
betaGuidDriftDocumentation: "docs/adobe-beta-guid-drift.md",
betaFrameRateDriftArtifact: "dist/resources/adobe-beta-frame-rate-drift.json",
betaFrameRateDriftCommand: "npm run adobe:beta-frame-rate-drift",
betaFrameRateDriftVerificationCommand: "npm run adobe:beta-frame-rate-drift:check",
betaFrameRateDriftDocumentation: "docs/adobe-beta-frame-rate-drift.md",
betaTickTimeDriftArtifact: "dist/resources/adobe-beta-tick-time-drift.json",
betaTickTimeDriftCommand: "npm run adobe:beta-tick-time-drift",
betaTickTimeDriftVerificationCommand: "npm run adobe:beta-tick-time-drift:check",
betaTickTimeDriftDocumentation: "docs/adobe-beta-tick-time-drift.md",
betaC2paDriftArtifact: "dist/resources/adobe-beta-c2pa-drift.json",
betaC2paDriftCommand: "npm run adobe:beta-c2pa-drift",
betaC2paDriftVerificationCommand: "npm run adobe:beta-c2pa-drift:check",
betaC2paDriftDocumentation: "docs/adobe-beta-c2pa-drift.md",
betaMediaDriftArtifact: "dist/resources/adobe-beta-media-drift.json",
betaMediaDriftCommand: "npm run adobe:beta-media-drift",
betaMediaDriftVerificationCommand: "npm run adobe:beta-media-drift:check",
betaMediaDriftDocumentation: "docs/adobe-beta-media-drift.md",
betaMediaManagerDriftArtifact: "dist/resources/adobe-beta-media-manager-drift.json",
betaMediaManagerDriftCommand: "npm run adobe:beta-media-manager-drift",
betaMediaManagerDriftVerificationCommand: "npm run adobe:beta-media-manager-drift:check",
betaMediaManagerDriftDocumentation: "docs/adobe-beta-media-manager-drift.md",
betaTranscriptDriftArtifact: "dist/resources/adobe-beta-transcript-drift.json",
betaTranscriptDriftCommand: "npm run adobe:beta-transcript-drift",
betaTranscriptDriftVerificationCommand: "npm run adobe:beta-transcript-drift:check",
betaTranscriptDriftDocumentation: "docs/adobe-beta-transcript-drift.md",
betaWorkAreaDriftArtifact: "dist/resources/adobe-beta-work-area-drift.json",
betaWorkAreaDriftCommand: "npm run adobe:beta-work-area-drift",
betaWorkAreaDriftVerificationCommand: "npm run adobe:beta-work-area-drift:check",
betaWorkAreaDriftDocumentation: "docs/adobe-beta-work-area-drift.md",
});
expect(registry.integrationSurfaces.find((surface) => surface.id === "uxp-javascript"))
.toMatchObject({
versionSource: "@adobe/cc-ext-uxp-types@7.3.1",
inventoryArtifact: "dist/resources/uxp-js-api-inventory.json",
inventoryState: "complete",
implementationState: "partial",
});
expect(registry.integrationSurfaces.find((surface) => surface.id === "cep-extendscript"))
.toMatchObject({
authorityUrls: ["https://github.com/Adobe-CEP/Samples/tree/master/PProPanel"],
communityReferenceUrls: [
"https://ppro-scripting.docsforadobe.dev/",
"https://github.com/docsforadobe/premiere-scripting-guide",
],
inventoryArtifact: "dist/resources/extendscript-api-inventory.json",
inventoryState: "complete",
implementationState: "partial",
});
expect(registry.integrationSurfaces.find((surface) => surface.id === "cep-platform"))
.toMatchObject({
inventoryArtifact: "dist/resources/cep-reference-inventory.json",
inventoryState: "complete",
implementationState: "partial",
});
expect(registry.integrationSurfaces.filter((surface) => surface.inventoryState === "complete"))
.toHaveLength(8);
for (const id of ["uxp-hybrid-cpp", "premiere-cpp-sdk"]) {
expect(registry.integrationSurfaces.find((surface) => surface.id === id)).toMatchObject({
inventoryArtifact: null,
inventoryState: "blocked_external_artifact",
inventoryCommand: "npm run native:sdk-header-inventory",
inventoryVerificationCommand: "npm run native:sdk-header-inventory:verify",
inventoryDocumentation: "docs/native-sdk-header-inventory.md",
});
}
expect(registry.integrationSurfaces.find((surface) => surface.id === "uxp-hybrid-cpp"))
.toMatchObject({
benchmarkEvidenceCommand: "npm run benchmark:uxp-hybrid:verify",
benchmarkEvidenceSchema: "benchmarks/uxp-hybrid/evidence.schema.json",
addonReceiptCommand: "npm run native:hybrid-addon-receipt",
addonReceiptVerificationCommand: "npm run native:hybrid-addon-receipt:verify",
addonReceiptDocumentation: "docs/uxp-hybrid-addon-receipt.md",
ccxReceiptCommand: "npm run native:hybrid-ccx-receipt",
ccxReceiptVerificationCommand: "npm run native:hybrid-ccx-receipt:verify",
ccxReceiptDocumentation: "docs/uxp-hybrid-ccx-receipt.md",
ccxReceiptSchemaVersion: 2,
});
expect(registry.integrationSurfaces.find((surface) => surface.id === "uxp-hybrid-cpp")?.notes)
.toContain("root main.js entrypoint and three-target bundle layout");
expect(registry.integrationSurfaces.find((surface) => surface.id === "uxp-hybrid-cpp")?.notes)
.toContain("schema-v2 local CCX archive receipt can bind that current layout receipt to the byte-identical required files, a full safe ZIP entry-name-set digest with unambiguous non-ASCII UTF-8 names and declared UTF-8 file comments, matching and feature-sufficient local ZIP version-needed, Deflate-only compression-option flags, framed non-ZIP64 extra fields, and core header fields, no unaccounted local-record or central-directory-to-end bytes, no declared Unix special file types, nonempty directory entries, or nonzero directory CRC-32 values, required streamed data-descriptor CRC/sizes, recomputed ZIP CRC-32, and exact DEFLATE range consumption for the already-required payloads; it also rejects encrypted-entry, central-directory-encryption, other unsupported general-purpose flags, or ZIP64 entry metadata before reading required payloads");
expect(registry.integrationSurfaces.find((surface) => surface.id === "uxp-hybrid-cpp")?.notes)
.toContain("schema-v3 candidate benchmark");
});
it("pins reviewed competitor sources and explicit safe-adoption boundaries", () => {
expect(registry.competitorSources).toHaveLength(4);
expect(new Set(registry.competitorSources.map((source) => source.repository)).size)
.toBe(registry.competitorSources.length);
for (const source of registry.competitorSources) {
expect(source.repository).toMatch(/^[\w.-]+\/[\w.-]+$/);
expect(source.commit).toMatch(/^[0-9a-f]{40}$/);
expect(source.observedAt).toBe(registry.researchedAt);
expect(source.featureFamilies.length).toBeGreaterThan(0);
expect(source.adoptionBoundary.length).toBeGreaterThan(40);
}
});
});
+41
View File
@@ -0,0 +1,41 @@
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createProjectBackup } from "../src/tools/recovery.js";
describe("createProjectBackup", () => {
it("proves the recovery copy is byte-identical and leaves the source unchanged", () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-project-backup-"));
const source = join(directory, "edit.prproj");
const contents = Buffer.from("premiere-project-fixture\nrevision=7\n", "utf8");
writeFileSync(source, contents);
const before = statSync(source);
const receipt = createProjectBackup(source, new Date("2026-08-23T17:00:00.000Z"));
expect(receipt).toMatchObject({
sourcePath: source,
sourceUnchanged: true,
byteIdentical: true,
sizeBytes: contents.length,
});
expect(readFileSync(source)).toEqual(contents);
expect(readFileSync(receipt.backupPath)).toEqual(contents);
expect(statSync(source).mtimeMs).toBe(before.mtimeMs);
});
it("fails closed for non-project files, missing files, and backup-name collisions", () => {
const directory = mkdtempSync(join(tmpdir(), "premiere-project-backup-"));
const textFile = join(directory, "notes.txt");
writeFileSync(textFile, "not a project");
expect(() => createProjectBackup(textFile)).toThrow(".prproj");
expect(() => createProjectBackup(join(directory, "missing.prproj"))).toThrow("does not exist");
const project = join(directory, "edit.prproj");
writeFileSync(project, "fixture");
const now = new Date("2026-08-23T17:00:00.000Z");
createProjectBackup(project, now);
expect(() => createProjectBackup(project, now)).toThrow();
});
});
+513
View File
@@ -0,0 +1,513 @@
import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
defaultProjectContextDirectory,
MAX_CONTEXT_RECORDS,
normalizeContextKeywords,
normalizeContextText,
ProjectContextRepository,
searchProjectContext,
} from "../src/context/project-context-store.js";
import {
buildContextDocumentFromSnapshot,
enrichContextDocument,
getProjectContextTools,
MAX_CONCURRENT_SOURCE_FINGERPRINTS,
type PremiereContextSnapshot,
} from "../src/tools/project-context.js";
const temporaryDirectories: string[] = [];
const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
const supportsNodeSqlite = nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 5);
afterEach(async () => {
vi.unstubAllEnvs();
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
function snapshot(overrides: Partial<PremiereContextSnapshot["sequence"]["clips"][number]> = {}): PremiereContextSnapshot {
return {
projectName: "Documentary",
projectPath: "D:/Projects/documentary.prproj",
sequence: {
id: "sequence-1",
name: "Assembly",
durationSeconds: 120,
truncated: false,
clips: [
{
nodeId: "timeline-1",
name: "Interview A",
startSeconds: 10,
endSeconds: 20,
inPointSeconds: 30,
outPointSeconds: 40,
speed: 1,
trackType: "video",
trackIndex: 0,
sourceId: "source-1",
sourceName: "Interview A.mov",
mediaPath: "D:/Media/interview-a.mov",
offline: false,
...overrides,
},
],
},
};
}
describe("project context index", () => {
it("separates source invalidation from timeline placement revisions", async () => {
let mediaVersion = 1;
const fingerprint = async () => ({ mediaPathHash: "path-hash", size: 1_000, modifiedMs: mediaVersion });
const first = await buildContextDocumentFromSnapshot(snapshot(), undefined, fingerprint);
const source = first.document.records.find((record) => record.kind === "source")!;
const enriched = enrichContextDocument(first.document, [
{
kind: "transcript",
source_id: "source-1",
source_revision: source.sourceRevision,
start_seconds: 31,
end_seconds: 35,
text: "The budget needs approval by Friday.",
keywords: ["budget", "approval"],
},
]).document;
const moved = await buildContextDocumentFromSnapshot(
snapshot({ startSeconds: 45, endSeconds: 55 }),
enriched,
fingerprint,
);
expect(moved.document.sourceRevision).toBe(first.document.sourceRevision);
expect(moved.document.timelineRevision).not.toBe(first.document.timelineRevision);
expect(moved.document.records.some((record) => record.kind === "transcript")).toBe(true);
expect(moved.invalidatedRecords).toBe(0);
mediaVersion = 2;
const changedSource = await buildContextDocumentFromSnapshot(snapshot(), moved.document, fingerprint);
expect(changedSource.document.sourceRevision).not.toBe(first.document.sourceRevision);
expect(changedSource.document.records.some((record) => record.kind === "transcript")).toBe(false);
expect(changedSource.invalidatedRecords).toBe(1);
});
it("bounds concurrent source fingerprint work during a large capture", async () => {
const largeSnapshot = snapshot();
largeSnapshot.sequence.clips = Array.from({ length: MAX_CONCURRENT_SOURCE_FINGERPRINTS * 3 }, (_, index) => ({
...largeSnapshot.sequence.clips[0]!,
nodeId: `timeline-${index}`,
sourceId: `source-${index}`,
mediaPath: `D:/Media/source-${index}.mov`,
}));
let active = 0;
let peak = 0;
await buildContextDocumentFromSnapshot(largeSnapshot, undefined, async () => {
active += 1;
peak = Math.max(peak, active);
await new Promise((resolve) => setTimeout(resolve, 0));
active -= 1;
return {};
});
expect(peak).toBe(MAX_CONCURRENT_SOURCE_FINGERPRINTS);
});
it("ranks bounded transcript and audio evidence while preserving revision provenance", async () => {
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
const source = built.document.records.find((record) => record.kind === "source")!;
const enriched = enrichContextDocument(built.document, [
{
id: "budget-transcript",
kind: "transcript",
name: "Budget answer",
source_id: "source-1",
source_revision: source.sourceRevision,
start_seconds: 31,
end_seconds: 35,
text: "The budget needs approval by Friday.",
keywords: ["budget", "approval"],
},
{
id: "room-tone",
kind: "audio",
source_id: "source-1",
source_revision: source.sourceRevision,
text: "Quiet room tone suitable for dialogue padding.",
keywords: ["room tone"],
},
]).document;
const results = searchProjectContext(enriched, { query: "budget approval", limit: 1 });
expect(results).toHaveLength(1);
expect(results[0].record.id).toBe("budget-transcript");
expect(results[0].record.sourceRevision).toBe(source.sourceRevision);
expect(results[0].matchedTerms).toEqual(expect.arrayContaining(["budget", "approval"]));
});
it("persists locally without retaining a native media path", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-test-"));
temporaryDirectories.push(directory);
const repository = new ProjectContextRepository({ backend: "json", directory });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "redacted-path-hash" }));
await repository.put(built.document);
const restored = await repository.get(built.document.projectId);
expect(restored).toEqual(built.document);
expect(await repository.backendName()).toBe("json");
const files = await readdir(directory);
const serialized = await readFile(path.join(directory, files.find((name) => name.endsWith(".json"))!), "utf8");
expect(serialized).not.toContain("D:/Media/interview-a.mov");
expect(serialized).toContain("redacted-path-hash");
});
it.runIf(supportsNodeSqlite)("uses SQLite when requested and supports complete repository lifecycle", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-sqlite-test-"));
temporaryDirectories.push(directory);
const repository = new ProjectContextRepository({ backend: "sqlite", directory });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
expect(await repository.backendName()).toBe("sqlite");
expect(await repository.get("missing")).toBeUndefined();
await repository.put(built.document);
expect(await repository.get(built.document.projectId)).toEqual(built.document);
expect(await repository.list()).toEqual([
expect.objectContaining({ projectId: built.document.projectId, recordCount: built.document.records.length }),
]);
const updated = { ...built.document, projectName: "Updated documentary" };
await repository.put(updated);
expect((await repository.get(built.document.projectId))?.projectName).toBe("Updated documentary");
expect(await repository.delete(built.document.projectId)).toBe(true);
expect(await repository.delete(built.document.projectId)).toBe(false);
await repository.close();
});
it("handles missing and corrupt JSON entries without hiding valid project summaries", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-json-test-"));
temporaryDirectories.push(directory);
const repository = new ProjectContextRepository({ backend: "json", directory });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
expect(await repository.get("missing")).toBeUndefined();
expect(await repository.delete("missing")).toBe(false);
await repository.put(built.document);
await writeFile(path.join(directory, "corrupt.json"), "not json", "utf8");
expect(await repository.list()).toEqual([
expect.objectContaining({ projectId: built.document.projectId }),
]);
const validFile = (await readdir(directory)).find((name) => name !== "corrupt.json")!;
await writeFile(path.join(directory, validFile), JSON.stringify({ schemaVersion: 999 }), "utf8");
await expect(repository.get(built.document.projectId)).rejects.toThrow("Unsupported project context schema version");
});
it("validates repository backend, record bounds, and context directory overrides", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "ppmcp-context-env-test-"));
temporaryDirectories.push(directory);
vi.stubEnv("PREMIERE_CONTEXT_DIR", directory);
expect(defaultProjectContextDirectory()).toBe(path.resolve(directory));
await expect(new ProjectContextRepository({ backend: "invalid" as any }).backendName())
.rejects.toThrow("must be auto, sqlite, json, or memory");
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
const repository = new ProjectContextRepository({ backend: "memory" });
await expect(repository.put({
...built.document,
records: Array.from({ length: MAX_CONTEXT_RECORDS + 1 }, () => built.document.records[0]),
})).rejects.toThrow(`limited to ${MAX_CONTEXT_RECORDS}`);
expect(await repository.get("missing")).toBeUndefined();
expect(await repository.list()).toEqual([]);
expect(await repository.delete("missing")).toBe(false);
});
it("normalizes bounded text and filters search by sequence, kind, and result limits", async () => {
expect(normalizeContextText(null)).toBe("");
expect(normalizeContextText(" multiple\n spaces ")).toBe("multiple spaces");
expect(normalizeContextKeywords("not-an-array")).toEqual([]);
expect(normalizeContextKeywords([" budget ", "budget", 4])).toEqual(["budget"]);
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
expect(() => searchProjectContext(built.document, { query: " " })).toThrow("must not be empty");
expect(searchProjectContext(built.document, {
query: "interview",
sequenceId: "different-sequence",
})).toEqual([]);
const sourceOnly = searchProjectContext(built.document, {
query: "interview",
kinds: ["source"],
limit: 100,
});
expect(sourceOnly).toHaveLength(1);
expect(sourceOnly[0].record.kind).toBe("source");
expect(searchProjectContext(built.document, { query: "not-present", limit: -4 })).toEqual([]);
});
it("supports enrich, status, search, plan, and clear through the MCP tool contracts", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
await repository.put(built.document);
const tools = getProjectContextTools({}, { repository });
const source = built.document.records.find((record) => record.kind === "source")!;
const enrich = await (tools.manage_project_context.handler as any)({
action: "enrich",
project_id: built.document.projectId,
records: [
{
kind: "transcript",
source_id: "source-1",
source_revision: source.sourceRevision,
start_seconds: 31,
end_seconds: 35,
text: "The budget needs approval by Friday.",
keywords: ["budget"],
metadata: { confidence: 0.97, source_path: "D:/private.mov", api_token: "secret" },
},
],
});
expect(enrich).toMatchObject({ success: true, data: { upserted: 1 } });
const status = await (tools.manage_project_context.handler as any)({
action: "status",
project_id: built.document.projectId,
});
expect(status).toMatchObject({ success: true, data: { backend: "memory", counts: { transcript: 1 } } });
const search = await (tools.search_project_context.handler as any)({
project_id: built.document.projectId,
query: "budget",
});
expect(search.data.results[0]).toMatchObject({ kind: "transcript", sourceId: "source-1" });
expect(search.data.results[0].metadata).toEqual({ confidence: 0.97 });
await expect((tools.search_project_context.handler as any)({
project_id: built.document.projectId,
query: "budget",
max_results: 51,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("1 through 50") });
const plan = await (tools.create_context_edit_plan.handler as any)({
project_id: built.document.projectId,
intent: "Use the budget answer",
});
expect(plan).toMatchObject({
success: true,
data: {
applied: false,
expectedTimelineRevision: built.document.timelineRevision,
candidates: [{ kind: "transcript", sourceId: "source-1" }],
},
});
expect(plan.data.nextSteps.join(" ")).toContain("preview_edit_plan");
const cleared = await (tools.manage_project_context.handler as any)({
action: "clear",
project_id: built.document.projectId,
});
expect(cleared).toEqual({ success: true, data: { projectId: built.document.projectId, cleared: true } });
});
it("imports explicit revision-bound editorial evidence without opening frames or calling a provider", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
await repository.put(built.document);
const source = built.document.records.find((record) => record.kind === "source")!;
const tools = getProjectContextTools({}, { repository });
const result = await (tools.manage_project_context.handler as any)({
action: "import_evidence",
project_id: built.document.projectId,
evidence: [
{
type: "transcript_passage",
text: "The budget needs approval by Friday.",
speaker_label: "Presenter",
source_id: "source-1",
source_revision: source.sourceRevision,
sequence_id: "sequence-1",
timeline_item_id: "timeline-1",
timeline_revision: built.document.timelineRevision,
start_seconds: 31,
end_seconds: 35,
metadata: { confidence: 0.97, source_path: "D:/private.mov", api_token: "secret" },
},
{
type: "shot_log",
text: "Medium interview framing with a visible product sample.",
source_id: "source-1",
source_revision: source.sourceRevision,
},
{
type: "audio_observation",
text: "Clean dialogue; brief room-tone gap before the answer.",
},
{
type: "operator_note",
text: "Prefer this explanation after the opening question.",
},
{
type: "frame_reference",
frame_reference_id: "contact-sheet-003",
sequence_id: "sequence-1",
timeline_revision: built.document.timelineRevision,
},
{
type: "speaker_label",
speaker_label: "Presenter",
source_id: "source-1",
source_revision: source.sourceRevision,
},
],
});
expect(result).toMatchObject({
success: true,
data: {
applied: false,
upserted: 6,
importedEvidenceTypes: expect.arrayContaining(["transcript_passage", "frame_reference"]),
},
});
const current = await repository.get(built.document.projectId);
const transcript = current!.records.find((record) => record.kind === "transcript")!;
const frame = current!.records.find((record) => record.metadata?.frameReferenceId === "contact-sheet-003")!;
expect(transcript).toMatchObject({
sourceRevision: source.sourceRevision,
timelineRevision: built.document.timelineRevision,
metadata: { evidenceType: "transcript_passage", speakerLabel: "Presenter", confidence: 0.97 },
});
expect(transcript.metadata).not.toHaveProperty("source_path");
expect(transcript.metadata).not.toHaveProperty("api_token");
expect(frame).toMatchObject({ kind: "shot", timelineRevision: built.document.timelineRevision });
const moved = await buildContextDocumentFromSnapshot(
snapshot({ startSeconds: 45, endSeconds: 55 }),
current,
async () => ({ mediaPathHash: "hash" }),
);
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "transcript_passage")).toBe(false);
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "frame_reference")).toBe(false);
expect(moved.document.records.some((record) => record.metadata?.evidenceType === "shot_log")).toBe(true);
expect(moved.invalidatedRecords).toBeGreaterThanOrEqual(2);
});
it("rejects unguarded, stale, unknown, and path-shaped editorial evidence references", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
await repository.put(built.document);
const tools = getProjectContextTools({}, { repository });
const importEvidence = (evidence: unknown[]) => (tools.manage_project_context.handler as any)({
action: "import_evidence",
project_id: built.document.projectId,
evidence,
});
await expect(importEvidence([{ type: "shot_log", text: "Shot", source_id: "source-1" }]))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("source_revision") });
await expect(importEvidence([{ type: "operator_note", text: "Note", sequence_id: "sequence-1" }]))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("timeline_revision") });
await expect(importEvidence([{ type: "frame_reference", frame_reference_id: "D:/private/frame.png" }]))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("opaque identifier") });
await expect(importEvidence([{ type: "operator_note", text: "Note", timeline_revision: "stale" }]))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("requires sequence_id") });
});
it("captures through the MCP contract and preserves bridge failures", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const captureSnapshot = vi.fn().mockResolvedValue({ success: true, data: snapshot() });
const tools = getProjectContextTools({}, { repository, captureSnapshot });
const captured = await (tools.manage_project_context.handler as any)({ action: "capture" });
expect(captured).toMatchObject({
success: true,
data: { backend: "memory", projectName: "Documentary", recordCount: 4, invalidatedRecords: 0 },
});
expect(captureSnapshot).toHaveBeenCalledOnce();
const statusList = await (tools.manage_project_context.handler as any)({ action: "status" });
expect(statusList.data.projects).toHaveLength(1);
const failedTools = getProjectContextTools({}, {
repository,
captureSnapshot: async () => ({ success: false, error: "Premiere offline" }),
});
await expect((failedTools.manage_project_context.handler as any)({ action: "capture" }))
.resolves.toEqual({ success: false, error: "Premiere offline" });
});
it("returns bounded not-found and unsupported-action errors", async () => {
const repository = new ProjectContextRepository({ backend: "memory" });
const tools = getProjectContextTools({}, { repository });
await expect((tools.manage_project_context.handler as any)({ action: "status", project_id: "missing" }))
.resolves.toEqual({ success: false, error: "Project context not found" });
await expect((tools.manage_project_context.handler as any)({ action: "enrich", project_id: "missing", records: [] }))
.resolves.toEqual({ success: false, error: "Project context not found; capture it before enrichment" });
await expect((tools.search_project_context.handler as any)({ project_id: "missing", query: "x" }))
.resolves.toEqual({ success: false, error: "Project context not found" });
await expect((tools.create_context_edit_plan.handler as any)({ project_id: "missing", intent: "x" }))
.resolves.toEqual({ success: false, error: "Project context not found" });
await expect((tools.manage_project_context.handler as any)({ action: "unknown" }))
.resolves.toEqual({ success: false, error: "Unsupported project context action: unknown" });
await expect((tools.manage_project_context.handler as any)({ action: "clear", project_id: "" }))
.rejects.toThrow("project_id is required");
});
it("rejects stale enrichment revisions", async () => {
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({ mediaPathHash: "hash" }));
expect(() => enrichContextDocument(built.document, [
{
kind: "transcript",
source_id: "source-1",
source_revision: "stale",
text: "Old analysis",
},
])).toThrow("stale source_revision");
});
it("validates enrichment shapes, bounds, replacement, and timeline revisions", async () => {
const built = await buildContextDocumentFromSnapshot(snapshot(), undefined, async () => ({}));
expect(() => enrichContextDocument(built.document, [])).toThrow("at least one enrichment");
expect(() => enrichContextDocument(built.document, Array.from({ length: 513 }, () => ({
kind: "note" as const,
text: "note",
})))).toThrow("limited to 512");
expect(() => enrichContextDocument(built.document, [{ kind: "source", text: "bad" }]))
.toThrow("kind must be transcript, shot, audio, or note");
expect(() => enrichContextDocument(built.document, [{ kind: "note", text: " " }]))
.toThrow("text must not be empty");
expect(() => enrichContextDocument(built.document, [{ kind: "audio", source_id: "missing", text: "quiet" }]))
.toThrow("unknown source_id");
expect(() => enrichContextDocument(built.document, [{
kind: "note",
timeline_revision: "stale",
text: "stale note",
}])).toThrow("stale timeline_revision");
expect(() => enrichContextDocument(built.document, [{ kind: "shot", start_seconds: -1, text: "bad" }]))
.toThrow("start_seconds must be non-negative");
expect(() => enrichContextDocument(built.document, [{ kind: "shot", start_seconds: 5, end_seconds: 4, text: "bad" }]))
.toThrow("end_seconds must be at or after");
const first = enrichContextDocument(built.document, [{ id: "replace-me", kind: "note", text: "first" }]).document;
const replaced = enrichContextDocument(first, [{
id: "replace-me",
kind: "note",
text: "second",
timeline_item_id: "timeline-1",
timeline_revision: first.timelineRevision,
track_type: "video",
track_index: 0,
metadata: { approved: true, score: 1, nullable: null, nested: { ignored: true } },
}], true).document;
expect(replaced.records.filter((record) => record.kind === "note")).toEqual([
expect.objectContaining({ text: "second", timelineRevision: first.timelineRevision }),
]);
});
it("validates incomplete and oversized Premiere snapshots", async () => {
await expect(buildContextDocumentFromSnapshot({ projectName: "bad" } as any))
.rejects.toThrow("incomplete context snapshot");
const oversized = snapshot();
oversized.sequence.clips = Array.from({ length: 2_001 }, (_, index) => ({
...snapshot().sequence.clips[0],
nodeId: `clip-${index}`,
}));
await expect(buildContextDocumentFromSnapshot(oversized))
.rejects.toThrow("limited to 2000 timeline items");
});
});
+118
View File
@@ -0,0 +1,118 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, describe, expect, it } from "vitest";
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "premiere-project-intake-host-report-"));
const validator = path.resolve("scripts/validate-project-intake-host-report.mjs");
function evidence(kind: string, reference: string) {
return { kind, reference: `evidence://${reference}`, sha256: "d".repeat(64) };
}
function completeSyntheticReport() {
return {
schemaVersion: "project-intake-host-report/v1",
sourceCommit: "a".repeat(40),
host: {
os: "Windows",
premiereVersion: "26.0.0",
premiereBuild: "26.0.0-build-1",
connector: { type: "cep", buildHash: "b".repeat(40) },
},
client: { name: "test-client", version: "1.0.0" },
fixture: { revision: "generated-fixture-v1", sha256: "c".repeat(64) },
privacy: {
containsOnlyGeneratedFixtureData: true,
localPathsRemoved: true,
mediaNamesRemoved: true,
promptsRemoved: true,
transcriptsRemoved: true,
credentialsRemoved: true,
},
cases: [
{
id: "PIP-CONNECT-001",
status: "passed",
executedAt: "2026-08-23T00:00:00Z",
assertions: { tool: "verify_premiere_connection", overall: "ready" },
evidence: [evidence("structured_connection_response", "connection")],
},
{
id: "PIP-PREVIEW-001",
status: "passed",
executedAt: "2026-08-23T00:01:00Z",
assertions: {
tool: "preview_project_intake",
applied: false,
pathDisclosure: "redacted",
organizationPlanApplied: false,
},
evidence: [evidence("structured_preview_response", "preview")],
},
{
id: "PIP-NO-MUTATION-001",
status: "passed",
executedAt: "2026-08-23T00:02:00Z",
assertions: { tool: "preview_project_intake", projectMutated: false, projectSaved: false },
evidence: [
evidence("before_project_panel", "before-panel"),
evidence("after_project_panel", "after-panel"),
evidence("structured_preview_response", "preview-no-mutation"),
],
},
],
};
}
function validate(report: unknown) {
const reportPath = path.join(tempDirectory, `${Math.random().toString(16).slice(2)}.json`);
fs.writeFileSync(reportPath, JSON.stringify(report));
return spawnSync(process.execPath, [validator, reportPath], { encoding: "utf8" });
}
afterAll(() => fs.rmSync(tempDirectory, { recursive: true, force: true }));
describe("Project Intake licensed-host report validator", () => {
it("accepts the not-run template without treating it as host verification", () => {
const template = JSON.parse(fs.readFileSync("docs/project-intake-host-report.template.json", "utf8"));
const result = validate(template);
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain('"not_run": 3');
expect(result.stdout).toContain('"licensedHostVerifiedByValidator": false');
});
it("validates a structurally complete synthetic fixture without treating it as host evidence", () => {
const result = validate(completeSyntheticReport());
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain('"passed": 3');
expect(result.stdout).toContain('"humanReviewRequired": true');
expect(result.stdout).toContain('"licensedHostVerifiedByValidator": false');
});
it("fails closed when a preview report reveals paths or fails its redacted postcondition", () => {
const report = completeSyntheticReport();
report.cases[1].assertions.pathDisclosure = "requested";
report.client.name = "C:/restricted";
const result = validate(report);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("pathDisclosure must be \"redacted\"");
expect(result.stderr).toContain("project data, a local path, or credential-like content");
});
it("requires no-mutation evidence whenever the Project Intake preview passes", () => {
const report = completeSyntheticReport();
report.cases[2].status = "not_run";
delete report.cases[2].executedAt;
delete report.cases[2].assertions;
report.cases[2].evidence = [];
const result = validate(report);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("PIP-PREVIEW-001 cannot pass unless PIP-NO-MUTATION-001 also passes");
});
});
+37
View File
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { validateFacilityIntakeTemplate } from "../src/intake/project-intake.js";
type StarterTemplateFile = {
templates: Array<{
key: string;
label: string;
description: string;
checks: string[];
template: unknown;
}>;
};
const starters = JSON.parse(
readFileSync(join(process.cwd(), "landing", "lib", "project-intake-starter-templates.json"), "utf8"),
) as StarterTemplateFile;
describe("Project Intake starter templates", () => {
it("keeps every public starter template valid for the real preview tool", () => {
expect(starters.templates.map((starter) => starter.key)).toEqual([
"editorial_handoff",
"proxy_ready",
"delivery_preflight",
]);
for (const starter of starters.templates) {
expect(starter.label).not.toHaveLength(0);
expect(starter.description).not.toHaveLength(0);
expect(starter.checks.length).toBeGreaterThan(0);
const template = validateFacilityIntakeTemplate(starter.template);
expect(template.approvedPathPrefixes).toEqual([]);
expect(JSON.stringify(template)).not.toMatch(/[A-Za-z]:[\\/]|\\\\/);
}
});
});
+276
View File
@@ -0,0 +1,276 @@
import { describe, expect, it } from "vitest";
import {
buildProjectIntakeReport,
validateFacilityIntakeTemplate,
validateProjectIntakeSnapshot,
} from "../src/intake/project-intake.js";
const template = {
schemaVersion: 1,
id: "facility-intake",
version: "2026-08",
requiredBins: [{ name: "Incoming" }, { name: "Interviews", parentPath: "Editorial" }],
allowedExtensions: ["mov", "wav"],
allowedFrameRates: [23.976, 24],
proxyPolicy: "require",
approvedPathPrefixes: [{ id: "show-storage", prefix: "D:/Shows/Current" }],
organizationRules: [{
id: "interviews",
destinationBinName: "Interviews",
match: { filenamePrefixes: ["INT_"], extensions: ["mov"] },
colorIndex: 3,
}],
};
const snapshot = {
project: { id: "project-1", name: "Documentary" },
truncated: false,
unavailableEvidence: [],
items: [
{ id: "bin-editorial", name: "Editorial", type: "bin", treePath: "Editorial" },
{ id: "bin-incoming", name: "Incoming", type: "bin", treePath: "Incoming" },
{ id: "bin-interviews", name: "Interviews", type: "bin", treePath: "Editorial/Interviews", parentId: "bin-editorial" },
{
id: "clip-1", name: "INT_A_001.mov", type: "clip", parentId: "bin-incoming",
mediaPath: "D:/Shows/Current/Camera/INT_A_001.mov", offline: false, hasProxy: true, frameRate: 23.976,
},
],
};
describe("project intake engine", () => {
it("validates a bounded facility template and rejects unknown or unsafe shapes", () => {
const normalized = validateFacilityIntakeTemplate({ ...template, allowedExtensions: ["WAV", ".MOV"] });
expect(normalized.allowedExtensions).toEqual(["mov", "wav"]);
expect(normalized.organizationRules[0].match).toEqual({ extensions: ["mov"], filenamePrefixes: ["int_"] });
expect(() => validateFacilityIntakeTemplate({ ...template, unexpected: true })).toThrow("not supported");
expect(() => validateFacilityIntakeTemplate({ ...template, organizationRules: [{
id: "unsafe", destinationBinName: "Unsafe", match: {},
}] })).toThrow("must include a filename prefix or extension");
expect(() => validateFacilityIntakeTemplate({ ...template, allowedExtensions: ["../mov"] })).toThrow("simple file extension");
});
it("produces a deterministic, read-only, path-redacted organization proposal", () => {
const report = buildProjectIntakeReport(snapshot, template);
const reordered = buildProjectIntakeReport({ ...snapshot, items: [...snapshot.items].reverse() }, template);
expect(report).toMatchObject({
applied: false,
status: "ready",
capture: { itemCount: 4, truncated: false, pathDisclosure: "redacted" },
organizationPlan: {
applied: false,
proposedBins: [{ name: "Interviews", exists: true, createIfApproved: false }],
proposedMoves: [{ projectItemId: "clip-1", expectedParentId: "bin-incoming", destinationBinName: "Interviews", ruleId: "interviews", colorIndex: 3 }],
},
});
expect(report.findings).toEqual([]);
expect(report.project.revision).toBe(reordered.project.revision);
expect(report.template.digest).toBe(reordered.template.digest);
expect(report.organizationPlan.planDigest).toBe(reordered.organizationPlan.planDigest);
expect(JSON.stringify(report)).not.toContain("D:/Shows/Current");
});
it("reports observed extension, frame-rate, offline, proxy, and path violations without exposing paths by default", () => {
const report = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? {
...item,
name: "INT_A_001.mp4",
mediaPath: "D:/Unapproved/INT_A_001.mp4",
frameRate: 29.97,
offline: true,
hasProxy: false,
} : item),
}, template);
expect(report.status).toBe("needs_attention");
expect(report.findings.map((finding) => finding.code)).toEqual(expect.arrayContaining([
"EXTENSION_NOT_ALLOWED", "FRAME_RATE_NOT_ALLOWED", "OFFLINE_MEDIA", "PROXY_MISSING", "MEDIA_PATH_NOT_APPROVED",
]));
const pathFinding = report.findings.find((finding) => finding.code === "MEDIA_PATH_NOT_APPROVED")!;
expect(pathFinding).toMatchObject({ certainty: "observed", observed: { mediaPathHash: expect.stringMatching(/^[a-f0-9]{64}$/) } });
expect(JSON.stringify(report)).not.toContain("D:/Unapproved");
const disclosed = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? { ...item, mediaPath: "D:/Unapproved/INT_A_001.mp4" } : item),
}, template, { includePaths: true });
expect(disclosed.capture.pathDisclosure).toBe("requested");
expect(JSON.stringify(disclosed)).toContain("D:/Unapproved/INT_A_001.mp4");
});
it("normalizes noisy Premiere frame-rate evidence without conflating canonical timebases", () => {
const noisy = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? { ...item, frameRate: 29.93295 } : item),
}, { ...template, allowedFrameRates: [29.97] });
expect(noisy.findings.some((finding) => finding.code === "FRAME_RATE_NOT_ALLOWED")).toBe(false);
const distinctCanonical = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? { ...item, frameRate: 23.976 } : item),
}, { ...template, allowedFrameRates: [24] });
expect(distinctCanonical.findings).toContainEqual(expect.objectContaining({
code: "FRAME_RATE_NOT_ALLOWED",
itemId: "clip-1",
}));
});
it.each([Number.NaN, Number.POSITIVE_INFINITY, 0, 241, null])(
"keeps malformed frame-rate evidence as an item-level unsupported finding: %s",
(frameRate) => {
const report = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? { ...item, frameRate } : item),
}, template);
expect(report.status).toBe("incomplete");
expect(report.capture.itemCount).toBe(snapshot.items.length);
expect(report.findings).toContainEqual(expect.objectContaining({
code: "FRAME_RATE_UNSUPPORTED",
certainty: "unavailable",
itemId: "clip-1",
}));
},
);
it("returns incomplete when required evidence is unavailable and distinguishes optional checks not performed", () => {
const incomplete = buildProjectIntakeReport({
project: { id: "project-2" },
truncated: false,
unavailableEvidence: ["frame_rate"],
items: [{ id: "clip-2", name: "Untitled", type: "clip" }],
}, template);
expect(incomplete.status).toBe("incomplete");
expect(incomplete.findings).toEqual(expect.arrayContaining([
expect.objectContaining({ code: "REQUIRED_EVIDENCE_UNAVAILABLE", certainty: "unavailable" }),
expect.objectContaining({ code: "EXTENSION_UNAVAILABLE", certainty: "unavailable" }),
expect.objectContaining({ code: "FRAME_RATE_UNAVAILABLE", certainty: "unavailable" }),
expect.objectContaining({ code: "PROXY_STATE_UNAVAILABLE", certainty: "unavailable" }),
expect.objectContaining({ code: "MEDIA_PATH_UNAVAILABLE", certainty: "unavailable" }),
]));
const explicitlyRequired = buildProjectIntakeReport({
project: { id: "project-3" },
truncated: false,
unavailableEvidence: [],
items: [{ id: "clip-3", name: "Untitled", type: "clip" }],
}, {
...template,
allowedExtensions: [],
allowedFrameRates: [],
proxyPolicy: "ignore",
approvedPathPrefixes: [],
requiredEvidence: ["extension", "frame_rate", "offline", "proxy", "path"],
});
expect(explicitlyRequired.status).toBe("incomplete");
expect(explicitlyRequired.findings.map((finding) => finding.code)).toEqual(expect.arrayContaining([
"EXTENSION_UNAVAILABLE", "FRAME_RATE_UNAVAILABLE", "OFFLINE_STATE_UNAVAILABLE", "PROXY_STATE_UNAVAILABLE", "MEDIA_PATH_UNAVAILABLE",
]));
const notChecked = buildProjectIntakeReport(snapshot, { ...template, proxyPolicy: "report_missing" });
const reportWithoutProxyState = buildProjectIntakeReport({
...snapshot,
items: snapshot.items.map((item) => item.id === "clip-1" ? { ...item, hasProxy: undefined } : item),
}, { ...template, proxyPolicy: "report_missing" });
expect(notChecked.findings).toEqual([]);
expect(reportWithoutProxyState.findings).toEqual(expect.arrayContaining([
expect.objectContaining({ code: "PROXY_STATE_NOT_CHECKED", certainty: "not_checked", severity: "info" }),
]));
});
it("flags missing bins, ambiguous organization rules, and truncated captures without proposing a mutation", () => {
const report = buildProjectIntakeReport({
...snapshot,
truncated: true,
items: snapshot.items.filter((item) => item.id !== "bin-incoming"),
}, {
...template,
organizationRules: [
template.organizationRules[0],
{ id: "also-interviews", destinationBinName: "Also Interviews", match: { filenamePrefixes: ["INT_"] } },
],
});
expect(report.status).toBe("incomplete");
expect(report.findings).toEqual(expect.arrayContaining([
expect.objectContaining({ code: "CAPTURE_TRUNCATED", certainty: "unavailable" }),
expect.objectContaining({ code: "REQUIRED_BIN_MISSING", certainty: "observed" }),
expect.objectContaining({ code: "AMBIGUOUS_ORGANIZATION_RULES", itemId: "clip-1", certainty: "observed" }),
]));
expect(report.organizationPlan.proposedMoves).toEqual([]);
expect(report.applied).toBe(false);
});
it("distinguishes unavailable, incorrect, and suffix-matched required-bin parents", () => {
const baseItems = snapshot.items.filter((item) => item.id !== "bin-interviews");
const unavailable = buildProjectIntakeReport({
...snapshot,
items: [...baseItems, { id: "bin-interviews", name: "Interviews", type: "bin" }],
}, template);
expect(unavailable.status).toBe("incomplete");
expect(unavailable.findings).toContainEqual(expect.objectContaining({
code: "REQUIRED_BIN_PARENT_UNAVAILABLE",
certainty: "unavailable",
}));
const wrong = buildProjectIntakeReport({
...snapshot,
items: [...baseItems, {
id: "bin-interviews", name: "Interviews", type: "bin", treePath: "Other/Interviews",
}],
}, template);
expect(wrong.status).toBe("needs_attention");
expect(wrong.findings).toContainEqual(expect.objectContaining({
code: "REQUIRED_BIN_WRONG_PARENT",
certainty: "observed",
}));
const suffixMatched = buildProjectIntakeReport({
...snapshot,
items: [...baseItems, {
id: "bin-interviews", name: "Interviews", type: "bin", treePath: "Show/Editorial/Interviews",
}],
}, template);
expect(suffixMatched.findings.some((finding) => finding.code.startsWith("REQUIRED_BIN_"))).toBe(false);
});
it("bounds snapshots and rejects malformed structural evidence before evaluation", () => {
expect(() => validateProjectIntakeSnapshot({
project: { id: "project" }, items: [{ id: "one", name: "One", type: "clip" }, { id: "one", name: "Two", type: "clip" }],
truncated: false, unavailableEvidence: [],
})).toThrow("duplicate ids");
expect(() => validateProjectIntakeSnapshot({
project: { id: "project" }, items: [], truncated: "no", unavailableEvidence: [],
})).toThrow("must be a boolean");
expect(() => validateProjectIntakeSnapshot({
project: { id: "project" }, items: [{ id: "one", name: "One", type: "asset" }],
truncated: false, unavailableEvidence: [],
})).toThrow("type must be clip, bin, sequence, or other");
});
it("rejects duplicate and out-of-contract facility rules", () => {
expect(() => validateFacilityIntakeTemplate({
...template,
approvedPathPrefixes: [{ id: "storage", prefix: "D:/One" }, { id: "storage", prefix: "D:/Two" }],
})).toThrow("approvedPathPrefixes contains duplicate ids");
expect(() => validateFacilityIntakeTemplate({
...template,
requiredEvidence: ["proxy", "proxy"],
})).toThrow("requiredEvidence contains duplicate entries");
expect(() => validateFacilityIntakeTemplate({
...template,
requiredEvidence: ["duration"],
})).toThrow("must be extension, frame_rate, offline, proxy, or path");
expect(() => validateFacilityIntakeTemplate({
...template,
organizationRules: [template.organizationRules[0], { ...template.organizationRules[0] }],
})).toThrow("organizationRules contains duplicate ids");
expect(() => validateFacilityIntakeTemplate({
...template,
organizationRules: [{ ...template.organizationRules[0], colorIndex: 15 }],
})).toThrow("colorIndex must be an integer from 0 through 14");
expect(() => validateFacilityIntakeTemplate({ ...template, proxyPolicy: "generate" })).toThrow(
"proxyPolicy must be ignore, report_missing, or require",
);
});
});
+23
View File
@@ -0,0 +1,23 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
describe("quick-start localization foundation", () => {
it("keeps translated quick starts structurally aligned with the English source", () => {
const output = execFileSync(process.execPath, ["scripts/check-quickstart-locales.mjs"], {
cwd: root,
encoding: "utf8",
});
expect(output).toContain("2 translations");
});
it("links the README proof path without calling an illustration a live screenshot", () => {
const readme = readFileSync(join(root, "README.md"), "utf8");
expect(readme).toContain("premiere-pro-mcp-demo-poster.png");
expect(readme).toContain("not a Premiere panel screenshot or licensed-host proof");
expect(readme).toContain("docs/quickstart/README.md");
});
});
+151
View File
@@ -0,0 +1,151 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
const readJson = (path: string) => JSON.parse(read(path));
describe("canonical release metadata", () => {
const release = readJson("release-metadata.json");
it("aligns every distributable manifest with the canonical version", () => {
expect(readJson("package.json").version).toBe(release.version);
expect(readJson("package.json").description).toContain(
`${release.coreTools} core AI video editing tools`,
);
const packageLock = readJson("package-lock.json");
expect(packageLock.version).toBe(release.version);
expect(packageLock.packages[""].version).toBe(release.version);
expect(readJson("claude-desktop/manifest.json").version).toBe(release.version);
expect(readJson("uxp-plugin/manifest.json").version).toBe(release.version);
expect(readJson("plugins/premiere-pro/.codex-plugin/plugin.json").version).toBe(
release.version,
);
expect(
readJson("claude-plugins/premiere-pro/.claude-plugin/plugin.json").version,
).toBe(release.version);
expect(readJson(".claude-plugin/marketplace.json").plugins[0].version).toBe(
release.version,
);
for (const path of [
"plugins/premiere-pro/.mcp.json",
"claude-plugins/premiere-pro/.mcp.json",
]) {
expect(read(path)).toContain(`premiere-pro-mcp@${release.version}`);
}
const cepManifest = read("cep-plugin/CSXS/manifest.xml");
expect(cepManifest).toContain(`ExtensionBundleVersion="${release.version}"`);
expect(cepManifest).toContain(`Version="${release.version}"`);
expect(read("cep-plugin/updater.cjs")).toContain(
`CURRENT_VERSION = "${release.version}"`,
);
expect(read("cep-plugin/index.html")).toContain(`Version ${release.version}`);
});
it("aligns public release and capability claims", () => {
const readme = read("README.md");
const llms = read("landing/public/llms.txt");
const llmsFull = read("landing/public/llms-full.txt");
const llmAlias = read("landing/public/llm.txt");
const landingProduct = read("landing/lib/product.ts");
const landingArticles = read("landing/lib/articles.ts");
const marketingAssets = read("docs/marketing-assets.md");
const supportedActions = read("docs/supported-actions.md");
expect(readme).toContain(`${release.coreTools} core tools`);
expect(readme).toContain(
`${release.defaultProfileTools} under the default profile`,
);
expect(readme).toContain(
`${release.defaultProfileWithUxpTools} with a connected UXP bridge`,
);
expect(readme).toContain(`${release.uxpAdditionalTools} capability-gated tools`);
expect(llms).toContain(`Current project release: ${release.version}`);
expect(llms).toContain(
`${release.coreTools} core structured MCP tools`,
);
expect(llmsFull).toContain(`Current release: ${release.version}`);
expect(llmsFull).toContain(
`${release.uxpAdditionalTools} capability-gated tools`,
);
expect(llmsFull).toContain(
`${release.defaultProfileWithUxpTools} connected tools`,
);
expect(llmAlias).toContain("https://premiere-pro-mcp.com/llms.txt");
expect(marketingAssets).toContain(
`${release.uxpAdditionalTools} additional capability-gated tools`,
);
expect(landingProduct).toContain(`version: "${release.version}"`);
expect(landingProduct).toContain(`coreToolCount: ${release.coreTools}`);
expect(landingProduct).toContain(`defaultProfileToolCount: ${release.defaultProfileTools}`);
expect(landingProduct).toContain(`connectedUxpToolCount: ${release.defaultProfileWithUxpTools}`);
expect(landingArticles).toContain(
`The server currently registers ${release.coreTools} core structured tools`,
);
expect(landingArticles).toContain(
`${release.uxpAdditionalTools} capability-gated tools, bringing the connected surface to ${release.defaultProfileWithUxpTools}`,
);
expect(landingProduct).toContain(
`/releases/download/v${release.version}/premiere-pro-mcp-${release.version}.mcpb`,
);
expect(landingProduct).toContain(
`/releases/download/v${release.version}/MCPBridgeCEP.zxp`,
);
expect(landingProduct).toContain(`/releases/tag/v${release.version}`);
expect(readme).toContain(`Latest release: ${release.version}`);
expect(readme).toContain(`registers ${release.coreTools} tools, filtered by authority profile`);
expect(read("landing/app/changelog/page.tsx")).toContain(
`version: "${release.version}"`,
);
expect(supportedActions).toContain(
`| Registered core actions | ${release.coreTools} |`,
);
expect(supportedActions).toContain(
`| Default-profile core actions | ${release.defaultProfileTools} |`,
);
expect(supportedActions).toContain(
`| Authenticated UXP additions | ${release.uxpAdditionalTools} |`,
);
expect(supportedActions).toContain(
`| Default profile with UXP | ${release.defaultProfileWithUxpTools} |`,
);
});
it("keeps computed tool-count relationships explicit", () => {
expect(release.defaultProfileTools + release.uxpAdditionalTools).toBe(
release.defaultProfileWithUxpTools,
);
expect(release.coreTools - release.defaultProfileTools).toBe(2);
});
it("keeps the CLI help count aligned with the default profile", () => {
expect(read("src/index.ts")).toContain(
`(${release.defaultProfileTools} default-profile tools)`,
);
});
it("keeps the generated public product manifest aligned with current release metadata", () => {
const manifest = readJson("public-product-manifest.json");
expect(manifest.schemaVersion).toBe("premiere-pro-mcp.public-product.v1");
expect(manifest.product.version).toBe(release.version);
expect(manifest.product.mcpName).toBe(readJson("package.json").mcpName);
expect(manifest.capabilitySurface).toMatchObject({
registeredCoreTools: release.coreTools,
defaultProfileTools: release.defaultProfileTools,
authenticatedUxpAdditions: release.uxpAdditionalTools,
defaultProfileWithUxp: release.defaultProfileWithUxpTools,
guidedWorkflows: release.guidedWorkflows,
});
expect(manifest.proofKit.status).toBe("runbook_and_redacted_template_only");
expect(manifest.proofKit.video).toBeNull();
expect(manifest.workflows.map((workflow: { id: string }) => workflow.id)).toEqual([
"safe-project-intake",
"transcript-backed-rough-cut",
"caption-review",
"verified-delivery",
]);
});
});
+79
View File
@@ -0,0 +1,79 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
describe("npm release package verification", () => {
it("checks the actual tarball and an isolated CLI installation before publication", () => {
const packageJson = JSON.parse(read("package.json"));
const verifier = read("scripts/verify-npm-package.mjs");
expect(packageJson.scripts["pack:check"]).toBe("node scripts/verify-npm-package.mjs");
expect(verifier).toContain("package/docs/premiere-surface-registry.md");
expect(verifier).toContain("package/docs/adobe-beta-aaf-export-options-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-project-options-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-transition-options-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-rectf-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-color-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-pointf-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-guid-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-frame-rate-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-tick-time-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-c2pa-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-media-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-media-manager-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-transcript-drift.md");
expect(verifier).toContain("package/docs/adobe-beta-work-area-drift.md");
expect(verifier).toContain("package/docs/uxp-hybrid-benchmark.md");
expect(verifier).toContain("package/docs/uxp-hybrid-addon-receipt.md");
expect(verifier).toContain("package/docs/uxp-hybrid-ccx-receipt.md");
expect(verifier).toContain("package/dist/resources/premiere-surface-registry.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-aaf-export-options-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-project-options-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-transition-options-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-rectf-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-color-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-pointf-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-guid-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-frame-rate-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-tick-time-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-c2pa-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-media-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-media-manager-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-transcript-drift.json");
expect(verifier).toContain("package/dist/resources/adobe-beta-work-area-drift.json");
expect(verifier).toContain("installedRegistry.integrationSurfaces");
expect(verifier).toContain("registry references a missing inventory artifact");
expect(verifier).toContain('"package/dist/index.js"');
expect(verifier).toContain('"package/cep-plugin/CSXS/manifest.xml"');
expect(verifier).toContain('"package/uxp-plugin/manifest.json"');
expect(verifier).toContain('"--ignore-scripts"');
expect(verifier).toContain('"--pack-destination"');
expect(verifier).toContain("Object.values(packed)");
expect(verifier).toContain('installedCli, "--help"');
expect(verifier).toContain("process.env.npm_execpath");
expect(verifier).toContain('"lib", "node_modules", "npm"');
});
it("requires provenance and package verification in every npm publish path", () => {
const publish = read(".github/workflows/npm-publish.yml");
const crossPlatform = read(".github/workflows/cross-platform.yml");
expect(publish).toContain("npm publish --provenance --access public");
expect(publish).toContain("npm run pack:check");
expect(publish).toContain("npm run premiere:docs-inventory:check");
expect(publish.indexOf("npm run premiere:docs-inventory:check"))
.toBeLessThan(publish.indexOf("npm run build"));
expect(crossPlatform).toContain("npm run pack:check");
});
it("validates the Claude bundle against its declared production dependencies", () => {
const validator = read("scripts/validate-distribution.mjs");
expect(validator).toContain("Object.keys(packageJson.dependencies ?? {})");
expect(validator).toContain("node_modules\", dependency, \"package.json");
expect(validator).not.toContain('"@modelcontextprotocol", "sdk"');
});
});
+46
View File
@@ -0,0 +1,46 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const root = process.cwd();
const read = (path: string) => readFileSync(join(root, path), "utf8");
const release = JSON.parse(read("release-metadata.json"));
function verify(tag?: string) {
return spawnSync(process.execPath, ["scripts/verify-release-tag.mjs"], {
cwd: root,
encoding: "utf8",
env: { ...process.env, RELEASE_TAG: tag ?? "" },
});
}
describe("release tag verification", () => {
it("accepts only the exact canonical tag and aligned distributable manifests", () => {
expect(verify(`v${release.version}`)).toMatchObject({
status: 0,
stdout: expect.stringContaining(`v${release.version}`),
});
expect(verify(`v${release.version}.1`)).toMatchObject({
status: 1,
stderr: expect.stringContaining("Release tag must be exactly"),
});
expect(verify()).toMatchObject({
status: 1,
stderr: expect.stringContaining("Release tag must be exactly"),
});
});
it("gates every release attachment workflow on the matching release tag", () => {
for (const workflow of [
".github/workflows/cep-release.yml",
".github/workflows/uxp-package.yml",
".github/workflows/claude-desktop-bundle.yml",
]) {
const source = read(workflow);
expect(source).toContain("npm run check");
expect(source).toContain("RELEASE_TAG: ${{ github.event.release.tag_name }}");
expect(source).toContain("node scripts/verify-release-tag.mjs");
}
});
});
+174
View File
@@ -0,0 +1,174 @@
import { describe, it, expect, vi } from "vitest";
import { z } from "zod";
// We need to test jsonSchemaToInputSchema which is not exported directly.
// We'll test it indirectly by importing createServer with mocked tool modules
// that exercise various parameter types.
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(),
}));
// Create a mock module with all parameter types to exercise the schema adapter.
const ALL_TYPES_TOOL = {
test_all_types: {
description: "Tests all parameter types",
parameters: {
type: "object",
properties: {
str_param: { type: "string", description: "A string" },
num_param: { type: "number", description: "A number" },
bool_param: { type: "boolean", description: "A boolean" },
arr_param: {
type: "array",
items: { type: "string" },
description: "An array",
},
obj_param: { type: "object", description: "An object" },
enum_param: {
type: "string",
enum: ["a", "b", "c"],
description: "An enum",
},
optional_str: { type: "string", description: "Optional string" },
},
required: ["str_param", "num_param"],
},
handler: vi.fn().mockResolvedValue({ success: true, data: {} }),
},
test_empty_params: {
description: "No parameters",
parameters: {},
handler: vi.fn().mockResolvedValue({ success: true, data: {} }),
},
test_no_required: {
description: "All optional",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Optional name" },
},
},
handler: vi.fn().mockResolvedValue({ success: true, data: {} }),
},
};
vi.mock("../src/tools/discovery.js", () => ({
getDiscoveryTools: () => ALL_TYPES_TOOL,
}));
vi.mock("../src/tools/project.js", () => ({ getProjectTools: () => ({}) }));
vi.mock("../src/tools/media.js", () => ({ getMediaTools: () => ({}) }));
vi.mock("../src/tools/sequence.js", () => ({ getSequenceTools: () => ({}) }));
vi.mock("../src/tools/timeline.js", () => ({ getTimelineTools: () => ({}) }));
vi.mock("../src/tools/effects.js", () => ({ getEffectsTools: () => ({}) }));
vi.mock("../src/tools/transitions.js", () => ({
getTransitionsTools: () => ({}),
}));
vi.mock("../src/tools/audio.js", () => ({ getAudioTools: () => ({}) }));
vi.mock("../src/tools/text.js", () => ({ getTextTools: () => ({}) }));
vi.mock("../src/tools/markers.js", () => ({ getMarkerTools: () => ({}) }));
vi.mock("../src/tools/tracks.js", () => ({ getTrackTools: () => ({}) }));
vi.mock("../src/tools/playhead.js", () => ({ getPlayheadTools: () => ({}) }));
vi.mock("../src/tools/metadata.js", () => ({ getMetadataTools: () => ({}) }));
vi.mock("../src/tools/export.js", () => ({ getExportTools: () => ({}) }));
vi.mock("../src/tools/media-analysis.js", () => ({ getMediaAnalysisTools: () => ({}) }));
vi.mock("../src/tools/interchange-analysis.js", () => ({ getInterchangeAnalysisTools: () => ({}) }));
vi.mock("../src/tools/advanced.js", () => ({ getAdvancedTools: () => ({}) }));
vi.mock("../src/tools/keyframes.js", () => ({ getKeyframeTools: () => ({}) }));
vi.mock("../src/tools/scripting.js", () => ({ getScriptingTools: () => ({}) }));
vi.mock("../src/tools/inspection.js", () => ({
getInspectionTools: () => ({}),
}));
vi.mock("../src/tools/selection.js", () => ({ getSelectionTools: () => ({}) }));
vi.mock("../src/tools/clipboard.js", () => ({ getClipboardTools: () => ({}) }));
vi.mock("../src/tools/source-monitor.js", () => ({
getSourceMonitorTools: () => ({}),
}));
vi.mock("../src/tools/track-targeting.js", () => ({
getTrackTargetingTools: () => ({}),
}));
vi.mock("../src/tools/utility.js", () => ({ getUtilityTools: () => ({}) }));
vi.mock("../src/tools/health.js", () => ({ getHealthTools: () => ({}) }));
vi.mock("../src/tools/workspace.js", () => ({ getWorkspaceTools: () => ({}) }));
vi.mock("../src/tools/captions.js", () => ({ getCaptionTools: () => ({}) }));
vi.mock("../src/tools/playback.js", () => ({ getPlaybackTools: () => ({}) }));
vi.mock("../src/tools/project-manager.js", () => ({
getProjectManagerTools: () => ({}),
}));
vi.mock("../src/resources/extendscript-reference.js", () => ({
EXTENDSCRIPT_REFERENCE: "mock",
}));
import { createServer } from "../src/server.js";
describe("Schema registration (jsonSchemaToInputSchema)", () => {
it("registers tools with all parameter types without error", () => {
// If the schema conversion fails, createServer will throw
expect(() => createServer({})).not.toThrow();
});
it("handles string parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles number parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles boolean parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles array parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles object parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles enum parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles empty parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
it("handles all-optional parameters", () => {
const server = createServer({});
expect(server).toBeDefined();
});
});
describe("Server tool registration callback", () => {
it("returns text content on successful tool call", async () => {
// The server wraps tool handlers in a callback that formats results.
// We test this by verifying the server creates successfully.
// (The actual MCP protocol callback is internal to the SDK,
// but we validated handler return values in tool-modules.test.ts)
const server = createServer({});
expect(server).toBeDefined();
});
it("registers premiere-instructions resource", () => {
// Resource registration doesn't throw if params are valid
const server = createServer({});
expect(server).toBeDefined();
});
it("registers extendscript-reference resource", () => {
const server = createServer({});
expect(server).toBeDefined();
});
});
+599
View File
@@ -0,0 +1,599 @@
import { describe, expect, it, vi } from "vitest";
import { capabilitiesForToolInvocation, capabilityForTool, guardToolHandler, isToolPermitted, resolveCapabilities } from "../src/security/capabilities.js";
import { buildPlatformCapabilityReport } from "../src/platform-capabilities.js";
import {
buildToolCapabilityReport,
deriveToolOperationalCapability,
} from "../src/tool-capability-report.js";
describe("capability profiles", () => {
it("fails closed for unsafe scripting by default", async () => {
const handler = vi.fn(async () => "ok");
const guarded = guardToolHandler("execute_extendscript", handler, resolveCapabilities(undefined), () => "op-1");
await expect(guarded({})).rejects.toMatchObject({ code: "CAPABILITY_DENIED", operationId: "op-1" });
expect(handler).not.toHaveBeenCalled();
});
it("permits unsafe scripting only when explicitly enabled", async () => {
const handler = vi.fn(async () => "ok");
const guarded = guardToolHandler("send_raw_script", handler, resolveCapabilities("inspect,unsafe-script"));
await expect(guarded({})).resolves.toBe("ok");
});
it("rejects unknown capabilities instead of silently widening access", () => {
expect(() => resolveCapabilities("inspect,admin")).toThrow("Unknown Premiere MCP capability");
});
it("classifies sensitive tools", () => {
expect(capabilityForTool("execute_extendscript")).toBe("unsafe-script");
expect(capabilityForTool("evaluate_expression")).toBe("unsafe-script");
expect(capabilityForTool("export_sequence")).toBe("export");
expect(capabilityForTool("export_sequence_review_frames")).toBe("export");
expect(capabilityForTool("export_sequence_marker_review_frames")).toBe("export");
expect(capabilityForTool("capture_frame")).toBe("export");
expect(capabilityForTool("validate_export_preset")).toBe("export");
expect(capabilityForTool("verify_delivery_file")).toBe("filesystem");
expect(capabilityForTool("verify_delivery_conformance")).toBe("filesystem");
expect(capabilityForTool("create_project_backup")).toBe("filesystem");
expect(capabilityForTool("analyze_loudness")).toBe("filesystem");
expect(capabilityForTool("detect_beats")).toBe("filesystem");
expect(capabilityForTool("detect_motion_peaks")).toBe("filesystem");
expect(capabilityForTool("read_video_scopes")).toBe("filesystem");
expect(capabilityForTool("plan_shot_match")).toBe("filesystem");
expect(capabilityForTool("analyze_video_qc")).toBe("filesystem");
expect(capabilityForTool("detect_source_scene_changes")).toBe("filesystem");
expect(capabilityForTool("normalize_loudness_file")).toBe("filesystem");
expect(capabilityForTool("import_media")).toBe("filesystem");
expect(capabilityForTool("manage_proxy_ingest_uxp")).toBe("edit");
expect(capabilityForTool("audition_source_monitor_uxp")).toBe("edit");
expect(capabilityForTool("relink_offline_media_uxp")).toBe("filesystem");
expect(capabilityForTool("import_project_media_uxp")).toBe("filesystem");
expect(capabilityForTool("edit_timeline_uxp")).toBe("edit");
expect(capabilityForTool("encode_media_uxp")).toBe("export");
expect(capabilityForTool("inspect_project_selection_uxp")).toBe("inspect");
expect(capabilityForTool("manage_markers_uxp")).toBe("edit");
expect(capabilityForTool("list_markers_uxp")).toBe("inspect");
expect(capabilityForTool("get_project_info")).toBe("inspect");
expect(capabilityForTool("preview_transcript_edit_uxp")).toBe("inspect");
expect(capabilityForTool("plan_transcript_rough_cut_uxp")).toBe("inspect");
expect(capabilityForTool("create_context_edit_plan")).toBe("inspect");
expect(capabilityForTool("search_project_context")).toBe("inspect");
expect(capabilityForTool("ping")).toBe("inspect");
expect(capabilityForTool("trim_clip")).toBe("edit");
});
it("enforces inspect and edit profiles instead of only guarding unsafe tools", async () => {
const handler = vi.fn(async () => "ok");
await expect(
guardToolHandler("get_project_info", handler, resolveCapabilities("edit"), () => "inspect-1")({}),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "inspect" });
await expect(
guardToolHandler("trim_clip", handler, resolveCapabilities("inspect"), () => "edit-1")({}),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit" });
expect(handler).not.toHaveBeenCalled();
});
it("enforces every capability required by consolidated UXP actions", async () => {
const handler = vi.fn(async () => "ok");
expect(capabilitiesForToolInvocation("manage_proxy_ingest_uxp", { action: "attach_proxy" }))
.toEqual(["edit", "filesystem"]);
expect(capabilitiesForToolInvocation("manage_proxy_ingest_uxp", { action: "inspect_proxy" }))
.toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_project_context", { action: "capture" }))
.toEqual(["inspect", "filesystem"]);
expect(capabilitiesForToolInvocation("manage_project_context", { action: "status" }))
.toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_project_context", { action: "clear" }))
.toEqual(["filesystem"]);
await expect(
guardToolHandler("manage_proxy_ingest_uxp", handler, resolveCapabilities("filesystem"), () => "proxy-edit")({ action: "set_ingest" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "proxy-edit" });
await expect(
guardToolHandler("manage_proxy_ingest_uxp", handler, resolveCapabilities("edit"), () => "proxy-file")({ action: "attach_proxy" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "filesystem", operationId: "proxy-file" });
await expect(
guardToolHandler("audition_source_monitor_uxp", handler, resolveCapabilities("filesystem"), () => "monitor-edit")({ action: "play" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit" });
await expect(
guardToolHandler("edit_timeline_uxp", handler, resolveCapabilities("filesystem"), () => "timeline-edit")({ action: "insert" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit" });
expect(handler).not.toHaveBeenCalled();
});
it("requires all authority dimensions for guarded MOGRT authoring", async () => {
const handler = vi.fn(async () => "ok");
expect(capabilitiesForToolInvocation("verify_after_effects_connection", {})).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("preview_mogrt_recipe", {})).toEqual(["inspect", "filesystem"]);
expect(capabilitiesForToolInvocation("create_mogrt_recipe", {})).toEqual(["edit", "export", "filesystem"]);
expect(capabilitiesForToolInvocation("verify_mogrt_artifact", {})).toEqual(["inspect", "filesystem"]);
expect(capabilitiesForToolInvocation("preview_mogrt_batch", {})).toEqual(["inspect", "filesystem"]);
expect(capabilitiesForToolInvocation("create_mogrt_batch", {})).toEqual(["edit", "export", "filesystem"]);
expect(capabilitiesForToolInvocation("publish_mogrt_to_library", {})).toEqual(["filesystem"]);
expect(capabilitiesForToolInvocation("enqueue_after_effects_render", {})).toEqual(["edit", "export", "filesystem"]);
expect(capabilitiesForToolInvocation("apply_mogrt_premiere_handoff", {})).toEqual(["edit", "filesystem"]);
expect(isToolPermitted("create_mogrt_batch", resolveCapabilities("inspect,edit,filesystem"))).toBe(false);
expect(isToolPermitted("enqueue_after_effects_render", resolveCapabilities("edit,export,filesystem"))).toBe(true);
await expect(
guardToolHandler("create_mogrt_recipe", handler, resolveCapabilities("edit,export"), () => "mogrt-filesystem")({}),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "filesystem", operationId: "mogrt-filesystem" });
expect(handler).not.toHaveBeenCalled();
});
it("lists sequence-range inspection for inspect authority but requires edit to update", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(isToolPermitted("manage_sequence_range_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("manage_sequence_range_uxp", { action: "inspect" })).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_sequence_range_uxp", { action: "update" })).toEqual(["edit"]);
await expect(
guardToolHandler("manage_sequence_range_uxp", handler, inspectOnly, () => "range-inspect")({ action: "inspect" }),
).resolves.toBe("ok");
await expect(
guardToolHandler("manage_sequence_range_uxp", handler, inspectOnly, () => "range-update")({ action: "update" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "range-update" });
expect(handler).toHaveBeenCalledTimes(1);
});
it("lists sequence-playhead inspection for inspect authority but requires edit to set", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(isToolPermitted("manage_sequence_playhead_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("manage_sequence_playhead_uxp", { action: "inspect" })).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_sequence_playhead_uxp", { action: "set" })).toEqual(["edit"]);
await expect(
guardToolHandler("manage_sequence_playhead_uxp", handler, inspectOnly, () => "playhead-inspect")({ action: "inspect" }),
).resolves.toBe("ok");
await expect(
guardToolHandler("manage_sequence_playhead_uxp", handler, inspectOnly, () => "playhead-set")({ action: "set" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "playhead-set" });
expect(handler).toHaveBeenCalledTimes(1);
});
it("lists app-preference inspection for inspect authority but requires edit to set", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(isToolPermitted("manage_app_preferences_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("manage_app_preferences_uxp", { action: "inspect" })).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_app_preferences_uxp", { action: "set" })).toEqual(["edit"]);
await expect(
guardToolHandler("manage_app_preferences_uxp", handler, inspectOnly, () => "preference-inspect")({ action: "inspect" }),
).resolves.toBe("ok");
await expect(
guardToolHandler("manage_app_preferences_uxp", handler, inspectOnly, () => "preference-set")({ action: "set" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "preference-set" });
expect(handler).toHaveBeenCalledTimes(1);
});
it("lists source-label inspection for inspect authority but requires edit to update", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(isToolPermitted("manage_timeline_source_label_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("manage_timeline_source_label_uxp", { action: "inspect" })).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("manage_timeline_source_label_uxp", { action: "update" })).toEqual(["edit"]);
await expect(
guardToolHandler("manage_timeline_source_label_uxp", handler, inspectOnly, () => "source-label-inspect")({ action: "inspect" }),
).resolves.toBe("ok");
await expect(
guardToolHandler("manage_timeline_source_label_uxp", handler, inspectOnly, () => "source-label-update")({ action: "update" }),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "source-label-update" });
expect(handler).toHaveBeenCalledTimes(1);
});
it("lists and invokes native sequence-timing inspection with inspect authority only", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(isToolPermitted("inspect_sequence_timing_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("inspect_sequence_timing_uxp", {})).toEqual(["inspect"]);
expect(isToolPermitted("inspect_frame_alignment_uxp", inspectOnly)).toBe(true);
expect(capabilitiesForToolInvocation("inspect_frame_alignment_uxp", { action: "align", frame_rate: 24, seconds: 1 })).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("inspect_project_insertion_bin_uxp", {})).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("inspect_installed_mogrt_directory_uxp", {})).toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("inspect_track_item_identity_uxp", {})).toEqual(["inspect"]);
await expect(
guardToolHandler("inspect_sequence_timing_uxp", handler, inspectOnly, () => "timing-inspect")({}),
).resolves.toBe("ok");
await expect(
guardToolHandler("inspect_frame_alignment_uxp", handler, inspectOnly, () => "frame-alignment-inspect")({ action: "frame", frame_rate: 24, frame_count: 1 }),
).resolves.toBe("ok");
expect(handler).toHaveBeenCalledTimes(2);
});
it("withholds and rejects non-undoable empty sequence creation without edit authority", async () => {
const handler = vi.fn(async () => "ok");
const inspectOnly = resolveCapabilities("inspect");
expect(capabilitiesForToolInvocation("create_empty_sequence_uxp", {})).toEqual(["edit"]);
expect(isToolPermitted("create_empty_sequence_uxp", inspectOnly)).toBe(false);
await expect(
guardToolHandler("create_empty_sequence_uxp", handler, inspectOnly, () => "empty-sequence-create")({}),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "edit", operationId: "empty-sequence-create" });
expect(handler).not.toHaveBeenCalled();
});
it.each([
["manage_clip_effects_uxp", "catalog"],
["batch_selected_clips_uxp", "inspect"],
["manage_timeline_selection_uxp", "inspect"],
["manage_timeline_selection_uxp", "inspect_targets"],
["manage_proxy_ingest_uxp", "inspect_proxy"],
["manage_metadata_uxp", "get"],
["inspect_project_panel_metadata_uxp", "panel"],
["inspect_project_panel_metadata_uxp", "item_columns"],
["manage_project_panel_metadata_uxp", "inspect"],
["create_project_metadata_field_uxp", "inspect"],
["manage_color_conformance_uxp", "preflight"],
["audition_source_monitor_uxp", "state"],
["preflight_production_storage_uxp", "preflight"],
["inspect_project_selection_uxp", "views"],
["manage_markers_uxp", "inspect"],
["organize_project_items_uxp", "inspect_bin"],
["manage_sequence_settings_uxp", "get"],
["manage_sequence_display_format_uxp", "inspect"],
["manage_sequence_range_uxp", "inspect"],
["manage_sequence_playhead_uxp", "inspect"],
["manage_app_preferences_uxp", "inspect"],
["manage_timeline_source_label_uxp", "inspect"],
["inspect_project_insertion_bin_uxp", undefined],
["inspect_installed_mogrt_directory_uxp", undefined],
["inspect_sequence_timing_uxp", undefined],
["inspect_frame_alignment_uxp", undefined],
["calculate_tick_time_uxp", undefined],
["inspect_sequence_timing_by_guid_uxp", undefined],
["inspect_track_item_identity_uxp", undefined],
["automate_effect_parameters_uxp", "inspect"],
["automate_effect_parameters_uxp", "inspect_point_value"],
["automate_effect_parameters_uxp", "inspect_point_displacement"],
["automate_effect_parameters_uxp", "inspect_color_value"],
["automate_effect_parameters_uxp", "inspect_keyframe", { keyframe_direction: "nearest" }],
["automate_effect_parameters_uxp", "inspect_time_varying"],
["transform_track_item_uxp", "inspect"],
["manage_sequences_uxp", "inspect"],
["encode_media_uxp", "preflight"],
])("keeps %s:%s available to inspect-only profiles", (toolName, action, args = {}) => {
expect(capabilitiesForToolInvocation(toolName, { action, ...args })).toEqual(["inspect"]);
expect(isToolPermitted(toolName, resolveCapabilities("inspect"))).toBe(true);
});
it.each([
["manage_clip_effects_uxp", "add", ["edit"]],
["batch_selected_clips_uxp", "remove_effect", ["edit"]],
["manage_timeline_selection_uxp", "replace", ["edit"]],
["manage_timeline_selection_uxp", "clear", ["edit"]],
["manage_metadata_uxp", "update", ["edit"]],
["manage_project_panel_metadata_uxp", "update", ["edit"]],
["create_project_metadata_field_uxp", "create", ["edit"]],
["manage_color_conformance_uxp", "update", ["edit"]],
["preflight_production_storage_uxp", "configure_project", ["edit"]],
["manage_markers_uxp", "remove", ["edit"]],
["manage_markers_uxp", "remove_many", ["edit"]],
["organize_project_items_uxp", "move", ["edit"]],
["manage_sequence_settings_uxp", "update", ["edit"]],
["manage_sequence_display_format_uxp", "update", ["edit"]],
["manage_sequence_range_uxp", "update", ["edit"]],
["manage_sequence_playhead_uxp", "set", ["edit"]],
["manage_app_preferences_uxp", "set", ["edit"]],
["manage_timeline_source_label_uxp", "update", ["edit"]],
["import_project_media_uxp", "files", ["edit", "filesystem"]],
["automate_effect_parameters_uxp", "set_point_value", ["edit"]],
["automate_effect_parameters_uxp", "set_color_value", ["edit"]],
["automate_effect_parameters_uxp", "add_keyframe", ["edit"]],
["automate_effect_parameters_uxp", "set_time_varying", ["edit"]],
["transform_track_item_uxp", "update", ["edit"]],
["manage_sequences_uxp", "delete", ["edit"]],
["encode_media_uxp", "sequence", ["export", "filesystem"]],
] as const)("requires exact mutation authority for %s:%s", (toolName, action, required) => {
expect(capabilitiesForToolInvocation(toolName, { action })).toEqual(required);
});
it("requires filesystem authority only for preset-based encoder preflight", async () => {
expect(capabilitiesForToolInvocation("encode_media_uxp", { action: "preflight" }))
.toEqual(["inspect"]);
expect(capabilitiesForToolInvocation("encode_media_uxp", { action: "preflight", preset_file: "D:/Approved/h264.epr" }))
.toEqual(["inspect", "filesystem"]);
const handler = vi.fn(async () => "ok");
await expect(
guardToolHandler("encode_media_uxp", handler, resolveCapabilities("inspect"), () => "preset-preflight")({
action: "preflight", preset_file: "D:/Approved/h264.epr",
}),
).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "filesystem", operationId: "preset-preflight" });
await expect(
guardToolHandler("encode_media_uxp", handler, resolveCapabilities("inspect"))({ action: "preflight" }),
).resolves.toBe("ok");
});
it.each([
["darwin", "macOS"],
["win32", "Windows"],
] as const)("reports static compatibility for %s", (platform, name) => {
const report = buildPlatformCapabilityReport(
resolveCapabilities("inspect,edit,export,filesystem"),
platform,
platform === "win32" ? "C:\\Temp\\premiere-mcp-bridge" : "/tmp/premiere-mcp-bridge",
);
expect(report.runtime).toMatchObject({ platform, platformName: name, supported: true });
expect(report.backends.cep.platforms).toEqual(["macOS", "Windows"]);
expect(report.backends.uxp.hostVerificationRequired).toBe(true);
expect(report.backends.uxp.commands).toContain("operation.cancel");
expect(report.backends.uxp.commands).toEqual(expect.arrayContaining([
"sequence.playhead.inspect",
"sequence.playhead.set",
]));
expect(report.backends.uxp.commands).toContain("markers.addBeatGrid");
expect(report.backends.uxp.commands).toContain("markers.removeMany");
expect(report.backends.uxp.events).toContain("premiere.state.changed");
expect(report.backends.uxp.operationSemantics.atomicRollback).toBe(false);
expect(report.authority.disabled).toContain("unsafe-script");
});
it("does not claim Premiere host support on Linux", () => {
const report = buildPlatformCapabilityReport(resolveCapabilities(undefined), "linux");
expect(report.runtime.supported).toBe(false);
expect(report.premiere.hostVerificationRequired).toBe(true);
});
it("derives tool operational metadata from catalog definitions and authority", () => {
const capabilities = resolveCapabilities("inspect");
const qeTool = deriveToolOperationalCapability(
"ripple_delete",
{ description: "Ripple delete a clip. Uses QE DOM." },
capabilities,
);
expect(qeTool).toMatchObject({
backend: "CEP/ExtendScript + QE",
backends: ["cep", "extendscript", "qe"],
status: "experimental",
minimumPremiereVersion: "2020 (QE availability varies by build)",
authority: { required: "edit", enabled: false },
verificationBoundary: "bridge_response",
hostVerificationRequired: true,
});
expect(qeTool.notes).toContain(
"Disabled by the current 'environment' authority profile.",
);
});
it("reports local discovery separately from live-host verification", () => {
const tool = deriveToolOperationalCapability(
"get_capabilities",
{ description: "Report static capabilities." },
resolveCapabilities(undefined),
);
expect(tool).toMatchObject({
backend: "local",
backends: ["local"],
status: "supported",
minimumPremiereVersion: null,
verificationBoundary: "static_metadata_only",
hostVerificationRequired: false,
});
});
it("classifies caption inventory with its UXP backend and baseline", () => {
const tool = deriveToolOperationalCapability(
"inspect_caption_tracks_uxp",
{ description: "Inventory native caption tracks on the active sequence." },
resolveCapabilities("inspect"),
);
expect(tool).toMatchObject({
backend: "UXP",
backends: ["uxp"],
minimumPremiereVersion: "25.6",
authority: { required: "inspect", enabled: true },
verificationBoundary: "host_response",
hostVerificationRequired: true,
});
expect(tool.notes).toEqual([
"Runs through the authenticated local UXP bridge using Premiere UXP APIs.",
]);
});
it("does not infer QE usage from a UXP tool description that rejects QE", () => {
const tool = deriveToolOperationalCapability(
"edit_timeline_uxp",
{ description: "Edit through documented UXP APIs without undocumented QE calls." },
resolveCapabilities("edit"),
);
expect(tool).toMatchObject({
backend: "UXP",
backends: ["uxp"],
status: "supported",
minimumPremiereVersion: "25.6",
});
expect(tool.notes).not.toEqual(expect.arrayContaining([
expect.stringContaining("undocumented QE DOM"),
]));
});
it("represents authenticated UXP-only tool metadata without a CEP fallback claim", () => {
const tool = deriveToolOperationalCapability(
"inspect_caption_tracks_uxp",
{
description: "Inspect native caption tracks.",
operationalCapability: {
backend: "UXP",
backends: ["uxp"],
minimumPremiereVersion: "25.6",
verificationBoundary: "structured_uxp_readback",
hostVerificationRequired: true,
},
},
resolveCapabilities("inspect"),
);
expect(tool).toMatchObject({
backend: "UXP",
backends: ["uxp"],
minimumPremiereVersion: "25.6",
verificationBoundary: "structured_uxp_readback",
hostVerificationRequired: true,
});
});
it("builds a deterministic summary from the registered catalog", () => {
const report = buildToolCapabilityReport(
{
trim_clip: { description: "Trim a clip." },
get_project_info: { description: "Read the project." },
add_transition: { description: "Add a transition. Uses QE DOM." },
},
resolveCapabilities("inspect,edit"),
);
expect(report.generatedFrom).toBe("registered-tool-catalog");
expect(report.total).toBe(3);
expect(report.byStatus).toEqual({
supported: 2,
limited: 0,
experimental: 1,
unsupported: 0,
});
expect(report.tools.map((tool) => tool.name)).toEqual([
"add_transition",
"get_project_info",
"trim_clip",
]);
});
it.each([
[
"verify_delivery_file",
"Verify a delivery file and calculate its checksum.",
{
backend: "local",
backends: ["local"],
authority: { required: "filesystem", enabled: true },
verificationBoundary: "local_filesystem",
hostVerificationRequired: false,
minimumPremiereVersion: null,
},
],
[
"get_advanced_feature_support",
"Report collaboration and AI feature support.",
{
backend: "local",
backends: ["local"],
authority: { required: "inspect", enabled: true },
verificationBoundary: "static_metadata_only",
hostVerificationRequired: false,
minimumPremiereVersion: null,
},
],
[
"preview_editorial_plan",
"Revalidate an editorial plan against saved project context.",
{
backend: "local",
backends: ["local"],
authority: { required: "inspect", enabled: true },
verificationBoundary: "plan_revalidation",
hostVerificationRequired: false,
minimumPremiereVersion: null,
},
],
[
"validate_export_preset",
"Validate an export preset and resolve its output extension.",
{
backend: "local + CEP/ExtendScript",
backends: ["local", "cep", "extendscript"],
authority: { required: "export", enabled: true },
verificationBoundary: "local_and_host_response",
hostVerificationRequired: true,
minimumPremiereVersion: "2020",
},
],
])("reports QA-forward boundaries for %s", (name, description, expected) => {
const tool = deriveToolOperationalCapability(
name,
{ description },
resolveCapabilities("inspect,export,filesystem"),
);
expect(tool).toMatchObject({ name, status: "supported", ...expected });
});
it("lets registration metadata override catalog defaults", () => {
const tool = deriveToolOperationalCapability(
"verify_delivery_file",
{
description: "Future host-backed delivery verification.",
operationalCapability: {
backend: "local + CEP/ExtendScript",
backends: ["local", "cep", "extendscript"],
minimumPremiereVersion: "26.0",
hostVerificationRequired: true,
verificationBoundary: "local_and_host_response",
notes: ["Explicit registration metadata."],
},
},
resolveCapabilities("filesystem"),
);
expect(tool).toMatchObject({
backend: "local + CEP/ExtendScript",
backends: ["local", "cep", "extendscript"],
minimumPremiereVersion: "26.0",
hostVerificationRequired: true,
verificationBoundary: "local_and_host_response",
notes: ["Explicit registration metadata."],
});
});
});
describe("isToolPermitted", () => {
it("withholds unsafe-script tools under the default profile", () => {
const config = resolveCapabilities(undefined);
expect(isToolPermitted("execute_extendscript", config)).toBe(false);
expect(isToolPermitted("evaluate_expression", config)).toBe(false);
expect(isToolPermitted("send_raw_script", config)).toBe(false);
});
it("permits unsafe-script tools once the authority is named", () => {
const config = resolveCapabilities("inspect,edit,export,filesystem,unsafe-script");
expect(isToolPermitted("execute_extendscript", config)).toBe(true);
expect(isToolPermitted("evaluate_expression", config)).toBe(true);
});
it("permits ordinary edit tools under the default profile", () => {
const config = resolveCapabilities(undefined);
expect(isToolPermitted("trim_clip", config)).toBe(true);
expect(isToolPermitted("get_project_info", config)).toBe(true);
expect(isToolPermitted("export_sequence", config)).toBe(true);
});
it("withholds edit and export tools under an inspect-only profile", () => {
const config = resolveCapabilities("inspect");
expect(isToolPermitted("trim_clip", config)).toBe(false);
expect(isToolPermitted("export_sequence", config)).toBe(false);
expect(isToolPermitted("import_media", config)).toBe(false);
expect(isToolPermitted("verify_delivery_conformance", config)).toBe(false);
expect(isToolPermitted("get_project_info", config)).toBe(true);
expect(isToolPermitted("manage_proxy_ingest_uxp", config)).toBe(true);
expect(isToolPermitted("audition_source_monitor_uxp", config)).toBe(true);
expect(isToolPermitted("edit_timeline_uxp", config)).toBe(false);
});
it("permits delivery conformance verification only with filesystem authority", () => {
expect(isToolPermitted("verify_delivery_conformance", resolveCapabilities("inspect"))).toBe(false);
expect(isToolPermitted("verify_delivery_conformance", resolveCapabilities("filesystem"))).toBe(true);
});
it("always advertises the diagnostic tools, even under a profile that excludes inspect", () => {
// Without this, a too-narrow profile leaves no supported way to ask the
// server which authority it actually has.
const config = resolveCapabilities("export");
expect(isToolPermitted("get_capabilities", config)).toBe(true);
expect(isToolPermitted("ping", config)).toBe(true);
expect(isToolPermitted("get_project_info", config)).toBe(false);
});
it("agrees with capabilityForTool for every capability in the profile", () => {
const config = resolveCapabilities("edit");
expect(capabilityForTool("trim_clip")).toBe("edit");
expect(isToolPermitted("trim_clip", config)).toBe(true);
});
});
+172
View File
@@ -0,0 +1,172 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createServer, SERVER_VERSION } from "../src/server.js";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { z } from "zod";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
const { getDiscoveryTools } = vi.hoisted(() => ({
getDiscoveryTools: vi.fn(() => ({
mock_discovery_tool: {
description: "A mock discovery tool",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Name param" },
},
required: ["name"],
},
handler: vi.fn().mockResolvedValue({ success: true, data: { found: true } }),
},
})),
}));
// Mock all tool modules to return simple tool definitions
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(),
}));
// We need the real tool modules to verify registration,
// but mock sendCommand so handlers don't actually do file I/O
vi.mock("../src/tools/discovery.js", () => ({
getDiscoveryTools,
}));
vi.mock("../src/tools/project.js", () => ({ getProjectTools: () => ({}) }));
vi.mock("../src/tools/media.js", () => ({ getMediaTools: () => ({}) }));
vi.mock("../src/tools/sequence.js", () => ({ getSequenceTools: () => ({}) }));
vi.mock("../src/tools/timeline.js", () => ({ getTimelineTools: () => ({}) }));
vi.mock("../src/tools/effects.js", () => ({ getEffectsTools: () => ({}) }));
vi.mock("../src/tools/transitions.js", () => ({ getTransitionsTools: () => ({}) }));
vi.mock("../src/tools/audio.js", () => ({ getAudioTools: () => ({}) }));
vi.mock("../src/tools/text.js", () => ({ getTextTools: () => ({}) }));
vi.mock("../src/tools/markers.js", () => ({ getMarkerTools: () => ({}) }));
vi.mock("../src/tools/tracks.js", () => ({ getTrackTools: () => ({}) }));
vi.mock("../src/tools/playhead.js", () => ({ getPlayheadTools: () => ({}) }));
vi.mock("../src/tools/metadata.js", () => ({ getMetadataTools: () => ({}) }));
vi.mock("../src/tools/export.js", () => ({ getExportTools: () => ({}) }));
vi.mock("../src/tools/media-analysis.js", () => ({ getMediaAnalysisTools: () => ({}) }));
vi.mock("../src/tools/interchange-analysis.js", () => ({ getInterchangeAnalysisTools: () => ({}) }));
vi.mock("../src/tools/advanced.js", () => ({ getAdvancedTools: () => ({}) }));
vi.mock("../src/tools/keyframes.js", () => ({ getKeyframeTools: () => ({}) }));
vi.mock("../src/tools/scripting.js", () => ({ getScriptingTools: () => ({}) }));
vi.mock("../src/tools/inspection.js", () => ({ getInspectionTools: () => ({}) }));
vi.mock("../src/tools/selection.js", () => ({ getSelectionTools: () => ({}) }));
vi.mock("../src/tools/clipboard.js", () => ({ getClipboardTools: () => ({}) }));
vi.mock("../src/tools/source-monitor.js", () => ({ getSourceMonitorTools: () => ({}) }));
vi.mock("../src/tools/track-targeting.js", () => ({ getTrackTargetingTools: () => ({}) }));
vi.mock("../src/tools/utility.js", () => ({ getUtilityTools: () => ({}) }));
vi.mock("../src/tools/health.js", () => ({ getHealthTools: () => ({}) }));
vi.mock("../src/tools/workspace.js", () => ({ getWorkspaceTools: () => ({}) }));
vi.mock("../src/tools/captions.js", () => ({ getCaptionTools: () => ({}) }));
vi.mock("../src/tools/playback.js", () => ({ getPlaybackTools: () => ({}) }));
vi.mock("../src/tools/project-manager.js", () => ({ getProjectManagerTools: () => ({}) }));
vi.mock("../src/resources/extendscript-reference.js", () => ({
EXTENDSCRIPT_REFERENCE: "mock reference",
}));
describe("createServer", () => {
it("returns an McpServer instance", () => {
const server = createServer({});
expect(server).toBeDefined();
expect(typeof server.registerTool).toBe("function");
expect(typeof server.registerResource).toBe("function");
expect(typeof server.connect).toBe("function");
});
it("registers tools from all modules", () => {
const toolSpy = vi.fn();
const originalTool = createServer({}).registerTool;
// Create a new server and spy on tool registration
const server = createServer({});
// The server should have been created successfully with registered tools
expect(server).toBeDefined();
});
it("does not throw with empty bridge options", () => {
expect(() => createServer({})).not.toThrow();
});
it("does not throw with custom bridge options", () => {
expect(() =>
createServer({ tempDir: "/custom/tmp", timeoutMs: 5000 })
).not.toThrow();
});
it("reuses immutable tool definitions for equivalent server configurations", () => {
getDiscoveryTools.mockClear();
const bridgeOptions = { tempDir: "/tmp/server-static-catalog-cache" };
createServer(bridgeOptions);
createServer(bridgeOptions);
expect(getDiscoveryTools).toHaveBeenCalledTimes(1);
});
it("registers and serves the bounded live context resources", async () => {
const server = createServer({});
const client = new Client({ name: "live-resource-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
try {
const resources = await client.listResources();
expect(resources.resources.map((resource) => resource.uri)).toEqual(expect.arrayContaining([
"premiere://project/info",
"premiere://project/sequences",
"premiere://project/media",
"premiere://project/bins",
"premiere://timeline/active",
"premiere://effects/available",
"premiere://effects/applied",
"premiere://transitions/available",
"premiere://export/presets",
"premiere://project/metadata",
]));
const read = await client.readResource({ uri: "premiere://project/info" });
expect(JSON.parse(read.contents[0].text as string)).toMatchObject({
ok: true,
resource: "premiere://project/info",
resourceSchemaVersion: 1,
});
} finally {
await client.close();
await server.close();
}
});
});
describe("jsonSchemaToInputSchema (tested via createServer)", () => {
// We test the schema adapter indirectly by verifying the server
// successfully registers tools with different parameter types.
// The function is private, so we test through the public API.
it("handles tools with string parameters", () => {
// The mock_discovery_tool has a required string parameter.
// If schema conversion fails, createServer would throw.
expect(() => createServer({})).not.toThrow();
});
it("handles tools with no parameters", () => {
// Many tools (health ping, etc.) have empty parameters.
expect(() => createServer({})).not.toThrow();
});
});
describe("SERVER_VERSION", () => {
it("matches the version in package.json", () => {
const pkg = JSON.parse(
readFileSync(join(process.cwd(), "package.json"), "utf-8"),
);
expect(SERVER_VERSION).toBe(pkg.version);
});
it("is a concrete semver, never the 'unknown' fallback", () => {
expect(SERVER_VERSION).toMatch(/^\d+\.\d+\.\d+/);
});
});
+243
View File
@@ -0,0 +1,243 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../src/bridge/file-bridge.js", async (importOriginal) => {
const original = await importOriginal<typeof import("../src/bridge/file-bridge.js")>();
return { ...original, sendCommand: vi.fn(async () => ({ success: true, data: { applied: true } })) };
});
import { sendCommand } from "../src/bridge/file-bridge.js";
import { capabilityForTool } from "../src/security/capabilities.js";
import {
getSpotWorkflowTools,
spotWorkflowConfirmationToken,
validateSpotWorkflowPlan,
} from "../src/tools/spot-workflows.js";
import { annotationsForTool } from "../src/workflows/tool-metadata.js";
const productArgs = {
sequence_id: "sequence-1",
asset_item_ids: ["item-a", "item-b"],
};
describe("spot workflow plans", () => {
beforeEach(() => vi.clearAllMocks());
it("advertises previews as inspect-only and the apply route as an edit", () => {
expect(capabilityForTool("preview_product_spot")).toBe("inspect");
expect(annotationsForTool("preview_product_spot").readOnlyHint).toBe(true);
expect(capabilityForTool("apply_spot_workflow_plan")).toBe("edit");
});
it("previews the product assembly locally without contacting Premiere", async () => {
const tools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect"]), source: "explicit" },
operationIdFactory: () => "preview-1",
});
const result = await tools.preview_product_spot.handler(productArgs);
expect(result).toMatchObject({
success: true,
data: {
operationId: "preview-1",
applied: false,
plan: {
workflow: "product_spot",
sequence_id: "sequence-1",
clip_duration_seconds: 4,
motion_style: "alternate",
transition: { name: "Cross Dissolve", duration_seconds: 0.5 },
},
},
});
expect(sendCommand).not.toHaveBeenCalled();
});
it("requires edit authority and the exact preview confirmation before applying", async () => {
const inspectOnly = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect"]), source: "explicit" },
auditSink: vi.fn(),
operationIdFactory: () => "apply-no-edit",
});
const preview = await inspectOnly.preview_product_spot.handler(productArgs);
const plan = preview.data.plan;
await expect(inspectOnly.apply_spot_workflow_plan.handler({
plan,
confirmation_token: preview.data.confirmationToken,
})).rejects.toMatchObject({ code: "CAPABILITY_DENIED" });
const tools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect", "edit"]), source: "explicit" },
auditSink: vi.fn(),
operationIdFactory: () => "apply-bad-token",
});
await expect(tools.apply_spot_workflow_plan.handler({ plan, confirmation_token: "different" })).rejects.toThrow("does not match");
expect(sendCommand).not.toHaveBeenCalled();
});
it("revalidates the plan and generates a contained, readback-oriented host command", async () => {
const tools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect", "edit"]), source: "explicit" },
auditSink: vi.fn(),
operationIdFactory: () => "apply-1",
});
const preview = await tools.preview_motion_graphics_demo.handler({
sequence_id: "sequence-1",
asset_item_ids: ["item-a", "item-b", "item-c"],
});
const plan = preview.data.plan;
const result = await tools.apply_spot_workflow_plan.handler({
plan,
confirmation_token: spotWorkflowConfirmationToken(plan),
});
expect(result).toMatchObject({ success: true, data: { applied: true, operationId: "apply-1" } });
const script = String(vi.mocked(sendCommand).mock.calls[0]?.[0]);
expect(script).toContain("Target sequence must be active before applying a spot workflow plan");
expect(script).toContain("Target video and audio tracks must be empty");
expect(script).toContain("Project item ID was not found exactly");
expect(script).toContain("function findPlacedClip(track, itemId, expectedStart)");
expect(script).toContain("function trimPlacedClip(clip, targetEnd)");
expect(script).toContain("var audioCountBefore = audioTrack.clips.numItems");
expect(script).toContain("if (audioCountAfter > audioCountBefore)");
expect(script).toContain("did not identify and trim the placed audio item");
expect(script).toContain("clip.end = requestedEnd");
expect(script).toContain("requestedDurationSeconds");
expect(script).toContain("scaleProperty.getValueAtKey");
expect(script).toContain("__findClip(placed[cutIndex + 1].nodeId)");
expect(script).toContain("__findQeClipByDomClip(qeTrack, placedInfo.clip)");
expect(script).toContain("typeof qeClip.addTransition !== \"function\"");
expect(script).toContain('qeClip.addTransition(transitionQE, true, String(durationFrames), "0", 0.5, false, true)');
expect(script).toContain("foundAtExpectedCut");
expect(script).toContain("Math.abs(((transitionStartTicks + transitionEndTicks) / 2) - expectedCutTicks) <= frameTicks / 2");
expect(script).not.toContain("qeTrack.addTransition(");
expect(script).toContain("renderVerified: false");
expect(script).not.toContain("importMedia");
expect(script).not.toContain("createNewSequence");
});
it("keeps a MOGRT within an approved workspace and requires filesystem authority at apply time", async () => {
const args = {
...productArgs,
mogrt_path: "D:/Approved/title.mogrt",
approved_workspace_path: "D:/Approved",
title_track_index: 1,
title_start_seconds: 0,
};
const previewTools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect", "edit"]), source: "explicit" },
auditSink: vi.fn(),
operationIdFactory: () => "brand-1",
fileExists: () => true,
});
const preview = await previewTools.preview_brand_spot.handler(args);
const plan = preview.data.plan;
await expect(previewTools.apply_spot_workflow_plan.handler({
plan,
confirmation_token: preview.data.confirmationToken,
})).rejects.toMatchObject({ code: "CAPABILITY_DENIED", capability: "filesystem" });
expect(sendCommand).not.toHaveBeenCalled();
const applyTools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect", "edit", "filesystem"]), source: "explicit" },
auditSink: vi.fn(),
operationIdFactory: () => "brand-2",
fileExists: (file) => file === "D:/Approved/title.mogrt",
});
await applyTools.apply_spot_workflow_plan.handler({
plan,
confirmation_token: preview.data.confirmationToken,
});
const script = String(vi.mocked(sendCommand).mock.calls[0]?.[0]);
expect(script).toContain("seq.importMGT");
expect(script).toContain("MOGRT track-count");
});
it("rejects plans that add an unpreviewed MOGRT or point outside the approved workspace", async () => {
await expect(getSpotWorkflowTools({}, { capabilities: { capabilities: new Set(["inspect"]), source: "explicit" } })
.preview_brand_spot.handler({
...productArgs,
mogrt_path: "D:/Elsewhere/title.mogrt",
approved_workspace_path: "D:/Approved",
})).rejects.toThrow("within approved_workspace_path");
expect(() => validateSpotWorkflowPlan({
schema_version: 1,
workflow: "product_spot",
sequence_id: "sequence-1",
asset_item_ids: ["item-a"],
clip_duration_seconds: 4,
video_track_index: 0,
audio_track_index: 0,
motion_style: "alternate",
mogrt: {
path: "D:/Approved/title.mogrt",
approved_workspace_path: "D:/Approved",
title_track_index: 1,
title_start_seconds: 0,
},
})).toThrow("only brand_spot");
});
it("validates preview inputs before a workflow token is issued", async () => {
const tools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect"]), source: "explicit" },
operationIdFactory: () => "invalid-preview",
});
await expect(tools.preview_product_spot.handler({ ...productArgs, unexpected: true } as any)).rejects.toThrow("unsupported field");
await expect(tools.preview_product_spot.handler({ ...productArgs, motion_style: "zoom" } as any)).rejects.toThrow("motion_style");
await expect(tools.preview_product_spot.handler({ ...productArgs, video_track_index: 33 } as any)).rejects.toThrow("video_track_index");
await expect(tools.preview_product_spot.handler({ ...productArgs, clip_duration_seconds: 0 } as any)).rejects.toThrow("clip_duration_seconds");
await expect(tools.preview_product_spot.handler({ sequence_id: "sequence-1", asset_item_ids: [] } as any)).rejects.toThrow("asset_item_ids");
await expect(tools.preview_brand_spot.handler({
...productArgs,
mogrt_path: "/approved/title.txt",
approved_workspace_path: "/approved",
} as any)).rejects.toThrow(".mogrt");
await expect(tools.preview_brand_spot.handler({
...productArgs,
mogrt_path: "relative/title.mogrt",
approved_workspace_path: "/approved",
} as any)).rejects.toThrow("absolute paths");
await expect(tools.preview_brand_spot.handler({
...productArgs,
mogrt_path: "/approved/title.mogrt",
approved_workspace_path: "/approved",
title_track_index: 0,
} as any)).rejects.toThrow("must differ");
});
it("handles portable paths and rejects stale or malformed confirmed plans", async () => {
const previewTools = getSpotWorkflowTools({}, {
capabilities: { capabilities: new Set(["inspect", "edit", "filesystem"]), source: "explicit" },
operationIdFactory: () => "portable-preview",
fileExists: () => false,
});
const preview = await previewTools.preview_brand_spot.handler({
...productArgs,
mogrt_path: "/approved/title.mogrt",
approved_workspace_path: "/approved",
transition_name: "none",
motion_style: "none",
});
expect(preview.data.plan.transition).toBeUndefined();
expect(preview.data.changes.motion).toMatchObject({ skipped: true });
await expect(previewTools.apply_spot_workflow_plan.handler({
plan: preview.data.plan,
confirmation_token: preview.data.confirmationToken,
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("no longer exists") });
expect(() => validateSpotWorkflowPlan({
schema_version: 2,
workflow: "product_spot",
})).toThrow("schema_version");
expect(() => validateSpotWorkflowPlan({
schema_version: 1,
workflow: "not-a-workflow",
})).toThrow("workflow");
});
});
+76
View File
@@ -0,0 +1,76 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const posthog = vi.hoisted(() => ({ constructor: vi.fn(), capture: vi.fn() }));
vi.mock("posthog-node", () => ({
PostHog: class {
constructor(apiKey: string, options: unknown) {
posthog.constructor(apiKey, options);
}
capture = posthog.capture;
shutdown = vi.fn(async () => {});
},
}));
const keys = [
"POSTHOG_API_KEY", "POSTHOG_HOST", "POSTHOG_DISTINCT_ID", "FLY_MACHINE_ID",
"POSTHOG_ENVIRONMENT", "NODE_ENV", "FLY_REGION", "PREMIERE_MCP_TRANSPORT",
"npm_package_version",
] as const;
const original = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
afterEach(() => {
vi.resetModules();
vi.clearAllMocks();
vi.restoreAllMocks();
for (const key of keys) {
const value = original[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
function clearTelemetryEnvironment(): void {
for (const key of keys) delete process.env[key];
}
describe("telemetry environment fallbacks", () => {
it("uses Fly identity, Node environment, and default host/metadata", async () => {
clearTelemetryEnvironment();
Object.assign(process.env, {
POSTHOG_API_KEY: "key",
FLY_MACHINE_ID: "machine-1",
NODE_ENV: "development",
});
vi.spyOn(console, "error").mockImplementation(() => {});
const { getTelemetry } = await import("../src/telemetry.js");
getTelemetry().capture("fallbacks");
expect(posthog.constructor).toHaveBeenCalledWith("key", expect.objectContaining({
host: "https://us.i.posthog.com",
}));
expect(posthog.capture).toHaveBeenCalledWith(expect.objectContaining({
distinctId: "machine-1",
properties: expect.objectContaining({
service_version: "unknown",
environment: "development",
transport: "unknown",
}),
}));
});
it("generates an anonymous server identity and defaults to production", async () => {
clearTelemetryEnvironment();
process.env.POSTHOG_API_KEY = "key";
vi.spyOn(console, "error").mockImplementation(() => {});
const { getTelemetry } = await import("../src/telemetry.js");
getTelemetry().capture("anonymous");
expect(posthog.capture).toHaveBeenCalledWith(expect.objectContaining({
distinctId: expect.stringMatching(/^server-[0-9a-f-]{36}$/i),
properties: expect.objectContaining({ environment: "production" }),
}));
});
});
+90
View File
@@ -0,0 +1,90 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const posthog = vi.hoisted(() => ({
capture: vi.fn(),
shutdown: vi.fn(async () => {}),
constructor: vi.fn(),
}));
vi.mock("posthog-node", () => ({
PostHog: class {
constructor(apiKey: string, options: unknown) {
posthog.constructor(apiKey, options);
}
capture = posthog.capture;
shutdown = posthog.shutdown;
},
}));
const ENV_KEYS = [
"POSTHOG_API_KEY", "POSTHOG_HOST", "POSTHOG_DISTINCT_ID", "FLY_MACHINE_ID",
"POSTHOG_ENVIRONMENT", "NODE_ENV", "FLY_REGION", "PREMIERE_MCP_TRANSPORT",
"npm_package_version",
] as const;
const original = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
afterEach(() => {
vi.resetModules();
vi.clearAllMocks();
for (const key of ENV_KEYS) {
const value = original[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
describe("telemetry", () => {
it("is a stable no-op when no API key is configured", async () => {
delete process.env.POSTHOG_API_KEY;
const { getTelemetry } = await import("../src/telemetry.js");
const first = getTelemetry();
expect(first.enabled).toBe(false);
expect(getTelemetry()).toBe(first);
expect(() => first.capture("ignored", { value: 1 })).not.toThrow();
await expect(first.shutdown()).resolves.toBeUndefined();
expect(posthog.constructor).not.toHaveBeenCalled();
});
it("configures PostHog and merges safe common and event properties", async () => {
Object.assign(process.env, {
POSTHOG_API_KEY: "test-key",
POSTHOG_HOST: "https://example.invalid",
POSTHOG_DISTINCT_ID: "installation-1",
POSTHOG_ENVIRONMENT: "test",
FLY_REGION: "sea",
PREMIERE_MCP_TRANSPORT: "stdio",
npm_package_version: "9.8.7",
});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const { getTelemetry } = await import("../src/telemetry.js");
const telemetry = getTelemetry();
telemetry.capture("tool_called", { service: "override", ok: true });
expect(posthog.constructor).toHaveBeenCalledWith("test-key", {
host: "https://example.invalid", flushAt: 1, flushInterval: 10_000,
});
expect(posthog.capture).toHaveBeenCalledWith({
distinctId: "installation-1",
event: "tool_called",
properties: expect.objectContaining({
service: "override", service_version: "9.8.7", environment: "test",
region: "sea", transport: "stdio", ok: true, $process_person_profile: false,
}),
});
await telemetry.shutdown();
expect(posthog.shutdown).toHaveBeenCalledOnce();
expect(error).toHaveBeenCalledWith(expect.stringContaining("telemetry enabled"));
error.mockRestore();
});
it("emits only the bounded activation properties", async () => {
const { captureActivationEvent } = await import("../src/telemetry.js");
const telemetry = { enabled: true, capture: vi.fn(), shutdown: vi.fn(async () => {}) };
captureActivationEvent(telemetry, { backend: "uxp" });
expect(telemetry.capture).toHaveBeenCalledOnce();
expect(telemetry.capture).toHaveBeenCalledWith("premiere_mcp_activation_completed", {
activation_stage: "verified_connection", backend: "uxp",
});
expect(JSON.stringify(telemetry.capture.mock.calls)).not.toMatch(/prompt|path|token|project|media|argument|result|profile/i);
});
});
+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();
});
});

Some files were not shown because too many files have changed in this diff Show More