mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Operators create agents and configure their runtimes in the board UI. > - The old creation flow presents several choices and a large form before an agent can start. > - The existing onboarding controls already provide clear provider connection steps. > - This pull request uses those controls in a new-agent wizard and organizes the full configuration pages. > - Operators can connect, test, save, and assign a first task while keeping the existing configuration tools. ## Linked Issues or Issue Description Related: #10974. That earlier open PR also reorganizes agent configuration. This PR follows the reviewed Storybook designs for agent creation and the current configuration tabs. **What existing behavior does this improve?** Agent creation, provider connection, runtime tests, and full agent configuration. **Current behavior** The creation dialog leads to a large manual configuration form. Provider login controls differ from onboarding. Environment variables and secret access appear in separate places. **Proposed behavior** Choose a name and adapter. Connect Claude or Codex through the existing onboarding controls. Configure and test the runtime, save the agent, and open a task dialog with that agent assigned. Use the same design on the existing configuration tabs. **Reason and benefit** The first setup asks for fewer decisions. The full editor keeps instructions, skills, runtime controls, secret access, permissions, keys, and revisions available in clear sections. **Breaking changes** The board creation and configuration layouts change. The test-environment API adds an optional, allowlisted `testCredentials` field for one-shot probes. Database contracts stay the same. Native ACPX tests now reject unsupported local platforms before a CLI login can mask the runtime restriction. ## What Changed - Added a new-agent wizard with numbered steps, adapter branding, provider connections, editable model choices, runtime tests, and confirmation. - Added Codex app-server, Claude ACPX, and OpenCode runner choices. - Stored API credentials through existing secret APIs and persisted references in agent configuration. New setup keys are isolated from credentials used by existing agents. - Preserved external-agent invitations beside the wizard, including optional messages, one-time prompts, and clipboard fallback. - Added OpenRouter provider and secret bindings for Pi and OpenCode. - Added adapter-specific prerequisite fields for Cursor, Gemini, Kimi, and Hermes. Cursor Cloud keys are saved as new organization secrets. - Fixed Cursor Cloud repository field mapping, omitted empty remote environment values, and added useful model and repository error messages. - Preserved complete MCP assignments when multiple valid profiles contain more than 250 tools in total. Generated profiles retain exact tool selectors. - Added service branding and deployment-aware adapter choices. Cloud setup offers Claude, Codex, and OpenCode; local native runners require the experimental setting. - Made the agent list responsive at intermediate widths. - Applied the reviewed design to the real agent configuration pages. Kept the instruction editor, skills, and existing mutations. - Combined secret access and environment variables under one Save and Discard action. - Added interactive Storybook screens for setup, configuration, confirmation, authentication, and test results. - Fixed Pi provider-error parsing and thinking-effort persistence. Native ACPX validates Linux x64 on the actual local, SSH, or sandbox target. - Redacted the complete transient probe-credential field from HTTP error logs, including rejected provider names. ## Verification - Current head `df0292fe6` has a fresh Greptile 5/5 review with no unresolved findings. All 31 executed CI checks passed, including the aggregate verification gate and all browser E2E shards. Storybook visual regression is skipped by its workflow; the local Storybook build passed. - Browser tests completed real assigned tasks with direct Codex, Claude, OpenCode, Pi, and native Codex. - Verified external-agent invitation generation and automatic prompt copying in the live browser. - Pi and OpenCode used an existing OpenRouter secret. Browser checks covered save and reload, instruction edits, skill selection, environment-variable Save and Discard, and assigned task creation. - Invalid Claude API credentials remained on the connection step with an error. A live Pi/OpenRouter invalid-key probe returned a provider failure and left the user-secret inventory unchanged (zero entries before and after). - Full workspace typecheck and build passed after rebasing onto current master. After review fixes, server and UI typechecks, token gates, and the full build passed again. Storybook built successfully. - All 5,542 local UI tests passed. The Cursor Cloud and Pi adapter regressions passed all 24 tests. Review regressions passed 69 server tests and all 18 agent-list tests. - The local full test command ran 6,971 general server tests successfully. Editing review fixes during that long run caused nine tests to use stale modules; fresh isolated runs passed. An unrelated embedded-Postgres fixture hit the host shared-memory limit; its 15 affected tests passed when the fixture groups ran separately. - Local workspace groups passed after rerunning 18 CLI tests sequentially to avoid host database limits and parallel-load timeouts. The local full command stopped at the general server phase, so serialized server verification comes from the five passing CI shards. - Browser testing at 390px confirmed that the agent action menu opens and the page has no horizontal overflow. CI browser E2E shards passed. - Review the `Onboarding / New agent` and `Agents / Configuration refresh` Storybook groups. In the real app, create an agent, run its connection test, save it, assign a task, and reload its configuration. ## Risks - This changes the main agent setup and configuration UI. Regression tests cover routing, persistence, secret bindings, and form actions. - Native Claude ACPX requires Linux x64. Direct Claude works on macOS. Remote checks execute a bounded platform probe and reject unsupported or unverified targets. - A native OpenCode task reached the provider context limit because of its tool payload. Its provider connection test passed. Direct OpenCode completed a task. This existing native execution limit is not fixed here. - Claude and Codex connection keys use the existing user-secret store. Other runtime setup keys use distinct organization secrets. Existing credentials are never rotated. Probes do not store entered keys. Failed agent creation removes newly staged credentials. - Cursor Cloud has not completed a live task. Its authenticated account still needs GitHub repository access. The live run passed MCP provisioning, remote environment validation, and explicit Auto model selection before the repository prerequisite blocked execution. - Generated runtime MCP profiles can exceed the public profile-edit request limit. They still contain exact catalog selectors and preserve permission boundaries. - No database migration, dependency, lockfile, or workflow changes are included. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, repository tools, shell execution, and browser automation. The runtime did not expose the exact model ID or context window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
|
|
import { execute } from "./execute.js";
|
|
|
|
type MockRunOptions = {
|
|
id?: string;
|
|
agentId?: string;
|
|
status?: string;
|
|
waitResult?: Record<string, unknown>;
|
|
streamMessages?: unknown[];
|
|
streamError?: Error | null;
|
|
};
|
|
|
|
type MockAgentOptions = {
|
|
agentId?: string;
|
|
sendRun?: ReturnType<typeof createMockRun>;
|
|
};
|
|
|
|
const { createMock, resumeMock, getRunMock } = vi.hoisted(() => ({
|
|
createMock: vi.fn(),
|
|
resumeMock: vi.fn(),
|
|
getRunMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@cursor/sdk", () => ({
|
|
Agent: {
|
|
create: createMock,
|
|
resume: resumeMock,
|
|
getRun: getRunMock,
|
|
},
|
|
}));
|
|
|
|
function createMockRun(options: MockRunOptions = {}) {
|
|
const runId = options.id ?? "run-123";
|
|
const agentId = options.agentId ?? "agent-123";
|
|
const status = options.status ?? "finished";
|
|
const waitResult = options.waitResult ?? {
|
|
id: runId,
|
|
status,
|
|
result: "Done\nWith detail",
|
|
model: { id: "gpt-5.4" },
|
|
durationMs: 1234,
|
|
};
|
|
const streamMessages = options.streamMessages ?? [];
|
|
const streamError = options.streamError ?? null;
|
|
|
|
return {
|
|
id: runId,
|
|
agentId,
|
|
status,
|
|
result: typeof waitResult.result === "string" ? waitResult.result : null,
|
|
model: waitResult.model ?? null,
|
|
durationMs: waitResult.durationMs ?? null,
|
|
git: waitResult.git ?? null,
|
|
supports(capability: string) {
|
|
return capability === "stream" || capability === "wait";
|
|
},
|
|
async *stream() {
|
|
for (const message of streamMessages) yield message;
|
|
if (streamError) throw streamError;
|
|
},
|
|
async wait() {
|
|
return waitResult;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createMockSdkAgent(options: MockAgentOptions = {}) {
|
|
const sendRun = options.sendRun ?? createMockRun();
|
|
return {
|
|
agentId: options.agentId ?? sendRun.agentId,
|
|
send: vi.fn(async () => sendRun),
|
|
[Symbol.asyncDispose]: vi.fn(async () => {}),
|
|
};
|
|
}
|
|
|
|
function createContext(
|
|
overrides: Partial<AdapterExecutionContext> = {},
|
|
): AdapterExecutionContext & {
|
|
logs: Array<{ stream: "stdout" | "stderr"; chunk: string }>;
|
|
meta: Record<string, unknown>[];
|
|
} {
|
|
const logs: Array<{ stream: "stdout" | "stderr"; chunk: string }> = [];
|
|
const meta: Record<string, unknown>[] = [];
|
|
const agent = overrides.agent ?? {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Cursor Cloud Agent",
|
|
adapterType: "cursor_cloud",
|
|
adapterConfig: {},
|
|
};
|
|
const runtime = overrides.runtime ?? {
|
|
sessionId: null,
|
|
sessionParams: null,
|
|
sessionDisplayId: null,
|
|
taskKey: null,
|
|
};
|
|
const config = overrides.config ?? {
|
|
env: {
|
|
CURSOR_API_KEY: "cursor-secret",
|
|
EXTRA_FLAG: "1",
|
|
},
|
|
repoUrl: "https://github.com/paperclipai/paperclip.git",
|
|
repoStartingRef: "main",
|
|
runtimeEnvType: "cloud",
|
|
promptTemplate: "Do the work for {{agent.name}}",
|
|
model: "gpt-5.4",
|
|
};
|
|
const context = overrides.context ?? {
|
|
taskId: "issue-1",
|
|
issueId: "issue-1",
|
|
wakeReason: "issue_commented",
|
|
};
|
|
|
|
const base: AdapterExecutionContext = {
|
|
runId: "run-heartbeat-1",
|
|
agent,
|
|
runtime,
|
|
config,
|
|
context,
|
|
authToken: "paperclip-run-jwt",
|
|
onLog: async (stream, chunk) => {
|
|
logs.push({ stream, chunk });
|
|
},
|
|
onMeta: async (entry) => {
|
|
meta.push(entry as unknown as Record<string, unknown>);
|
|
},
|
|
};
|
|
|
|
return {
|
|
...base,
|
|
...overrides,
|
|
logs,
|
|
meta,
|
|
};
|
|
}
|
|
|
|
describe("cursor_cloud execute", () => {
|
|
beforeEach(() => {
|
|
createMock.mockReset();
|
|
resumeMock.mockReset();
|
|
getRunMock.mockReset();
|
|
});
|
|
|
|
it("creates a fresh Cursor agent and injects Paperclip env without CURSOR_API_KEY", async () => {
|
|
const run = createMockRun({
|
|
agentId: "agent-fresh",
|
|
streamMessages: [
|
|
{
|
|
type: "assistant",
|
|
message: {
|
|
content: [{ type: "text", text: "Working" }],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-fresh", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(createMock).toHaveBeenCalledTimes(1);
|
|
expect(resumeMock).not.toHaveBeenCalled();
|
|
expect(getRunMock).not.toHaveBeenCalled();
|
|
expect(createMock.mock.calls[0]?.[0]).toMatchObject({
|
|
apiKey: "cursor-secret",
|
|
name: "Paperclip Cursor Cloud Agent",
|
|
model: { id: "gpt-5.4" },
|
|
cloud: {
|
|
env: { type: "cloud" },
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
});
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).toMatchObject({
|
|
EXTRA_FLAG: "1",
|
|
PAPERCLIP_RUN_ID: "run-heartbeat-1",
|
|
PAPERCLIP_TASK_ID: "issue-1",
|
|
PAPERCLIP_WAKE_REASON: "issue_commented",
|
|
PAPERCLIP_API_KEY: "paperclip-run-jwt",
|
|
});
|
|
// When a run JWT is present the callback URL is retained so the worker can
|
|
// authenticate its Paperclip API calls.
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).toHaveProperty("PAPERCLIP_API_URL");
|
|
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).not.toHaveProperty("CURSOR_API_KEY");
|
|
|
|
expect(result).toMatchObject({
|
|
exitCode: 0,
|
|
errorMessage: null,
|
|
sessionId: "agent-fresh",
|
|
model: "gpt-5.4",
|
|
summary: "Done",
|
|
sessionParams: {
|
|
cursorAgentId: "agent-fresh",
|
|
latestRunId: "run-123",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
});
|
|
expect(ctx.logs.map((entry) => entry.chunk)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('"type":"cursor_cloud.init"'),
|
|
expect.stringContaining('"type":"cursor_cloud.message"'),
|
|
expect.stringContaining('"type":"cursor_cloud.result"'),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("omits empty environment values while preserving nonempty values exactly", async () => {
|
|
createMock.mockResolvedValue(createMockSdkAgent());
|
|
const ctx = createContext();
|
|
ctx.config.env = {
|
|
CURSOR_API_KEY: "cursor-secret",
|
|
GH_TOKEN: "",
|
|
GITHUB_TOKEN: "",
|
|
GIT_AUTHOR_NAME: "",
|
|
SSH_AUTH_SOCK: "",
|
|
EMPTY_PLAIN: { type: "plain", value: "" },
|
|
EXTRA_FLAG: "0",
|
|
PADDED_VALUE: " retain whitespace ",
|
|
};
|
|
|
|
await execute(ctx);
|
|
|
|
const env = createMock.mock.calls[0]?.[0]?.cloud?.envVars;
|
|
expect(env).not.toHaveProperty("GH_TOKEN");
|
|
expect(env).not.toHaveProperty("GITHUB_TOKEN");
|
|
expect(env).not.toHaveProperty("GIT_AUTHOR_NAME");
|
|
expect(env).not.toHaveProperty("SSH_AUTH_SOCK");
|
|
expect(env).not.toHaveProperty("EMPTY_PLAIN");
|
|
expect(env).not.toHaveProperty("CURSOR_API_KEY");
|
|
expect(env).toMatchObject({ EXTRA_FLAG: "0", PADDED_VALUE: " retain whitespace " });
|
|
expect(Object.values(env).every((value) => value !== "")).toBe(true);
|
|
});
|
|
|
|
it("reports dispatch before starting the first remote SDK operation", async () => {
|
|
const run = createMockRun({ agentId: "agent-dispatch" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-dispatch", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const onDispatch = vi.fn();
|
|
|
|
await execute(createContext({ onDispatch }));
|
|
|
|
expect(onDispatch).toHaveBeenCalledTimes(1);
|
|
expect(onDispatch.mock.invocationCallOrder[0]).toBeLessThan(createMock.mock.invocationCallOrder[0]!);
|
|
});
|
|
|
|
it("omits the Paperclip API callback when no run JWT is issued (remote worker cannot call home)", async () => {
|
|
const run = createMockRun({ agentId: "agent-no-jwt" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-no-jwt", sendRun: run });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
// cursor_cloud is registered with supportsLocalAgentJwt=false, so heartbeat
|
|
// passes no authToken. A remote cloud worker must not receive a callback URL
|
|
// it can neither reach nor authenticate against (the source of 401 noise).
|
|
const ctx = createContext({ authToken: undefined });
|
|
|
|
await execute(ctx);
|
|
|
|
const envVars = (createMock.mock.calls[0]?.[0]?.cloud?.envVars ?? {}) as Record<string, string>;
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_KEY");
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_URL");
|
|
expect(envVars).not.toHaveProperty("PAPERCLIP_API_BRIDGE_MODE");
|
|
// Informational Paperclip env (non-credential) still flows through.
|
|
expect(envVars).toMatchObject({
|
|
PAPERCLIP_RUN_ID: "run-heartbeat-1",
|
|
PAPERCLIP_AGENT_ID: "agent-1",
|
|
PAPERCLIP_COMPANY_ID: "company-1",
|
|
});
|
|
});
|
|
|
|
it("resumes a matching saved session when no active run can be reattached", async () => {
|
|
getRunMock.mockResolvedValue(createMockRun({ status: "finished" }));
|
|
const resumedRun = createMockRun({ id: "run-resumed", agentId: "agent-resumed" });
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-resumed", sendRun: resumedRun });
|
|
resumeMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionDisplayId: "agent-previous",
|
|
taskKey: null,
|
|
sessionParams: {
|
|
cursorAgentId: "agent-previous",
|
|
latestRunId: "run-previous",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(getRunMock).toHaveBeenCalledWith("run-previous", {
|
|
runtime: "cloud",
|
|
agentId: "agent-previous",
|
|
apiKey: "cursor-secret",
|
|
});
|
|
expect(resumeMock).toHaveBeenCalledTimes(1);
|
|
expect(createMock).not.toHaveBeenCalled();
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
expect(result.sessionId).toBe("agent-resumed");
|
|
});
|
|
|
|
it("reattaches to an active run, drains it, then sends the heartbeat as a follow-up", async () => {
|
|
const attachedRun = createMockRun({
|
|
id: "run-attached",
|
|
agentId: "agent-attached",
|
|
status: "running",
|
|
waitResult: {
|
|
id: "run-attached",
|
|
status: "finished",
|
|
result: "Prior result",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
streamMessages: [
|
|
{
|
|
type: "status",
|
|
status: "running",
|
|
message: "Still working",
|
|
},
|
|
],
|
|
});
|
|
getRunMock.mockResolvedValue(attachedRun);
|
|
const followUpRun = createMockRun({
|
|
id: "run-followup",
|
|
agentId: "agent-attached",
|
|
waitResult: {
|
|
id: "run-followup",
|
|
status: "finished",
|
|
result: "Follow-up result",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-attached", sendRun: followUpRun });
|
|
resumeMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext({
|
|
runtime: {
|
|
sessionId: null,
|
|
sessionDisplayId: "agent-attached",
|
|
taskKey: null,
|
|
sessionParams: {
|
|
cursorAgentId: "agent-attached",
|
|
latestRunId: "run-attached",
|
|
runtime: "cloud",
|
|
envType: "cloud",
|
|
repos: [{ url: "https://github.com/paperclipai/paperclip.git", startingRef: "main" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(getRunMock).toHaveBeenCalledTimes(1);
|
|
expect(createMock).not.toHaveBeenCalled();
|
|
expect(resumeMock).toHaveBeenCalledTimes(1);
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
exitCode: 0,
|
|
sessionId: "agent-attached",
|
|
summary: "Follow-up result",
|
|
resultJson: {
|
|
cursorRunId: "run-followup",
|
|
},
|
|
});
|
|
const logChunks = ctx.logs.map((entry) => entry.chunk);
|
|
expect(logChunks).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining("Reattached to existing Cursor run run-attached."),
|
|
expect.stringContaining("Prior Cursor run run-attached finished"),
|
|
expect.stringContaining("Started Cursor run run-followup."),
|
|
expect.stringContaining('"runId":"run-attached"'),
|
|
expect.stringContaining('"runId":"run-followup"'),
|
|
]),
|
|
);
|
|
expect(ctx.meta[0]?.context).toMatchObject({
|
|
cursorCloud: {
|
|
canReuseSession: true,
|
|
repoUrl: "https://github.com/paperclipai/paperclip.git",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("explains a rejected Cursor default without silently choosing another model", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
sdkAgent.send.mockRejectedValue(new Error("[invalid_model] Model 'gpt-5' is not available or invalid."));
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
delete ctx.config.model;
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(createMock.mock.calls[0]?.[0]).not.toHaveProperty("model");
|
|
expect(sdkAgent.send).toHaveBeenCalledWith(expect.any(String), {});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.errorMessage).toContain("Cursor rejected its configured default model");
|
|
expect(result.errorMessage).toContain("https://cursor.com/dashboard/cloud-agents");
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("makes Cursor repository setup failures actionable without launching another run", async () => {
|
|
const sdkAgent = createMockSdkAgent();
|
|
sdkAgent.send.mockRejectedValue(new Error("[validation_error] Failed to determine repository default branch"));
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
|
|
const result = await execute(createContext());
|
|
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.errorMessage).toContain("Cursor's GitHub integration can access https://github.com/paperclipai/paperclip.git");
|
|
expect(result.errorMessage).toContain("https://cursor.com/dashboard/cloud-agents");
|
|
expect(sdkAgent.send).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("maps non-finished Cursor results to failing Paperclip runs", async () => {
|
|
const cancelledRun = createMockRun({
|
|
id: "run-cancelled",
|
|
agentId: "agent-cancelled",
|
|
status: "cancelled",
|
|
waitResult: {
|
|
id: "run-cancelled",
|
|
status: "cancelled",
|
|
result: "",
|
|
model: { id: "gpt-5.4" },
|
|
},
|
|
});
|
|
const sdkAgent = createMockSdkAgent({ agentId: "agent-cancelled", sendRun: cancelledRun });
|
|
createMock.mockResolvedValue(sdkAgent);
|
|
const ctx = createContext();
|
|
|
|
const result = await execute(ctx);
|
|
|
|
expect(result).toMatchObject({
|
|
exitCode: 1,
|
|
errorMessage: "Cursor run cancelled",
|
|
sessionId: "agent-cancelled",
|
|
resultJson: {
|
|
status: "cancelled",
|
|
cursorAgentId: "agent-cancelled",
|
|
cursorRunId: "run-cancelled",
|
|
},
|
|
});
|
|
});
|
|
});
|