mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
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:
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);
|
||||
Reference in new issue
Block a user