797 lines
23 KiB
TypeScript
797 lines
23 KiB
TypeScript
declare const Deno: {
|
|
test(name: string, fn: () => void | Promise<void>): void;
|
|
};
|
|
|
|
import {
|
|
createSubjektivSubject,
|
|
parseMemoryStagingListResponse,
|
|
parseSubjektivMemoryListChangesResponse,
|
|
parseSubjektivMemoryQueryResponse,
|
|
parseSubjektivMemoryReadResponse,
|
|
parseSubjektivResidentSurfaceResponse,
|
|
parseSubjektivSubjectListResponse,
|
|
parseSubjektivSubjectResponse,
|
|
SubjektivSubjectCreateError,
|
|
updateSubjektivSubjectBehavior,
|
|
validateSubjektivSubjectCreateRequest,
|
|
} from "../src/lib/workspace/memory/api.ts";
|
|
|
|
function stable(value: unknown): unknown {
|
|
if (Array.isArray(value)) return value.map(stable);
|
|
if (typeof value === "object" && value !== null) {
|
|
return Object.fromEntries(
|
|
Object.entries(value as Record<string, unknown>)
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(([key, item]) => [key, stable(item)]),
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function assertEquals(actual: unknown, expected: unknown): void {
|
|
if (JSON.stringify(stable(actual)) !== JSON.stringify(stable(expected))) {
|
|
throw new Error(
|
|
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertThrows(fn: () => void, expectedMessage: string): void {
|
|
try {
|
|
fn();
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes(expectedMessage)) {
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
throw new Error(`expected function to throw ${expectedMessage}`);
|
|
}
|
|
|
|
async function assertSubjectCreateRejects(
|
|
fn: () => Promise<unknown>,
|
|
expectedKind: SubjektivSubjectCreateError["kind"],
|
|
expectedMessage: string,
|
|
expectedStatus: number | null = null,
|
|
): Promise<void> {
|
|
try {
|
|
await fn();
|
|
} catch (error) {
|
|
if (!(error instanceof SubjektivSubjectCreateError)) throw error;
|
|
assertEquals(error.kind, expectedKind);
|
|
assertEquals(error.status, expectedStatus);
|
|
if (!error.message.includes(expectedMessage)) {
|
|
throw new Error(
|
|
`expected ${JSON.stringify(error.message)} to include ${JSON.stringify(expectedMessage)}`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
throw new Error(`expected Subject creation to reject with ${expectedKind}`);
|
|
}
|
|
|
|
function subject(id = "subject-1") {
|
|
return {
|
|
id,
|
|
role: "Release coordinator",
|
|
behavior_md: "Prefer explicit evidence.",
|
|
state: "active",
|
|
memory_fingerprint: "fingerprint-12",
|
|
created_at: "2026-09-01T00:00:00Z",
|
|
updated_at: "2026-09-02T00:00:00Z",
|
|
};
|
|
}
|
|
|
|
function currentWorker() {
|
|
return {
|
|
runtime_id: "runtime-1",
|
|
worker_id: "worker-1",
|
|
host_id: "host-1",
|
|
display_name: "Release Worker",
|
|
label: "Release Worker",
|
|
profile: "builtin:companion",
|
|
singleton_key: "subjektiv:subject-1",
|
|
tags: [],
|
|
workspace: {
|
|
visibility: "workspace",
|
|
identity: "workspace-1",
|
|
workspace_id: "workspace-1",
|
|
},
|
|
state: "idle",
|
|
last_seen_at: "2026-09-02T00:00:00Z",
|
|
pinned: false,
|
|
retention_state: "retained",
|
|
implementation: { kind: "embedded", display_hint: "Embedded" },
|
|
diagnostics: [],
|
|
};
|
|
}
|
|
|
|
function queryItem(id = "memory-1") {
|
|
return {
|
|
id,
|
|
change_id: "change-current",
|
|
kind: "decision",
|
|
state: "active",
|
|
claim: "Keep provenance typed.",
|
|
excerpt: "Preserve source and derivation references.",
|
|
updated_at: "2026-09-02T03:04:05Z",
|
|
};
|
|
}
|
|
|
|
function detailFixture() {
|
|
const origin = {
|
|
kind: "flow_instruction",
|
|
workspace_id: "workspace-1",
|
|
runtime_id: "runtime-1",
|
|
worker_id: "worker-1",
|
|
flow_selector: "builtin:coder-review",
|
|
flow_definition_id: "flow-1",
|
|
};
|
|
return {
|
|
memory_id: "memory-1",
|
|
change_id: "change-earlier",
|
|
current_change_id: "change-current",
|
|
kind: "decision",
|
|
state: "resolved",
|
|
claim: "Keep provenance typed.",
|
|
body_md: "# Committed Memory\n\nBounded body segment.",
|
|
why_useful: "Prevents origin loss.",
|
|
staleness: null,
|
|
change_reason: "Resolved after implementation.",
|
|
created_at: "2026-09-01T00:00:00Z",
|
|
updated_at: "2026-09-02T03:04:05Z",
|
|
body_offset: 0,
|
|
body_byte_offset: 0,
|
|
body_next_offset: 2,
|
|
body_next_byte_offset: 0,
|
|
body_truncated: true,
|
|
source_candidate_ids: ["candidate-1"],
|
|
source_candidates: [{
|
|
candidate_id: "candidate-1",
|
|
evidence: [{
|
|
id: "evidence-1",
|
|
kind: "message",
|
|
entry_range: [10, 12],
|
|
origin,
|
|
excerpt: "Use exact DTOs.",
|
|
summary: "The decision was explicit.",
|
|
}],
|
|
evidence_total: 2,
|
|
evidence_truncated: true,
|
|
source_refs: [{
|
|
session_id: "session-1",
|
|
segment_id: "segment-1",
|
|
entry_range: [10, 12],
|
|
evidence_id: "evidence-1",
|
|
origin,
|
|
evidence_kind: "message",
|
|
label: "Decision discussion",
|
|
summary: "Bounded host summary.",
|
|
}],
|
|
source_refs_total: 2,
|
|
source_refs_truncated: true,
|
|
}],
|
|
derived_from: [{ memory_id: "memory-parent", change_id: "change-parent" }],
|
|
evidence_next_cursor: "evidence-page-2",
|
|
evidence_has_more: true,
|
|
};
|
|
}
|
|
|
|
Deno.test("Subject parsers enforce identity, exact enums, bounded fingerprints, and bounds", () => {
|
|
assertEquals(
|
|
parseSubjektivSubjectResponse(subject(), "subject-1"),
|
|
subject(),
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivSubjectResponse(subject(), "another-subject"),
|
|
"identity does not match",
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivSubjectResponse({ ...subject(), state: "paused" }),
|
|
"unknown Subject state",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivSubjectResponse({
|
|
...subject(),
|
|
memory_fingerprint: 12,
|
|
}),
|
|
"bounded identifier",
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivSubjectResponse({ ...subject(), id: "x".repeat(513) }),
|
|
"bounded identifier",
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivSubjectResponse({ ...subject(), future: true }),
|
|
"unknown field",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivSubjectListResponse({
|
|
limit: 100,
|
|
items: Array.from({ length: 101 }, (_, index) => subject(`subject-${index}`)),
|
|
has_more: false,
|
|
}),
|
|
"bounded array",
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject parser preserves live Worker connection state", () => {
|
|
assertEquals(
|
|
parseSubjektivSubjectResponse({
|
|
...subject(),
|
|
current_worker: currentWorker(),
|
|
}),
|
|
{ ...subject(), current_worker: currentWorker() },
|
|
);
|
|
assertEquals(
|
|
parseSubjektivSubjectResponse({ ...subject(), current_worker: null }),
|
|
{ ...subject(), current_worker: null },
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivSubjectResponse({
|
|
...subject(),
|
|
current_worker: { display_name: "Incomplete Worker" },
|
|
}),
|
|
"current worker.runtime_id",
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject list parser preserves the exact bounded response", () => {
|
|
const response = { limit: 100, items: [subject()], has_more: false };
|
|
assertEquals(parseSubjektivSubjectListResponse(response), response);
|
|
const nextPage = {
|
|
limit: 1,
|
|
items: [subject()],
|
|
next_cursor: "subjektiv.subjects.next",
|
|
has_more: true,
|
|
};
|
|
assertEquals(parseSubjektivSubjectListResponse(nextPage), nextPage);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivSubjectListResponse({
|
|
limit: 1,
|
|
items: [subject()],
|
|
has_more: true,
|
|
}),
|
|
"cursor does not match has_more",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivSubjectListResponse({
|
|
limit: 100,
|
|
items: [subject("duplicate"), subject("duplicate")],
|
|
has_more: false,
|
|
}),
|
|
"Subject ids must be unique",
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject create request mirrors Backend role validation without rewriting input", () => {
|
|
assertEquals(
|
|
validateSubjektivSubjectCreateRequest({ role: " Release coordinator " }),
|
|
{ role: " Release coordinator ", behavior_md: "" },
|
|
);
|
|
assertThrows(
|
|
() => validateSubjektivSubjectCreateRequest({ role: " " }),
|
|
"must not be empty",
|
|
);
|
|
assertThrows(
|
|
() => validateSubjektivSubjectCreateRequest({ role: "line\nbreak" }),
|
|
"control characters",
|
|
);
|
|
assertThrows(
|
|
() => validateSubjektivSubjectCreateRequest({ role: "界".repeat(86) }),
|
|
"at most 256 bytes",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
validateSubjektivSubjectCreateRequest({
|
|
role: "Reviewer",
|
|
behavior_md: " ".repeat(3),
|
|
}),
|
|
"empty or contain non-whitespace",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
validateSubjektivSubjectCreateRequest({
|
|
role: "Reviewer",
|
|
behavior_md: "x".repeat(16 * 1024 + 1),
|
|
}),
|
|
"at most 16384 bytes",
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject behavior update uses CAS and preserves exact text", async () => {
|
|
const requests: Array<{ path: string; init?: RequestInit }> = [];
|
|
const fetchFn = (async (path: string | URL | Request, init?: RequestInit) => {
|
|
requests.push({ path: String(path), init });
|
|
return Response.json({
|
|
...subject("subject/one"),
|
|
behavior_md: "Line one.\nLine two.",
|
|
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
const updated = await updateSubjektivSubjectBehavior(
|
|
fetchFn,
|
|
"workspace one",
|
|
"subject/one",
|
|
{ expected_behavior_md: "Prefer explicit evidence.", behavior_md: "Line one.\nLine two." },
|
|
);
|
|
assertEquals(updated.behavior_md, "Line one.\nLine two.");
|
|
assertEquals(
|
|
requests[0].path,
|
|
"/api/w/workspace%20one/subjektiv/subjects/subject%2Fone/behavior",
|
|
);
|
|
assertEquals(requests[0].init?.method, "PATCH");
|
|
assertEquals(
|
|
requests[0].init?.body,
|
|
'{"expected_behavior_md":"Prefer explicit evidence.","behavior_md":"Line one.\\nLine two."}',
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject behavior update reports conflicts without claiming Worker application", async () => {
|
|
const conflictFetch =
|
|
(async () =>
|
|
Response.json({ message: "behavior content conflict" }, {
|
|
status: 409,
|
|
})) as typeof fetch;
|
|
await assertSubjectCreateRejects(
|
|
() =>
|
|
updateSubjektivSubjectBehavior(conflictFetch, "workspace", "subject", {
|
|
expected_behavior_md: "old",
|
|
behavior_md: "new",
|
|
}),
|
|
"rejected",
|
|
"changed elsewhere",
|
|
409,
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject create client sends the exact typed body once and parses the created Subject", async () => {
|
|
const requests: Array<{ path: string; init?: RequestInit }> = [];
|
|
const fetchFn = (async (path: string | URL | Request, init?: RequestInit) => {
|
|
requests.push({ path: String(path), init });
|
|
return Response.json({
|
|
...subject("subject-created"),
|
|
role: " Release coordinator ",
|
|
behavior_md: "Stay concise.",
|
|
}, { status: 201 });
|
|
}) as typeof fetch;
|
|
|
|
const created = await createSubjektivSubject(fetchFn, "workspace one", {
|
|
role: " Release coordinator ",
|
|
behavior_md: "Stay concise.",
|
|
});
|
|
assertEquals(created, {
|
|
...subject("subject-created"),
|
|
role: " Release coordinator ",
|
|
behavior_md: "Stay concise.",
|
|
});
|
|
assertEquals(requests.length, 1);
|
|
assertEquals(requests[0].path, "/api/w/workspace%20one/subjektiv/subjects");
|
|
assertEquals(requests[0].init?.method, "POST");
|
|
assertEquals(
|
|
requests[0].init?.body,
|
|
'{"role":" Release coordinator ","behavior_md":"Stay concise."}',
|
|
);
|
|
});
|
|
|
|
Deno.test("Subject create client distinguishes rejected and unknown outcomes without retrying", async () => {
|
|
let rejectedCalls = 0;
|
|
const rejectedFetch = (async () => {
|
|
rejectedCalls += 1;
|
|
return Response.json(
|
|
{
|
|
error: "Forbidden",
|
|
message: "workspace permission denied",
|
|
diagnostics: [],
|
|
},
|
|
{ status: 403 },
|
|
);
|
|
}) as typeof fetch;
|
|
await assertSubjectCreateRejects(
|
|
() =>
|
|
createSubjektivSubject(rejectedFetch, "workspace-1", {
|
|
role: "Reviewer",
|
|
}),
|
|
"rejected",
|
|
"workspace permission denied",
|
|
403,
|
|
);
|
|
assertEquals(rejectedCalls, 1);
|
|
|
|
let serverErrorCalls = 0;
|
|
const serverErrorFetch = (async () => {
|
|
serverErrorCalls += 1;
|
|
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
}) as typeof fetch;
|
|
await assertSubjectCreateRejects(
|
|
() =>
|
|
createSubjektivSubject(serverErrorFetch, "workspace-1", {
|
|
role: "Reviewer",
|
|
}),
|
|
"unknown_outcome",
|
|
"may have been created",
|
|
500,
|
|
);
|
|
assertEquals(serverErrorCalls, 1);
|
|
|
|
let mismatchCalls = 0;
|
|
const mismatchFetch = (async () => {
|
|
mismatchCalls += 1;
|
|
return Response.json(subject("subject-created"), { status: 201 });
|
|
}) as typeof fetch;
|
|
await assertSubjectCreateRejects(
|
|
() =>
|
|
createSubjektivSubject(mismatchFetch, "workspace-1", {
|
|
role: "Reviewer",
|
|
}),
|
|
"unknown_outcome",
|
|
"could not be confirmed",
|
|
);
|
|
assertEquals(mismatchCalls, 1);
|
|
|
|
let unknownCalls = 0;
|
|
const unknownFetch = (async () => {
|
|
unknownCalls += 1;
|
|
throw new TypeError("connection reset");
|
|
}) as typeof fetch;
|
|
await assertSubjectCreateRejects(
|
|
() => createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
|
|
"unknown_outcome",
|
|
"may have been created",
|
|
);
|
|
assertEquals(unknownCalls, 1);
|
|
});
|
|
|
|
Deno.test("Resident surface parser accepts ready-empty and enforces snapshot invariants", () => {
|
|
const readyEmpty = {
|
|
subject_id: "subject-1",
|
|
availability: "ready",
|
|
snapshot: {
|
|
snapshot_id: "snapshot-empty",
|
|
body_md: "",
|
|
memory_refs: [],
|
|
built_from_memory_fingerprint: "fingerprint-0",
|
|
created_at: "2026-09-02T00:00:00Z",
|
|
},
|
|
};
|
|
assertEquals(
|
|
parseSubjektivResidentSurfaceResponse(readyEmpty, "subject-1"),
|
|
readyEmpty,
|
|
);
|
|
assertEquals(
|
|
parseSubjektivResidentSurfaceResponse({
|
|
subject_id: "subject-1",
|
|
availability: "ungenerated",
|
|
}),
|
|
{ subject_id: "subject-1", availability: "ungenerated" },
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivResidentSurfaceResponse({
|
|
subject_id: "subject-1",
|
|
availability: "ready",
|
|
}),
|
|
"requires a snapshot",
|
|
);
|
|
for (const availability of ["ungenerated", "stale", "failed"]) {
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivResidentSurfaceResponse({
|
|
...readyEmpty,
|
|
availability,
|
|
}),
|
|
"must not include a snapshot",
|
|
);
|
|
}
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivResidentSurfaceResponse({
|
|
subject_id: "subject-1",
|
|
availability: "refreshing",
|
|
}),
|
|
"unknown resident surface availability",
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivResidentSurfaceResponse(readyEmpty, "subject-2"),
|
|
"identity does not match",
|
|
);
|
|
});
|
|
|
|
Deno.test("Current Memory list parser rejects unknown variants, invalid change IDs, cursors, and oversized pages", () => {
|
|
const response = {
|
|
items: [queryItem()],
|
|
has_more: true,
|
|
next_cursor: "memory-page-2",
|
|
};
|
|
assertEquals(parseSubjektivMemoryQueryResponse(response), response);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryQueryResponse({
|
|
...response,
|
|
items: [{ ...queryItem(), state: "pending" }],
|
|
}),
|
|
"unknown Memory state",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryQueryResponse({
|
|
...response,
|
|
items: [{ ...queryItem(), kind: "future_kind" }],
|
|
}),
|
|
"unknown Memory candidate kind",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryQueryResponse({
|
|
...response,
|
|
items: [{ ...queryItem(), change_id: "" }],
|
|
}),
|
|
"bounded identifier",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryQueryResponse({
|
|
items: [queryItem()],
|
|
has_more: true,
|
|
}),
|
|
"cursor does not match",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryQueryResponse({
|
|
items: Array.from({ length: 101 }, (_, index) => queryItem(`memory-${index}`)),
|
|
has_more: false,
|
|
}),
|
|
"bounded array",
|
|
);
|
|
});
|
|
|
|
Deno.test("Memory detail parser preserves body/evidence continuation, sources, origins, and derivation", () => {
|
|
const fixture = detailFixture();
|
|
const parsed = parseSubjektivMemoryReadResponse(fixture, "memory-1");
|
|
assertEquals(parsed, fixture);
|
|
assertEquals(parsed.body_next_offset, 2);
|
|
assertEquals(parsed.body_next_byte_offset, 0);
|
|
assertEquals(parsed.evidence_next_cursor, "evidence-page-2");
|
|
assertEquals(
|
|
parsed.source_candidates[0].evidence[0].origin,
|
|
fixture.source_candidates[0].evidence[0].origin,
|
|
);
|
|
assertEquals(
|
|
parsed.source_candidates[0].source_refs,
|
|
fixture.source_candidates[0].source_refs,
|
|
);
|
|
assertEquals(parsed.derived_from, [{
|
|
memory_id: "memory-parent",
|
|
change_id: "change-parent",
|
|
}]);
|
|
});
|
|
|
|
Deno.test("Memory detail parser fails closed on identity, variants, bounds, and continuation mismatch", () => {
|
|
const fixture = detailFixture();
|
|
assertThrows(
|
|
() => parseSubjektivMemoryReadResponse(fixture, "memory-2"),
|
|
"identity does not match",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryReadResponse({
|
|
...fixture,
|
|
current_change_id: 3,
|
|
}),
|
|
"bounded identifier",
|
|
);
|
|
const missingBodyContinuation = structuredClone(fixture) as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
delete missingBodyContinuation.body_next_byte_offset;
|
|
assertThrows(
|
|
() => parseSubjektivMemoryReadResponse(missingBodyContinuation),
|
|
"body continuation fields",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryReadResponse({
|
|
...fixture,
|
|
source_candidate_ids: ["different-candidate"],
|
|
}),
|
|
"source candidate ids do not match",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryReadResponse({
|
|
...fixture,
|
|
source_candidates: [{
|
|
...fixture.source_candidates[0],
|
|
evidence: [{
|
|
...fixture.source_candidates[0].evidence[0],
|
|
entry_range: [12, 10],
|
|
}],
|
|
}],
|
|
}),
|
|
"must be ordered",
|
|
);
|
|
const unknownOrigin = structuredClone(fixture);
|
|
unknownOrigin.source_candidates[0].evidence[0].origin.kind = "future_origin";
|
|
assertThrows(
|
|
() => parseSubjektivMemoryReadResponse(unknownOrigin),
|
|
"unknown Memory evidence origin kind",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryReadResponse({
|
|
...fixture,
|
|
evidence_has_more: false,
|
|
}),
|
|
"cursor does not match",
|
|
);
|
|
});
|
|
|
|
Deno.test("Memory change parser preserves immutable states and checks response identity", () => {
|
|
const response = {
|
|
memory_id: "memory-1",
|
|
current_change_id: "change-current",
|
|
items: [{
|
|
change_id: "change-current",
|
|
kind: "decision",
|
|
state: "active",
|
|
claim: "Current claim.",
|
|
change_reason: "Clarified wording.",
|
|
updated_at: "2026-09-03T00:00:00Z",
|
|
}, {
|
|
change_id: "change-earlier",
|
|
kind: "decision",
|
|
state: "resolved",
|
|
claim: "Earlier claim.",
|
|
change_reason: "Resolved.",
|
|
updated_at: "2026-09-02T00:00:00Z",
|
|
}, {
|
|
change_id: "change-original",
|
|
kind: "decision",
|
|
state: "retracted",
|
|
claim: "Original claim.",
|
|
change_reason: "Retracted.",
|
|
updated_at: "2026-09-01T00:00:00Z",
|
|
}],
|
|
has_more: false,
|
|
};
|
|
assertEquals(
|
|
parseSubjektivMemoryListChangesResponse(response, "memory-1"),
|
|
response,
|
|
);
|
|
assertThrows(
|
|
() => parseSubjektivMemoryListChangesResponse(response, "memory-2"),
|
|
"identity does not match",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryListChangesResponse({
|
|
...response,
|
|
items: [{ ...response.items[0], state: "archived" }],
|
|
}),
|
|
"unknown Memory state",
|
|
);
|
|
assertThrows(
|
|
() =>
|
|
parseSubjektivMemoryListChangesResponse({
|
|
...response,
|
|
items: [response.items[0], response.items[0]],
|
|
}),
|
|
"Memory changes must be unique",
|
|
);
|
|
});
|
|
|
|
Deno.test("Memory changes preserve server order without interpreting opaque identities", () => {
|
|
const item = { kind: "decision", state: "active", claim: "Claim", change_reason: "Reason", updated_at: "2026-09-01T00:00:00Z" };
|
|
const response = {
|
|
memory_id: "memory-1",
|
|
current_change_id: "a/current",
|
|
items: [{ ...item, change_id: "z/earlier" }, { ...item, change_id: "a/current" }],
|
|
has_more: false,
|
|
};
|
|
assertEquals(parseSubjektivMemoryListChangesResponse(response), response);
|
|
assertEquals(parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: "z/earlier", current_change_id: "a/current" }).change_id, "z/earlier");
|
|
});
|
|
|
|
Deno.test("Memory identities reject numeric counters, empty and oversized strings, and unknown fields", () => {
|
|
for (const value of [3, "", "x".repeat(513)]) {
|
|
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: value }), "bounded identifier");
|
|
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), current_change_id: value }), "bounded identifier");
|
|
assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), memory_fingerprint: value }), "bounded identifier");
|
|
}
|
|
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), unexpected_field: true }), "unknown field");
|
|
assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), unexpected_field: true }), "unknown field");
|
|
});
|
|
|
|
Deno.test("Behavior CAS sends the observed text byte-for-byte including empty content", async () => {
|
|
for (const expected of ["", " Previous\nbehavior.\n"]) {
|
|
let body: unknown;
|
|
const fetchFn = (async (_path: string | URL | Request, init?: RequestInit) => {
|
|
body = JSON.parse(String(init?.body));
|
|
return Response.json({ ...subject(), behavior_md: "Replacement" });
|
|
}) as typeof fetch;
|
|
await updateSubjektivSubjectBehavior(fetchFn, "workspace", "subject-1", { expected_behavior_md: expected, behavior_md: "Replacement" });
|
|
assertEquals(body, { expected_behavior_md: expected, behavior_md: "Replacement" });
|
|
}
|
|
});
|
|
|
|
Deno.test("Behavior saves with mismatched response text have unknown outcome and are not retried", async () => {
|
|
let calls = 0;
|
|
const fetchFn = (async () => {
|
|
calls += 1;
|
|
return Response.json(subject());
|
|
}) as typeof fetch;
|
|
await assertSubjectCreateRejects(() => updateSubjektivSubjectBehavior(fetchFn, "workspace", "subject-1", {
|
|
expected_behavior_md: "Prefer explicit evidence.", behavior_md: "Replacement",
|
|
}), "unknown_outcome", "could not be confirmed");
|
|
assertEquals(calls, 1);
|
|
});
|
|
|
|
Deno.test("Legacy staging parser remains strict for deprecated compatibility", () => {
|
|
const origin = { kind: "worker_input", worker_id: "worker-1" };
|
|
const response = {
|
|
limit: 100,
|
|
returned_count: 1,
|
|
total_valid_count: 1,
|
|
invalid_count: 0,
|
|
truncated: false,
|
|
order: "imported_at_desc_candidate_id_asc",
|
|
record_authority: "sqlite_workspace_authority.memory_staging",
|
|
items: [{
|
|
id: "candidate-1",
|
|
byte_len: 128,
|
|
record: {
|
|
schema_version: 2,
|
|
id: "candidate-1",
|
|
extract_run_id: "extract-run-1",
|
|
source: { segment_id: "segment-1", range: [10, 20] },
|
|
kind: "decision",
|
|
claim: "Keep provenance typed.",
|
|
why_useful: "Prevents origin loss.",
|
|
staleness: null,
|
|
evidence: [{
|
|
id: "evidence-1",
|
|
kind: "message",
|
|
entry_range: [10, 10],
|
|
origin,
|
|
excerpt: null,
|
|
summary: "bounded summary",
|
|
}],
|
|
source_refs: [{
|
|
session_id: "session-1",
|
|
segment_id: "segment-1",
|
|
entry_range: [10, 10],
|
|
evidence_id: "evidence-1",
|
|
origin,
|
|
evidence_kind: "message",
|
|
label: "source",
|
|
summary: null,
|
|
}],
|
|
},
|
|
}],
|
|
diagnostics: [],
|
|
};
|
|
assertEquals(parseMemoryStagingListResponse(response), response);
|
|
assertThrows(
|
|
() =>
|
|
parseMemoryStagingListResponse({
|
|
...response,
|
|
items: [{
|
|
...response.items[0],
|
|
record: {
|
|
...response.items[0].record,
|
|
source: { segment_id: "segment-1", range: [20, 10] },
|
|
},
|
|
}],
|
|
}),
|
|
"must be ordered",
|
|
);
|
|
});
|