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>
64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { Miniflare } from "miniflare";
|
|
import { describe, expect, it } from "vitest";
|
|
import { applyMigrationSql, seedUser } from "../../helpers/db";
|
|
|
|
const migrationsDirectory = join(process.cwd(), "migrations");
|
|
const migrationFile = "0056_task_version.sql";
|
|
|
|
describe("0056 Task version migration", () => {
|
|
it("preserves existing Tasks at version one and requires a monotonic version on new rows", async () => {
|
|
const mf = new Miniflare({
|
|
modules: true,
|
|
script: "export default { fetch() { return new Response('ok'); } }",
|
|
d1Databases: { DB: "task-version-migration" },
|
|
});
|
|
try {
|
|
const db = await mf.getD1Database("DB");
|
|
for (const file of readdirSync(migrationsDirectory)
|
|
.filter((name) => name.endsWith(".sql") && name < migrationFile)
|
|
.sort()) {
|
|
await applyMigrationSql(db, readFileSync(join(migrationsDirectory, file), "utf8"));
|
|
}
|
|
await seedUser(db, "task-version-owner", "task-version@example.test");
|
|
await db
|
|
.prepare(
|
|
`INSERT INTO boards (id, owner_id, name, type, created_at, updated_at)
|
|
VALUES ('task-version-board', 'task-version-owner', 'Versioned Tasks', 'ops', '2026-09-04T00:00:00.000Z', '2026-09-04T00:00:00.000Z')`,
|
|
)
|
|
.run();
|
|
await db
|
|
.prepare(
|
|
`INSERT INTO tasks (id, board_id, seq, status, title, created_at, updated_at)
|
|
VALUES ('existing-task', 'task-version-board', 1, 'in_progress', 'Preserved Task', '2026-09-04T00:00:00.000Z', '2026-09-04T00:00:00.000Z')`,
|
|
)
|
|
.run();
|
|
|
|
await applyMigrationSql(db, readFileSync(join(migrationsDirectory, migrationFile), "utf8"));
|
|
|
|
await expect(db.prepare("SELECT id, status, title, version FROM tasks WHERE id = 'existing-task'").first()).resolves.toEqual({
|
|
id: "existing-task",
|
|
status: "in_progress",
|
|
title: "Preserved Task",
|
|
version: 1,
|
|
});
|
|
const versionColumn = await db.prepare("PRAGMA table_info('tasks')").all<{ name: string; notnull: number; dflt_value: string | null }>();
|
|
expect(versionColumn.results.find(({ name }) => name === "version")).toMatchObject({ notnull: 1, dflt_value: "1" });
|
|
|
|
await db
|
|
.prepare(
|
|
`INSERT INTO tasks (id, board_id, seq, status, title, created_at, updated_at)
|
|
VALUES ('new-task', 'task-version-board', 2, 'todo', 'New Task', '2026-09-04T00:01:00.000Z', '2026-09-04T00:01:00.000Z')`,
|
|
)
|
|
.run();
|
|
await expect(db.prepare("SELECT version FROM tasks WHERE id = 'new-task'").first()).resolves.toEqual({ version: 1 });
|
|
await expect(db.prepare("UPDATE tasks SET version = NULL WHERE id = 'new-task'").run()).rejects.toThrow(/NOT NULL constraint failed/);
|
|
} finally {
|
|
await mf.dispose();
|
|
}
|
|
});
|
|
});
|