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

211 lines
9.6 KiB
TypeScript
Executable File

import { createRequire } from "node:module";
import { describe, expect, it, vi } from "vitest";
const require = createRequire(import.meta.url);
const Events = require("../../uxp-plugin/events.cjs");
describe("UXP host event journal", () => {
it("maps only documented stable SnapEvent constants to passive timeline receipts", () => {
const definitions = Events.createTimelineSnapEventDefinitions({
EVENT_SNAP_TO_KEYFRAME: "snap-keyframe",
EVENT_SNAP_TO_TRACKITEM: "snap-track-item",
EVENT_SNAP_TO_GUIDES: "snap-guides",
EVENT_SNAP_RAZOR_TO_PLAYHEAD: "snap-razor-playhead",
EVENT_SNAP_RAZOR_TO_MARKER: "snap-razor-marker",
EVENT_SNAP_PLAYHEAD_TO_TRACKITEM_EDGE: "snap-playhead-edge",
UNDOCUMENTED_EVENT: "must-not-register",
});
expect(definitions).toEqual([
{ category: "timeline", name: "timeline.snap.keyframe", eventName: "snap-keyframe", stateInvalidating: false, coalesceKey: null },
{ category: "timeline", name: "timeline.snap.trackItem", eventName: "snap-track-item", stateInvalidating: false, coalesceKey: null },
{ category: "timeline", name: "timeline.snap.guides", eventName: "snap-guides", stateInvalidating: false, coalesceKey: null },
{ category: "timeline", name: "timeline.snap.razor.playhead", eventName: "snap-razor-playhead", stateInvalidating: false, coalesceKey: null },
{ category: "timeline", name: "timeline.snap.razor.marker", eventName: "snap-razor-marker", stateInvalidating: false, coalesceKey: null },
{ category: "timeline", name: "timeline.snap.playhead.trackItemEdge", eventName: "snap-playhead-edge", stateInvalidating: false, coalesceKey: null },
]);
expect(Events.createTimelineSnapEventDefinitions({
EVENT_SNAP_TO_KEYFRAME: 1,
EVENT_SNAP_TO_TRACKITEM: "",
})).toEqual([]);
expect(Events.createTimelineSnapEventDefinitions(null)).toEqual([]);
});
it("filters timeline SnapEvent receipts without exposing host payload fields", () => {
const journal = Events.createEventJournal({ capacity: 16 });
journal.recordHostEvent({
category: "timeline",
name: "timeline.snap.keyframe",
detail: { state: 4, target: "private clip", path: "D:/private.prproj" },
});
expect(journal.list({ categories: ["timeline"], eventNames: ["timeline.snap.keyframe"] }))
.toMatchObject({ events: [{ category: "timeline", name: "timeline.snap.keyframe", detail: { state: 4 } }] });
});
it("maps only documented root operation-boundary events and coalesces drag-over receipts", () => {
const definitions = Events.createOperationBoundaryEventDefinitions({
EVENT_CLIP_EXTEND_REACHED: "clip-extend-reached",
EVENT_EFFECT_DRAG_OVER: "effect-drag-over",
EVENT_EXPORT_MEDIA_COMPLETE: "already-handled-completion",
});
expect(definitions).toEqual([
{ category: "operation", name: "operation.clip.extend.reached", eventName: "clip-extend-reached", stateInvalidating: false, coalesceKey: null },
{ category: "operation", name: "operation.effect.drag.over", eventName: "effect-drag-over", stateInvalidating: false, coalesceKey: "operation.effect.drag.over" },
]);
expect(Events.createOperationBoundaryEventDefinitions({
EVENT_CLIP_EXTEND_REACHED: 1,
EVENT_EFFECT_DRAG_OVER: "",
})).toEqual([]);
expect(Events.createOperationBoundaryEventDefinitions(null)).toEqual([]);
const journal = Events.createEventJournal({ capacity: 16 });
journal.recordHostEvent({
category: "operation", name: definitions[1].name, coalesceKey: definitions[1].coalesceKey,
detail: { state: 1, path: "D:/private.prproj" },
});
journal.recordHostEvent({
category: "operation", name: definitions[1].name, coalesceKey: definitions[1].coalesceKey,
detail: { state: 2, target: "private clip" },
});
expect(journal.list({ eventNames: ["operation.effect.drag.over"] })).toMatchObject({
latestRevision: 2,
events: [{ category: "operation", name: "operation.effect.drag.over", detail: { state: 2 }, coalesced: 1 }],
});
});
it("keeps a bounded revisioned history and reports overflow", () => {
let now = 1_700_000_000_000;
const journal = Events.createEventJournal({ capacity: 16, now: () => now++ });
for (let index = 0; index < 20; index += 1) {
journal.append({ category: "project", name: "project.dirty", detail: { state: index } });
}
expect(journal.status()).toMatchObject({ capacity: 16, size: 16, latestRevision: 20, oldestRevision: 5, dropped: 4 });
expect(journal.list({ afterRevision: 1, limit: 3 })).toMatchObject({
overflow: true,
dropped: 4,
events: [
{ revision: 5, category: "project", name: "project.dirty" },
{ revision: 6 },
{ revision: 7 },
],
});
});
it("coalesces consecutive noisy progress receipts without exposing raw fields", () => {
const journal = Events.createEventJournal({ capacity: 16 });
journal.append({
category: "encoder", name: "encoder.progress", coalesceKey: "encoder.progress",
detail: { progress: 0.1, path: "D:/secret.mov" },
});
journal.append({
category: "encoder", name: "encoder.progress", coalesceKey: "encoder.progress",
detail: { progress: 0.8, projectName: "Private" },
});
expect(journal.list({})).toMatchObject({
latestRevision: 2,
events: [{ revision: 2, detail: { progress: 0.8 }, coalesced: 1 }],
});
expect(journal.list({ afterRevision: 1 })).toMatchObject({
overflow: false,
events: [{ revision: 2, coalesced: 1 }],
});
});
it("returns immediately when a filtered cursor has fallen behind evicted history", async () => {
const journal = Events.createEventJournal({ capacity: 16 });
for (let index = 0; index < 20; index += 1) {
journal.append({ category: "project", name: "project.dirty", detail: { state: index } });
}
await expect(journal.wait({
afterRevision: 1,
categories: ["encoder"],
timeoutMs: 60_000,
})).resolves.toMatchObject({ overflow: true, timedOut: false, events: [] });
});
it("settles a pending filtered waiter as soon as its cursor history is evicted", async () => {
const journal = Events.createEventJournal({ capacity: 16 });
for (let index = 0; index < 16; index += 1) {
journal.append({ category: "project", name: "project.dirty", detail: { state: index } });
}
const pending = journal.wait({
afterRevision: 1,
categories: ["encoder"],
timeoutMs: 60_000,
});
journal.append({ category: "project", name: "project.dirty" });
journal.append({ category: "project", name: "project.dirty" });
await expect(pending).resolves.toMatchObject({ overflow: true, timedOut: false, events: [] });
});
it("waits for a matching receipt and times out cleanly", async () => {
vi.useFakeTimers();
try {
const journal = Events.createEventJournal({ capacity: 16 });
const matching = journal.wait({ afterRevision: 0, categories: ["operation"], timeoutMs: 1000 });
journal.append({ category: "project", name: "project.dirty" });
journal.append({ category: "operation", name: "operation.import.complete", detail: { state: 0 } });
await expect(matching).resolves.toMatchObject({
timedOut: false,
events: [{ category: "operation", name: "operation.import.complete", detail: { state: 0 } }],
});
const timeout = journal.wait({ afterRevision: 2, eventNames: ["encoder.complete"], timeoutMs: 500 });
await vi.advanceTimersByTimeAsync(500);
await expect(timeout).resolves.toMatchObject({ timedOut: true, events: [] });
} finally {
vi.useRealTimers();
}
});
it("resolves pending waiters when the journal closes", async () => {
const journal = Events.createEventJournal({ capacity: 16 });
const pending = journal.wait({ timeoutMs: 60000 });
journal.close();
await expect(pending).resolves.toMatchObject({ closed: true, events: [] });
});
it("attributes encoder events only while exactly one job is active", async () => {
const journal = Events.createEventJournal({ capacity: 16 });
const first = journal.beginEncodeJob({ kind: "sequence", operationId: "encode-one" });
journal.markEncodeAccepted(first.jobId);
journal.recordHostEvent({ category: "encoder", name: "encoder.queued" });
journal.recordHostEvent({ category: "encoder", name: "encoder.progress", detail: { progress: 0.5 } });
expect(journal.listEncodeJobs({ jobId: first.jobId })).toMatchObject({
correlation: "single-active-job-only",
jobs: [{ jobId: "encode-one", state: "rendering", progress: 0.5, terminal: false }],
});
const terminal = journal.waitForEncodeJob({ jobId: first.jobId, timeoutMs: 1000 });
journal.recordHostEvent({ category: "encoder", name: "encoder.complete" });
await expect(terminal).resolves.toMatchObject({
timedOut: false,
job: { state: "completed", terminal: true, terminalReason: "completed" },
});
});
it("does not guess job correlation when multiple encodes are active", () => {
const journal = Events.createEventJournal({ capacity: 16 });
journal.beginEncodeJob({ kind: "sequence", operationId: "encode-one" });
journal.beginEncodeJob({ kind: "file", operationId: "encode-two" });
const receipt = journal.recordHostEvent({ category: "encoder", name: "encoder.complete" });
expect(receipt).toMatchObject({ detail: { attributed: false } });
expect(journal.listEncodeJobs({})).toMatchObject({
unattributedEncoderEvents: 1,
jobs: expect.arrayContaining([
expect.objectContaining({ jobId: "encode-one", terminal: false }),
expect.objectContaining({ jobId: "encode-two", terminal: false }),
]),
});
});
});