fix: fence canonical chat children to their confirmed route identity

This commit is contained in:
2026-10-31 14:53:56 +09:00
parent 86b2981d63
commit 2924e43773
5 changed files with 170 additions and 30 deletions
@@ -154,7 +154,7 @@ function subject(
memory_fingerprint: memoryFingerprint,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z",
...(currentWorker ? { current_worker: currentWorker } : {}),
...(currentWorker ? { current_worker: currentWorker, current_body: { runtime_id: currentWorker.runtime_id, worker_id: currentWorker.worker_id, generation: "17" } } : {}),
};
}
@@ -596,6 +596,22 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
const foundSubject = subjects.find((item) => item.id === subjectId);
if (!foundSubject) return json({}, 404);
// 合成HTTPでもBody GETを明示する。未対応APIへのHTML fallbackを観測成功にしない。
// mutationはこのfixtureでは提供せず、IA検証で起動停止の成功を偽装しない。
if (parts.length === 2 && parts[1] === "body" && request.method === "GET") {
const worker = foundSubject.current_worker;
return json({
workspace_id: workspaceId,
subject_id: subjectId,
current: worker ? { runtime_id: worker.runtime_id, worker_id: worker.worker_id, generation: "17" } : null,
observation: worker ? {
kind: "available",
worker: { ...worker, resource_key: "W-fixture", availability: "observed", worker_state: { last_command_id: 1, state: { kind: "idle" } }, restore_observation_token: "synthetic-body-observation" },
} : { kind: "absent" },
can_start: foundSubject.state === "active",
can_stop: foundSubject.state === "active",
});
}
if (parts.length === 2 && parts[1] === "journal-browser") {
return await journalBrowserFixture(request, subjectId);
}
@@ -337,7 +337,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await paginationPage.getByText("Memory fingerprint", { exact: true }).count(),
0,
);
assert((await paginationPage.getByText("Not connected", { exact: true }).count()) > 0);
assert((await paginationPage.getByText("No current body", { exact: true }).count()) > 0);
assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0);
await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole(
"link",
@@ -451,10 +451,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await createPage.locator("code.subject-id", { hasText: "created-subject-0001" }).count(),
1,
);
const currentWorker = createPage.getByText("Worker connection", { exact: true }).locator(
"..",
);
assert((await currentWorker.textContent())?.includes("Not connected"));
await createPage.locator(".subject-body").getByText("No current conversation body.", { exact: true }).waitFor();
await createPage.getByText("Resident context has not been generated.").waitFor();
await createPage.getByText("No committed Memories yet.").waitFor();
await createPage.getByRole("button", { name: "Edit" }).click();
@@ -28,8 +28,7 @@ const captures: Record<string, unknown>[] = [],
failures: string[] = [],
faults: Record<string, unknown>[] = [];
const manifest = {
evidence:
"actual-static-Web-build / synthetic-HTTP / NOT-real-Backend-auth-or-Runtime",
evidence: "actual-static-Web-build / synthetic-HTTP / NOT-real-Backend-auth-or-Runtime",
source: new TextDecoder().decode(
(await new Deno.Command("git", { args: ["rev-parse", "HEAD"], cwd: root })
.output()).stdout,
@@ -128,24 +127,23 @@ try {
"**/api/w/memory-review/chat/conversations**",
(route) =>
route.fulfill({
json:
new URL(route.request().url()).pathname.endsWith("/conversations")
? {
json: new URL(route.request().url()).pathname.endsWith("/conversations")
? {
id: "conversation",
subject_id: "empty-subject",
participant_account_id: "owner",
last_user_message_id: null,
}
: {
conversation: {
id: "conversation",
subject_id: "empty-subject",
participant_account_id: "owner",
last_user_message_id: null,
}
: {
conversation: {
id: "conversation",
subject_id: "empty-subject",
participant_account_id: "owner",
last_user_message_id: null,
},
messages: [],
has_more: false,
},
messages: [],
has_more: false,
},
}),
);
async function direct(path: string, selector: string, canonical = path) {
@@ -185,6 +183,10 @@ try {
);
}
async function capture(id: string) {
if (id === "detail" || id === "chat") {
await page.locator(".subject-body .actions").waitFor();
assertEquals(await page.locator(".subject-body [role=alert]").count(), 0);
}
const image = `${colorScheme}-${width}-${id}.png`;
await page.screenshot({ path: join(output, image) });
const overflow = await page.evaluate(
@@ -205,9 +207,7 @@ try {
route: page.url(),
colorScheme,
width,
mainWidth: await page.locator("main").evaluate((el) =>
el.clientWidth
),
mainWidth: await page.locator("main").evaluate((el) => el.clientWidth),
overflow,
alerts: await page.getByRole("alert").allTextContents(),
snapshot: await page.locator("body").ariaSnapshot(),
@@ -374,9 +374,7 @@ try {
"This legacy path has no corresponding Subject page.",
).waitFor();
assertEquals(
requests.slice(beforeUnknown).filter((p) =>
p.includes("/subjektiv/subjects/")
),
requests.slice(beforeUnknown).filter((p) => p.includes("/subjektiv/subjects/")),
[],
);
// 読取り拒否を空一覧/新権限付与へ変えず、UIがBackend失敗をそのまま示す。