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,27 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { join } from "node:path";
|
||||
|
||||
vi.mock("../../src/bridge/file-bridge.js", () => ({
|
||||
sendCommand: vi.fn().mockResolvedValue({ success: true, data: {} }),
|
||||
}));
|
||||
|
||||
import { sendCommand } from "../../src/bridge/file-bridge.js";
|
||||
import {
|
||||
AFTER_EFFECTS_BRIDGE_HELPERS,
|
||||
AFTER_EFFECTS_DEFAULT_TEMP_DIR_NAME,
|
||||
getAfterEffectsTempDir,
|
||||
sendAfterEffectsCommand,
|
||||
} from "../../src/bridge/after-effects-bridge.js";
|
||||
|
||||
describe("After Effects bridge", () => {
|
||||
it("uses an independent bridge directory and AE-only helpers", async () => {
|
||||
expect(getAfterEffectsTempDir(undefined, "D:/Temp")).toBe(join("D:/Temp", AFTER_EFFECTS_DEFAULT_TEMP_DIR_NAME));
|
||||
await sendAfterEffectsCommand("return __aeResult({});", { tempDir: "D:/PremiereOnly" });
|
||||
expect(sendCommand).toHaveBeenCalledWith("return __aeResult({});", expect.objectContaining({
|
||||
tempDir: getAfterEffectsTempDir(),
|
||||
helpers: AFTER_EFFECTS_BRIDGE_HELPERS,
|
||||
}));
|
||||
expect(AFTER_EFFECTS_BRIDGE_HELPERS.source).toContain("__aeResult");
|
||||
expect(AFTER_EFFECTS_BRIDGE_HELPERS.source).not.toContain("__findSequence");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
import { once } from "node:events";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
unlinkSync,
|
||||
watch,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { WebSocket } from "ws";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sendCommand } from "../../src/bridge/file-bridge.js";
|
||||
import {
|
||||
UxpWebSocketBridge,
|
||||
type UxpHello,
|
||||
} from "../../src/bridge/uxp-websocket-bridge.js";
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
readdirSync: vi.fn(),
|
||||
renameSync: vi.fn(),
|
||||
statSync: vi.fn(),
|
||||
chmodSync: vi.fn(),
|
||||
watch: vi.fn(),
|
||||
}));
|
||||
|
||||
const fs = {
|
||||
exists: vi.mocked(existsSync),
|
||||
mkdir: vi.mocked(mkdirSync),
|
||||
write: vi.mocked(writeFileSync),
|
||||
read: vi.mocked(readFileSync),
|
||||
unlink: vi.mocked(unlinkSync),
|
||||
readdir: vi.mocked(readdirSync),
|
||||
rename: vi.mocked(renameSync),
|
||||
stat: vi.mocked(statSync),
|
||||
chmod: vi.mocked(chmodSync),
|
||||
watch: vi.mocked(watch),
|
||||
};
|
||||
|
||||
const originalGetuid = Object.getOwnPropertyDescriptor(process, "getuid");
|
||||
|
||||
function ownedStat(mode = 0o700): ReturnType<typeof statSync> {
|
||||
return {
|
||||
uid: 4242,
|
||||
mode,
|
||||
mtimeMs: Date.now(),
|
||||
} as unknown as ReturnType<typeof statSync>;
|
||||
}
|
||||
|
||||
function usePosixProcess(uid = 4242): void {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
||||
Object.defineProperty(process, "getuid", {
|
||||
configurable: true,
|
||||
value: () => uid,
|
||||
});
|
||||
}
|
||||
|
||||
function restoreGetuid(): void {
|
||||
if (originalGetuid) {
|
||||
Object.defineProperty(process, "getuid", originalGetuid);
|
||||
} else {
|
||||
delete (process as { getuid?: () => number }).getuid;
|
||||
}
|
||||
}
|
||||
|
||||
describe("file bridge uncovered security and fallback paths", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
fs.watch.mockImplementation(() => {
|
||||
throw new Error("watch unavailable");
|
||||
});
|
||||
fs.stat.mockReturnValue(ownedStat());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
restoreGetuid();
|
||||
});
|
||||
|
||||
it("rejects a pre-existing POSIX directory owned by another user", async () => {
|
||||
usePosixProcess();
|
||||
fs.exists.mockReturnValue(true);
|
||||
fs.stat.mockReturnValue(ownedStat());
|
||||
fs.stat.mockReturnValueOnce({
|
||||
uid: 31337,
|
||||
mode: 0o700,
|
||||
mtimeMs: Date.now(),
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
await expect(sendCommand("var unsafe = true;", {
|
||||
tempDir: "/tmp/untrusted-bridge",
|
||||
})).rejects.toThrow(/owned by uid 31337/);
|
||||
});
|
||||
|
||||
it("restores private POSIX permissions on an owned directory", async () => {
|
||||
usePosixProcess();
|
||||
fs.exists.mockReturnValue(true);
|
||||
fs.stat.mockReturnValueOnce(ownedStat(0o755));
|
||||
fs.read.mockReturnValue('{"success":true,"data":{"repaired":true}}');
|
||||
|
||||
await expect(sendCommand("var repair = true;", {
|
||||
tempDir: "/tmp/owned-bridge",
|
||||
})).resolves.toEqual({ success: true, data: { repaired: true } });
|
||||
|
||||
expect(fs.chmod).toHaveBeenCalledWith("/tmp/owned-bridge", 0o700);
|
||||
});
|
||||
|
||||
it("reports a busy Premiere operation when its heartbeat cannot be statted", async () => {
|
||||
usePosixProcess();
|
||||
fs.exists.mockImplementation((path) => {
|
||||
const value = String(path);
|
||||
if (value.includes("res_")) return false;
|
||||
if (value.includes("busy_")) return true;
|
||||
return true;
|
||||
});
|
||||
fs.stat.mockImplementation(((path) => {
|
||||
if (String(path).includes("busy_")) throw new Error("heartbeat locked");
|
||||
return ownedStat();
|
||||
}) as typeof statSync);
|
||||
|
||||
const response = sendCommand("var waiting = true;", {
|
||||
tempDir: "/tmp/busy-stat-error",
|
||||
timeoutMs: 25,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(110);
|
||||
|
||||
await expect(response).resolves.toMatchObject({
|
||||
success: false,
|
||||
error: expect.stringContaining("modal dialog"),
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes non-Error response read failures into command results", async () => {
|
||||
usePosixProcess();
|
||||
fs.exists.mockImplementation((path) => !String(path).includes("busy_"));
|
||||
fs.read.mockImplementation(() => {
|
||||
throw "response file is locked";
|
||||
});
|
||||
|
||||
const response = sendCommand("var parse = true;", {
|
||||
tempDir: "/tmp/non-error-read-failure",
|
||||
timeoutMs: 25,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(110);
|
||||
|
||||
await expect(response).resolves.toEqual({
|
||||
success: false,
|
||||
error: "Failed to parse response: response file is locked",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const TOKEN = "bridge-edge-token-at-least-16-characters";
|
||||
const liveBridges: UxpWebSocketBridge[] = [];
|
||||
|
||||
async function startBridge(options: {
|
||||
port?: number;
|
||||
handshakeTimeoutMs?: number;
|
||||
requestTimeoutMs?: number;
|
||||
} = {}): Promise<UxpWebSocketBridge> {
|
||||
const bridge = new UxpWebSocketBridge({ token: TOKEN, port: 0, ...options });
|
||||
liveBridges.push(bridge);
|
||||
await bridge.start();
|
||||
return bridge;
|
||||
}
|
||||
|
||||
function bridgeUrl(bridge: UxpWebSocketBridge, token = TOKEN): string {
|
||||
const address = bridge.address();
|
||||
return `ws://${address.host}:${address.port}${address.path}?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
async function connectHost(
|
||||
bridge: UxpWebSocketBridge,
|
||||
protocolVersion = 1,
|
||||
commands: Record<string, { supported: boolean }> = { "state.get": { supported: true } },
|
||||
): Promise<WebSocket> {
|
||||
const client = new WebSocket(bridgeUrl(bridge));
|
||||
await once(client, "open");
|
||||
const connected = once(bridge, "connected");
|
||||
client.send(JSON.stringify({
|
||||
protocolVersion,
|
||||
type: "hello",
|
||||
payload: {
|
||||
backend: "uxp",
|
||||
protocolVersion,
|
||||
commands,
|
||||
},
|
||||
}));
|
||||
await connected;
|
||||
return client;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(liveBridges.splice(0).map((bridge) => bridge.stop()));
|
||||
});
|
||||
|
||||
describe("UXP WebSocket bridge rejected-message and cleanup paths", () => {
|
||||
it("serves a 404 for regular HTTP traffic and ignores duplicate starts", async () => {
|
||||
const bridge = await startBridge();
|
||||
const listening = vi.fn();
|
||||
bridge.on("listening", listening);
|
||||
|
||||
await bridge.start();
|
||||
const address = bridge.address();
|
||||
const response = await fetch(`http://${address.host}:${address.port}/not-a-websocket`);
|
||||
|
||||
expect(listening).not.toHaveBeenCalled();
|
||||
expect(response.status).toBe(404);
|
||||
await expect(response.text()).resolves.toBe("Not found");
|
||||
});
|
||||
|
||||
it("uses configured address data before it has started", () => {
|
||||
const bridge = new UxpWebSocketBridge({ token: TOKEN, port: 7911, path: "/panel" });
|
||||
expect(bridge.address()).toEqual({ host: "127.0.0.1", port: 7911, path: "/panel" });
|
||||
expect(bridge.getState()).toEqual({ status: "stopped", connected: false });
|
||||
});
|
||||
|
||||
it("uses a timing-safe comparison for equal-length incorrect tokens", async () => {
|
||||
const bridge = await startBridge();
|
||||
const client = new WebSocket(bridgeUrl(bridge, "x".repeat(TOKEN.length)));
|
||||
client.on("error", () => {});
|
||||
const [, response] = await once(client, "unexpected-response");
|
||||
|
||||
expect(response.statusCode).toBe(401);
|
||||
response.resume();
|
||||
});
|
||||
|
||||
it("closes a host that never sends the versioned hello", async () => {
|
||||
const bridge = await startBridge({ handshakeTimeoutMs: 20 });
|
||||
const client = new WebSocket(bridgeUrl(bridge));
|
||||
await once(client, "open");
|
||||
|
||||
const [code, reason] = await once(client, "close");
|
||||
expect(code).toBe(1008);
|
||||
expect(reason.toString()).toContain("Versioned hello required");
|
||||
});
|
||||
|
||||
it("rejects each malformed hello shape before accepting capabilities", async () => {
|
||||
const bridge = await startBridge();
|
||||
const malformedMessages = [
|
||||
{ protocolVersion: 1, type: "event", payload: {} },
|
||||
{ protocolVersion: 1, type: "hello", payload: { backend: "cep", protocolVersion: 1, commands: {} } },
|
||||
{ protocolVersion: 3, type: "hello", payload: { backend: "uxp", protocolVersion: 3, commands: {} } },
|
||||
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 2, commands: {} } },
|
||||
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1 } },
|
||||
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1, commands: "invalid" } },
|
||||
{ protocolVersion: 1, type: "hello", payload: { backend: "uxp", protocolVersion: 1, commands: [] } },
|
||||
];
|
||||
|
||||
for (const message of malformedMessages) {
|
||||
const client = new WebSocket(bridgeUrl(bridge));
|
||||
await once(client, "open");
|
||||
const closed = once(client, "close");
|
||||
client.send(JSON.stringify(message));
|
||||
const [code] = await closed;
|
||||
expect(code).toBe(1008);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts protocol 2 and closes the connection when its protocol changes", async () => {
|
||||
const bridge = await startBridge();
|
||||
const client = await connectHost(bridge, 2);
|
||||
expect(bridge.getState()).toMatchObject({ connected: true, protocolVersion: 2 });
|
||||
|
||||
const closed = once(client, "close");
|
||||
client.send(JSON.stringify({ protocolVersion: 1, type: "event", payload: { kind: "wrong-version" } }));
|
||||
const [code, reason] = await closed;
|
||||
expect(code).toBe(1008);
|
||||
expect(reason.toString()).toContain("Protocol version changed");
|
||||
});
|
||||
|
||||
it("ignores unknown messages and reports default host command failures", async () => {
|
||||
const bridge = await startBridge();
|
||||
const client = await connectHost(bridge);
|
||||
client.send(JSON.stringify({ protocolVersion: 1, type: "notice", payload: {} }));
|
||||
client.send(JSON.stringify({
|
||||
protocolVersion: 1,
|
||||
type: "result",
|
||||
requestId: "unknown-request",
|
||||
payload: { ok: true, result: "ignored" },
|
||||
}));
|
||||
|
||||
client.once("message", (data) => {
|
||||
const command = JSON.parse(data.toString());
|
||||
client.send(JSON.stringify({
|
||||
protocolVersion: 1,
|
||||
type: "result",
|
||||
requestId: command.requestId,
|
||||
payload: { ok: false },
|
||||
}));
|
||||
});
|
||||
await expect(bridge.request("state.get")).rejects.toMatchObject({
|
||||
code: "UXP_COMMAND_FAILED",
|
||||
message: "UXP command 'state.get' failed",
|
||||
});
|
||||
expect(bridge.getState()).toMatchObject({ connected: true });
|
||||
});
|
||||
|
||||
it("forwards client errors from an accepted connection", async () => {
|
||||
const bridge = await startBridge();
|
||||
await connectHost(bridge);
|
||||
const emitted = once(bridge, "clientError");
|
||||
const expected = new Error("client transport error");
|
||||
const serverClient = (bridge as unknown as { socket: WebSocket }).socket;
|
||||
|
||||
serverClient.emit("error", expected);
|
||||
await expect(emitted).resolves.toEqual([expected]);
|
||||
});
|
||||
|
||||
it("rejects a request if the socket reports a send failure exactly once", async () => {
|
||||
const bridge = new UxpWebSocketBridge({ token: TOKEN });
|
||||
const internal = bridge as unknown as {
|
||||
socket: {
|
||||
readyState: number;
|
||||
send: (message: string, callback: (error?: Error) => void) => void;
|
||||
close: () => void;
|
||||
} | null;
|
||||
hello: UxpHello | null;
|
||||
};
|
||||
internal.socket = {
|
||||
readyState: WebSocket.OPEN,
|
||||
send: (_message, callback) => {
|
||||
callback(new Error("socket write failed"));
|
||||
callback(new Error("duplicate completion"));
|
||||
},
|
||||
close: vi.fn(),
|
||||
};
|
||||
internal.hello = {
|
||||
backend: "uxp",
|
||||
protocolVersion: 1,
|
||||
commands: { "state.get": { supported: true } },
|
||||
};
|
||||
|
||||
await expect(bridge.request("state.get")).rejects.toMatchObject({
|
||||
code: "UXP_SEND_FAILED",
|
||||
message: "socket write failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
unlinkSync,
|
||||
watch,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { cleanupTempDir, sendCommand } from "../../src/bridge/file-bridge.js";
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
readdirSync: vi.fn(),
|
||||
renameSync: vi.fn(),
|
||||
statSync: vi.fn(),
|
||||
chmodSync: vi.fn(),
|
||||
watch: vi.fn(),
|
||||
}));
|
||||
|
||||
const fs = {
|
||||
exists: vi.mocked(existsSync),
|
||||
mkdir: vi.mocked(mkdirSync),
|
||||
write: vi.mocked(writeFileSync),
|
||||
read: vi.mocked(readFileSync),
|
||||
unlink: vi.mocked(unlinkSync),
|
||||
readdir: vi.mocked(readdirSync),
|
||||
rename: vi.mocked(renameSync),
|
||||
stat: vi.mocked(statSync),
|
||||
chmod: vi.mocked(chmodSync),
|
||||
watch: vi.mocked(watch),
|
||||
};
|
||||
|
||||
describe("file bridge fallback and cleanup branches", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
fs.stat.mockReturnValue({
|
||||
uid: typeof process.getuid === "function" ? process.getuid() : 0,
|
||||
mode: 0o700,
|
||||
mtimeMs: Date.now(),
|
||||
} as ReturnType<typeof statSync>);
|
||||
fs.watch.mockImplementation(() => { throw new Error("watch unavailable"); });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("extends a busy operation, then reports a likely modal dialog", async () => {
|
||||
let busy = true;
|
||||
fs.exists.mockImplementation((path) => {
|
||||
const value = String(path);
|
||||
if (value.includes("res_")) return false;
|
||||
if (value.includes("busy_")) return busy;
|
||||
return true;
|
||||
});
|
||||
fs.stat.mockImplementation(() => ({
|
||||
uid: typeof process.getuid === "function" ? process.getuid() : 0,
|
||||
mode: 0o700,
|
||||
mtimeMs: Date.now(),
|
||||
} as ReturnType<typeof statSync>));
|
||||
|
||||
const response = sendCommand("var operation = true;", {
|
||||
tempDir: "/tmp/busy-bridge",
|
||||
timeoutMs: 100,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
busy = false;
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
|
||||
await expect(response).resolves.toMatchObject({
|
||||
success: false,
|
||||
error: expect.stringContaining("modal dialog"),
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores unrelated watch events and disables a failed watcher", async () => {
|
||||
let responseExists = false;
|
||||
let change: ((event: string, filename: string | Buffer | null) => void) | undefined;
|
||||
let watcherError: (() => void) | undefined;
|
||||
const watcher = {
|
||||
close: vi.fn(),
|
||||
on: vi.fn((event: string, callback: () => void) => {
|
||||
if (event === "error") watcherError = callback;
|
||||
return watcher;
|
||||
}),
|
||||
};
|
||||
fs.watch.mockImplementation(((_path, _options, callback) => {
|
||||
change = callback;
|
||||
return watcher;
|
||||
}) as typeof watch);
|
||||
fs.exists.mockImplementation((path) => String(path).includes("res_") ? responseExists : true);
|
||||
fs.read.mockReturnValue('{"success":true,"data":{"watched":true}}');
|
||||
|
||||
const response = sendCommand("var watched = true;", { tempDir: "/tmp/watch-bridge" });
|
||||
change?.("rename", null);
|
||||
change?.("rename", "unrelated.json");
|
||||
watcherError?.();
|
||||
responseExists = true;
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
await expect(response).resolves.toEqual({ success: true, data: { watched: true } });
|
||||
expect(watcher.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tolerates unlink failures and removes busy protocol files", async () => {
|
||||
fs.exists.mockReturnValue(true);
|
||||
fs.read.mockReturnValue('{"success":true}');
|
||||
fs.unlink.mockImplementation(() => { throw new Error("locked"); });
|
||||
|
||||
const response = sendCommand("var cleanup = true;", { tempDir: "/tmp/cleanup-bridge" });
|
||||
await expect(response).resolves.toEqual({ success: true });
|
||||
|
||||
fs.readdir.mockReturnValue([
|
||||
"busy_1.json", "cmd_1.jsx", "res_1.json", "helpers.jsx",
|
||||
] as ReturnType<typeof readdirSync>);
|
||||
expect(() => cleanupTempDir({ tempDir: "/tmp/cleanup-bridge" })).not.toThrow();
|
||||
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("busy_1.json"));
|
||||
});
|
||||
|
||||
it("skips POSIX ownership changes on Windows while checking response bounds", async () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
fs.exists.mockReturnValue(true);
|
||||
fs.read.mockReturnValue('{"success":true}');
|
||||
|
||||
await expect(sendCommand("var windows = true;", {
|
||||
tempDir: "C:\\Temp\\premiere-bridge",
|
||||
})).resolves.toEqual({ success: true });
|
||||
expect(fs.stat).toHaveBeenCalledWith(expect.stringContaining("res_"));
|
||||
expect(fs.chmod).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+612
@@ -0,0 +1,612 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
unlinkSync,
|
||||
readdirSync,
|
||||
renameSync,
|
||||
statSync,
|
||||
chmodSync,
|
||||
watch,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
getDarwinUserTempDirectory,
|
||||
getTempDir,
|
||||
getDefaultBridgeTempDir,
|
||||
getBridgeLiveness,
|
||||
MAX_BRIDGE_RESPONSE_BYTES,
|
||||
sendCommand,
|
||||
sendRawCommand,
|
||||
cleanupTempDir,
|
||||
} from "../../src/bridge/file-bridge.js";
|
||||
|
||||
// Mock fs module
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
unlinkSync: vi.fn(),
|
||||
readdirSync: vi.fn(),
|
||||
renameSync: vi.fn(),
|
||||
statSync: vi.fn(),
|
||||
chmodSync: vi.fn(),
|
||||
watch: vi.fn(() => {
|
||||
throw new Error("watch unavailable in unit-test fallback");
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedExistsSync = vi.mocked(existsSync);
|
||||
const mockedMkdirSync = vi.mocked(mkdirSync);
|
||||
const mockedWriteFileSync = vi.mocked(writeFileSync);
|
||||
const mockedReadFileSync = vi.mocked(readFileSync);
|
||||
const mockedUnlinkSync = vi.mocked(unlinkSync);
|
||||
const mockedReaddirSync = vi.mocked(readdirSync);
|
||||
const mockedRenameSync = vi.mocked(renameSync);
|
||||
const mockedStatSync = vi.mocked(statSync);
|
||||
const mockedChmodSync = vi.mocked(chmodSync);
|
||||
const mockedWatch = vi.mocked(watch);
|
||||
const mockedExecFileSync = vi.mocked(execFileSync);
|
||||
|
||||
// ensureDir on an existing dir stat-checks ownership; default to a dir owned by us
|
||||
// with safe perms so the existing tests exercise the happy path.
|
||||
const myUid = typeof process.getuid === "function" ? process.getuid() : 0;
|
||||
mockedStatSync.mockReturnValue({ uid: myUid, mode: 0o700 } as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
describe("getTempDir", () => {
|
||||
const originalEnv = process.env.PREMIERE_TEMP_DIR;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.PREMIERE_TEMP_DIR = originalEnv;
|
||||
} else {
|
||||
delete process.env.PREMIERE_TEMP_DIR;
|
||||
}
|
||||
});
|
||||
|
||||
it("returns custom dir from options", () => {
|
||||
expect(getTempDir({ tempDir: "/custom/dir" })).toBe("/custom/dir");
|
||||
});
|
||||
|
||||
it("returns env var when no options", () => {
|
||||
process.env.PREMIERE_TEMP_DIR = "/env/dir";
|
||||
expect(getTempDir()).toBe("/env/dir");
|
||||
});
|
||||
|
||||
it("returns env var when options have no tempDir", () => {
|
||||
process.env.PREMIERE_TEMP_DIR = "/env/dir";
|
||||
expect(getTempDir({})).toBe("/env/dir");
|
||||
});
|
||||
|
||||
it("returns default when no options or env", () => {
|
||||
delete process.env.PREMIERE_TEMP_DIR;
|
||||
const result = getTempDir();
|
||||
expect(result).toBe(join(tmpdir(), "premiere-mcp-bridge"));
|
||||
});
|
||||
|
||||
it("uses the Darwin per-user temporary root when a GUI client omits TMPDIR", () => {
|
||||
expect(getDefaultBridgeTempDir(
|
||||
"darwin",
|
||||
"/tmp",
|
||||
() => "/var/folders/example/T",
|
||||
{},
|
||||
).replaceAll("\\", "/")).toBe("/var/folders/example/T/premiere-mcp-bridge");
|
||||
});
|
||||
|
||||
it("falls back safely when the Darwin temporary-root lookup is unavailable", () => {
|
||||
expect(getDefaultBridgeTempDir("darwin", "/tmp", () => null, {}).replaceAll("\\", "/"))
|
||||
.toBe("/tmp/premiere-mcp-bridge");
|
||||
});
|
||||
|
||||
it("preserves a configured Darwin temporary root", () => {
|
||||
expect(getDefaultBridgeTempDir(
|
||||
"darwin",
|
||||
"/configured/T",
|
||||
() => "/var/folders/example/T",
|
||||
{ TMPDIR: "/configured/T" },
|
||||
).replaceAll("\\", "/")).toBe("/configured/T/premiere-mcp-bridge");
|
||||
});
|
||||
|
||||
it("prefers options.tempDir over env var", () => {
|
||||
process.env.PREMIERE_TEMP_DIR = "/env/dir";
|
||||
expect(getTempDir({ tempDir: "/custom/dir" })).toBe("/custom/dir");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDarwinUserTempDirectory", () => {
|
||||
it("uses the per-user macOS temporary root reported by getconf", () => {
|
||||
mockedExecFileSync.mockReturnValue("/var/folders/example/T/\n" as never);
|
||||
|
||||
expect(getDarwinUserTempDirectory()).toBe("/var/folders/example/T/");
|
||||
expect(mockedExecFileSync).toHaveBeenCalledWith("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a non-absolute getconf result", () => {
|
||||
mockedExecFileSync.mockReturnValue("relative-temp\n" as never);
|
||||
|
||||
expect(getDarwinUserTempDirectory()).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back when the getconf lookup fails", () => {
|
||||
mockedExecFileSync.mockImplementation(() => { throw new Error("unavailable"); });
|
||||
|
||||
expect(getDarwinUserTempDirectory()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendCommand", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
mockedStatSync.mockReturnValue({ uid: myUid, mode: 0o700 } as unknown as ReturnType<typeof statSync>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("creates temp directory if it does not exist", async () => {
|
||||
let dirCreated = false;
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const p = String(path);
|
||||
// The temp dir itself does not exist (until mkdirSync is called)
|
||||
if (p === "/tmp/test-bridge" && !dirCreated) return false;
|
||||
if (p.includes("res_")) return true; // response exists immediately
|
||||
return true;
|
||||
});
|
||||
mockedMkdirSync.mockImplementation(() => {
|
||||
dirCreated = true;
|
||||
return undefined;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"ok":true}}');
|
||||
|
||||
const promise = sendCommand("test script", { tempDir: "/tmp/test-bridge" });
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await promise;
|
||||
|
||||
expect(mockedMkdirSync).toHaveBeenCalledWith("/tmp/test-bridge", {
|
||||
recursive: true,
|
||||
mode: 0o700,
|
||||
});
|
||||
});
|
||||
|
||||
// Security: the bridge temp dir sits at a predictable, world-accessible path, and the
|
||||
// CEP panel executes any cmd_*.jsx it finds there. On shared machines that dir must be
|
||||
// ours and private. See the ensureDir hardening.
|
||||
it("refuses to use an existing temp dir owned by another user", async () => {
|
||||
if (typeof process.getuid !== "function") return; // POSIX-only guard
|
||||
mockedExistsSync.mockReturnValue(true); // dir already exists
|
||||
mockedStatSync.mockReturnValueOnce({
|
||||
uid: process.getuid!() + 1, // someone else owns it
|
||||
mode: 0o700,
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
await expect(sendCommand("var x = 1;", { tempDir: "/tmp/evil-bridge" })).rejects.toThrow(
|
||||
/owned by uid .* not this user/
|
||||
);
|
||||
});
|
||||
|
||||
it("clamps a group/world-accessible existing temp dir back to 0700", async () => {
|
||||
if (typeof process.getuid !== "function") return;
|
||||
mockedExistsSync.mockImplementation((p) => (String(p).includes("res_") ? true : true));
|
||||
mockedStatSync.mockReturnValueOnce({
|
||||
uid: process.getuid!(),
|
||||
mode: 0o755, // ours, but world-readable
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
|
||||
|
||||
const promise = sendCommand("var x = 1;", { tempDir: "/tmp/test-bridge" });
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await promise;
|
||||
|
||||
expect(mockedChmodSync).toHaveBeenCalledWith("/tmp/test-bridge", 0o700);
|
||||
});
|
||||
|
||||
it("atomically publishes a complete command file as .jsx", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
if (String(path).includes("res_")) return true;
|
||||
return true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
|
||||
|
||||
const promise = sendCommand("var x = 1;", { tempDir: "/tmp/test-bridge" });
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await promise;
|
||||
|
||||
const writeCall = mockedWriteFileSync.mock.calls.find(([path]) => String(path).endsWith(".jsx.staged"));
|
||||
expect(writeCall).toBeDefined();
|
||||
expect(String(writeCall?.[0])).toMatch(/cmd_.*\.jsx\.staged$/);
|
||||
const publishedPath = mockedRenameSync.mock.calls[0]?.[1];
|
||||
expect(String(publishedPath)).toMatch(/cmd_.*\.jsx$/);
|
||||
// command = one-line helpers bootstrap, then the script itself
|
||||
const content = String(writeCall?.[1]);
|
||||
expect(content.endsWith("\nvar x = 1;")).toBe(true);
|
||||
expect(content.replaceAll("\\\\", "/")).toContain('$.evalFile("/tmp/test-bridge/helpers_');
|
||||
expect(writeCall?.[2]).toBe("utf-8");
|
||||
});
|
||||
|
||||
it("returns parsed JSON response", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
if (String(path).includes("res_")) return true;
|
||||
return true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"version":"24.0"}}');
|
||||
|
||||
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual({ success: true, data: { version: "24.0" } });
|
||||
});
|
||||
|
||||
it("rejects an oversized bridge response before reading it into memory", async () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedStatSync.mockReturnValue({
|
||||
uid: myUid,
|
||||
mode: 0o700,
|
||||
size: MAX_BRIDGE_RESPONSE_BYTES + 1,
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
await expect(sendCommand("test", { tempDir: "/tmp/test-bridge" })).resolves.toEqual({
|
||||
success: false,
|
||||
error: `Bridge response exceeds the ${MAX_BRIDGE_RESPONSE_BYTES}-byte limit`,
|
||||
});
|
||||
expect(mockedReadFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses distinct cryptographic IDs for concurrently-created command files", async () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReadFileSync.mockReturnValue('{"success":true}');
|
||||
|
||||
await Promise.all([
|
||||
sendCommand("first", { tempDir: "/tmp/test-bridge" }),
|
||||
sendCommand("second", { tempDir: "/tmp/test-bridge" }),
|
||||
]);
|
||||
|
||||
const publishedPaths = mockedRenameSync.mock.calls.map(([, target]) => String(target));
|
||||
expect(publishedPaths).toHaveLength(2);
|
||||
expect(new Set(publishedPaths).size).toBe(2);
|
||||
expect(publishedPaths.every((path) => /cmd_[0-9a-f-]{36}\.jsx$/.test(path))).toBe(true);
|
||||
});
|
||||
|
||||
it("attempts event-driven response watching before using the polling fallback", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => String(path).includes("res_"));
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
|
||||
|
||||
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
await promise;
|
||||
|
||||
expect(mockedWatch).toHaveBeenCalledWith(
|
||||
dirname(join("/tmp/test-bridge", "response.json")),
|
||||
{ persistent: false },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves immediately when the watcher reports the response file", async () => {
|
||||
let responseExists = false;
|
||||
let onChange: ((event: string, filename: string) => void) | undefined;
|
||||
const fakeWatcher = { on: vi.fn().mockReturnThis(), close: vi.fn() };
|
||||
mockedWatch.mockImplementationOnce(((_path, _options, listener) => {
|
||||
onChange = listener as (event: string, filename: string) => void;
|
||||
return fakeWatcher;
|
||||
}) as typeof watch);
|
||||
mockedExistsSync.mockImplementation((path) =>
|
||||
String(path).includes("res_") ? responseExists : true,
|
||||
);
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"eventDriven":true}}');
|
||||
|
||||
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
|
||||
const responsePath = mockedWatch.mock.calls[0]?.[0];
|
||||
expect(responsePath).toBeDefined();
|
||||
responseExists = true;
|
||||
const commandPath = String(mockedRenameSync.mock.calls[0]?.[1]);
|
||||
const responseName = commandPath.replace(/.*[\\/]+cmd_/, "res_").replace(/\.jsx$/, ".json");
|
||||
onChange?.("rename", responseName);
|
||||
|
||||
await expect(promise).resolves.toEqual({ success: true, data: { eventDriven: true } });
|
||||
expect(fakeWatcher.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("serializes concurrent bridge commands before they reach CEP", async () => {
|
||||
const readyResponses = new Set<string>();
|
||||
let onChange: ((event: string, filename: string) => void) | undefined;
|
||||
const fakeWatcher = { on: vi.fn().mockReturnThis(), close: vi.fn() };
|
||||
mockedWatch.mockImplementation(((_path, _options, listener) => {
|
||||
onChange = listener as (event: string, filename: string) => void;
|
||||
return fakeWatcher;
|
||||
}) as typeof watch);
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
const value = String(path);
|
||||
return value.includes("res_") ? readyResponses.has(value) : true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{"shared":true}}');
|
||||
|
||||
const pending = [
|
||||
sendCommand("first", { tempDir: "/tmp/shared-watch-bridge" }),
|
||||
sendCommand("second", { tempDir: "/tmp/shared-watch-bridge" }),
|
||||
];
|
||||
expect(mockedWatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockedRenameSync).toHaveBeenCalledTimes(1);
|
||||
|
||||
const signalResponse = (commandPath: unknown) => {
|
||||
const responsePath = String(commandPath)
|
||||
.replace(/cmd_/, "res_")
|
||||
.replace(/\.jsx$/, ".json");
|
||||
readyResponses.add(responsePath);
|
||||
onChange?.("rename", responsePath.split(/[\\/]/).pop()!);
|
||||
};
|
||||
signalResponse(mockedRenameSync.mock.calls[0]?.[1]);
|
||||
await expect(pending[0]).resolves.toEqual({ success: true, data: { shared: true } });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(mockedRenameSync).toHaveBeenCalledTimes(2);
|
||||
expect(mockedWatch).toHaveBeenCalledTimes(2);
|
||||
signalResponse(mockedRenameSync.mock.calls[1]?.[1]);
|
||||
|
||||
await expect(pending[1]).resolves.toEqual({ success: true, data: { shared: true } });
|
||||
expect(fakeWatcher.close).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("fails fast when a bridge directory already has the bounded command backlog", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => !String(path).includes("res_"));
|
||||
const commands = Array.from({ length: 34 }, (_, index) => sendCommand(`queued-${index}`, {
|
||||
tempDir: "/tmp/queue-capacity-bridge",
|
||||
}));
|
||||
|
||||
await expect(commands[33]).resolves.toEqual({
|
||||
success: false,
|
||||
error: "Bridge command queue is full (32 waiting); retry after an active command finishes",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps polling a malformed response without resending the command", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
if (String(path).includes("res_")) return true;
|
||||
if (String(path).includes("busy_")) return false;
|
||||
return true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue("not valid json{{{");
|
||||
|
||||
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge", timeoutMs: 500 });
|
||||
await vi.advanceTimersByTimeAsync(700);
|
||||
const result = await promise;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Failed to parse response");
|
||||
expect(mockedRenameSync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns timeout error when response file never appears", async () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
|
||||
const promise = sendCommand("test", {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
timeoutMs: 500,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(600);
|
||||
const result = await promise;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("timed out");
|
||||
expect(result.error).toContain("CEP plugin");
|
||||
});
|
||||
|
||||
it("fails health-style commands before publication when a current connector is waiting", async () => {
|
||||
vi.setSystemTime(new Date(10_000));
|
||||
mockedExistsSync.mockImplementation((path) => String(path).includes("bridge-heartbeat"));
|
||||
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"waiting"}');
|
||||
mockedStatSync.mockReturnValue({
|
||||
uid: myUid,
|
||||
mode: 0o700,
|
||||
mtimeMs: 9_500,
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
await expect(sendCommand("var health = true;", {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
failFastOnUnreadyHeartbeat: true,
|
||||
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("not running") });
|
||||
expect(mockedRenameSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails health-style commands before publication when a known connector heartbeat is stale", async () => {
|
||||
vi.setSystemTime(new Date(10_000));
|
||||
mockedExistsSync.mockImplementation((path) => String(path).includes("bridge-heartbeat"));
|
||||
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"running"}');
|
||||
mockedStatSync.mockReturnValue({
|
||||
uid: myUid,
|
||||
mode: 0o700,
|
||||
mtimeMs: 1_000,
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
|
||||
await expect(sendCommand("var health = true;", {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
failFastOnUnreadyHeartbeat: true,
|
||||
})).resolves.toMatchObject({ success: false, error: expect.stringContaining("heartbeat is stale") });
|
||||
expect(mockedRenameSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to normal command delivery when no heartbeat exists", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => String(path).includes("res_"));
|
||||
mockedReadFileSync.mockReturnValue('{"success":true}');
|
||||
|
||||
await expect(sendCommand("var legacy = true;", {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
failFastOnUnreadyHeartbeat: true,
|
||||
})).resolves.toEqual({ success: true });
|
||||
expect(mockedRenameSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cleans up command and response files after success", async () => {
|
||||
let responseExists = false;
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
if (String(path).includes("res_")) return responseExists;
|
||||
return true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
|
||||
|
||||
const promise = sendCommand("test", { tempDir: "/tmp/test-bridge" });
|
||||
responseExists = true;
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
await promise;
|
||||
|
||||
// unlinkSync should be called for both cmd and res files
|
||||
expect(mockedUnlinkSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects scripts containing eval()", async () => {
|
||||
await expect(
|
||||
sendCommand('eval("dangerous")', { tempDir: "/tmp/test-bridge" })
|
||||
).rejects.toThrow("blocked pattern");
|
||||
});
|
||||
|
||||
it("rejects scripts containing new Function()", async () => {
|
||||
await expect(
|
||||
sendCommand('new Function("code")', { tempDir: "/tmp/test-bridge" })
|
||||
).rejects.toThrow("blocked pattern");
|
||||
});
|
||||
|
||||
it("rejects scripts containing System.callSystem()", async () => {
|
||||
await expect(
|
||||
sendCommand('System.callSystem("rm -rf /")', {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
})
|
||||
).rejects.toThrow("blocked pattern");
|
||||
});
|
||||
|
||||
it("rejects scripts exceeding 500KB", async () => {
|
||||
const largeScript = "x".repeat(501 * 1024);
|
||||
await expect(
|
||||
sendCommand(largeScript, { tempDir: "/tmp/test-bridge" })
|
||||
).rejects.toThrow("500KB size limit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBridgeLiveness", () => {
|
||||
it("reports fresh, stale, and unknown states without exposing heartbeat contents", () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReadFileSync.mockReturnValue('{"protocolVersion":1,"state":"running"}');
|
||||
mockedStatSync.mockReturnValue({ mtimeMs: 9_000 } as ReturnType<typeof statSync>);
|
||||
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
|
||||
state: "running",
|
||||
ageMs: 1_000,
|
||||
});
|
||||
|
||||
mockedStatSync.mockReturnValue({ mtimeMs: 1_000 } as ReturnType<typeof statSync>);
|
||||
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
|
||||
state: "stale",
|
||||
ageMs: 9_000,
|
||||
});
|
||||
|
||||
mockedReadFileSync.mockReturnValue('{"state":"running"}');
|
||||
expect(getBridgeLiveness({ tempDir: "/tmp/test-bridge" }, 10_000)).toEqual({
|
||||
state: "unknown",
|
||||
ageMs: null,
|
||||
});
|
||||
|
||||
// This module-level fs mock is shared with the following raw-command
|
||||
// suite, which exercises the POSIX ownership guard.
|
||||
mockedStatSync.mockReturnValue({
|
||||
uid: myUid,
|
||||
mode: 0o700,
|
||||
mtimeMs: Date.now(),
|
||||
} as unknown as ReturnType<typeof statSync>);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendRawCommand", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("allows eval() in raw commands", async () => {
|
||||
mockedExistsSync.mockImplementation((path) => {
|
||||
if (String(path).includes("res_")) return true;
|
||||
return true;
|
||||
});
|
||||
mockedReadFileSync.mockReturnValue('{"success":true,"data":{}}');
|
||||
|
||||
const promise = sendRawCommand('eval("1+1")', {
|
||||
tempDir: "/tmp/test-bridge",
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
const result = await promise;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("still enforces size limit on raw commands", async () => {
|
||||
const largeScript = "x".repeat(501 * 1024);
|
||||
await expect(
|
||||
sendRawCommand(largeScript, { tempDir: "/tmp/test-bridge" })
|
||||
).rejects.toThrow("500KB size limit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanupTempDir", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("removes cmd_ and res_ files", () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReaddirSync.mockReturnValue([
|
||||
"cmd_123.jsx" as any,
|
||||
"cmd_124.jsx.staged" as any,
|
||||
"res_123.json" as any,
|
||||
"other_file.txt" as any,
|
||||
]);
|
||||
|
||||
cleanupTempDir({ tempDir: "/tmp/test-bridge" });
|
||||
|
||||
// Should unlink cmd_ and res_ files but not other_file.txt
|
||||
const unlinkCalls = mockedUnlinkSync.mock.calls.map((c) => String(c[0]));
|
||||
expect(unlinkCalls).toContainEqual(
|
||||
join("/tmp/test-bridge", "cmd_123.jsx")
|
||||
);
|
||||
expect(unlinkCalls).toContainEqual(
|
||||
join("/tmp/test-bridge", "res_123.json")
|
||||
);
|
||||
expect(unlinkCalls).toContainEqual(
|
||||
join("/tmp/test-bridge", "cmd_124.jsx.staged")
|
||||
);
|
||||
expect(unlinkCalls).not.toContainEqual(
|
||||
join("/tmp/test-bridge", "other_file.txt")
|
||||
);
|
||||
});
|
||||
|
||||
it("does nothing if temp dir does not exist", () => {
|
||||
mockedExistsSync.mockReturnValue(false);
|
||||
cleanupTempDir({ tempDir: "/tmp/nonexistent" });
|
||||
expect(mockedReaddirSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles errors gracefully", () => {
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
mockedReaddirSync.mockImplementation(() => {
|
||||
throw new Error("permission denied");
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => cleanupTempDir({ tempDir: "/tmp/test-bridge" })).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildScript, escapeForExtendScript, buildToolScript, getHelpersSource, buildBootstrap, helpersFileName, HELPERS_VERSION } from "../../src/bridge/script-builder.js";
|
||||
import { runInNewContext } from "node:vm";
|
||||
|
||||
describe("buildScript", () => {
|
||||
it("wraps code in an IIFE with try/catch", () => {
|
||||
const result = buildScript("return __result({ ok: true });");
|
||||
expect(result).toContain("(function() {");
|
||||
expect(result).toContain("return __result({ ok: true });");
|
||||
expect(result).toContain("} catch(e) {");
|
||||
expect(result).toContain("return __error(e.toString());");
|
||||
expect(result).toContain("})();");
|
||||
});
|
||||
|
||||
it("defines helper functions in the helpers source", () => {
|
||||
const result = getHelpersSource();
|
||||
expect(result).toContain("var TICKS_PER_SECOND = 254016000000;");
|
||||
expect(result).toContain("function __ticksToSeconds(ticks)");
|
||||
expect(result).toContain("function __secondsToTicks(seconds)");
|
||||
expect(result).toContain("function __ticksToTimecode(ticks, fps)");
|
||||
expect(result).toContain("function __pad(n)");
|
||||
expect(result).toContain("function __findSequence(idOrName)");
|
||||
expect(result).toContain("function __findProjectItem(nodeIdOrName, rootItem)");
|
||||
expect(result).toContain("function __findClip(nodeId)");
|
||||
expect(result).toContain("function __getAllClips(seq)");
|
||||
expect(result).toContain("function __jsonStringify(obj)");
|
||||
expect(result).toContain("function __result(data)");
|
||||
expect(result).toContain("function __error(msg)");
|
||||
});
|
||||
|
||||
it("preserves multi-line code blocks", () => {
|
||||
const code = `var x = 1;
|
||||
var y = 2;
|
||||
return __result({ sum: x + y });`;
|
||||
const result = buildScript(code);
|
||||
expect(result).toContain("var x = 1;");
|
||||
expect(result).toContain("var y = 2;");
|
||||
expect(result).toContain("return __result({ sum: x + y });");
|
||||
});
|
||||
|
||||
it("handles empty code", () => {
|
||||
const result = buildScript("");
|
||||
expect(result).toContain("(function() {");
|
||||
expect(result).toContain("})();");
|
||||
});
|
||||
|
||||
it("returns a string", () => {
|
||||
expect(typeof buildScript("")).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildToolScript (alias)", () => {
|
||||
it("is the same function as buildScript", () => {
|
||||
expect(buildToolScript).toBe(buildScript);
|
||||
});
|
||||
|
||||
it("produces identical output to buildScript", () => {
|
||||
const code = "return __result({ test: true });";
|
||||
expect(buildToolScript(code)).toBe(buildScript(code));
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeForExtendScript", () => {
|
||||
it("escapes backslashes", () => {
|
||||
expect(escapeForExtendScript("C:\\Users\\test")).toBe("C:\\\\Users\\\\test");
|
||||
});
|
||||
|
||||
it("escapes double quotes", () => {
|
||||
expect(escapeForExtendScript('say "hello"')).toBe('say \\"hello\\"');
|
||||
});
|
||||
|
||||
it("escapes single quotes", () => {
|
||||
expect(escapeForExtendScript("it's")).toBe("it\\'s");
|
||||
});
|
||||
|
||||
it("escapes newlines", () => {
|
||||
expect(escapeForExtendScript("line1\nline2")).toBe("line1\\nline2");
|
||||
});
|
||||
|
||||
it("escapes carriage returns", () => {
|
||||
expect(escapeForExtendScript("line1\rline2")).toBe("line1\\rline2");
|
||||
});
|
||||
|
||||
it("escapes tabs", () => {
|
||||
expect(escapeForExtendScript("col1\tcol2")).toBe("col1\\tcol2");
|
||||
});
|
||||
|
||||
it("handles empty strings", () => {
|
||||
expect(escapeForExtendScript("")).toBe("");
|
||||
});
|
||||
|
||||
it("handles strings with no special characters", () => {
|
||||
expect(escapeForExtendScript("hello world")).toBe("hello world");
|
||||
});
|
||||
|
||||
it("handles multiple escape characters in one string", () => {
|
||||
const input = 'C:\\path\\to\n"file"\t\'test\'';
|
||||
const result = escapeForExtendScript(input);
|
||||
expect(result).toBe('C:\\\\path\\\\to\\n\\"file\\"\\t\\\'test\\\'');
|
||||
});
|
||||
|
||||
it("handles unicode characters (passes through)", () => {
|
||||
expect(escapeForExtendScript("日本語")).toBe("日本語");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generated script structure", () => {
|
||||
it("bootstrap loads this exact helpers version via $.evalFile", () => {
|
||||
const bootstrap = buildBootstrap("/tmp/x/" + helpersFileName());
|
||||
expect(bootstrap).toContain(`__HELPERS_V !== "${HELPERS_VERSION}"`);
|
||||
expect(bootstrap).toContain(`$.evalFile("/tmp/x/helpers_${HELPERS_VERSION}.jsx")`);
|
||||
expect(getHelpersSource()).toContain(`var __HELPERS_V = "${HELPERS_VERSION}";`);
|
||||
});
|
||||
|
||||
it("bootstrap escapes quotes and backslashes in the helpers path", () => {
|
||||
const bootstrap = buildBootstrap('C:\\temp\\he"rs.jsx');
|
||||
expect(bootstrap).toContain('$.evalFile("C:\\\\temp\\\\he\\"rs.jsx")');
|
||||
});
|
||||
|
||||
it("__findProjectItem recursively searches bins", () => {
|
||||
const result = getHelpersSource();
|
||||
expect(result).toContain("if (item.type === 2)");
|
||||
expect(result).toContain("var found = __findProjectItem(nodeIdOrName, item);");
|
||||
});
|
||||
|
||||
it("normalizes numeric host IDs without changing exact name matching", () => {
|
||||
const result = getHelpersSource();
|
||||
expect(result).toContain("var wantedId = String(idOrName);");
|
||||
expect(result).toContain("String(seq.sequenceID) === wantedId || seq.name === idOrName");
|
||||
expect(result).toContain("var wantedId = String(nodeIdOrName);");
|
||||
expect(result).toContain("String(item.nodeId) === wantedId || item.name === nodeIdOrName");
|
||||
expect(result).toContain("var wantedId = String(nodeId);");
|
||||
expect(result).toContain("String(clip.nodeId) === wantedId");
|
||||
});
|
||||
|
||||
it("__findClip searches both video and audio tracks", () => {
|
||||
const result = getHelpersSource();
|
||||
expect(result).toContain("seq.videoTracks.numTracks");
|
||||
expect(result).toContain("seq.audioTracks.numTracks");
|
||||
expect(result).toContain('trackType: "video"');
|
||||
expect(result).toContain('trackType: "audio"');
|
||||
});
|
||||
|
||||
it("__jsonStringify handles all types", () => {
|
||||
const result = getHelpersSource();
|
||||
expect(result).toContain('if (obj === null) return "null"');
|
||||
expect(result).toContain('if (typeof obj === "string")');
|
||||
expect(result).toContain('if (typeof obj === "number"');
|
||||
expect(result).toContain("if (obj instanceof Array)");
|
||||
expect(result).toContain('if (typeof obj === "object")');
|
||||
});
|
||||
});
|
||||
|
||||
describe("helpers execute correctly in an ES3-like engine", () => {
|
||||
it("__result works when JSON is undefined (polyfill must not recurse into itself)", () => {
|
||||
// Regression: __jsonStringify once delegated to JSON.stringify while the JSON
|
||||
// polyfill delegated back to __jsonStringify — infinite mutual recursion that
|
||||
// stack-overran the shared ExtendScript engine on every tool response.
|
||||
const sandbox: Record<string, unknown> = { JSON: undefined };
|
||||
const out = runInNewContext(
|
||||
getHelpersSource() + '\n__result({ connected: true, nested: { n: 1, arr: [1, "a", false, null] } });',
|
||||
sandbox
|
||||
);
|
||||
expect(out).toBe('{"success":true,"data":{"connected":true,"nested":{"n":1,"arr":[1,"a",false,null]}}}');
|
||||
});
|
||||
|
||||
it("a stale wrapper from an older helpers version gets replaced", () => {
|
||||
// Simulate a polluted long-lived engine: JSON.stringify is our old-style wrapper.
|
||||
const stale = { stringify: function badWrapper(o: unknown) { return "__jsonStringify" + String(o); } };
|
||||
const sandbox: Record<string, unknown> = { JSON: stale };
|
||||
runInNewContext(getHelpersSource(), sandbox);
|
||||
const json = sandbox.JSON as { stringify: (o: unknown) => string; __mcpPolyfill?: boolean };
|
||||
expect(json.__mcpPolyfill).toBe(true);
|
||||
expect(json.stringify({ ok: 1 })).toBe('{"ok":1}');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user