mirror of
https://github.com/saltbo/agent-kanban.git
synced 2026-09-19 09:01:47 +08:00
96fdd70d2c
* feat(api)!: redesign task resource operations
Publish the complete resource-oriented OpenAPI contract, enforce scope-based authorization at the HTTP boundary, and replace lifecycle command routes with Task PATCH and nested Claim/Event resources.
Add server-side Task CAS, migration coverage, updated Agent Skills, and browser/API regression tests.
BREAKING CHANGE: remove the legacy task lifecycle endpoints and move bounded waits to /tasks/{taskId}/events.
* refactor(tasks)!: make claims create-only
Remove Task Claim retrieval and deletion routes, the release scope, live deletion logic, and release workflow guidance.
Keep historical release records readable for migration and audit compatibility.
BREAKING CHANGE: remove GET and DELETE /tasks/{taskId}/claims/{claimId} and the task:release scope.
* test(ci): align actor and projection contracts
Cover machine and service Task Action writes and update Agent detail fixtures to the canonical assignedTo query and paged lowerCamelCase response.
* fix(api): preserve merge patch fields and consume browser pagination
* test(ui): include terminal pagination in agent task fixture
---------
Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { authorizeScope } from "../../../server/auth/middleware";
|
|
import type { RealmrootPrincipal } from "../../../server/auth/realmroot";
|
|
|
|
describe("authorizeScope", () => {
|
|
it.each([
|
|
["session", "human"],
|
|
["token", "human"],
|
|
["token", "agent"],
|
|
["token", "service"],
|
|
] as const)("allows a %s %s principal solely from its granted scope", async (source, type) => {
|
|
const next = vi.fn(async () => undefined);
|
|
const context = contextWith({ source, type, scopes: ["task:read"] });
|
|
|
|
await authorizeScope("task:read")(context, next);
|
|
|
|
expect(next).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("returns RFC Problem 403 when the principal lacks the required scope", async () => {
|
|
const next = vi.fn(async () => undefined);
|
|
const context = contextWith({ source: "session", type: "human", scopes: ["board:read"] });
|
|
|
|
const response = await authorizeScope("task:read")(context, next);
|
|
|
|
expect(response?.status).toBe(403);
|
|
expect(response?.headers.get("content-type")).toContain("application/problem+json");
|
|
await expect(response?.json()).resolves.toMatchObject({
|
|
title: "Permission denied",
|
|
status: 403,
|
|
detail: "Missing scope: task:read",
|
|
});
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function contextWith(input: Pick<RealmrootPrincipal, "source" | "type" | "scopes">): Parameters<ReturnType<typeof authorizeScope>>[0] {
|
|
const values: Record<string, unknown> = {
|
|
principal: {
|
|
...input,
|
|
subjectId: "subject-1",
|
|
tenantId: "tenant-1",
|
|
},
|
|
requestId: "request-1",
|
|
};
|
|
return {
|
|
req: { method: "GET", path: "/api/tasks", url: "https://agent-kanban.test/api/tasks" },
|
|
get(key: string) {
|
|
return values[key];
|
|
},
|
|
json(body: unknown, status: number, headers: HeadersInit) {
|
|
return Response.json(body, { status, headers });
|
|
},
|
|
} as Parameters<ReturnType<typeof authorizeScope>>[0];
|
|
}
|