Add persistent Subject behavior context

This commit is contained in:
2026-10-05 16:27:23 +09:00
parent cc97987482
commit 8891236db3
28 changed files with 1999 additions and 190 deletions
@@ -66,7 +66,14 @@ ${"detail-wide-code-".repeat(18)}
<script>alert("not rendered")</script>`;
const subjects = [
subject(representativeSubjectId, "Release coordination", "active", 42),
subject(
representativeSubjectId,
"Release coordination",
"active",
42,
"Prefer explicit evidence over assumptions.\nAsk before irreversible actions, and state uncertainty plainly.",
3,
),
subject(emptySubjectId, "Empty ready subject", "active", 0),
subject(staleSubjectId, "Stale surface subject", "active", 19),
subject(failedSubjectId, "Failed surface subject", "active", 8),
@@ -75,10 +82,19 @@ const subjects = [
];
const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", 1);
function subject(id: string, role: string, state: "active" | "retired", storeRevision: number) {
function subject(
id: string,
role: string,
state: "active" | "retired",
storeRevision: number,
behaviorMd = "",
behaviorRevision = 0,
) {
return {
id,
role,
behavior_md: behaviorMd,
behavior_revision: behaviorRevision,
state,
store_revision: storeRevision,
created_at: "2026-01-01T00:00:00Z",
@@ -403,7 +419,10 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
if (url.pathname === subjectsPath) {
if (request.method === "POST") {
const payload = await request.json().catch(() => null) as { role?: unknown } | null;
const payload = await request.json().catch(() => null) as {
role?: unknown;
behavior_md?: unknown;
} | null;
subjectCreateRequests.push(payload);
if (!payload || typeof payload.role !== "string" || payload.role.trim().length === 0) {
return json({ error: "Bad Request", message: "subject role must not be empty" }, 400);
@@ -411,9 +430,12 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
if (payload.role === "Permission denied") {
return json({ error: "Forbidden", message: "workspace permission denied" }, 403);
}
if (typeof payload.behavior_md !== "string") {
return json({ error: "Bad Request", message: "subject behavior must be a string" }, 400);
}
createdSubjectCount += 1;
const id = `created-subject-${String(createdSubjectCount).padStart(4, "0")}`;
const created = subject(id, payload.role, "active", 0);
const created = subject(id, payload.role, "active", 0, payload.behavior_md);
createdSubjectIds.add(id);
subjects.unshift(created);
return json(created, 201);
@@ -442,6 +464,27 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
const foundSubject = subjects.find((item) => item.id === subjectId);
if (!foundSubject) return json({}, 404);
if (parts.length === 2 && parts[1] === "behavior" && request.method === "PATCH") {
const payload = await request.json().catch(() => null) as {
expected_behavior_revision?: unknown;
behavior_md?: unknown;
} | null;
if (
!payload ||
typeof payload.expected_behavior_revision !== "number" ||
typeof payload.behavior_md !== "string"
) {
return json({ error: "Bad Request", message: "invalid behavior update" }, 400);
}
if (payload.expected_behavior_revision !== foundSubject.behavior_revision) {
return json({ error: "Conflict", message: "subject behavior revision conflict" }, 409);
}
foundSubject.behavior_md = payload.behavior_md;
foundSubject.behavior_revision += 1;
foundSubject.updated_at = "2026-01-03T04:05:06Z";
return json(foundSubject);
}
if (request.method !== "GET") return new Response("method not allowed", { status: 405 });
if (parts.length === 1) return json(foundSubject);
if (parts.length === 2 && parts[1] === "surface") return json(surfaceFor(subjectId));
if (parts.length === 2 && parts[1] === "memories") {
@@ -16,7 +16,7 @@ const memoryId =
const subjectPageCursor = "fixture-subject-page-2";
const createInteractionArtifacts = join(
repositoryRoot,
"target/web-ux/t-689-browser-interactions",
"target/web-ux/t-693-subject-behavior",
);
async function freePort(): Promise<number> {
@@ -111,6 +111,7 @@ async function openSubjectPage(
await hideSidebar.click();
await showSidebar.waitFor();
await page.waitForTimeout(250);
assertEquals(await showSidebar.getAttribute("aria-expanded"), "false");
assertEquals(
await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert),
@@ -217,6 +218,13 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
});
page.on("pageerror", (error) => errors.push(String(error)));
await openSubjectPage(page, baseUrl, scenario.mobile);
assertEquals(
await page.getByRole("article", { name: "User-managed Subject behavior" }).count(),
1,
);
await page.screenshot({
path: join(createInteractionArtifacts, `subject-${scenario.label}.png`),
});
const initial = await mainScrollState(page);
assert(initial.scrollHeight > initial.clientHeight);
@@ -425,7 +433,9 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
const createPage = await createContext.newPage();
const mutationPaths: string[] = [];
createPage.on("request", (request) => {
if (request.method() === "POST") mutationPaths.push(new URL(request.url()).pathname);
if (["POST", "PATCH"].includes(request.method())) {
mutationPaths.push(new URL(request.url()).pathname);
}
});
await createPage.goto(`${baseUrl}/w/${workspaceId}/memory?cursor=${subjectPageCursor}`);
await createPage.getByText("Subject on the next page").waitFor();
@@ -444,9 +454,21 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await createPage.getByRole("button", { name: "Cancel" }).click();
await createPage.getByRole("button", { name: "New Subject" }).click();
await createPage.getByRole("textbox", { name: "Role" }).fill(" Release steward ");
await createPage.getByRole("textbox", { name: /Behavior/ }).fill(
"Challenge unsupported assumptions before release.",
);
await createPage.getByRole("button", { name: "Create Subject" }).click();
await createPage.waitForURL("**/memory/created-subject-0001");
await createPage.getByRole("heading", { name: "Release steward", level: 1 }).waitFor();
assertEquals(
await createPage.getByRole("article", { name: "User-managed Subject behavior" }).count(),
1,
);
assert(
(await createPage.getByRole("article", {
name: "User-managed Subject behavior",
}).textContent())?.includes("Challenge unsupported assumptions before release."),
);
assertEquals(
await createPage.locator("code.subject-id", { hasText: "created-subject-0001" }).count(),
1,
@@ -455,15 +477,40 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
hasText: "Current worker",
});
assert((await currentWorker.textContent())?.includes("None"));
await createPage.getByRole("button", { name: "Edit" }).click();
const behaviorEditor = createPage.getByRole("textbox", { name: "Subject behavior" });
await behaviorEditor.fill(
"Challenge unsupported assumptions before release.\nName the evidence and any remaining uncertainty.",
);
await createPage.screenshot({
path: join(createInteractionArtifacts, "behavior-edit-desktop-light.png"),
});
await createPage.getByRole("button", { name: "Save behavior" }).click();
await createPage.getByText(/Behavior storage confirmed at revision 1/).waitFor();
assert(
(await createPage.getByRole("status").filter({
hasText: "This page does not confirm application",
}).textContent())?.includes("does not confirm application"),
);
await createPage.screenshot({
path: join(createInteractionArtifacts, "behavior-saved-desktop-light.png"),
});
assertEquals(mutationPaths, [
`/api/w/${workspaceId}/subjektiv/subjects`,
`/api/w/${workspaceId}/subjektiv/subjects`,
`/api/w/${workspaceId}/subjektiv/subjects/created-subject-0001/behavior`,
]);
const createEvidence = await (await fetch(`${baseUrl}/fixture/subject-create-requests`))
.json();
assertEquals(createEvidence, {
count: 2,
requests: [{ role: "Permission denied" }, { role: " Release steward " }],
requests: [
{ role: "Permission denied", behavior_md: "" },
{
role: " Release steward ",
behavior_md: "Challenge unsupported assumptions before release.",
},
],
});
await assertNoPageWideOverflow(createPage);
await createContext.close();