import { describe, expect, test, vi } from "vitest"; import type { PluginCapabilityGrant, PluginManifest } from "@toolctrl/plugin-runtime"; import { handlePluginInvokeJob, type PluginInvokeDeps, type PluginInvokeJobPayload, type PluginInvokeRow } from "./plugin-invoke.js"; function manifest(): PluginManifest { return { schemaVersion: 1, id: "de.toolctrl.test", version: "1.0.0", name: "Test", entry: "plugin.wasm", wasmSha256: "0".repeat(64), signature: "0".repeat(128), signedBy: "0".repeat(64), allowedHosts: [], capabilities: [{ name: "log" }], hooks: [{ name: "ping", export: "ping" }], }; } function hookOnlyManifest(): PluginManifest { return { ...manifest(), capabilities: [], hooks: [{ name: "tenant.created", export: "onTenantCreated" }], }; } function outboundManifest(): PluginManifest { return { ...manifest(), capabilities: [{ name: "http.outbound", scope: { allowedHosts: ["api.example.com"] } }], }; } function row(): PluginInvokeRow { return { tenantId: "t-1", pluginId: "de.toolctrl.test", version: "1.0.0", manifest: manifest(), wasmSha256: "0".repeat(64), signaturePublicKey: "0".repeat(64), enabled: true, }; } function grants(): PluginCapabilityGrant[] { return [ { capabilityName: "log", scopeJson: null }, { capabilityName: "hook:ping", scopeJson: null }, ]; } function payload(): PluginInvokeJobPayload { return { tenantId: "t-1", pluginId: "de.toolctrl.test", version: "1.0.0", hookName: "ping", input: { x: 1 }, }; } function defaultDeps(overrides: Partial = {}): PluginInvokeDeps { return { loadPlugin: vi.fn().mockResolvedValue(row()), loadCapabilityGrants: vi.fn().mockResolvedValue(grants()), loadBinary: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), verifySignature: vi.fn().mockReturnValue({ ok: true }), consumeRateLimit: vi.fn().mockResolvedValue({ allowed: true, remaining: 99 }), invokeInSandbox: vi.fn().mockResolvedValue({ ok: true, output: "pong", fuelUsed: 5 }), recordAudit: vi.fn().mockResolvedValue(undefined), trustedPublicKeysHex: ["0".repeat(64)], now: (() => { let n = 1_000; return () => (n += 10); })(), ...overrides, }; } describe("handlePluginInvokeJob", () => { test("happy path returns the sandbox output and records audit", async () => { const deps = defaultDeps(); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(true); expect(result.pluginInvocationId).toMatch(/^[0-9a-f-]{36}$/); if (result.ok) expect(result.output).toBe("pong"); expect(deps.recordAudit).toHaveBeenCalledTimes(1); expect(deps.invokeInSandbox).toHaveBeenCalledTimes(1); expect(deps.loadCapabilityGrants).toHaveBeenCalledWith({ tenantId: "t-1", pluginId: "de.toolctrl.test", }); const sandboxArgs = vi.mocked(deps.invokeInSandbox).mock.calls[0]?.[0]; const auditArgs = vi.mocked(deps.recordAudit).mock.calls[0]?.[0]; expect(sandboxArgs?.pluginInvocationId).toBe(result.pluginInvocationId); expect(sandboxArgs?.capabilityGrants).toEqual([ { capabilityName: "log", scopeJson: null }, { capabilityName: "hook:ping", scopeJson: null }, ]); expect(auditArgs?.pluginInvocationId).toBe(result.pluginInvocationId); expect(auditArgs?.outcome.pluginInvocationId).toBe(result.pluginInvocationId); }); test("returns plugin_not_found when the row is missing", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue(null) }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe("plugin_not_found"); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); expect(deps.recordAudit).toHaveBeenCalledTimes(1); }); test("returns plugin_disabled when the row is disabled", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), enabled: false }) }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe("plugin_disabled"); expect(deps.loadCapabilityGrants).not.toHaveBeenCalled(); }); test("rejects queue hook names that are not declared as manifest exports", async () => { const deps = defaultDeps(); const result = await handlePluginInvokeJob({ ...payload(), hookName: "rogueExport" }, deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("hook_not_declared"); expect(result.message).toBe('Hook export "rogueExport" is not declared by plugin manifest.'); } expect(deps.loadCapabilityGrants).not.toHaveBeenCalled(); expect(deps.loadBinary).not.toHaveBeenCalled(); expect(deps.consumeRateLimit).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); expect(deps.recordAudit).toHaveBeenCalledTimes(1); }); test("allows manifest hook exports even when logical hook names differ", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), manifest: { ...manifest(), hooks: [{ name: "tenant.created", export: "onTenantCreated" }] }, }), loadCapabilityGrants: vi.fn().mockResolvedValue([ { capabilityName: "log", scopeJson: null }, { capabilityName: "hook:tenant.created", scopeJson: null }, ]), }); const result = await handlePluginInvokeJob( { ...payload(), hookName: "onTenantCreated", metadata: { logicalHookName: "tenant.created" }, input: { tenantId: "t-1", slug: "acme", ownerEmail: "owner@example.com", createdAt: "2026-05-28T10:00:00.000Z" }, }, deps ); expect(result.ok).toBe(true); expect(deps.invokeInSandbox).toHaveBeenCalledWith( expect.objectContaining({ hookName: "onTenantCreated", payload: expect.objectContaining({ tenantId: "t-1", slug: "acme" }), }) ); }); test("returns capability_grant_missing before loading the binary", async () => { const deps = defaultDeps({ loadCapabilityGrants: vi.fn().mockResolvedValue([]) }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("capability_grant_missing"); expect(result.message).toContain("log"); } expect(deps.loadBinary).not.toHaveBeenCalled(); expect(deps.consumeRateLimit).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); expect(deps.recordAudit).toHaveBeenCalledTimes(1); }); test("treats stale grant scopes as missing consent", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), manifest: outboundManifest() }), loadCapabilityGrants: vi .fn() .mockResolvedValue([{ capabilityName: "http.outbound", scopeJson: { allowedHosts: ["api.example.com", "internal.example"] } }]), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("capability_grant_missing"); expect(result.message).toContain("http.outbound"); } expect(deps.loadBinary).not.toHaveBeenCalled(); }); test("requires hook grants even when the manifest declares no capabilities", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), manifest: hookOnlyManifest() }), loadCapabilityGrants: vi.fn().mockResolvedValue([]), }); const result = await handlePluginInvokeJob({ ...payload(), hookName: "onTenantCreated", metadata: { logicalHookName: "tenant.created" } }, deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("capability_grant_missing"); expect(result.message).toContain("hook:tenant.created"); } expect(deps.loadCapabilityGrants).toHaveBeenCalledWith({ tenantId: "t-1", pluginId: "de.toolctrl.test" }); expect(deps.loadBinary).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); }); test("runs hook-only plugins when the logical hook grant is present", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), manifest: hookOnlyManifest() }), loadCapabilityGrants: vi.fn().mockResolvedValue([{ capabilityName: "hook:tenant.created", scopeJson: null }]), }); const result = await handlePluginInvokeJob( { ...payload(), hookName: "onTenantCreated", metadata: { logicalHookName: "tenant.created" }, input: { tenantId: "t-1", slug: "acme", plan: null, createdAt: "2026-05-28T10:00:00.000Z" }, }, deps ); expect(result.ok).toBe(true); expect(deps.invokeInSandbox).toHaveBeenCalledWith( expect.objectContaining({ hookName: "onTenantCreated", capabilityGrants: [{ capabilityName: "hook:tenant.created", scopeJson: null }], }) ); }); test("rejects invalid registered hook payloads before loading the binary", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), manifest: hookOnlyManifest() }), loadCapabilityGrants: vi.fn().mockResolvedValue([{ capabilityName: "hook:tenant.created", scopeJson: null }]), }); const result = await handlePluginInvokeJob( { ...payload(), hookName: "onTenantCreated", metadata: { logicalHookName: "tenant.created" }, input: { tenantId: "t-1" }, }, deps ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("invalid_hook_payload"); expect(result.message).toBe('Invalid payload for plugin hook "tenant.created".'); } expect(deps.loadBinary).not.toHaveBeenCalled(); expect(deps.verifySignature).not.toHaveBeenCalled(); expect(deps.consumeRateLimit).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); expect(deps.recordAudit).toHaveBeenCalledTimes(1); }); test("returns binary_load_failed when the .wasm bytes cannot be loaded", async () => { const deps = defaultDeps({ loadBinary: vi.fn().mockResolvedValue(null) }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe("binary_load_failed"); }); test("returns signature_invalid when verifySignature rejects", async () => { const deps = defaultDeps({ verifySignature: vi.fn().mockReturnValue({ ok: false, reason: "public_key_not_trusted" }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("signature_invalid"); expect(result.message).toBe("public_key_not_trusted"); } expect(deps.invokeInSandbox).not.toHaveBeenCalled(); }); test("rejects rows whose stored wasm hash no longer matches the manifest", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), wasmSha256: "1".repeat(64) }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("signature_invalid"); expect(result.message).toBe("stored_wasm_sha256_mismatch"); } expect(deps.verifySignature).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); }); test("rejects rows whose stored signing key no longer matches the manifest", async () => { const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue({ ...row(), signaturePublicKey: "1".repeat(64) }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("signature_invalid"); expect(result.message).toBe("stored_signature_public_key_mismatch"); } expect(deps.verifySignature).not.toHaveBeenCalled(); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); }); test("returns rate_limit_exceeded when the limiter denies", async () => { const deps = defaultDeps({ consumeRateLimit: vi.fn().mockResolvedValue({ allowed: false, remaining: 0 }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe("rate_limit_exceeded"); expect(deps.invokeInSandbox).not.toHaveBeenCalled(); }); test("returns sandbox_failed when the sandbox reports failure", async () => { const deps = defaultDeps({ invokeInSandbox: vi.fn().mockResolvedValue({ ok: false, reason: "trap", fuelUsed: 12 }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("sandbox_failed"); expect(result.message).toBe("trap"); } }); test("preserves sandbox failure messages for operator diagnostics", async () => { const deps = defaultDeps({ invokeInSandbox: vi.fn().mockResolvedValue({ ok: false, reason: "timeout", fuelUsed: 0, message: "plugin sandbox wall-clock timeout", }), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("sandbox_failed"); expect(result.message).toBe("plugin sandbox wall-clock timeout"); } }); test("returns sandbox_failed when invokeInSandbox throws", async () => { const deps = defaultDeps({ invokeInSandbox: vi.fn().mockRejectedValue(new Error("boom")), }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(false); if (!result.ok) { expect(result.reason).toBe("sandbox_failed"); expect(result.message).toBe("boom"); } }); test("audit failure rejects so the queue can retry without losing forensics", async () => { const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const deps = defaultDeps({ recordAudit: vi.fn().mockRejectedValue(new Error("audit-down")), logger, }); await expect(handlePluginInvokeJob(payload(), deps)).rejects.toThrow("audit-down"); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ pluginId: "de.toolctrl.test", pluginInvocationId: expect.any(String), error: "audit-down", }), "plugin.invoke.audit_failed" ); }); test("crash-tracker seam receives sandbox_failed outcomes", async () => { const record = vi.fn().mockResolvedValue(undefined); const deps = defaultDeps({ invokeInSandbox: vi.fn().mockResolvedValue({ ok: false, reason: "trap", fuelUsed: 1 }), recordOutcomeForCrashTracking: record, }); await handlePluginInvokeJob(payload(), deps); expect(record).toHaveBeenCalledTimes(1); const args = record.mock.calls[0]?.[0] as { outcome: { ok: boolean } }; expect(args.outcome.ok).toBe(false); }); test("crash-tracker seam ignores plugin_not_found (config issue)", async () => { const record = vi.fn().mockResolvedValue(undefined); const deps = defaultDeps({ loadPlugin: vi.fn().mockResolvedValue(null), recordOutcomeForCrashTracking: record, }); await handlePluginInvokeJob(payload(), deps); expect(record).not.toHaveBeenCalled(); }); test("crash-tracker seam failure does not break the handler", async () => { const record = vi.fn().mockRejectedValue(new Error("redis-down")); const deps = defaultDeps({ recordOutcomeForCrashTracking: record }); const result = await handlePluginInvokeJob(payload(), deps); expect(result.ok).toBe(true); }); });