mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
fix: preserve Codex runner intent and private eval evidence
Retain native setup links and built-in settings, record explicit runner choices, and qualify native/legacy skill fixtures. Keep full grading artifacts private while projecting safe first-task summaries. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
38199ea8e6
commit
1be8502f27
15 files changed
+454
-32
No files matched your search
@@ -3,6 +3,17 @@ import { readFile } from "node:fs/promises";
|
||||
import request from "supertest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runnerTarget = vi.hoisted(() => ({ driver: "local", platform: "linux", architecture: "x64" }));
|
||||
vi.mock("../services/agent-runner-selection.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../services/agent-runner-selection.js")>();
|
||||
return {
|
||||
...actual,
|
||||
// Exercise the real selection/translation policy on an explicit target.
|
||||
resolveNewAgentRunnerForCompany: vi.fn(async (...[_db, _companyId, input]: Parameters<typeof actual.resolveNewAgentRunnerForCompany>) =>
|
||||
actual.resolveNewAgentRunner({ ...input, target: runnerTarget })),
|
||||
};
|
||||
});
|
||||
|
||||
const mockAgentService = vi.hoisted(() => ({
|
||||
getById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
@@ -252,6 +263,8 @@ function makeAgent(adapterType: string) {
|
||||
|
||||
describe("agent skill routes", () => {
|
||||
beforeEach(async () => {
|
||||
runnerTarget.platform = "linux";
|
||||
runnerTarget.architecture = "x64";
|
||||
vi.resetModules();
|
||||
vi.doUnmock("../routes/agents.js");
|
||||
vi.doUnmock("../routes/authz.js");
|
||||
@@ -1244,31 +1257,45 @@ describe("agent skill routes", () => {
|
||||
.toContain("paperclipai/paperclip/first-task");
|
||||
});
|
||||
|
||||
it.each(["codex_local", "claude_local"].flatMap((adapterType) => [
|
||||
const codexRunnerCases = [
|
||||
{ adapterType: "codex_local", runner: "auto", platform: "linux", expectedType: "paperclip_runner" },
|
||||
{ adapterType: "codex_local", runner: "legacy", platform: "linux", expectedType: "codex_local" },
|
||||
{ adapterType: "codex_local", runner: "auto", platform: "darwin", expectedType: "codex_local" },
|
||||
];
|
||||
it.each([...codexRunnerCases, { adapterType: "claude_local", runner: "auto", platform: "linux", expectedType: "claude_local" }].flatMap((runnerCase) => [
|
||||
["agents", "paperclipai/paperclip/paperclip-create-agent"],
|
||||
["agent-hires", "paperclipai/paperclip/paperclip-create-agent"],
|
||||
["agents", "paperclip"],
|
||||
["agent-hires", "paperclip"],
|
||||
["agents", "paperclipai/paperclip/first-task"],
|
||||
["agent-hires", "paperclipai/paperclip/first-task"],
|
||||
].map(([route, skill]) => ({ adapterType, route, skill }))))("gives $adapterType onboarding agents core and first-task skills via $route, preserving $skill pins", async ({ adapterType, route, skill }) => {
|
||||
].map(([route, skill]) => ({ ...runnerCase, route, skill }))))("assigns onboarding skills via $route for $adapterType/$runner on $platform with $skill pins", async ({ adapterType, runner, platform, expectedType, route, skill }) => {
|
||||
runnerTarget.platform = platform;
|
||||
runnerTarget.architecture = platform === "darwin" ? "arm64" : "x64";
|
||||
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableBetaSkills: true });
|
||||
const versionId = "22222222-2222-4222-8222-222222222222";
|
||||
const res = await request(await createApp(createDb(route === "agent-hires")))
|
||||
.post(`/api/companies/company-1/${route}`)
|
||||
.send({
|
||||
name: "Chiff", role: "general", adapterType,
|
||||
name: "Chiff", role: "general", adapterType, ...(runner === "auto" ? {} : { runner }),
|
||||
onboardingFirstAgent: true,
|
||||
desiredSkills: [{ key: skill, versionId }],
|
||||
});
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
const input = mockAgentService.create.mock.calls[0][1];
|
||||
expect(input.role).toBe("general");
|
||||
expect(input.adapterType).toBe(expectedType);
|
||||
const native = expectedType === "paperclip_runner";
|
||||
if (native) expect(input.adapterConfig.provider).toBe("codex");
|
||||
const canonicalKey = skill === "paperclip" ? "paperclipai/paperclip/paperclip" : skill;
|
||||
const expected = ["paperclip", "paperclip-board", "paperclip-converting-plans-to-tasks", "paperclip-create-agent", "para-memory-files", "first-task"]
|
||||
// Native PRP supplies the operational contract; role skills and their pins remain assigned.
|
||||
.filter((name) => !native || name !== "paperclip")
|
||||
.map((name) => ({ key: `paperclipai/paperclip/${name}`, versionId: `paperclipai/paperclip/${name}` === canonicalKey ? versionId : null }));
|
||||
expect(input.adapterConfig.paperclipSkillSync.desiredSkills).toEqual(expect.arrayContaining(expected));
|
||||
expect(input.adapterConfig.paperclipSkillSync.desiredSkills).toHaveLength(6);
|
||||
const desiredSkills = input.adapterConfig.paperclipSkillSync.desiredSkills.map((entry: string | { key: string; versionId: string | null }) =>
|
||||
typeof entry === "string" ? { key: entry, versionId: null } : entry);
|
||||
expect(desiredSkills).toEqual(expect.arrayContaining(expected));
|
||||
expect(desiredSkills).toHaveLength(expected.length);
|
||||
});
|
||||
|
||||
it.each(["agents", "agent-hires"])("leaves ordinary general agents' defaults unchanged via %s", async (route) => {
|
||||
@@ -1279,13 +1306,23 @@ describe("agent skill routes", () => {
|
||||
expect(mockAgentService.create.mock.calls[0][1].adapterConfig.paperclipSkillSync).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["agents", "agent-hires"])("does not assign first-task to ordinary CEOs via %s", async (route) => {
|
||||
it.each(codexRunnerCases.flatMap((runnerCase) => ["agents", "agent-hires"].map((route) => ({ ...runnerCase, route }))))("assigns core skills without first-task to ordinary CEOs via $route using $runner on $platform", async ({ route, runner, platform, expectedType }) => {
|
||||
runnerTarget.platform = platform;
|
||||
runnerTarget.architecture = platform === "darwin" ? "arm64" : "x64";
|
||||
const res = await request(await createApp())
|
||||
.post(`/api/companies/company-1/${route}`)
|
||||
.send({ name: "CEO", role: "ceo", adapterType: "codex_local" });
|
||||
.send({ name: "CEO", role: "ceo", adapterType: "codex_local", ...(runner === "auto" ? {} : { runner }) });
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
const desiredSkills = mockAgentService.create.mock.calls[0][1].adapterConfig.paperclipSkillSync.desiredSkills;
|
||||
expect(desiredSkills).toContain("paperclipai/paperclip/paperclip");
|
||||
const input = mockAgentService.create.mock.calls[0][1];
|
||||
expect(input.adapterType).toBe(expectedType);
|
||||
const native = expectedType === "paperclip_runner";
|
||||
if (native) expect(input.adapterConfig.provider).toBe("codex");
|
||||
const desiredSkills = input.adapterConfig.paperclipSkillSync.desiredSkills;
|
||||
const expected = ["paperclip", "paperclip-board", "paperclip-converting-plans-to-tasks", "paperclip-create-agent", "para-memory-files"]
|
||||
.filter((name) => !native || name !== "paperclip")
|
||||
.map((name) => `paperclipai/paperclip/${name}`);
|
||||
expect(desiredSkills).toEqual(expect.arrayContaining(expected));
|
||||
expect(desiredSkills).toHaveLength(expected.length);
|
||||
expect(desiredSkills).not.toContain("paperclipai/paperclip/first-task");
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import * as codexAdapter from "@paperclipai/adapter-codex-local/server";
|
||||
import { WORKSPACE_RESTORE_LOCK_TIMEOUT_CODE } from "@paperclipai/adapter-utils/workspace-restore-merge";
|
||||
import * as executionTarget from "@paperclipai/adapter-utils/execution-target";
|
||||
import * as runnerProbe from "../vendor/paperclip-runner/index.js";
|
||||
import * as nativeCodexRunner from "../services/native-runtime/native-codex-runner.js";
|
||||
import { testNativeRunnerAuthentication } from "../services/native-runtime/setup-readiness.js";
|
||||
import { prepareManagedAiRuntime, withManagedAiProbe, assertManagedAiProjectAuth, isAiConnectionBusy } from "../services/ai-connection-runtime.js";
|
||||
import { execute as executeGemini, testEnvironment as testGeminiEnvironment } from "@paperclipai/adapter-gemini-local/server";
|
||||
@@ -180,7 +181,10 @@ describe("managed AI connections", () => {
|
||||
await service.save(companyId, owner, { provider: "openai", method: "subscription", ownership: "personal", name: owner, loginSessionId: "fixture", agentIds: [], allAgents: true }, auth("10"));
|
||||
const [row] = await service.quotaAccounts(companyId, owner);
|
||||
const [before] = await db.select({ adapterType: agents.adapterType, adapterConfig: agents.adapterConfig }).from(agents).where(eq(agents.id, agentId));
|
||||
const runnerBinary = path.join(home, "fixture-runnerd");
|
||||
const resolveBinary = vi.spyOn(nativeCodexRunner, "resolvePaperclipRunnerBinary").mockReturnValue(runnerBinary);
|
||||
const probe = vi.spyOn(runnerProbe, "probeNativeRunnerEnvironment").mockImplementation(async input => {
|
||||
expect(input.transportOptions?.runnerBinary).toBe(runnerBinary);
|
||||
expect(input.transportOptions?.sourceCodexHome).toContain("/provider");
|
||||
expect(await readFile(path.join(input.transportOptions!.sourceCodexHome!, "auth.json"), "utf8")).toBe(auth("10"));
|
||||
const providerHome = path.join(input.runtimeDirectory, "codex-home");
|
||||
@@ -203,7 +207,7 @@ describe("managed AI connections", () => {
|
||||
await expect(access(managedHome!)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
const [after] = await db.select({ adapterType: agents.adapterType, adapterConfig: agents.adapterConfig }).from(agents).where(eq(agents.id, agentId));
|
||||
expect(after).toEqual(before);
|
||||
} finally { probe.mockRestore(); }
|
||||
} finally { probe.mockRestore(); resolveBinary.mockRestore(); }
|
||||
});
|
||||
|
||||
it("rejects preparation when reconnect replaces the selected credential reference", async () => {
|
||||
|
||||
Reference in new issue
Block a user