Integrate final shared profile contracts and verification fixes

Preserve the independently verified Cursor dependency lock while accepting the prerequisite profile-v3 parser and portable fixture corrections.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-28 16:23:48 -05:00
commit 3525dce1ad
5 files changed
+73 -69

No files matched your search

@@ -280,7 +280,7 @@ describe("NativeExecutionInputV1", () => {
})).toThrow("eventExpiryDays");
});
it("accepts only a closed ACPX profile matching the driver and agent", () => {
it.each([1, 2, 3] as const)("accepts only a closed ACPX profile matching the driver and agent at profile version %s", (agentProfileVersion) => {
const provider = {
kind: "acpx",
agent: "pi",
@@ -291,7 +291,7 @@ describe("NativeExecutionInputV1", () => {
protocolVersion: 1,
acpxVersion: "0.13.1",
agent: "pi",
agentProfileVersion: 1,
agentProfileVersion,
agentServerPackage: "pi-acp",
agentServerVersion: "0.0.33",
agentRuntimePackage: "@earendil-works/pi-coding-agent",
@@ -312,6 +312,13 @@ describe("NativeExecutionInputV1", () => {
profile: provider.profile,
});
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
for (const unsupportedVersion of [0, 4, 1.5, "3", null]) {
expect(() => parseNativeExecutionInput({
...input,
session: { ...input.session, driverKind: "acpx_runtime" },
provider: { ...provider, profile: { ...provider.profile, agentProfileVersion: unsupportedVersion } },
})).toThrow("qualified ACPX v1 profile");
}
expect(buildNativeModelEnvelope(parsed).workspace).toEqual({ cwd: "/safe/workspace" });
expect(() => parseNativeExecutionInput({
...input,
@@ -90,7 +90,7 @@ export interface NativeAcpxProfileSnapshot {
protocolVersion: 1;
acpxVersion: "0.13.1";
agent: NativeAcpxAgent;
agentProfileVersion: 1 | 2;
agentProfileVersion: 1 | 2 | 3;
agentServerPackage: string;
agentServerVersion: string;
agentRuntimePackage: string | null;
@@ -596,7 +596,7 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
|| profile.protocolVersion !== 1
|| profile.acpxVersion !== "0.13.1"
|| profile.agent !== provider.agent
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2)
|| (profile.agentProfileVersion !== 1 && profile.agentProfileVersion !== 2 && profile.agentProfileVersion !== 3)
) {
throw new NativeExecutionInputError("input.provider.profile does not match the qualified ACPX v1 profile");
}
@@ -11,7 +11,7 @@ export interface QualifiedAcpxProfile {
readonly protocolVersion: typeof ACPX_DRIVER_PROTOCOL_VERSION;
readonly acpxVersion: typeof QUALIFIED_ACPX_VERSION;
readonly agent: QualifiedAcpxAgent;
readonly agentProfileVersion: 1 | 2;
readonly agentProfileVersion: 1 | 2 | 3;
readonly qualificationStatus?: "pending";
readonly modelPolicy?: "explicit-provider-verified";
/** Wire identity: an npm package name or a runner-owned builtin: identifier. */
@@ -72,61 +72,60 @@ const mockIssueReferenceService = vi.hoisted(() => ({
syncIssue: vi.fn(async () => undefined),
}));
function registerModuleMocks() {
vi.doMock("@paperclipai/shared/telemetry", () => ({
trackAgentTaskCompleted: vi.fn(),
trackErrorHandlerCrash: vi.fn(),
}));
// Keep service factories stable while each request uses the hoisted test doubles.
vi.mock("@paperclipai/shared/telemetry", () => ({
trackAgentTaskCompleted: vi.fn(),
trackErrorHandlerCrash: vi.fn(),
}));
vi.doMock("../telemetry.js", () => ({
getTelemetryClient: vi.fn(() => ({ track: vi.fn() })),
}));
vi.mock("../telemetry.js", () => ({
getTelemetryClient: vi.fn(() => ({ track: vi.fn() })),
}));
vi.doMock("../services/index.js", () => ({
companyService: () => ({
getById: vi.fn(async () => ({ id: "company-1" })),
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
companySkillService: () => ({
completeTestRunForIssue: vi.fn(async () => null),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => mockExecutionWorkspaceService,
goalService: () => ({}),
heartbeatService: () => mockHeartbeatService,
issueApprovalService: () => ({}),
issueReferenceService: () => mockIssueReferenceService,
issueRecoveryActionService: () => ({
getActiveForIssue: vi.fn(async () => null),
listActiveForIssues: vi.fn(async () => new Map()),
}),
issueService: () => mockIssueService,
issueThreadInteractionService: () => mockIssueThreadInteractionService,
logActivity: mockLogActivity,
projectService: () => ({}),
routineService: () => mockRoutineService,
workProductService: () => ({}),
}));
vi.mock("../services/index.js", () => ({
companyService: () => ({
getById: vi.fn(async () => ({ id: "company-1" })),
}),
accessService: () => mockAccessService,
agentService: () => mockAgentService,
companySkillService: () => ({
completeTestRunForIssue: vi.fn(async () => null),
}),
documentAnnotationService: () => ({ remapOpenThreadsForDocument: async () => [] }),
documentService: () => ({}),
executionWorkspaceService: () => mockExecutionWorkspaceService,
goalService: () => ({}),
heartbeatService: () => mockHeartbeatService,
issueApprovalService: () => ({}),
issueReferenceService: () => mockIssueReferenceService,
issueRecoveryActionService: () => ({
getActiveForIssue: vi.fn(async () => null),
listActiveForIssues: vi.fn(async () => new Map()),
}),
issueService: () => mockIssueService,
issueThreadInteractionService: () => mockIssueThreadInteractionService,
logActivity: mockLogActivity,
projectService: () => ({}),
routineService: () => mockRoutineService,
workProductService: () => ({}),
}));
vi.doMock("../services/environments.js", () => ({
environmentService: () => mockEnvironmentService,
}));
vi.mock("../services/environments.js", () => ({
environmentService: () => mockEnvironmentService,
}));
vi.doMock("../services/execution-workspaces.js", () => ({
executionWorkspaceService: () => mockExecutionWorkspaceService,
STALE_REOPEN_PENDING_CONSUMPTION_GRACE_MS: 5 * 60 * 1000,
}));
vi.mock("../services/execution-workspaces.js", () => ({
executionWorkspaceService: () => mockExecutionWorkspaceService,
STALE_REOPEN_PENDING_CONSUMPTION_GRACE_MS: 5 * 60 * 1000,
}));
vi.doMock("../services/feedback.js", () => ({
feedbackService: () => mockFeedbackService,
}));
vi.mock("../services/feedback.js", () => ({
feedbackService: () => mockFeedbackService,
}));
vi.doMock("../services/instance-settings.js", () => ({
instanceSettingsService: () => mockInstanceSettingsService,
}));
}
vi.mock("../services/instance-settings.js", () => ({
instanceSettingsService: () => mockInstanceSettingsService,
}));
async function createApp(actor: Record<string, unknown>) {
const [{ issueRoutes }, { errorHandler }] = await Promise.all([
@@ -152,17 +151,6 @@ async function createApp(actor: Record<string, unknown>) {
describe("issue feedback trace routes", () => {
beforeEach(() => {
vi.resetModules();
vi.doUnmock("@paperclipai/shared/telemetry");
vi.doUnmock("../telemetry.js");
vi.doUnmock("../services/index.js");
vi.doUnmock("../services/environments.js");
vi.doUnmock("../services/execution-workspaces.js");
vi.doUnmock("../services/feedback.js");
vi.doUnmock("../services/instance-settings.js");
vi.doUnmock("../routes/issues.js");
vi.doUnmock("../middleware/index.js");
registerModuleMocks();
vi.clearAllMocks();
mockFeedbackExportService.flushPendingFeedbackTraces.mockResolvedValue({
attempted: 1,
@@ -260,6 +248,8 @@ describe("issue feedback trace routes", () => {
const res = await request(app).get("/api/feedback-traces/trace-1");
expect(res.status, JSON.stringify({ body: res.body, errors: app.locals.routeErrors })).toBe(404);
expect(mockFeedbackService.getFeedbackTraceById).toHaveBeenCalledExactlyOnceWith("trace-1", true);
expect(mockFeedbackService.getFeedbackTraceBundle).not.toHaveBeenCalled();
});
it("returns 404 for bundle fetches when a board user lacks access to the trace company", async () => {
@@ -280,5 +270,7 @@ describe("issue feedback trace routes", () => {
const res = await request(app).get("/api/feedback-traces/trace-1/bundle");
expect(res.status, JSON.stringify({ body: res.body, errors: app.locals.routeErrors })).toBe(404);
expect(mockFeedbackService.getFeedbackTraceBundle).toHaveBeenCalledExactlyOnceWith("trace-1");
expect(mockFeedbackService.getFeedbackTraceById).not.toHaveBeenCalled();
});
});
@@ -51,8 +51,10 @@ function localClient(commands: string[]): SandboxManagedRuntimeClient {
};
}
// macOS adds per-file process and filesystem overhead to the real deletion lane.
const realGitTimeoutMs = process.platform === "darwin" ? 300_000 : 180_000;
it("streams and stages all four real Git filename lanes above 32 MiB through the shared scheduler", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-streaming-real-git-"));
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "pc-stream-git-")));
directories.push(root);
const repo = path.join(root, "repo");
await fs.mkdir(repo);
@@ -60,7 +62,9 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the
const git = (args: string[]) => runLocalGit(repo, args, { env, timeout: 120_000, maxBuffer: 64 * 1024 });
await git(["init"]);
await git(["commit", "--allow-empty", "-qm", "fixture"]);
const parent = path.join(repo, ...Array.from({ length: 4 }, () => "nested-".repeat(30)), "storybook-output");
// Leave room for the fixture/staging root below macOS's 1,024-byte path
// limit while keeping each 40,000-name Git lane above the 32 MiB boundary.
const parent = path.join(repo, ...Array.from({ length: 3 }, () => "nested-".repeat(30)), "storybook-output");
await fs.mkdir(parent, { recursive: true });
const fileName = (index: number) => `${"asset-".repeat(36)}${index}.js`;
const writeFiles = async (content: string) => {
@@ -92,7 +96,8 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the
bytes += Buffer.byteLength(relative) + 1;
}
expect(count).toBe(40_000);
expect(bytes).toBe(43_428_890);
expect(bytes).toBe(34_988_890);
expect(bytes).toBeGreaterThan(32 * 1024 * 1024);
};
// Standalone callers must use the same streaming semantics.
setExpensiveWorkspaceGitExecutor(null);
@@ -147,4 +152,4 @@ it("streams and stages all four real Git filename lanes above 32 MiB through the
expect(remaining).toBe(0);
expect(commands.every((command) => command.length < 16 * 1024)).toBe(true);
expect(scheduler.snapshot()).toMatchObject({ activeCount: 0, queuedCount: 0, inFlightCount: 0, cacheBytes: 0 });
}, 180_000);
}, realGitTimeoutMs);