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:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user