Files
João Henrique 2c9bdf2ff0 feat: criado monitoramento contínuo de trilhas com varredura recur
- criado monitoramento contínuo de trilhas com varredura recursiva e baixa prioridade
- gerado e validado plano editorial revisável do depoimento da Erika sem aplicação na timeline
- adicionada leitura e comparação estrutural da timeline após aplicar planos de edição
- limitadas as classificações de trilhas às duas maiores probabilidades de cada categoria
- limpo o catálogo musical para reiniciar as análises sem remover dados de vídeo

Resumo:
- 34 arquivos alterados
- 6 novos
- 28 modificados
- 0 removidos

 28 files changed, 833 insertions(+), 75 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/engine/analisar_trilhas.py
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/consultar_detalhes_de_trilha.py
  - code/engine/editor/__init__.py
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/backup_de_sequencia.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/editor/modelos.py
  - code/engine/editor/validacao_semantica.py
  - code/engine/integracoes/audio/classificador_de_musica_essentia.py
  - code/engine/integracoes/audio/modelos_de_analise_musical.py
  - code/engine/integracoes/audio/provider_de_analise_musical_essentia.py
  - code/engine/persistencia/esquema.py
  - code/engine/persistencia/repositorio_de_trilhas.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/engine/testes/test_aplicar_plano_de_edicao.py
  - code/engine/testes/test_escrita_no_editor.py
  - code/engine/testes/test_leitor_de_plano_de_edicao.py
  - code/engine/testes/test_validador_semantico_de_plano.py
  - code/plugins/premiere-pro/skills/editar-por-voz/SKILL.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/08-formato-de-saida.md
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/10-revisao-humana.md
  - code/src/tools/timeline.ts
  - code/tests/tools/tool-modules.test.ts
  - .jhonny/planos/
  - code/.jhonny/analises.db-shm
  - code/.jhonny/analises.db-wal
  - code/engine/editor/verificador_da_aplicacao.py
  - code/engine/testes/test_verificador_da_aplicacao.py
  - code/plugins/premiere-pro/skills/editar-por-voz/criterios/11-gancho.md
2026-09-10 15:09:27 -04:00

1006 lines
45 KiB
TypeScript
Executable File

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 344 tools", () => {
let total = 0;
for (const mod of ALL_MODULES) {
total += Object.keys(mod.getter(bridgeOptions)).length;
}
expect(total).toBe(344);
});
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("duplicates a linked range at sequence start and preserves the original", async () => {
const tools = getTimelineTools(bridgeOptions);
await (tools.duplicate_linked_range_to_start.handler as any)({ node_ids: ["video-1", "audio-1"] });
const script = mockedSendCommand.mock.calls[0][0];
expect(script).toContain('first.trackType === second.trackType');
expect(script).toContain('video.clip.projectItem.nodeId !== audio.clip.projectItem.nodeId');
expect(script).toContain('Retimed or reversed clips cannot be duplicated');
expect(script).toContain('groups[g][t].clips.numItems > 0');
expect(script).toContain('seq.insertClip(item, "0", video.trackIndex, audio.trackIndex)');
expect(script).toContain('item.setInPoint(String(sourceInTicks), 4)');
expect(script).toContain('item.setOutPoint(String(sourceOutTicks), 4)');
expect(script).toContain('var preservedFirst = __findClip("video-1")');
expect(script).toContain('videoStart + durationTicks');
expect(script).toContain('beforeEndTicks + durationTicks');
expect(script).not.toContain('originalVideo.clip.remove');
expect(script).toContain('clipEffectsPreserved: false');
});
it("rejects an invalid linked-range move before contacting Premiere", async () => {
const tools = getTimelineTools(bridgeOptions);
await expect((tools.duplicate_linked_range_to_start.handler as any)({ node_ids: ["video-only"] }))
.resolves.toMatchObject({ success: false, error: expect.stringContaining("exactly two") });
expect(mockedSendCommand).not.toHaveBeenCalled();
});
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");
});
});