mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
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>
106 lines
7.3 KiB
TypeScript
106 lines
7.3 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { eq } from "drizzle-orm";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
agents, companies, createDb, dotAgentBindings, dotRunnerAssignments, dotRunnerOperations, heartbeatRuns,
|
|
issues, museAgentBindings, museRunnerAssignments, museRunnerOperations, nativeRunFinalizations,
|
|
} from "@paperclipai/db";
|
|
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../__tests__/helpers/embedded-postgres.js";
|
|
import { executionProjectionsForRuns } from "./execution-projection.js";
|
|
|
|
const support = await getEmbeddedPostgresTestSupport();
|
|
const now = new Date("2026-10-13T18:36:00Z");
|
|
(support.supported ? describe : describe.skip)("external provider acceptance projection", () => {
|
|
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
|
let db: ReturnType<typeof createDb>;
|
|
beforeAll(async () => {
|
|
// Always a disposable test database; never use an operator's DATABASE_URL.
|
|
database = await startEmbeddedPostgresTestDatabase("external-provider-projection-");
|
|
db = createDb(database.connectionString);
|
|
}, 60_000);
|
|
afterAll(async () => { await database?.cleanup(); });
|
|
|
|
async function fixture(provider: "muse" | "openai_dot", options: {
|
|
companyId?: string; generation?: number; revoked?: boolean;
|
|
claimed?: boolean; nativeAccepted?: boolean; assignmentCompanyId?: string;
|
|
} = {}) {
|
|
const companyId = options.companyId ?? randomUUID(), agentId = randomUUID();
|
|
const issueId = randomUUID(), runId = randomUUID(), bindingId = randomUUID(), assignmentId = randomUUID();
|
|
if (!options.companyId) await db.insert(companies).values({ id: companyId, name: "Projection test", issuePrefix: `W${companyId.slice(0, 8)}` });
|
|
await db.insert(agents).values({ id: agentId, companyId, name: "External agent", adapterType: "paperclip_runner" });
|
|
await db.insert(issues).values({ id: issueId, companyId, title: "Assigned work", assigneeAgentId: agentId });
|
|
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, nativeIssueId: issueId,
|
|
runtimeMode: "native", status: "running", driverKind: provider === "muse" ? "muse_external" : "openai_dot_mcp", startedAt: now });
|
|
await db.insert(nativeRunFinalizations).values({ runId, companyId, issueId, phase: "observed",
|
|
leaseExpiresAt: new Date(now.getTime() + 60_000), controllerGeneration: 1 });
|
|
const binding = { id: bindingId, companyId, agentId, operatorId: "projection-operator", generation: 1,
|
|
status: "ready" as const, revokedAt: options.revoked ? now : null };
|
|
const assignment = { id: assignmentId, companyId: options.assignmentCompanyId ?? companyId, agentId, bindingId,
|
|
bindingGeneration: options.generation ?? 1, runId, normalizedSessionId: randomUUID(), turnId: randomUUID(),
|
|
controllerGeneration: 1, catalogDigest: "projection-test", projection: {},
|
|
acceptBy: new Date(now.getTime() + 60_000), expiresAt: new Date(now.getTime() + 120_000) };
|
|
if (provider === "muse") {
|
|
await db.insert(museAgentBindings).values(binding);
|
|
await db.insert(museRunnerAssignments).values({ ...assignment,
|
|
status: options.claimed ? "claimed" : "offered", claimedAt: options.claimed ? now : null,
|
|
nativeAcceptedAt: options.nativeAccepted ? now : null });
|
|
} else {
|
|
await db.insert(dotAgentBindings).values(binding);
|
|
await db.insert(dotRunnerAssignments).values({ ...assignment, status: "offered", acceptedAt: options.nativeAccepted ? now : null });
|
|
}
|
|
return { companyId, runId, assignmentId, project: () => executionProjectionsForRuns(db, companyId, [runId], now) };
|
|
}
|
|
|
|
it("batches Muse offered/claimed and Dot offered runs without calling them working", async () => {
|
|
const muse = await fixture("muse");
|
|
const claimed = await fixture("muse", { companyId: muse.companyId, claimed: true });
|
|
const dot = await fixture("openai_dot", { companyId: muse.companyId });
|
|
// Reservation/claim is not the Runner's native acceptance receipt.
|
|
for (const [table, assignmentId] of [[museRunnerOperations, claimed.assignmentId], [dotRunnerOperations, dot.assignmentId]] as const) {
|
|
await db.insert(table).values({ companyId: muse.companyId, assignmentId, requestId: randomUUID(),
|
|
digest: "reserved-accept", command: { action: "accept" }, status: "reserved" });
|
|
}
|
|
const select = vi.spyOn(db, "select");
|
|
try {
|
|
const projections = await executionProjectionsForRuns(db, muse.companyId, [muse.runId, claimed.runId, dot.runId], now);
|
|
expect(projections.get(muse.runId)).toMatchObject({ phase: "waiting_for_provider", label: "Waiting for Muse" });
|
|
expect(projections.get(claimed.runId)).toMatchObject({ phase: "waiting_for_provider", label: "Waiting for Muse" });
|
|
expect(projections.get(dot.runId)).toMatchObject({ phase: "waiting_for_provider", label: "Waiting for Dot" });
|
|
// Runs, coordinators, interactions, recoveries, and one read per provider.
|
|
expect(select).toHaveBeenCalledTimes(6);
|
|
} finally { select.mockRestore(); }
|
|
});
|
|
|
|
it.each(["muse", "openai_dot"] as const)("uses the %s native acceptance timestamp, then respects terminal status", async provider => {
|
|
const f = await fixture(provider, { claimed: true });
|
|
expect((await f.project()).get(f.runId)?.phase).toBe("waiting_for_provider");
|
|
if (provider === "muse") await db.update(museRunnerAssignments).set({ nativeAcceptedAt: now }).where(eq(museRunnerAssignments.id, f.assignmentId));
|
|
else await db.update(dotRunnerAssignments).set({ acceptedAt: now }).where(eq(dotRunnerAssignments.id, f.assignmentId));
|
|
expect((await f.project()).get(f.runId)).toMatchObject({ phase: "working", label: "Working" });
|
|
await db.update(heartbeatRuns).set({ status: "failed", errorCode: "native_provider_terminal_failed", finishedAt: now }).where(eq(heartbeatRuns.id, f.runId));
|
|
expect((await f.project()).get(f.runId)).toMatchObject({ phase: "failed", label: "Failed" });
|
|
});
|
|
|
|
it("does not treat an expired controller lease as confirmed provider waiting", async () => {
|
|
const f = await fixture("muse");
|
|
await db.update(nativeRunFinalizations).set({ leaseExpiresAt: new Date(now.getTime() - 1) }).where(eq(nativeRunFinalizations.runId, f.runId));
|
|
expect((await f.project()).get(f.runId)).toMatchObject({ phase: "reconnecting", label: "Confirming execution" });
|
|
});
|
|
|
|
it("isolates company, requested run, binding generation and revocation", async () => {
|
|
const own = await fixture("muse"), foreign = await fixture("muse");
|
|
const stale = await fixture("muse", { companyId: own.companyId, generation: 2 });
|
|
const revoked = await fixture("openai_dot", { companyId: own.companyId, revoked: true });
|
|
const wrongCompany = await fixture("muse", { companyId: own.companyId, assignmentCompanyId: foreign.companyId });
|
|
const omitted = await fixture("openai_dot", { companyId: own.companyId });
|
|
const projections = await executionProjectionsForRuns(db, own.companyId,
|
|
[own.runId, foreign.runId, stale.runId, revoked.runId, wrongCompany.runId], now);
|
|
expect(projections.get(own.runId)?.phase).toBe("waiting_for_provider");
|
|
expect(projections.has(foreign.runId)).toBe(false);
|
|
expect(projections.has(omitted.runId)).toBe(false);
|
|
for (const runId of [stale.runId, revoked.runId, wrongCompany.runId]) {
|
|
expect(projections.get(runId)?.phase).toBe("working");
|
|
}
|
|
});
|
|
});
|