mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
Read current GitHub response identity and policy fields without transferring saved message bodies, instructions, or automatic policy wrappers. Preserve company predicates, shared locks, publication leases, malformed input handling, and stored content. Verify workspace typecheck, server build, 414 selected GitHub tests, three focused browser cases, all 55 checks, and Apex 5/5. Preserve the original browser cleanup deadlock failure; always restore experiment settings while reporting cancellation errors. Co-Authored-By: Paperclip <noreply@paperclip.ing>
180 lines
5.6 KiB
TypeScript
180 lines
5.6 KiB
TypeScript
import path from "node:path";
|
|
import { createLocalAgentJwt } from "../../server/src/agent-auth-jwt";
|
|
import {
|
|
test,
|
|
expect,
|
|
type APIRequestContext,
|
|
type Page,
|
|
} from "@playwright/test";
|
|
|
|
// Per-file Playwright configuration (test.use, test.setTimeout) must stay in
|
|
// the spec files themselves: calling test.use() from a shared helper makes
|
|
// Playwright fail the whole worker with "inconsistent test.use() options".
|
|
export async function json(response: Awaited<ReturnType<APIRequestContext["get"]>>) {
|
|
expect(response.ok(), `${response.status()} ${await response.text()}`).toBe(
|
|
true,
|
|
);
|
|
return response.json();
|
|
}
|
|
export async function setup(request: APIRequestContext) {
|
|
const company = await json(
|
|
await request.post("/api/companies", {
|
|
data: { name: `Agent Chat ${Date.now()}` },
|
|
}),
|
|
);
|
|
const original = await json(
|
|
await request.get("/api/instance/settings/experimental"),
|
|
);
|
|
await json(
|
|
await request.patch("/api/instance/settings/experimental", {
|
|
data: { enableAgentChat: true, enableClassicTaskInterface: false },
|
|
}),
|
|
);
|
|
const agents = [];
|
|
for (const name of ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"])
|
|
agents.push(
|
|
await json(
|
|
await request.post(`/api/companies/${company.id}/agents`, {
|
|
data: {
|
|
name,
|
|
adapterType: "process",
|
|
adapterConfig: {
|
|
command: process.execPath,
|
|
args: [path.resolve("tests/e2e/fixtures/agent-chat.mjs")],
|
|
graceSec: 1,
|
|
},
|
|
runtimeConfig: {
|
|
heartbeat: { enabled: false, wakeOnDemand: true },
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
const agent = agents[0];
|
|
const chatPath = `/api/companies/${company.id}/chats/${agent.id}`;
|
|
const route = `/${company.issuePrefix}/chats/${agent.id}`;
|
|
return {
|
|
company,
|
|
agents,
|
|
agent,
|
|
chatPath,
|
|
route,
|
|
restore: async () => {
|
|
try {
|
|
const chat = await request.get(chatPath);
|
|
const history = chat.ok() ? await chat.json() : null;
|
|
const tasks = await json(
|
|
await request.get(`/api/companies/${company.id}/issues`),
|
|
);
|
|
const projects = await json(
|
|
await request.get(`/api/companies/${company.id}/projects`),
|
|
);
|
|
const ledger = await json(
|
|
await request.get(`/api/companies/${company.id}/heartbeat-runs`),
|
|
);
|
|
const documents = await Promise.all(
|
|
[...(history ? [history] : []), ...tasks].map(async (task: any) => ({
|
|
taskId: task.id,
|
|
documents: await json(
|
|
await request.get(`/api/issues/${task.id}/documents`),
|
|
),
|
|
})),
|
|
);
|
|
await test.info().attach("chat-persisted-state", {
|
|
contentType: "application/json",
|
|
body: Buffer.from(
|
|
JSON.stringify(
|
|
{
|
|
chat: history,
|
|
tasks,
|
|
projects,
|
|
documents,
|
|
comments: history
|
|
? await json(
|
|
await request.get(`/api/issues/${history.id}/comments`),
|
|
)
|
|
: [],
|
|
runs: ledger.map((run: any) => ({
|
|
id: run.id,
|
|
status: run.status,
|
|
startedAt: run.startedAt,
|
|
finishedAt: run.finishedAt,
|
|
sessionIdBefore: run.sessionIdBefore,
|
|
sessionIdAfter: run.sessionIdAfter,
|
|
issueId: run.contextSnapshot?.issueId,
|
|
generation:
|
|
run.contextSnapshot?.conversationSessionGeneration,
|
|
reset: run.contextSnapshot?.conversationReset,
|
|
})),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
),
|
|
});
|
|
} finally {
|
|
try {
|
|
const runs = await json(
|
|
await request.get(`/api/companies/${company.id}/live-runs`),
|
|
);
|
|
for (const run of runs)
|
|
await json(
|
|
await request.post(`/api/heartbeat-runs/${run.id}/cancel`),
|
|
);
|
|
} finally {
|
|
await json(
|
|
await request.patch("/api/instance/settings/experimental", {
|
|
data: {
|
|
enableAgentChat: original.enableAgentChat,
|
|
enableClassicTaskInterface: original.enableClassicTaskInterface,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|
|
export async function send(page: Page, value: unknown) {
|
|
await page
|
|
.getByTestId("task-chat-composer-input")
|
|
.last()
|
|
.locator('[contenteditable="true"],textarea')
|
|
.first()
|
|
.fill(
|
|
typeof value === "string"
|
|
? value
|
|
: `fixture:${Buffer.from(JSON.stringify(value)).toString("base64url")}`,
|
|
);
|
|
await page.getByTestId("task-chat-composer-send").last().click();
|
|
}
|
|
export async function idle(
|
|
request: APIRequestContext,
|
|
chatPath: string,
|
|
minimumReplies = 1,
|
|
) {
|
|
let issue: any;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
issue = await json(await request.get(chatPath));
|
|
if (!issue) return false;
|
|
const replies = await json(
|
|
await request.get(`/api/issues/${issue.id}/comments`),
|
|
);
|
|
const live = await json(
|
|
await request.get(`/api/issues/${issue.id}/live-runs`),
|
|
);
|
|
return (
|
|
live.length === 0 &&
|
|
issue.conversationState === "waiting" &&
|
|
issue.status === "in_review" &&
|
|
replies.filter((c: any) => c.authorAgentId).length >= minimumReplies
|
|
);
|
|
},
|
|
{ timeout: 60_000, intervals: [100, 250, 500] },
|
|
)
|
|
.toBe(true);
|
|
return issue;
|
|
}
|