- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
132 lines
7.3 KiB
TypeScript
Executable File
132 lines
7.3 KiB
TypeScript
Executable File
import { createRequire } from "node:module";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const Commands = require("../../uxp-plugin/commands.cjs");
|
|
const Protocol = require("../../uxp-plugin/protocol.cjs");
|
|
|
|
type HostOptions = { pauseLockedPreflight?: boolean };
|
|
|
|
function sourceLabelHost(options: HostOptions = {}) {
|
|
let colorIndex = 3;
|
|
let colorGetterCalls = 0;
|
|
let notifyLockedPreflight: (() => void) | null = null;
|
|
let releaseLockedPreflight: (() => void) | null = null;
|
|
const lockedPreflight = new Promise<void>((resolve) => { notifyLockedPreflight = resolve; });
|
|
const lockedPreflightRelease = new Promise<void>((resolve) => { releaseLockedPreflight = resolve; });
|
|
const source = {
|
|
getId: vi.fn(async () => "source-1"),
|
|
getColorLabelIndex: vi.fn(async () => {
|
|
colorGetterCalls += 1;
|
|
if (options.pauseLockedPreflight && colorGetterCalls === 2) {
|
|
notifyLockedPreflight?.();
|
|
await lockedPreflightRelease;
|
|
}
|
|
return colorIndex;
|
|
}),
|
|
createSetColorLabelAction: vi.fn((next: number) => ({ apply: () => { colorIndex = next; } })),
|
|
};
|
|
const item = {
|
|
getProjectItem: vi.fn(async () => source),
|
|
getStartTime: vi.fn(async () => ({ seconds: 12 })),
|
|
getEndTime: vi.fn(async () => ({ seconds: 20 })),
|
|
};
|
|
const videoTrack = { getTrackItems: vi.fn(async () => [item]) };
|
|
const audioTrack = { getTrackItems: vi.fn(async () => [item]) };
|
|
const sequence = {
|
|
guid: "sequence-1",
|
|
getVideoTrackCount: vi.fn(async () => 1), getVideoTrack: vi.fn(async () => videoTrack),
|
|
getAudioTrackCount: vi.fn(async () => 1), getAudioTrack: vi.fn(async () => audioTrack),
|
|
};
|
|
const addAction = vi.fn((action: { apply: () => void }) => { action.apply(); return true; });
|
|
const project = {
|
|
guid: "project-1", getActiveSequence: vi.fn(async () => sequence),
|
|
lockedAccess: vi.fn((callback: () => void) => callback()),
|
|
executeTransaction: vi.fn((callback: (compound: { addAction: typeof addAction }) => void) => { callback({ addAction }); return true; }),
|
|
};
|
|
const ppro = {
|
|
Project: { getActiveProject: vi.fn(async () => project) },
|
|
ClipProjectItem: { cast: vi.fn((value: unknown) => value === source ? source : null) },
|
|
Constants: { TrackItemType: { CLIP: 1 } },
|
|
};
|
|
return {
|
|
project, source, get colorIndex() { return colorIndex; }, lockedPreflight,
|
|
releaseLockedPreflight: () => releaseLockedPreflight?.(),
|
|
registry: Commands.createCommandRegistry({ ppro, Protocol }),
|
|
};
|
|
}
|
|
|
|
const target = { mediaType: "video", trackIndex: 0, clipIndex: 0 };
|
|
const expectedSnapshot = {
|
|
projectGuid: "project-1", sequenceId: "sequence-1", mediaType: "video", trackIndex: 0, clipIndex: 0,
|
|
trackItemCount: 1, sourceProjectItemId: "source-1", sourceColorLabelIndex: 3, startSeconds: 12, endSeconds: 20,
|
|
};
|
|
|
|
describe("guarded documented UXP timeline source-label workflow", () => {
|
|
it("advertises bounded readback and returns an active-coordinate source snapshot without a transaction", async () => {
|
|
const value = sourceLabelHost();
|
|
await expect(value.registry.capabilities()).resolves.toMatchObject({ commands: {
|
|
"timeline.sourceLabel.inspect": { supported: true, readOnly: true, minHostVersion: "25.6.0" },
|
|
"timeline.sourceLabel.update": { supported: true, destructive: true, undoable: true, idempotent: true, targetCapabilityProbe: "invocation" },
|
|
} });
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.inspect", target)).resolves.toEqual(expectedSnapshot);
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.inspect", { ...target, mediaType: "audio" })).resolves.toMatchObject({ ...expectedSnapshot, mediaType: "audio" });
|
|
expect(value.project.executeTransaction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("sets exactly the reviewed source label in one transaction and reads the coordinate back", async () => {
|
|
const value = sourceLabelHost();
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 9, expectedSnapshot, confirmSetLabel: true, operationId: "source-label-1",
|
|
})).resolves.toMatchObject({
|
|
operationId: "source-label-1", sourceLabelUpdated: true, outcome: "verified", before: expectedSnapshot,
|
|
after: { ...expectedSnapshot, sourceColorLabelIndex: 9 },
|
|
verificationBoundary: "timeline_coordinate_source_color_label_readback", undoLabel: "Set timeline source label",
|
|
});
|
|
expect(value.project.executeTransaction).toHaveBeenCalledTimes(1);
|
|
expect(value.project.executeTransaction.mock.calls[0]?.[1]).toBe("Set timeline source label");
|
|
expect(value.source.createSetColorLabelAction).toHaveBeenCalledWith(9);
|
|
expect(value.colorIndex).toBe(9);
|
|
});
|
|
|
|
it("requires authority and a complete fresh snapshot before action creation", async () => {
|
|
const value = sourceLabelHost();
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 9, expectedSnapshot, operationId: "missing-confirmation",
|
|
})).rejects.toMatchObject({ code: "UXP_CONFIRMATION_REQUIRED" });
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 9, expectedSnapshot, confirmSetLabel: true,
|
|
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 9, expectedSnapshot: { ...expectedSnapshot, sourceColorLabelIndex: 2 }, confirmSetLabel: true, operationId: "stale",
|
|
})).rejects.toMatchObject({ code: "UXP_STALE_SOURCE_LABEL" });
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.inspect", { ...target, clipIndex: 1 })).rejects.toMatchObject({ code: "UXP_TARGET_NOT_FOUND" });
|
|
expect(value.project.executeTransaction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("replays an accepted operation ID without a second host transaction", async () => {
|
|
const value = sourceLabelHost();
|
|
const args = { ...target, colorIndex: 9, expectedSnapshot, confirmSetLabel: true, operationId: "source-label-replay" };
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", args)).resolves.toMatchObject({ outcome: "verified" });
|
|
await expect(value.registry.dispatch("timeline.sourceLabel.update", args)).resolves.toMatchObject({ outcome: "verified", replayed: true });
|
|
expect(value.project.executeTransaction).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("serializes distinct operation IDs so a second reviewed label cannot overwrite the first", async () => {
|
|
const value = sourceLabelHost({ pauseLockedPreflight: true });
|
|
const first = value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 9, expectedSnapshot, confirmSetLabel: true, operationId: "source-label-first",
|
|
});
|
|
await value.lockedPreflight;
|
|
const second = value.registry.dispatch("timeline.sourceLabel.update", {
|
|
...target, colorIndex: 11, expectedSnapshot, confirmSetLabel: true, operationId: "source-label-second",
|
|
});
|
|
const secondExpectation = expect(second).rejects.toMatchObject({ code: "UXP_STALE_SOURCE_LABEL" });
|
|
value.releaseLockedPreflight();
|
|
await expect(first).resolves.toMatchObject({ outcome: "verified", after: { sourceColorLabelIndex: 9 } });
|
|
await secondExpectation;
|
|
expect(value.project.executeTransaction).toHaveBeenCalledTimes(1);
|
|
expect(value.colorIndex).toBe(9);
|
|
});
|
|
});
|