Files
saltbo__agent-kanban/tests/unit/application/merge-patch.test.ts
realmroot[bot] 96fdd70d2c feat(api)!: redesign task resource operations (#265)
* 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>
2026-09-05 02:40:42 +00:00

31 lines
1.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { mergePatch } from "../../../server/http/resource-server/mergePatch";
describe("JSON Merge Patch", () => {
it.each([
{ name: "an object over an absent value", target: undefined, patch: { nested: { removed: null, added: 1 } }, expected: { nested: { added: 1 } } },
{ name: "an object over an array", target: [1, 2], patch: { added: true }, expected: { added: true } },
{ name: "an array over an object", target: { keep: 1 }, patch: [3], expected: [3] },
{ name: "a scalar over an object", target: { keep: 1 }, patch: false, expected: false },
{ name: "null over an object", target: { keep: 1 }, patch: null, expected: null },
])("[spec: tasks/structured-fields] replaces $name according to merge patch semantics", ({ target, patch, expected }) => {
expect(mergePatch(target, patch)).toEqual(expected);
});
it("[spec: tasks/structured-fields] preserves ordinary JSON prototype keys without mutating either input", () => {
const target = JSON.parse('{"__proto__":{"keep":true,"remove":1},"constructor":{"keep":true},"untouched":1}');
const patch = JSON.parse('{"__proto__":{"remove":null,"added":2},"constructor":{"added":3},"toString":{"added":4}}');
const targetBefore = JSON.stringify(target);
const patchBefore = JSON.stringify(patch);
const result = mergePatch(target, patch);
expect(result).toEqual(
JSON.parse('{"__proto__":{"keep":true,"added":2},"constructor":{"keep":true,"added":3},"untouched":1,"toString":{"added":4}}'),
);
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
expect(JSON.stringify(target)).toBe(targetBefore);
expect(JSON.stringify(patch)).toBe(patchBefore);
});
});