- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
127 lines
5.3 KiB
TypeScript
Executable File
127 lines
5.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 Options = {
|
|
replaceItemOnSecondRootRead?: boolean;
|
|
getRootItem?: boolean;
|
|
};
|
|
|
|
function nativeGuid(value: string) {
|
|
return { toString: vi.fn(() => value) };
|
|
}
|
|
|
|
function uniqueIdentityHost(options: Options = {}) {
|
|
const originalItem = { uniqueId: "unique-item-1", getId: vi.fn(async () => "item-1") };
|
|
const replacementItem = { uniqueId: "unique-item-2", getId: vi.fn(async () => "item-1") };
|
|
let currentItem = originalItem;
|
|
const root = {
|
|
getId: vi.fn(async () => "root"),
|
|
getItems: vi.fn(async () => [currentItem]),
|
|
};
|
|
const sequence = { guid: nativeGuid("sequence-1"), uniqueId: "unique-sequence-1" };
|
|
let rootReads = 0;
|
|
const project = {
|
|
guid: nativeGuid("project-1"),
|
|
getActiveSequence: vi.fn(async () => null),
|
|
getRootItem: vi.fn(async () => {
|
|
rootReads += 1;
|
|
if (options.replaceItemOnSecondRootRead && rootReads > 1) currentItem = replacementItem;
|
|
return root;
|
|
}),
|
|
getSequence: vi.fn(async (guid: { toString: () => string }) =>
|
|
guid.toString() === "sequence-1" ? sequence : null),
|
|
};
|
|
const ppro = {
|
|
Project: { getActiveProject: vi.fn(async () => project) },
|
|
Guid: { fromString: vi.fn((value: string) => nativeGuid(value)) },
|
|
UniqueSerializeable: {
|
|
cast: vi.fn((target: { uniqueId?: string }) => ({
|
|
getUniqueID: vi.fn(async () => nativeGuid(target.uniqueId || "")),
|
|
})),
|
|
},
|
|
};
|
|
return {
|
|
root,
|
|
project,
|
|
ppro,
|
|
registry: Commands.createCommandRegistry({ ppro, Protocol }),
|
|
};
|
|
}
|
|
|
|
describe("bounded documented UXP unique serializable identity inspection", () => {
|
|
it("advertises a read-only target-probed command and double-reads one project-item identity", async () => {
|
|
const value = uniqueIdentityHost();
|
|
await expect(value.registry.capabilities()).resolves.toMatchObject({ commands: {
|
|
"object.uniqueIdentity.inspect": {
|
|
supported: true,
|
|
readOnly: true,
|
|
minHostVersion: "25.6.0",
|
|
targetCapabilityProbe: "invocation",
|
|
},
|
|
} });
|
|
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", {
|
|
projectItemId: "item-1",
|
|
expectedProjectGuid: "project-1",
|
|
expectedUniqueId: "unique-item-1",
|
|
})).resolves.toEqual({
|
|
projectGuid: "project-1",
|
|
target: { kind: "project_item", projectItemId: "item-1", uniqueId: "unique-item-1" },
|
|
verificationBoundary: "bounded_unique_serializable_double_readback",
|
|
});
|
|
expect(value.project.getRootItem).toHaveBeenCalledTimes(2);
|
|
expect(value.ppro.UniqueSerializeable.cast).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("resolves an exact sequence GUID without traversing the project tree", async () => {
|
|
const value = uniqueIdentityHost();
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", {
|
|
sequenceGuid: "sequence-1",
|
|
})).resolves.toMatchObject({
|
|
projectGuid: "project-1",
|
|
target: { kind: "sequence", sequenceGuid: "sequence-1", uniqueId: "unique-sequence-1" },
|
|
});
|
|
expect(value.project.getRootItem).not.toHaveBeenCalled();
|
|
expect(value.project.getSequence).toHaveBeenCalledTimes(2);
|
|
expect(value.ppro.Guid.fromString).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("rejects ambiguous, stale, unsupported, and oversized requests", async () => {
|
|
const value = uniqueIdentityHost();
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", {}))
|
|
.rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", {
|
|
projectItemId: "item-1", sequenceGuid: "sequence-1",
|
|
})).rejects.toMatchObject({ code: "UXP_INVALID_ARGUMENT" });
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", {
|
|
projectItemId: "item-1", expectedUniqueId: "different",
|
|
})).rejects.toMatchObject({ code: "UXP_STALE_UNIQUE_IDENTITY" });
|
|
|
|
const tooLarge = uniqueIdentityHost();
|
|
tooLarge.root.getItems.mockResolvedValueOnce(Array.from({ length: 512 }, (_, index) => ({
|
|
getId: vi.fn(async () => `item-${index}`),
|
|
})));
|
|
await expect(tooLarge.registry.dispatch("object.uniqueIdentity.inspect", { projectItemId: "missing" }))
|
|
.rejects.toMatchObject({ code: "UXP_PROJECT_TOO_LARGE" });
|
|
});
|
|
|
|
it("fails closed when a retained project-item ID resolves to a replacement during the second read", async () => {
|
|
const value = uniqueIdentityHost({ replaceItemOnSecondRootRead: true });
|
|
await expect(value.registry.dispatch("object.uniqueIdentity.inspect", { projectItemId: "item-1" }))
|
|
.rejects.toMatchObject({ code: "UXP_STALE_UNIQUE_IDENTITY" });
|
|
});
|
|
|
|
it("does not advertise compatibility when the documented unique serializable static API is absent", async () => {
|
|
const value = uniqueIdentityHost();
|
|
delete (value.ppro as { UniqueSerializeable?: unknown }).UniqueSerializeable;
|
|
const registry = Commands.createCommandRegistry({ ppro: value.ppro, Protocol });
|
|
await expect(registry.capabilities()).resolves.toMatchObject({ commands: {
|
|
"object.uniqueIdentity.inspect": { supported: false, reason: expect.any(String) },
|
|
} });
|
|
});
|
|
});
|