test(agent-api): auth, all five routes, and the accept-on-move transition

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-09-26 10:00:14 +02:00
parent 19705cc8d9
commit 05d53e9bb8

View file

@ -0,0 +1,229 @@
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import PocketBase from "pocketbase";
import { APP_URL, DEMO, adminPb, requireServices } from "./helpers";
const TOKEN = process.env.AGENT_API_TOKEN;
let admin: PocketBase;
let projectId: string;
let inboxColumnId: string;
let backlogColumnId: string;
function authed(path: string, init: RequestInit = {}) {
return fetch(`${APP_URL}${path}`, {
...init,
headers: { ...(init.headers ?? {}), authorization: `Bearer ${TOKEN}` },
});
}
beforeAll(async () => {
await requireServices();
if (!TOKEN) {
throw new Error(
"AGENT_API_TOKEN must be set in the environment running this test (see .env.local)."
);
}
admin = await adminPb();
const project = await admin
.collection("projects")
.getFirstListItem(`slug = "${DEMO.projectSlug}"`);
projectId = project.id;
const inbox = await admin
.collection("columns")
.getFirstListItem(`project = "${projectId}" && is_inbox = true`);
inboxColumnId = inbox.id;
const backlog = await admin
.collection("columns")
.getFirstListItem(`project = "${projectId}" && is_inbox = false && is_done = false`);
backlogColumnId = backlog.id;
});
describe("authentication", () => {
it("refuses a request with no token", async () => {
const response = await fetch(`${APP_URL}/api/agent/projects`);
expect(response.status).toBe(401);
});
it("refuses a request with the wrong token", async () => {
const response = await fetch(`${APP_URL}/api/agent/projects`, {
headers: { authorization: "Bearer not-the-real-token" },
});
expect(response.status).toBe(401);
});
it("accepts a request with the right token", async () => {
const response = await authed("/api/agent/projects");
expect(response.status).toBe(200);
});
});
describe("GET /api/agent/projects", () => {
it("includes the demo project with its client", async () => {
const response = await authed("/api/agent/projects");
const projects = await response.json();
const found = projects.find((p: { id: string }) => p.id === projectId);
expect(found).toBeTruthy();
expect(found.client.slug).toBe(DEMO.clientSlug);
});
});
describe("GET /api/agent/projects/:projectId/cards", () => {
let cardId: string;
afterAll(async () => {
if (cardId) await admin.collection("cards").delete(cardId).catch(() => {});
});
it("lists a newly created Inbox card, and the ?column=inbox filter excludes non-Inbox cards", async () => {
const stamp = Date.now();
const card = await admin.collection("cards").create({
project: projectId,
column: inboxColumnId,
title: `Agent API probe ${stamp}`,
description: "",
position: 999999,
priority: "medium",
created_by_role: "owner",
accepted: false,
archived: false,
});
cardId = card.id;
const all = await authed(`/api/agent/projects/${projectId}/cards`).then((r) => r.json());
expect(all.some((c: { id: string }) => c.id === cardId)).toBe(true);
const inboxOnly = await authed(`/api/agent/projects/${projectId}/cards?column=inbox`).then(
(r) => r.json()
);
expect(inboxOnly.every((c: { column: { is_inbox: boolean } }) => c.column.is_inbox)).toBe(
true
);
expect(inboxOnly.some((c: { id: string }) => c.id === cardId)).toBe(true);
});
it("404s for an unknown project", async () => {
const response = await authed("/api/agent/projects/does-not-exist/cards");
expect(response.status).toBe(404);
});
});
describe("card detail, comments, subtasks, and move", () => {
let cardId: string;
beforeAll(async () => {
const card = await admin.collection("cards").create({
project: projectId,
column: inboxColumnId,
title: `Agent API detail probe ${Date.now()}`,
description: "A card for the agent API's own integration tests.",
position: 999998,
priority: "medium",
created_by_role: "owner",
accepted: false,
archived: false,
});
cardId = card.id;
});
afterAll(async () => {
await admin.collection("cards").delete(cardId).catch(() => {});
});
it("GET returns the card with empty subtasks/comments", async () => {
const response = await authed(`/api/agent/cards/${cardId}`);
const detail = await response.json();
expect(detail.id).toBe(cardId);
expect(detail.subtasks).toEqual([]);
expect(detail.comments).toEqual([]);
});
it("POST comments prefixes the body and attributes it to the owner", async () => {
const response = await authed(`/api/agent/cards/${cardId}/comments`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ body: "Here is the plan." }),
});
expect(response.status).toBe(201);
const stored = await admin.collection("comments").getFullList({
filter: `card = "${cardId}"`,
});
expect(stored).toHaveLength(1);
expect(stored[0].body.startsWith("🤖 Drafted by your agent:")).toBe(true);
expect(stored[0].body).toContain("Here is the plan.");
const ownerUser = await admin.collection("users").getFirstListItem('role = "owner"');
expect(stored[0].author).toBe(ownerUser.id);
});
it("rejects an empty comment body", async () => {
const response = await authed(`/api/agent/cards/${cardId}/comments`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ body: " " }),
});
expect(response.status).toBe(400);
});
it("POST subtasks creates them in order after any existing ones", async () => {
const response = await authed(`/api/agent/cards/${cardId}/subtasks`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ titles: ["Step one", "Step two", "Step three"] }),
});
expect(response.status).toBe(201);
const stored = await admin.collection("subtasks").getFullList({
filter: `card = "${cardId}"`,
sort: "position",
});
expect(stored.map((s) => s.title)).toEqual(["Step one", "Step two", "Step three"]);
});
it("rejects a subtasks request with no usable titles", async () => {
const response = await authed(`/api/agent/cards/${cardId}/subtasks`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ titles: [" ", ""] }),
});
expect(response.status).toBe(400);
});
it("PATCH move rejects an unknown column name and lists the real ones", async () => {
const response = await authed(`/api/agent/cards/${cardId}/move`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ column: "Not A Real Column" }),
});
expect(response.status).toBe(400);
const body = await response.json();
expect(body.error).toContain("Not A Real Column");
});
it("PATCH move to a working column accepts the card and logs it", async () => {
const backlog = await admin.collection("columns").getOne(backlogColumnId);
const response = await authed(`/api/agent/cards/${cardId}/move`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ column: backlog.name }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.accepted).toBe(true);
const updated = await admin.collection("cards").getOne(cardId);
expect(updated.column).toBe(backlogColumnId);
expect(updated.accepted).toBe(true);
expect(updated.start_date).not.toBe("");
const activity = await admin.collection("activity").getFullList({
filter: `card = "${cardId}" && action = "card_accepted"`,
});
expect(activity.length).toBeGreaterThan(0);
});
});