Files
PaperClipAI/server/src/__tests__/muse-qualification-routes.test.ts
T
DottaandPaperclip e6dcc6b095 Connect personal Muse through secure native execution and Vault OAuth (#16448)
Add company-scoped Muse native control, OAuth, wake receiver and recovery.

Recheck current authority after waits and before effects or receipt replay. Preserve task-read scope, assignment expiry, independent holds, nonblocking reassignment and truthful cleanup provenance.

Retain source trust on progress comments and verify current Muse V8 authority through real signed-JWT HTTP requests. Preserve the upstream trimmed ordinary authentication lookup.

Refs #16448.

Preserve captured canonical queue recovery and concurrent environment preparation while retaining the scoped Muse V8 exclusion. Normal migration 0354 and existing Muse SQL remain unchanged. Fresh current-head Greptile review is 5/5 with the project-write finding addressed. Required CI and independent technical review gate the merge; maintainer override is reserved for formal approval rules.

Recheck current Muse project-write authority after all receipt/task/run/admission waits, before creation or replay. Real PostgreSQL regressions reproduce four late effects/replays on the old route; the repaired three-file treatment passes 354 cases with no skips, and the scoped server typecheck passes. Ordinary/Dot behavior is preserved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-17 22:44:36 -05:00

118 lines
8.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import express from "express";
import request from "supertest";
import type { Db } from "@paperclipai/db";
import { museQualificationRoutes } from "../routes/muse-qualification.js";
import { museBoardRoutes } from "../routes/muse.js";
const mocks = vi.hoisted(() => ({ beginQualification: vi.fn(), endQualification: vi.fn(), readQualificationEvidence: vi.fn(), verify: vi.fn(), attestDetectorCleanup: vi.fn(), decide: vi.fn(), getById: vi.fn() }));
vi.mock("../services/muse-runner-broker.js", () => ({ museRunnerBroker: () => mocks }));
vi.mock("../services/access.js", () => ({ accessService: () => mocks }));
vi.mock("../services/authorization.js", () => ({ authorizationService: () => mocks }));
vi.mock("../modules/external-agents/index.js", () => ({ museExternalAgents: () => mocks }));
vi.mock("../services/agents.js", () => ({ agentService: () => mocks }));
const companyId = "10000000-0000-4000-8000-000000000001", agentId = "20000000-0000-4000-8000-000000000001";
const bindingId = "30000000-0000-4000-8000-000000000001", qualificationId = "40000000-0000-4000-8000-000000000001";
const path = `/companies/${companyId}/agents/${agentId}/muse-binding/qualification`;
const body = { bindingId, qualificationId, generation: 1, expectedRevision: 2 };
function app(actor: Record<string, unknown> = { type: "board", source: "board_key", userId: "operator", companyIds: [companyId] }) {
const result = express(); result.use(express.json());
result.use((req, _res, next) => { Object.assign(req, { actor }); next(); });
result.use(museQualificationRoutes({} as Db));
result.use(museBoardRoutes({} as Db));
result.use((error: { status?: number; name?: string }, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
res.status(error.status ?? (error.name === "ZodError" ? 400 : 500)).json({ error: "request_rejected" });
});
return result;
}
beforeEach(() => {
vi.clearAllMocks();
mocks.getById.mockResolvedValue({ id: agentId, companyId });
mocks.decide.mockResolvedValue({ allowed: true });
mocks.beginQualification.mockResolvedValue({ qualificationId, startedAt: "2099-01-01T00:00:00.000Z", expiresAt: "2099-01-02T00:00:00.000Z", revision: 3 });
mocks.endQualification.mockResolvedValue(undefined);
mocks.readQualificationEvidence.mockResolvedValue({ assignments: [], contacts: [] });
mocks.verify.mockResolvedValue({ status: "pending" });
});
describe("operator-only Muse qualification routes", () => {
it("uses current operator permission and a server-bounded 24h deadline", async () => {
const before = Date.now();
const response = await request(app()).post(path).send(body);
expect(response.status).toBe(201); expect(response.headers["cache-control"]).toBe("no-store");
const input = mocks.beginQualification.mock.calls[0]![0];
expect(input).toMatchObject({ ...body, companyId, agentId, operatorId: "operator" });
expect(input.expiresAt.getTime()).toBeGreaterThanOrEqual(before + 86_400_000);
expect(input.expiresAt.getTime()).toBeLessThanOrEqual(Date.now() + 86_400_000);
expect(mocks.decide).toHaveBeenCalledWith(expect.objectContaining({ action: "agent_config:update", scope: { requiresChangeGrant: true } }));
});
it("rejects agent, human-MCP, implicit and cross-company callers before starting", async () => {
for (const actor of [
{ type: "agent", source: "agent_key", agentId, companyId },
{ type: "board", source: "mcp_oauth", userId: "operator", companyIds: [companyId] },
{ type: "board", source: "local_implicit", userId: "operator", companyIds: [companyId] },
{ type: "board", source: "board_key", userId: "operator", companyIds: [] },
]) expect((await request(app(actor)).post(path).send(body)).status).toBe(403);
expect(mocks.beginQualification).not.toHaveBeenCalled();
});
it("honors current permission denial and does not allow a caller-selected duration", async () => {
mocks.decide.mockResolvedValueOnce({ allowed: false, explanation: "Not allowed" });
expect((await request(app()).post(path).send(body)).status).toBe(403);
expect((await request(app()).post(path).send({ ...body, expiresAt: "2099-12-31T00:00:00Z" })).status).toBe(400);
expect(mocks.beginQualification).not.toHaveBeenCalled();
});
it("requires matching agent/company for evidence and cleanup", async () => {
mocks.getById.mockResolvedValue({ id: agentId, companyId: "50000000-0000-4000-8000-000000000001" });
expect((await request(app()).get(path).query({ bindingId, qualificationId })).status).toBe(404);
expect((await request(app()).delete(path).send({ bindingId, qualificationId })).status).toBe(404);
expect(mocks.readQualificationEvidence).not.toHaveBeenCalled(); expect(mocks.endQualification).not.toHaveBeenCalled();
});
it("stops only the exact selected qualification and exposes non-cacheable evidence", async () => {
const read = await request(app()).get(path).query({ bindingId, qualificationId });
expect(read.status).toBe(200); expect(read.headers["cache-control"]).toBe("no-store");
expect(mocks.readQualificationEvidence).toHaveBeenCalledWith(companyId, agentId, bindingId, qualificationId);
expect((await request(app()).delete(path).send({ bindingId, qualificationId })).status).toBe(204);
expect(mocks.endQualification).toHaveBeenCalledWith({ companyId, agentId, operatorId: "operator", bindingId, qualificationId });
});
});
describe("board Muse verification attribution", () => {
const verifyPath = `/companies/${companyId}/agents/${agentId}/muse-binding/verify`;
const verifyBody = { bindingId, generation: 1, expectedRevision: 2 };
it("passes only the authorized server-derived board caller to verification", async () => {
const actor = { type: "board", source: "session", userId: "actual-board-caller", companyIds: [companyId] };
expect((await request(app(actor)).post(verifyPath).send(verifyBody)).status).toBe(200);
expect(mocks.verify).toHaveBeenCalledExactlyOnceWith(companyId, agentId, verifyBody, "actual-board-caller");
expect(mocks.decide).toHaveBeenCalledWith(expect.objectContaining({ actor, action: "agent_config:update", scope: { requiresChangeGrant: true } }));
});
it("rejects caller-supplied audit identity before verification", async () => {
expect((await request(app()).post(verifyPath).send({ ...verifyBody, operatorId: "spoofed-caller" })).status).toBe(400);
expect(mocks.verify).not.toHaveBeenCalled();
});
it("does not verify when current board authority or company scope is denied", async () => {
mocks.decide.mockResolvedValueOnce({ allowed: false, explanation: "Not allowed" });
expect((await request(app()).post(verifyPath).send(verifyBody)).status).toBe(403);
expect((await request(app({ type: "board", source: "board_key", userId: "operator", companyIds: [] })).post(verifyPath).send(verifyBody)).status).toBe(403);
expect(mocks.verify).not.toHaveBeenCalled();
});
});
describe("board-only exact Muse detector cleanup", () => {
const cleanupPath = `/companies/${companyId}/agents/${agentId}/muse-binding/attest-detector-cleanup`;
const input = { bindingId, qualificationId, generation: 1, expectedRevision: 2, hookScript: `~/hooks/scripts/paperclip-muse-${bindingId}.sh`, detectorRemoved: true,
observation: { method: "hooks.list", includeDisabled: true, hookId: "test-hook", outcome: "exact_hook_absent", observedAt: "2099-01-02T00:00:01.000Z", sha256: "a".repeat(64) } };
it("uses actual Board caller and normal change-grant authorization", async () => {
const actor = { type: "board", source: "session", userId: "actual-operator", companyIds: [companyId] };
const response = await request(app(actor)).post(cleanupPath).send(input);
expect(response.status).toBe(200); expect(response.headers["cache-control"]).toBe("no-store");
expect(mocks.attestDetectorCleanup).toHaveBeenCalledExactlyOnceWith(companyId, agentId, "actual-operator", input, actor);
expect(mocks.decide).toHaveBeenCalledWith(expect.objectContaining({ action: "agent_config:update", scope: { requiresChangeGrant: true } }));
});
it("rejects denied authority, agent callers, cross-company callers and supplied actor metadata", async () => {
mocks.decide.mockResolvedValueOnce({ allowed: false, explanation: "Denied" });
expect((await request(app()).post(cleanupPath).send(input)).status).toBe(403);
for (const actor of [{ type: "agent", source: "agent_key", agentId, companyId }, { type: "board", source: "board_key", userId: "operator", companyIds: [] }]) expect((await request(app(actor)).post(cleanupPath).send(input)).status).toBe(403);
expect((await request(app()).post(cleanupPath).send({ ...input, operatorId: "spoofed" })).status).toBe(400);
expect(mocks.attestDetectorCleanup).not.toHaveBeenCalled();
});
});