Merge commit '226d0ecfce8a525ffef5dfcdadcbcf50cf9e4d17' into work/t-808-subject-chat

This commit is contained in:
2026-10-31 14:38:39 +09:00
53 changed files with 930 additions and 180 deletions
@@ -17,10 +17,10 @@ import {
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const artifacts = join(root, "target/web-ux/t-737-browser-interactions");
const subjectPath = `/w/${fixture.workspaceId}/memory/${fixture.subjectId}`;
const subjectPath = `/w/${fixture.workspaceId}/subjektiv/${fixture.subjectId}`;
const journalPath = `${subjectPath}/journal`;
const memoryPath =
`${subjectPath}/${fixture.memoryId}?change_id=change-current`;
`${subjectPath}/memories/${fixture.memoryId}?change_id=change-current`;
const claim = "Keep subject Memory provenance typed and visible";
const entryTitle = "Release milestone 00 · release [a+b] %_";
const detailPath = `${journalPath}?journal=journal-release&entry=entry-00`;
@@ -262,7 +262,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
.waitFor();
assertEquals(
new URL(page.url()).pathname,
`${subjectPath}/${fixture.memoryId}`,
`${subjectPath}/memories/${fixture.memoryId}`,
);
assertEquals(
new URL(page.url()).searchParams.get("change_id"),
@@ -539,7 +539,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
const state of [
{
path:
`/w/${fixture.workspaceId}/memory/empty-subject/journal?view=sessions`,
`/w/${fixture.workspaceId}/subjektiv/empty-subject/journal?view=sessions`,
text: "Journalはまだありません。",
role: "status" as const,
},
@@ -570,7 +570,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
await assertNoPageWideOverflow(page);
}
await page.goto(
`${base}${subjectPath}/memory-resolved-0002?change_id=change-earlier`,
`${base}${subjectPath}/memories/memory-resolved-0002?change_id=change-earlier`,
);
await page.locator("#related-journal").getByText(
"対応するJournalは未接続",
@@ -591,7 +591,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
r.payload.selector.candidate_id === fixture.candidateId
));
await page.goto(
`${base}${subjectPath}/memory-sources-failure?change_id=change-current`,
`${base}${subjectPath}/memories/memory-sources-failure?change_id=change-current`,
);
await page.getByRole("alert").filter({
hasText: "Journal参照を取得できません。",
@@ -16,7 +16,8 @@ if (
}
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
const permissionsMode = Deno.args[3] === "permissions";
const navigationMode = Deno.args[3] === "navigation";
const permissionsMode = Deno.args[3] === "permissions" || navigationMode;
if (permissionsMode) {
// 権限の証拠は親から渡された実製品buildを使って採取する。
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
@@ -105,6 +106,12 @@ if (permissionsMode) {
...permissionFixtureSubjects.map(([id, role]) => subject(id, role, "active", "fingerprint-0")),
);
}
if (navigationMode) {
subjects.push(
subject("staging", "Subject named staging", "active", "fingerprint-0"),
subject("encoded/Subject ?#日本%", "Encoded identity Subject", "active", "fingerprint-0"),
);
}
const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", "fingerprint-1");
function connectedWorker(displayName: string) {
@@ -513,6 +520,9 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
});
}
if (navigationMode && url.pathname === `/api/w/${workspaceId}/memory/staging`) {
return json({ limit: 200, returned_count: 0, total_valid_count: 0, invalid_count: 0, truncated: false, order: "fixture", record_authority: "synthetic-http", items: [], diagnostics: [] });
}
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
if (url.pathname === subjectsPath) {
if (request.method === "POST") {
@@ -45,7 +45,7 @@ async function openSubjectPage(
baseUrl: string,
mobile = false,
): Promise<void> {
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`);
await page.goto(`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}`);
const heading = page.locator("h1", { hasText: "Release coordination" });
await heading.waitFor({ state: "attached" });
if (mobile) {
@@ -189,10 +189,10 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await assertNoPageWideOverflow(page);
await assertMainOwnsVerticalScroll(page);
assertEquals(
await page.locator('aside a[href$="/memory"]', { hasText: "Subjects" }).count(),
await page.locator('aside a[href$="/subjektiv"]', { hasText: "Subjects" }).count(),
1,
);
assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 0);
assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 1);
if (scenario.label === "wheel") {
assertEquals(
@@ -269,7 +269,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
const detailContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const detailPage = await detailContext.newPage();
await detailPage.goto(
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}/${
`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}/memories/${
encodeURIComponent(memoryId)
}`,
);
@@ -324,7 +324,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
const paginationContext = await browser.newContext({ viewport: { width: 768, height: 900 } });
const paginationPage = await paginationContext.newPage();
await paginationPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
await paginationPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv`);
await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor();
assertEquals(
await paginationPage.getByText(
@@ -354,7 +354,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
);
await paginationPage.goto(
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`,
`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}`,
);
await paginationPage.getByRole("heading", { name: "Current Memories", level: 2 }).waitFor();
await paginationPage.getByRole("navigation", { name: "Current Memory pages" }).getByRole(
@@ -382,7 +382,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
}
await route.continue();
});
await emptyPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
await emptyPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv`);
await emptyPage.getByText("No Memory subjects.").waitFor();
await emptyPage.getByRole("button", { name: "New Subject" }).click();
const emptyRole = emptyPage.getByRole("textbox", { name: "Role" });
@@ -415,7 +415,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
mutationPaths.push(new URL(request.url()).pathname);
}
});
await createPage.goto(`${baseUrl}/w/${workspaceId}/memory?cursor=${subjectPageCursor}`);
await createPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv?cursor=${subjectPageCursor}`);
await createPage.getByText("Subject on the next page").waitFor();
await createPage.getByRole("button", { name: "New Subject" }).click();
const createRole = createPage.getByRole("textbox", { name: "Role" });
@@ -436,7 +436,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
"Challenge unsupported assumptions before release.",
);
await createPage.getByRole("button", { name: "Create Subject" }).click();
await createPage.waitForURL("**/memory/created-subject-0001");
await createPage.waitForURL("**/subjektiv/created-subject-0001");
await createPage.getByRole("heading", { name: "Release steward", level: 1 }).waitFor();
assertEquals(
await createPage.getByRole("article", { name: "User-managed Subject behavior" }).count(),
@@ -539,7 +539,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
];
for (const stateCase of stateCases) {
const page = await stateContext.newPage();
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${stateCase.id}`);
await page.goto(`${baseUrl}/w/${workspaceId}/subjektiv/${stateCase.id}`);
const state = page.getByRole(stateCase.role).filter({ hasText: stateCase.text });
await state.waitFor();
assert((await state.textContent())?.includes(stateCase.text));
@@ -24,7 +24,7 @@ await Deno.mkdir(output);
const port = await freePort();
const base = `http://127.0.0.1:${port}`;
const subject = "empty-subject";
const subjectRoute = `/w/memory-review/memory/${subject}`;
const subjectRoute = `/w/memory-review/${mode === "before" ? "memory" : "subjektiv"}/${subject}`;
const captures: unknown[] = [];
const failures: string[] = [];
const faults: { kind: string; message: string; expected: boolean }[] = [];
@@ -233,7 +233,7 @@ try {
faultMode = "normal";
const secondTab = await context.newPage();
try {
await secondTab.goto(`${base}${subjectRoute}/conversation`);
await secondTab.goto(`${base}${subjectRoute}/chat`);
await secondTab.getByRole("textbox").fill("Independent input from another tab");
await secondTab.getByRole("button", { name: "Send message", exact: true }).click();
await secondTab.getByText(
@@ -257,7 +257,7 @@ try {
};
manifest.scenarios.push(scenario);
try {
await page.goto(`${base}/w/memory-review/memory`);
await page.goto(`${base}/w/memory-review/subjektiv`);
await page.getByRole("heading", { name: "Subjects", level: 1, exact: true }).waitFor();
await page.getByRole("button", { name: "New Subject", exact: true }).click();
const role = page.getByRole("textbox", { name: "Role", exact: true });
@@ -354,7 +354,7 @@ try {
await page.getByRole("alert").scrollIntoViewIfNeeded();
await capture(page, observation, `${prefix}-later-403-reopened-pending`);
await page.getByRole("button", { name: "Create Subject", exact: true }).click();
await page.waitForURL("**/memory/created-subject-0001");
await page.waitForURL("**/subjektiv/created-subject-0001");
await page.getByRole("heading", { name: roleText.trim(), level: 1, exact: true })
.waitFor();
assertEquals(
@@ -0,0 +1,426 @@
/** SubjectのIA移行を実production buildと隔離HTTPで検証する。Backend認可の証拠ではない。 */
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join, resolve } from "@std/path";
import { chromium, type Page } from "playwright";
import { sha256File } from "../src/artifacts.ts";
import { startOwnedProcesses, stopOwnedProcesses } from "../src/processes.ts";
import { freePort } from "./memory_document_harness.ts";
const root = resolve(dirname(fromFileUrl(import.meta.url)), "../../..");
const [buildArg, runId] = Deno.args;
if (!buildArg || !runId || !/^[a-zA-Z0-9._-]+$/.test(runId)) {
throw new Error(
"usage: subject_navigation_review.ts <actual-build> <unique-run-id>",
);
}
const build = resolve(buildArg);
await Deno.stat(join(build, "index.html"));
const output = join(root, "target/web-ux/t817", runId);
await Deno.mkdir(output, { recursive: true });
const port = await freePort(), base = `http://127.0.0.1:${port}`;
const ws = "/w/memory-review", subjects = `${ws}/subjektiv`;
const subject =
"release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review";
const detail = `${subjects}/${subject}`;
const memory =
"memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001";
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",
source: new TextDecoder().decode(
(await new Deno.Command("git", { args: ["rev-parse", "HEAD"], cwd: root })
.output()).stdout,
).trim(),
buildIndexSha256: await sha256File(join(build, "index.html")),
runnerSha256: await sha256File(fromFileUrl(import.meta.url)),
browser: "",
captures,
faults,
failures,
status: "running",
visualVerdict: "UNREVIEWED",
};
const save = () =>
Deno.writeTextFile(
join(output, "review-context.json"),
JSON.stringify(manifest, null, 2),
);
const owned = await startOwnedProcesses(
[{
id: "navigation-synthetic-http",
command: Deno.execPath(),
cwd: root,
args: [
"run",
"--config",
"tools/web-ux/deno.json",
"-A",
"tools/web-ux/browser-tests/memory_document_fixture_server.ts",
String(port),
build,
"3",
"navigation",
],
readyUrl: `${base}/health`,
}],
join(output, "scenario.json"),
join(output, "logs"),
[],
);
const browser = await chromium.launch({ headless: true });
manifest.browser = browser.version();
try {
for (const colorScheme of ["light", "dark"] as const) {
for (const width of [1440, 768, 390]) {
const context = await browser.newContext({
colorScheme,
viewport: { width, height: 900 },
reducedMotion: "reduce",
});
const page = await context.newPage();
page.setDefaultTimeout(15000);
let expectedFailure = false;
const requests: string[] = [];
page.on("request", (r) => requests.push(new URL(r.url()).pathname));
page.on(
"pageerror",
(e) =>
faults.push({
kind: "pageerror",
message: e.message,
expected: false,
}),
);
page.on("console", (m) => {
if (m.type() === "error") {
faults.push({
kind: "console",
message: m.text(),
expected: expectedFailure,
});
}
});
page.on(
"requestfailed",
(r) =>
faults.push({
kind: "request",
path: new URL(r.url()).pathname,
message: r.failure()?.errorText,
expected: r.failure()?.errorText === "net::ERR_ABORTED",
}),
);
page.on("response", (r) => {
if (r.status() >= 400) {
faults.push({
kind: "http",
path: new URL(r.url()).pathname,
status: r.status(),
expected: expectedFailure,
});
}
});
// 公開chat APIの外側だけを合成。会話UI・loader・routerは本番のまま。
await context.route(
"**/api/w/memory-review/chat/conversations**",
(route) =>
route.fulfill({
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,
},
messages: [],
has_more: false,
},
}),
);
async function direct(path: string, selector: string, canonical = path) {
const response = await page.goto(base + path);
assertEquals(response?.status(), 200);
assert(response?.headers()["content-type"].includes("text/html"));
await page.waitForURL(base + canonical);
await page.locator(selector).waitFor();
assertEquals(
new URL(page.url()).pathname + new URL(page.url()).search +
new URL(page.url()).hash,
canonical,
);
}
async function selected(label = "Subjects") {
const nav = page.getByRole("navigation", {
name: "Workspace sections",
exact: true,
includeHidden: true,
});
assertEquals(
await nav.getByRole("link", {
name: label,
exact: true,
includeHidden: true,
}).getAttribute("aria-current"),
"page",
);
const other = label === "Subjects" ? "Staging" : "Subjects";
assertEquals(
await nav.getByRole("link", {
name: other,
exact: true,
includeHidden: true,
}).getAttribute("aria-current"),
null,
);
}
async function capture(id: string) {
const image = `${colorScheme}-${width}-${id}.png`;
await page.screenshot({ path: join(output, image) });
const overflow = await page.evaluate(
"document.documentElement.scrollWidth > innerWidth",
);
assertEquals(overflow, false, image);
const legacyLinks = await page.locator(
"main a[href], .sidebar-sections a[href], .workspace-breadcrumbs a[href]",
).evaluateAll((links) =>
links.map((l) => l.getAttribute("href")!).filter((h) =>
/\/memory(?:\/|$)/.test(h) && !h.includes("/memory/staging")
)
);
assertEquals(legacyLinks, []);
captures.push({
image,
sha256: await sha256File(join(output, image)),
route: page.url(),
colorScheme,
width,
mainWidth: await page.locator("main").evaluate((el) =>
el.clientWidth
),
overflow,
alerts: await page.getByRole("alert").allTextContents(),
snapshot: await page.locator("body").ariaSnapshot(),
});
await save();
}
try {
await direct(subjects, '[data-memory-view="subjects"]');
await selected();
await capture("list");
if (width === 390) {
await page.getByRole("button", { name: "Show sidebar", exact: true })
.click();
await page.getByRole("navigation", {
name: "Workspace sections",
exact: true,
}).waitFor();
await capture("mobile-navigation");
await page.getByRole("button", { name: "Hide sidebar", exact: true })
.click();
}
await page.locator(`a.subject-row[href="${detail}"]`).click();
await page.locator(".subject-overview").waitFor();
assertEquals(new URL(page.url()).pathname, detail);
await selected();
await capture("detail");
const breadcrumbs = page.getByRole("navigation", {
name: "Current workspace location",
});
assertEquals(
await breadcrumbs.getByRole("link", { name: "Subjects", exact: true })
.getAttribute("href"),
subjects,
);
await page.getByRole("link", { name: /Journal ·/ }).click();
await page.locator('[data-journal-view="list"] .entry-list').waitFor();
assertEquals(new URL(page.url()).pathname, detail + "/journal");
await capture("journal");
await page.goBack();
await page.locator(".subject-overview").waitFor();
await page.goForward();
await page.locator('[data-journal-view="list"]').waitFor();
await direct(
`${detail}/memories/${memory}?change_id=change-earlier#related-journal`,
'[data-memory-view="detail"]',
);
assertEquals(
await page.locator('.workspace-breadcrumbs a[href$="/memories"]')
.count(),
0,
);
await capture("memory-history");
await direct(
subjects + "/permissions-edit/permissions",
"#permissions-heading",
);
await page.getByRole("combobox", { name: "Operation", exact: true })
.waitFor();
await selected();
await capture("permissions-owner");
await direct(
subjects + "/permissions-reader/permissions",
"#permissions-heading",
);
await page.getByRole("status").filter({ hasText: "Read-only." })
.waitFor();
await capture("permissions-reader");
await direct(
subjects + "/empty-subject/chat",
".conversation-page textarea",
);
await page.getByText("No public messages yet.").waitFor();
await selected();
await capture("chat");
await direct(`${ws}/memory/staging`, ".staging-summary-grid");
await selected("Staging");
await capture("staging");
await direct(subjects + "/staging", ".subject-overview");
await selected();
const encoded = encodeURIComponent("encoded/Subject ?#日本%");
await direct(
`${ws}/memory/${encoded}?cursor=a%2Fb%26c#subject-heading`,
".subject-overview",
`${subjects}/${encoded}?cursor=a%2Fb%26c#subject-heading`,
);
assert(
requests.includes(
`/api/w/memory-review/subjektiv/subjects/${encoded}`,
),
);
await page.reload();
await page.locator(".subject-overview").waitFor();
// 旧URLの曖昧性はstatic予約語優先。新memories namespaceで同名IDを読める。
for (
const id of [
"permissions",
"journal",
"chat",
"conversation",
"a/b ?#%",
]
) {
await direct(
`${detail}/memories/${encodeURIComponent(id)}`,
'[data-memory-view="detail"]',
);
}
for (
const [old, next, selector] of [
[
`${ws}/memory?cursor=a%2Fb#memory-subjects-heading`,
`${subjects}?cursor=a%2Fb#memory-subjects-heading`,
'[data-memory-view="subjects"]',
],
[
`${ws}/memory/${subject}?cursor=a%2Fb#subject-heading`,
`${detail}?cursor=a%2Fb#subject-heading`,
".subject-overview",
],
[
`${ws}/memory/${subject}/${memory}?change_id=change-earlier#related-journal`,
`${detail}/memories/${memory}?change_id=change-earlier#related-journal`,
'[data-memory-view="detail"]',
],
[
`${ws}/memory/${subject}/journal?journal=journal-release#journal-title`,
`${detail}/journal?journal=journal-release#journal-title`,
"[data-journal-view]",
],
[
`${ws}/memory/permissions-edit/permissions#permissions-heading`,
`${subjects}/permissions-edit/permissions#permissions-heading`,
"#permissions-heading",
],
[
`${ws}/memory/empty-subject/conversation?from=bookmark#conversation-subject-heading`,
`${subjects}/empty-subject/chat?from=bookmark#conversation-subject-heading`,
".conversation-page textarea",
],
[
`${ws}/memory/empty-subject/chat`,
`${subjects}/empty-subject/chat`,
".conversation-page textarea",
],
]
) await direct(old, selector, next);
// replace遷移は旧entryを残さず、戻る/進むで同じURLを往復しない。
await direct(subjects, '[data-memory-view="subjects"]');
await direct(`${ws}/memory/${subject}`, ".subject-overview", detail);
await page.getByRole("link", { name: /Journal ·/ }).click();
await page.locator("[data-journal-view]").waitFor();
await page.goBack();
await page.waitForURL(base + detail);
await page.goBack();
await page.waitForURL(base + subjects);
await page.goForward();
await page.waitForURL(base + detail);
const beforeUnknown = requests.length;
await direct(
`${ws}/memory/empty-subject/unknown/deeper`,
".legacy-subject-route",
);
await page.getByText(
"This legacy path has no corresponding Subject page.",
).waitFor();
assertEquals(
requests.slice(beforeUnknown).filter((p) =>
p.includes("/subjektiv/subjects/")
),
[],
);
// 読取り拒否を空一覧/新権限付与へ変えず、UIがBackend失敗をそのまま示す。
expectedFailure = true;
await context.route(
"**/subjektiv/subjects/empty-subject",
(route) =>
route.fulfill({
status: 403,
json: { error: "Forbidden", message: "synthetic read denied" },
}),
);
await direct(
subjects + "/empty-subject/chat",
'.conversation-page [role="alert"]',
);
await capture("chat-denied");
expectedFailure = false;
} catch (e) {
failures.push(
`${colorScheme}-${width}: ${e instanceof Error ? e.stack : e}`,
);
await page.screenshot({
path: join(output, `${colorScheme}-${width}-failure.png`),
});
throw e;
} finally {
await context.close();
await save();
}
}
}
assertEquals(failures, []);
assertEquals(faults.filter((f) => !f.expected), []);
manifest.status = "passed-automated-not-visual-verdict";
} finally {
await browser.close();
await stopOwnedProcesses(owned);
await save();
}
console.log(
JSON.stringify({
output,
status: manifest.status,
captures: captures.length,
}),
);
@@ -194,7 +194,7 @@ async function capturePoint(page: Page, id: string, expectedErrors: string[] = [
await assertNoPageWideOverflow(page);
}
async function open(page: Page, base: string, id: string, state = "ready") {
await page.goto(`${base}/w/memory-review/memory/${id}/permissions`);
await page.goto(`${base}/w/memory-review/subjektiv/${id}/permissions`);
await page.locator(`[data-permission-state=${state}][aria-busy=false]`).waitFor();
}
async function add(
@@ -268,7 +268,7 @@ try {
const page = await context.newPage();
observe(page);
// 通常のSubject詳細から管理導線へ進む。
await page.goto(`${base}/w/memory-review/memory/permissions-empty`);
await page.goto(`${base}/w/memory-review/subjektiv/permissions-empty`);
await page.locator(".subject-overview").waitFor();
await page.locator('main a[href$="/permissions"]').click();
await page.locator("[data-permission-state=ready]").waitFor();
@@ -35,7 +35,7 @@
{
"id": "sessions",
"label": "Session journals",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?view=sessions",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?view=sessions",
"goal": "Choose among saved Session journals and distinguish pending work from source presence.",
"dataState": "Two Sessions, one with unreflected sources; deleted versus unavailable originals; bounded next page.",
"ready": {
@@ -62,7 +62,7 @@
{
"id": "list",
"label": "Literal search results",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=release%20%5Ba%2Bb%5D%20%25_",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=release%20%5Ba%2Bb%5D%20%25_",
"goal": "Find literal text across Sessions and continue a bounded search.",
"dataState": "Sixteen matching events across two Sessions with fixed/open states and a continuation.",
"ready": {
@@ -79,7 +79,7 @@
{
"id": "detail-missing",
"label": "Saved Journal with deleted original",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-release&entry=entry-00",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-release&entry=entry-00",
"goal": "Read narrative, correction and related Memory without restoring a Worker or original Session.",
"dataState": "Open event, three of eight related Memories, correcting entry; original conversation is deleted.",
"ready": {
@@ -116,7 +116,7 @@
{
"id": "detail-unavailable",
"label": "Journal with unavailable original",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-review&entry=entry-01",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-review&entry=entry-01",
"goal": "Distinguish inability to check the original from confirmed deletion.",
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
"ready": {
@@ -139,7 +139,7 @@
{
"id": "memory",
"label": "Memory links back to exact Journal events",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001?change_id=change-current",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001?change_id=change-current",
"goal": "Identify Journal targets by title and date, with bounded continuation and separate original-source status.",
"dataState": "Three of eight persisted candidate links, including missing/unavailable originals and reconsideration.",
"ready": {
@@ -172,7 +172,7 @@
{
"id": "memory-unconnected",
"label": "Unconnected Memory",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-resolved-0002?change_id=change-earlier",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-resolved-0002?change_id=change-earlier",
"goal": "Recognize a missing persisted connection without fabricating a target or pending reason.",
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
"ready": {
@@ -195,7 +195,7 @@
{
"id": "no-journal",
"label": "No saved Journal",
"path": "/w/memory-review/memory/empty-subject/journal?view=sessions",
"path": "/w/memory-review/subjektiv/empty-subject/journal?view=sessions",
"goal": "Distinguish no saved Journal from pending, no search results and read failure.",
"dataState": "Successful empty Session-journal page.",
"ready": {
@@ -212,7 +212,7 @@
{
"id": "no-search-result",
"label": "No literal search matches",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=definitely-no-such-literal",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=definitely-no-such-literal",
"goal": "Keep the query visible when search succeeds with zero results.",
"dataState": "Successful empty search, not an unavailable Journal.",
"ready": {
@@ -229,7 +229,7 @@
{
"id": "denied",
"label": "Journal read denied",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-denied&entry=entry-00",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-denied&entry=entry-00",
"goal": "Show a read failure rather than inventing an empty or deleted Journal.",
"dataState": "Intentional fixture HTTP 403; verifies error presentation only, not backend authorization.",
"ready": {
@@ -246,7 +246,7 @@
{
"id": "request-failure",
"label": "Journal list unavailable",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=fixture-failure",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=fixture-failure",
"goal": "Keep request failure distinct from zero matching entries.",
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
"ready": {
@@ -35,7 +35,7 @@
{
"id": "sessions",
"label": "Session journals",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?view=sessions",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?view=sessions",
"goal": "Choose among saved Session journals and distinguish pending work from source presence.",
"dataState": "Two Sessions, one with unreflected sources; deleted versus unavailable originals; bounded next page.",
"ready": {
@@ -62,7 +62,7 @@
{
"id": "list",
"label": "Literal search results",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=release%20%5Ba%2Bb%5D%20%25_",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=release%20%5Ba%2Bb%5D%20%25_",
"goal": "Find literal text across Sessions and continue a bounded search.",
"dataState": "Sixteen matching events across two Sessions with fixed/open states and a continuation.",
"ready": {
@@ -79,7 +79,7 @@
{
"id": "detail-missing",
"label": "Saved Journal with deleted original",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-release&entry=entry-00",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-release&entry=entry-00",
"goal": "Read narrative, correction and related Memory without restoring a Worker or original Session.",
"dataState": "Open event, three of eight related Memories, correcting entry; original conversation is deleted.",
"ready": {
@@ -116,7 +116,7 @@
{
"id": "detail-unavailable",
"label": "Journal with unavailable original",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-review&entry=entry-01",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-review&entry=entry-01",
"goal": "Distinguish inability to check the original from confirmed deletion.",
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
"ready": {
@@ -139,7 +139,7 @@
{
"id": "memory",
"label": "Memory links back to exact Journal events",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001?change_id=change-current",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001?change_id=change-current",
"goal": "Identify Journal targets by title and date, with bounded continuation and separate original-source status.",
"dataState": "Three of eight persisted candidate links, including missing/unavailable originals and reconsideration.",
"ready": {
@@ -172,7 +172,7 @@
{
"id": "memory-unconnected",
"label": "Unconnected Memory",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-resolved-0002?change_id=change-earlier",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-resolved-0002?change_id=change-earlier",
"goal": "Recognize a missing persisted connection without fabricating a target or pending reason.",
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
"ready": {
@@ -195,7 +195,7 @@
{
"id": "no-journal",
"label": "No saved Journal",
"path": "/w/memory-review/memory/empty-subject/journal?view=sessions",
"path": "/w/memory-review/subjektiv/empty-subject/journal?view=sessions",
"goal": "Distinguish no saved Journal from pending, no search results and read failure.",
"dataState": "Successful empty Session-journal page.",
"ready": {
@@ -212,7 +212,7 @@
{
"id": "no-search-result",
"label": "No literal search matches",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=definitely-no-such-literal",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=definitely-no-such-literal",
"goal": "Keep the query visible when search succeeds with zero results.",
"dataState": "Successful empty search, not an unavailable Journal.",
"ready": {
@@ -229,7 +229,7 @@
{
"id": "denied",
"label": "Journal read denied",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-denied&entry=entry-00",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?journal=journal-denied&entry=entry-00",
"goal": "Show a read failure rather than inventing an empty or deleted Journal.",
"dataState": "Intentional fixture HTTP 403; verifies error presentation only, not backend authorization.",
"ready": {
@@ -246,7 +246,7 @@
{
"id": "request-failure",
"label": "Journal list unavailable",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=fixture-failure",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/journal?q=fixture-failure",
"goal": "Keep request failure distinct from zero matching entries.",
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
"ready": {
@@ -20,7 +20,7 @@
{
"id": "subjects",
"label": "Subject index",
"path": "/w/memory-review/memory",
"path": "/w/memory-review/subjektiv",
"goal": "Choose an explicit Memory subject or create one with a Backend-validated role.",
"dataState": "Active and retired subjects across a paginated list, plus create-form and permission-rejection states.",
"ready": {
@@ -67,7 +67,7 @@
{
"id": "subjects-empty",
"label": "Empty Subject index",
"path": "/w/memory-review/memory?cursor=fixture-empty-subject-page",
"path": "/w/memory-review/subjektiv?cursor=fixture-empty-subject-page",
"goal": "Create the first visible Subject when the current bounded page is empty.",
"dataState": "Successful empty Subject page with a return-to-first-page recovery link.",
"ready": {
@@ -92,7 +92,7 @@
{
"id": "subjects-error",
"label": "Subject list error",
"path": "/w/memory-review/memory?cursor=fixture-error-subject-page",
"path": "/w/memory-review/subjektiv?cursor=fixture-error-subject-page",
"goal": "Keep explicit Subject creation available while the existing list cannot load.",
"dataState": "Intentional Subject-list HTTP 503 with the independent create disclosure still available.",
"ready": {
@@ -117,7 +117,7 @@
{
"id": "representative",
"label": "Ready subject surface",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
"goal": "Read the ready resident surface and scan active, resolved, and retracted committed Memories without nested vertical scrolling.",
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
"ready": {
@@ -153,7 +153,7 @@
{
"id": "detail",
"label": "Committed Memory detail",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
"goal": "Audit immutable Memory body, candidate evidence, source refs, derivations, and change history.",
"dataState": "Current active change with bounded typed provenance and active/resolved/retracted immutable history.",
"ready": {
@@ -165,7 +165,7 @@
{
"id": "empty",
"label": "Ready-empty subject",
"path": "/w/memory-review/memory/empty-subject",
"path": "/w/memory-review/subjektiv/empty-subject",
"goal": "Distinguish a valid ready-empty snapshot and an empty committed Memory list from failure.",
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
"ready": {
@@ -177,7 +177,7 @@
{
"id": "ungenerated",
"label": "Ungenerated subject context",
"path": "/w/memory-review/memory/ungenerated-subject",
"path": "/w/memory-review/subjektiv/ungenerated-subject",
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
"ready": {
@@ -189,7 +189,7 @@
{
"id": "stale",
"label": "Stale subject surface",
"path": "/w/memory-review/memory/stale-subject",
"path": "/w/memory-review/subjektiv/stale-subject",
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
"ready": {
@@ -201,7 +201,7 @@
{
"id": "failed",
"label": "Failed subject surface",
"path": "/w/memory-review/memory/failed-subject",
"path": "/w/memory-review/subjektiv/failed-subject",
"goal": "See generation failure without displaying a failed or partial snapshot.",
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
"ready": {
@@ -213,7 +213,7 @@
{
"id": "error",
"label": "Subject endpoint error",
"path": "/w/memory-review/memory/error-subject",
"path": "/w/memory-review/subjektiv/error-subject",
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
"ready": {
@@ -20,7 +20,7 @@
{
"id": "subjects",
"label": "Subject index",
"path": "/w/memory-review/memory",
"path": "/w/memory-review/subjektiv",
"goal": "Choose an explicit Memory subject or create one with a Backend-validated role.",
"dataState": "Active and retired subjects across a paginated list, plus create-form and permission-rejection states.",
"ready": {
@@ -67,7 +67,7 @@
{
"id": "subjects-empty",
"label": "Empty Subject index",
"path": "/w/memory-review/memory?cursor=fixture-empty-subject-page",
"path": "/w/memory-review/subjektiv?cursor=fixture-empty-subject-page",
"goal": "Create the first visible Subject when the current bounded page is empty.",
"dataState": "Successful empty Subject page with a return-to-first-page recovery link.",
"ready": {
@@ -92,7 +92,7 @@
{
"id": "subjects-error",
"label": "Subject list error",
"path": "/w/memory-review/memory?cursor=fixture-error-subject-page",
"path": "/w/memory-review/subjektiv?cursor=fixture-error-subject-page",
"goal": "Keep explicit Subject creation available while the existing list cannot load.",
"dataState": "Intentional Subject-list HTTP 503 with the independent create disclosure still available.",
"ready": {
@@ -117,7 +117,7 @@
{
"id": "representative",
"label": "Ready subject surface",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
"goal": "Read the ready resident surface and scan active, resolved, and retracted committed Memories without nested vertical scrolling.",
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
"ready": {
@@ -153,7 +153,7 @@
{
"id": "detail",
"label": "Committed Memory detail",
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
"path": "/w/memory-review/subjektiv/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memories/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
"goal": "Audit immutable Memory body, candidate evidence, source refs, derivations, and change history.",
"dataState": "Current active change with bounded typed provenance and active/resolved/retracted immutable history.",
"ready": {
@@ -165,7 +165,7 @@
{
"id": "empty",
"label": "Ready-empty subject",
"path": "/w/memory-review/memory/empty-subject",
"path": "/w/memory-review/subjektiv/empty-subject",
"goal": "Distinguish a valid ready-empty snapshot and an empty committed Memory list from failure.",
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
"ready": {
@@ -177,7 +177,7 @@
{
"id": "ungenerated",
"label": "Ungenerated subject context",
"path": "/w/memory-review/memory/ungenerated-subject",
"path": "/w/memory-review/subjektiv/ungenerated-subject",
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
"ready": {
@@ -189,7 +189,7 @@
{
"id": "stale",
"label": "Stale subject surface",
"path": "/w/memory-review/memory/stale-subject",
"path": "/w/memory-review/subjektiv/stale-subject",
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
"ready": {
@@ -201,7 +201,7 @@
{
"id": "failed",
"label": "Failed subject surface",
"path": "/w/memory-review/memory/failed-subject",
"path": "/w/memory-review/subjektiv/failed-subject",
"goal": "See generation failure without displaying a failed or partial snapshot.",
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
"ready": {
@@ -213,7 +213,7 @@
{
"id": "error",
"label": "Subject endpoint error",
"path": "/w/memory-review/memory/error-subject",
"path": "/w/memory-review/subjektiv/error-subject",
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
"ready": {
@@ -35,7 +35,7 @@
{
"id": "subject",
"label": "Subject detail",
"path": "/w/memory-review/memory/pending-subject",
"path": "/w/memory-review/subjektiv/pending-subject",
"goal": "Review the Subject before irreversible deletion.",
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
"ready": {
@@ -98,7 +98,7 @@
{
"id": "legacy",
"label": "Legacy ended Subject",
"path": "/w/memory-review/memory/legacy-subject",
"path": "/w/memory-review/subjektiv/legacy-subject",
"goal": "Delete a legacy ended Subject without reactivating it.",
"dataState": "Saved retired wire state, owner.",
"ready": {
@@ -129,7 +129,7 @@
{
"id": "non-owner",
"label": "Nonowner Subject view",
"path": "/w/memory-review/memory/legacy-subject?persona=reader",
"path": "/w/memory-review/subjektiv/legacy-subject?persona=reader",
"goal": "Read Subject without presenting unauthorized deletion.",
"dataState": "Backend delete_subjects=false; no deletion controls.",
"ready": {
@@ -35,7 +35,7 @@
{
"id": "subject",
"label": "Subject detail",
"path": "/w/memory-review/memory/pending-subject",
"path": "/w/memory-review/subjektiv/pending-subject",
"goal": "Review the Subject before irreversible deletion.",
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
"ready": {
@@ -98,7 +98,7 @@
{
"id": "legacy",
"label": "Legacy ended Subject",
"path": "/w/memory-review/memory/legacy-subject",
"path": "/w/memory-review/subjektiv/legacy-subject",
"goal": "Delete a legacy ended Subject without reactivating it.",
"dataState": "Saved retired wire state, owner.",
"ready": {
@@ -129,7 +129,7 @@
{
"id": "non-owner",
"label": "Nonowner Subject view",
"path": "/w/memory-review/memory/legacy-subject?persona=reader",
"path": "/w/memory-review/subjektiv/legacy-subject?persona=reader",
"goal": "Read Subject without presenting unauthorized deletion.",
"dataState": "Backend delete_subjects=false; no deletion controls.",
"ready": {
@@ -35,7 +35,7 @@
{
"id": "permissions-empty",
"label": "Unset permissions",
"path": "/w/memory-review/memory/permissions-empty/permissions",
"path": "/w/memory-review/subjektiv/permissions-empty/permissions",
"goal": "Distinguish unset permission policy from unavailable or read-only; locate separate direct and delegation scopes.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unset permissions.",
"ready": {
@@ -52,7 +52,7 @@
{
"id": "permissions-edit",
"label": "Configured permissions",
"path": "/w/memory-review/memory/permissions-edit/permissions",
"path": "/w/memory-review/subjektiv/permissions-edit/permissions",
"goal": "Read exact operation and target before editing.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
"ready": {
@@ -69,7 +69,7 @@
{
"id": "permissions-legacy",
"label": "Preserved unsupported permission",
"path": "/w/memory-review/memory/permissions-legacy/permissions",
"path": "/w/memory-review/subjektiv/permissions-legacy/permissions",
"goal": "Recognize preserved legacy Worker target without treating T-803 messaging as available.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Preserved unsupported permission.",
"ready": {
@@ -86,7 +86,7 @@
{
"id": "permissions-reader",
"label": "Read-only permissions",
"path": "/w/memory-review/memory/permissions-reader/permissions",
"path": "/w/memory-review/subjektiv/permissions-reader/permissions",
"goal": "Read policy without unauthorized editing controls.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
"ready": {
@@ -103,7 +103,7 @@
{
"id": "permissions-unavailable",
"label": "Unavailable permission service",
"path": "/w/memory-review/memory/permissions-unavailable/permissions",
"path": "/w/memory-review/subjektiv/permissions-unavailable/permissions",
"goal": "Recover without success claims.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
"ready": {
@@ -35,7 +35,7 @@
{
"id": "permissions-empty",
"label": "Unset permissions",
"path": "/w/memory-review/memory/permissions-empty/permissions",
"path": "/w/memory-review/subjektiv/permissions-empty/permissions",
"goal": "Distinguish unset permission policy from unavailable or read-only; locate separate direct and delegation scopes.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unset permissions.",
"ready": {
@@ -52,7 +52,7 @@
{
"id": "permissions-edit",
"label": "Configured permissions",
"path": "/w/memory-review/memory/permissions-edit/permissions",
"path": "/w/memory-review/subjektiv/permissions-edit/permissions",
"goal": "Read exact operation and target before editing.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
"ready": {
@@ -69,7 +69,7 @@
{
"id": "permissions-legacy",
"label": "Preserved unsupported permission",
"path": "/w/memory-review/memory/permissions-legacy/permissions",
"path": "/w/memory-review/subjektiv/permissions-legacy/permissions",
"goal": "Recognize preserved legacy Worker target without treating T-803 messaging as available.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Preserved unsupported permission.",
"ready": {
@@ -86,7 +86,7 @@
{
"id": "permissions-reader",
"label": "Read-only permissions",
"path": "/w/memory-review/memory/permissions-reader/permissions",
"path": "/w/memory-review/subjektiv/permissions-reader/permissions",
"goal": "Read policy without unauthorized editing controls.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
"ready": {
@@ -103,7 +103,7 @@
{
"id": "permissions-unavailable",
"label": "Unavailable permission service",
"path": "/w/memory-review/memory/permissions-unavailable/permissions",
"path": "/w/memory-review/subjektiv/permissions-unavailable/permissions",
"goal": "Recover without success claims.",
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
"ready": {
+2 -1
View File
@@ -6,13 +6,14 @@
"dev": "deno run -A npm:vite@8.3.2 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/navigation/deferred.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/auth/login.test.ts src/lib/workspace/auth/routes.test.ts src/lib/workspace/auth/hosting.test.ts src/lib/workspace/auth/hosting-enrollment.test.ts src/lib/workspace/auth/activation.test.ts src/lib/workspace/auth/hosting-accounts.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/alert-order.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts src/lib/workspace/memory/permissions.test.ts src/lib/workspace/memory/conversation-attempt.test.ts src/lib/workspace/memory/conversation-api.test.ts tests/subject-delete.test.ts tests/journal-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
"test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/navigation/deferred.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/auth/login.test.ts src/lib/workspace/auth/routes.test.ts src/lib/workspace/auth/hosting.test.ts src/lib/workspace/auth/hosting-enrollment.test.ts src/lib/workspace/auth/activation.test.ts src/lib/workspace/auth/hosting-accounts.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/subjektiv/routes.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/alert-order.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts src/lib/workspace/memory/permissions.test.ts src/lib/workspace/memory/conversation-attempt.test.ts src/lib/workspace/memory/conversation-api.test.ts tests/subject-delete.test.ts tests/journal-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
"test:journal:backend-contract": "cd ../.. && mkdir -p target && cargo test -p yoi-workspace-server --lib journal_browser_web_contract -- --nocapture > target/journal-browser-contract.log && cd web/workspace && deno run --allow-read=../../target/journal-browser-contract.log test/journal-backend-contract.ts ../../target/journal-browser-contract.log",
"test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts",
"test:browser:activation": "mkdir -p ../../target/web-ux/tmp && TMPDIR=../../target/web-ux/tmp deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/activation-browser/activation-shell.browser.test.ts",
"test:browser:hosting": "mkdir -p ../../target/web-ux/tmp && TMPDIR=../../target/web-ux/tmp deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/hosting-browser/hosting-shell.browser.test.ts test/hosting-browser/setup-shell.browser.test.ts",
"test:browser:empty-workspace": "deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/visual-review/empty-workspace.browser.test.ts",
"test:browser:drive": "deno test --config test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/drive-browser/drive.browser.test.ts",
"test:browser:subject-navigation": "cd ../.. && deno run --config tools/web-ux/deno.json -A tools/web-ux/browser-tests/subject_navigation_review.ts web/workspace/build navigation-task",
"test:browser:subject-handling": "deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/visual-review/subject-handling.browser.test.ts",
"test:ticket:real-api": "cd ../.. && deno task test:ticket:real-api",
"test:drive:real-api": "cd ../.. && cargo test -p yoi-workspace-server --lib drive_web_roundtrip_tests -- --ignored",
@@ -438,53 +438,44 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
);
const subjectIndexLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/+page.ts",
"./../../../routes/w/[workspaceId]/subjektiv/+page.ts",
import.meta.url,
),
);
const subjectIndexPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/PageContent.svelte",
"./../../../routes/w/[workspaceId]/subjektiv/PageContent.svelte",
import.meta.url,
),
);
const subjectPageLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/+page.ts",
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/+page.ts",
import.meta.url,
),
);
const subjectPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte",
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/PageContent.svelte",
import.meta.url,
),
);
const memoryDetailLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/+page.ts",
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/memories/[memoryId]/+page.ts",
import.meta.url,
),
);
const memoryDetailPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/PageContent.svelte",
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/memories/[memoryId]/PageContent.svelte",
import.meta.url,
),
);
assert(
memoryNav.includes('workspaceRoute(workspaceId, "/memory")') &&
memoryNav.includes(
'<h2 class="sidebar-nav-section__header">Memory</h2>',
) &&
memoryNav.includes("Subjects</a>") &&
memoryNav.includes("currentPath.startsWith(`${subjectsHref}/`)") &&
memoryNav.includes("sidebar-nav-section--category") &&
memoryNav.includes('class="sidebar-link"') &&
!memoryNav.includes("Staging</a>") &&
!memoryNav.includes("Document</a>"),
"Memory sidebar section should expose one Subjects link for the full nested route tree",
memoryNav.includes('"/memory/staging"') && !memoryNav.includes('>Subjects</a>'),
"Memory navigation keeps staging separate from Subjects",
);
assert(
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
@@ -1,3 +1,5 @@
import { subjectHref, subjectsHref } from "#lib/workspace/subjektiv/routes.ts";
export type WorkspaceBreadcrumb = {
label: string;
href?: string;
@@ -23,6 +25,31 @@ export function buildWorkspaceBreadcrumbs(
.filter(Boolean)
.map(decodeURIComponent);
if (segments[0] === "memory" && segments[1] === "staging") {
return [{ label: "Memory" }, { label: "Staging" }];
}
if (segments[0] === "subjektiv") {
const crumbs: WorkspaceBreadcrumb[] = [{ label: "Subjects" }];
if (!segments[1]) return crumbs;
crumbs[0].href = subjectsHref(workspaceId);
crumbs.push({ label: segments[1] });
if (!segments[2]) return crumbs;
crumbs[1].href = subjectHref(workspaceId, segments[1]);
// memoriesは詳細URLのnamespaceであり、存在しない一覧へリンクしない。
if (segments[2] === "memories" && segments[3]) {
crumbs.push({ label: segments[3] });
} else {
const label = {
permissions: "Operation permissions",
journal: "Journal",
chat: "Conversation",
}[segments[2]];
crumbs.push({ label: label ?? segments[2] });
}
return crumbs;
}
if (
segments[0] === "workers" &&
segments[1] &&
@@ -1,9 +1,9 @@
<script lang="ts">
import { subjectsHref } from '#lib/workspace/subjektiv/routes.ts';
import { untrack } from 'svelte';
import { deleteSubjektivSubject, SubjektivSubjectDeleteError } from './api.ts';
import { clearSubjectDeleteAttempt, readSubjectDeleteAttempt, saveSubjectDeleteAttempt, type SubjectDeleteAttempt } from './subject-delete.ts';
import type { SubjektivSubjectResponse } from '#lib/generated/memory-api.ts';
import { workspaceRoute } from '#lib/workspace/api/http.ts';
let { workspaceId, subjectId, subject, onstarted }: {
workspaceId: string;
@@ -108,7 +108,7 @@
<p role="alert"><strong>Deletion request rejected.</strong> Completion is not confirmed. Resolve the reported condition, then retry the same request.</p>
{:else if phase === 'completed'}
<p role="status"><strong>Subject deletion completed.</strong> Backend confirmed stopping and deletion of Subject-owned data. Separately retained Worker/Session text, archives, Workdirs, and Tickets remain.</p>
<a href={workspaceRoute(workspaceId, '/memory')}>Back to subjects →</a>
<a href={subjectsHref(workspaceId)}>Back to subjects →</a>
{/if}
{#if error}<p class="workspace-callout is-error" role="alert">{error}</p>{/if}
{#if attempt && phase !== 'completed'}
@@ -1,3 +1,4 @@
import { subjectJournalHref } from "#lib/workspace/subjektiv/routes.ts";
import { loadJson, workspaceApiPath } from "#lib/workspace/api/http.ts";
export type JournalMetadata = {
@@ -411,9 +412,7 @@ export function journalHref(
if (value != null && value !== "") params.set(key, value);
}
const suffix = params.toString();
return `/w/${encodeURIComponent(workspaceId)}/memory/${
encodeURIComponent(subjectId)
}/journal${suffix ? `?${suffix}` : ""}`;
return `${subjectJournalHref(workspaceId, subjectId)}${suffix ? `?${suffix}` : ""}`;
}
export function boundedParam(
params: URLSearchParams,
@@ -1,27 +1,13 @@
<script lang="ts">
import { workspaceRoute } from "#lib/workspace/api/http.ts";
import { ownsRoutePath } from './route-ownership.ts';
type Props = {
currentPath?: string;
workspaceId: string;
};
let { currentPath = "/", workspaceId }: Props = $props();
let subjectsHref = $derived(
workspaceId ? workspaceRoute(workspaceId, "/memory") : "/",
);
let subjectsActive = $derived(
currentPath === subjectsHref || currentPath.startsWith(`${subjectsHref}/`),
);
let { workspaceId, currentPath = '/' }: { workspaceId: string; currentPath?: string } = $props();
const href = $derived(workspaceRoute(workspaceId, "/memory/staging"));
const active = $derived(ownsRoutePath(href, currentPath));
</script>
<section class="sidebar-nav-section sidebar-nav-section--category">
<h2 class="sidebar-nav-section__header">Memory</h2>
<a
class="sidebar-link"
class:active={subjectsActive}
href={subjectsHref}
aria-current={subjectsActive ? "page" : undefined}
>Subjects</a>
<a class="sidebar-link" class:active {href} aria-current={active ? 'page' : undefined}>Staging</a>
</section>
@@ -0,0 +1,12 @@
<script lang="ts">
import { subjectsHref } from '#lib/workspace/subjektiv/routes.ts';
import { ownsRoutePath } from './route-ownership.ts';
let { workspaceId, currentPath = '/' }: { workspaceId: string; currentPath?: string } = $props();
const href = $derived(subjectsHref(workspaceId));
const active = $derived(ownsRoutePath(href, currentPath));
</script>
<section class="sidebar-nav-section sidebar-nav-section--resource">
<a class="sidebar-link" class:active {href} aria-current={active ? 'page' : undefined}>Subjects</a>
</section>
@@ -7,6 +7,7 @@
import { workspaceRoute } from '#lib/workspace/api/http.ts';
import { ownsRoutePath } from './route-ownership';
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
import SubjectsNavSection from './SubjectsNavSection.svelte';
import MemoryNavSection from './MemoryNavSection.svelte';
import MergeRequestsNavSection from './MergeRequestsNavSection.svelte';
import TicketsNavSection from './TicketsNavSection.svelte';
@@ -85,6 +86,7 @@
<ObjectivesNavSection {currentPath} {workspaceId} />
<MergeRequestsNavSection {currentPath} {workspaceId} />
<DriveNavSection {currentPath} {workspaceId} />
<SubjectsNavSection {currentPath} {workspaceId} />
<MemoryNavSection {currentPath} {workspaceId} />
<WorkersNavSection {currentPath} {workspaceId} />
</nav>
@@ -0,0 +1,34 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { legacySubjectDestination, subjectsHref } from './routes.ts';
const destination = $derived(legacySubjectDestination(page.url));
let navigationFailed = $state(false);
// static SPAではHTTP redirectを返せない。旧entryを置換し、戻るで往復させない。
// hashはloadではなくブラウザのpage.urlから引き継ぐ。
$effect(() => {
const target = destination;
navigationFailed = false;
if (target) void goto(target, { replaceState: true }).catch(() => {
navigationFailed = true;
});
});
</script>
<svelte:head><title>Subject URL moved · Yoi Workspace</title></svelte:head>
<section class="legacy-subject-route">
<h1>Subject URL moved</h1>
{#if destination}
<p>{navigationFailed ? 'Automatic navigation failed.' : 'Opening the new Subject URL…'}</p>
<a href={destination}>Continue to the new URL →</a>
{:else}
<p>This legacy path has no corresponding Subject page.</p>
<a href={subjectsHref(page.params.workspaceId ?? '')}>Open Subjects →</a>
{/if}
</section>
<style>
.legacy-subject-route { padding: var(--space-4); }
</style>
@@ -0,0 +1,47 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import SubjectsNavSection from "../sidebar/SubjectsNavSection.svelte";
import MemoryNavSection from "../sidebar/MemoryNavSection.svelte";
afterEach(cleanup);
test("Subjects is a direct resource link, distinct from Memory staging, and follows route changes", async () => {
const view = render(SubjectsNavSection, {
workspaceId: "w/s",
currentPath: "/w/w%2Fs/subjektiv/a/chat",
});
render(MemoryNavSection, {
workspaceId: "w/s",
currentPath: "/w/w%2Fs/subjektiv/a/chat",
});
const subjects = screen.getByRole("link", { name: "Subjects" });
expect(subjects.getAttribute("href")).toBe("/w/w%2Fs/subjektiv");
expect(subjects.getAttribute("aria-current")).toBe("page");
expect(subjects.closest("section")?.querySelector("h2")).toBeNull();
expect(screen.getByRole("link", { name: "Staging" }).getAttribute("href"))
.toBe("/w/w%2Fs/memory/staging");
expect(
screen.getByRole("link", { name: "Staging" }).getAttribute("aria-current"),
).toBeNull();
await view.rerender({
workspaceId: "w/s",
currentPath: "/w/w%2Fs/subjektiv-other",
});
expect(subjects.getAttribute("aria-current")).toBeNull();
});
test("Memory staging selection never activates the Subject resource", () => {
render(SubjectsNavSection, {
workspaceId: "w",
currentPath: "/w/w/memory/staging",
});
render(MemoryNavSection, {
workspaceId: "w",
currentPath: "/w/w/memory/staging",
});
expect(
screen.getByRole("link", { name: "Staging" }).getAttribute("aria-current"),
).toBe("page");
expect(
screen.getByRole("link", { name: "Subjects" }).getAttribute("aria-current"),
).toBeNull();
});
@@ -0,0 +1,119 @@
declare const Deno: { test(name: string, fn: () => void): void };
function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
);
}
}
import {
legacySubjectDestination,
subjectChatHref,
subjectHref,
subjectMemoryHref,
subjectsHref,
} from "./routes.ts";
import { buildWorkspaceBreadcrumbs } from "../header/breadcrumb-model.ts";
import { ownsRoutePath } from "../sidebar/route-ownership.ts";
Deno.test("Subject routes encode opaque identities once and namespace Memory IDs", () => {
assertEquals(subjectsHref("ws /&"), "/w/ws%20%2F%26/subjektiv");
assertEquals(subjectHref("w", "staging"), "/w/w/subjektiv/staging");
assertEquals(
subjectChatHref("w", "a/b ?#日本%"),
"/w/w/subjektiv/a%2Fb%20%3F%23%E6%97%A5%E6%9C%AC%25/chat",
);
for (
const id of ["journal", "permissions", "chat", "conversation", "a/b ?#%"]
) {
assertEquals(
subjectMemoryHref("w", "s", id),
`/w/w/subjektiv/s/memories/${encodeURIComponent(id)}`,
);
}
});
Deno.test("legacy migration preserves query/hash and static ownership without API lookup", () => {
for (
const [old, canonical] of [
["", ""],
["/", ""],
["/s", "/s"],
["/s/", "/s"],
["/s/permissions", "/s/permissions"],
["/s/journal", "/s/journal"],
["/s/chat", "/s/chat"],
["/s/conversation", "/s/chat"],
["/s/m-1", "/s/memories/m-1"],
["/a%2Fb/m%252F", "/a%2Fb/memories/m%252F"],
["/s/memories", "/s/memories/memories"],
["/staging/journal", "/staging/journal"],
["/staging/m-1", "/staging/memories/m-1"],
]
) {
const url = new URL(
`https://example.test/w/ws%2Fa/memory${old}?cursor=a%2Fb+%26&change_id=%252F#memory%2Fsection`,
);
const expected = `/w/ws%2Fa/subjektiv${canonical}${url.search}${url.hash}`;
assertEquals(legacySubjectDestination(url), expected);
assertEquals(legacySubjectDestination(new URL(expected, url)), null);
}
for (
const path of [
"/w/w/memory/staging",
"/w/w/memory/staging/",
"/w/w/memory/staging/unknown/deeper",
"/w/w/memory/s/x/y",
"/w/w/memory/s//x",
"/w/w/memory/%zz",
"/w/w/memories/s",
"/w/w/subjektiv/s",
]
) {
assertEquals(
legacySubjectDestination(new URL(path, "https://example.test")),
null,
);
}
});
Deno.test("Subject sidebar ownership and breadcrumbs never lead to legacy or nonexistent Memory index", () => {
const root = subjectsHref("w");
assertEquals(ownsRoutePath(root, "/w/w/subjektiv/s/chat"), true);
for (
const path of [
"/w/w/subjektiv-other",
"/w/w/memory/staging",
"/w/other/subjektiv",
]
) {
assertEquals(ownsRoutePath(root, path), false);
}
assertEquals(buildWorkspaceBreadcrumbs("/w/w/memory/staging", "w"), [{
label: "Memory",
}, { label: "Staging" }]);
assertEquals(buildWorkspaceBreadcrumbs(root, "w"), [{ label: "Subjects" }]);
assertEquals(
buildWorkspaceBreadcrumbs("/w/w/subjektiv/a%2Fb/memories/permissions", "w"),
[
{ label: "Subjects", href: root },
{ label: "a/b", href: "/w/w/subjektiv/a%2Fb" },
{ label: "permissions" },
],
);
for (
const [route, label] of [["chat", "Conversation"], ["journal", "Journal"], [
"permissions",
"Operation permissions",
]]
) {
assertEquals(
buildWorkspaceBreadcrumbs(`/w/w/subjektiv/staging/${route}`, "w"),
[
{ label: "Subjects", href: root },
{ label: "staging", href: "/w/w/subjektiv/staging" },
{ label },
],
);
}
});
@@ -0,0 +1,86 @@
import { workspaceRoute } from "#lib/workspace/api/http.ts";
export function subjectsHref(workspaceId: string): string {
return workspaceRoute(workspaceId, "/subjektiv");
}
export function subjectHref(workspaceId: string, subjectId: string): string {
return `${subjectsHref(workspaceId)}/${encodeURIComponent(subjectId)}`;
}
export function subjectPermissionsHref(
workspaceId: string,
subjectId: string,
): string {
return `${subjectHref(workspaceId, subjectId)}/permissions`;
}
export function subjectJournalHref(
workspaceId: string,
subjectId: string,
): string {
return `${subjectHref(workspaceId, subjectId)}/journal`;
}
export function subjectChatHref(
workspaceId: string,
subjectId: string,
): string {
return `${subjectHref(workspaceId, subjectId)}/chat`;
}
export function subjectMemoryHref(
workspaceId: string,
subjectId: string,
memoryId: string,
): string {
return `${subjectHref(workspaceId, subjectId)}/memories/${
encodeURIComponent(memoryId)
}`;
}
/** 旧SPAの静的予約語を優先する。Memory IDを新しい画面の予約語と競合させない。
* pathnameを分割してからdecodeするため、ID内のencoded slashを階層に読み替えない。
* query/hashは解析・再生成せず引き継ぐ。未知の階層から移行先を推測しない。
*/
export function legacySubjectDestination(
url: Pick<URL, "pathname" | "search" | "hash">,
): string | null {
const segments = url.pathname.split("/").slice(1);
if (segments.at(-1) === "") segments.pop();
if (segments[0] !== "w" || !segments[1] || segments[2] !== "memory") {
return null;
}
let decoded: string[];
try {
decoded = segments.map(decodeURIComponent);
} catch {
return null;
}
const workspaceId = decoded[1];
const subjectId = decoded[3];
let destination: string;
if (decoded.length === 3) destination = subjectsHref(workspaceId);
// stagingは旧一覧と同階層の完全一致だけを所有し、その子routeは動的Subjectだった。
else if (!subjectId || (subjectId === "staging" && decoded.length === 4)) {
return null;
} else if (decoded.length === 4) {
destination = subjectHref(workspaceId, subjectId);
} else if (decoded.length === 5 && decoded[4]) {
switch (decoded[4]) {
case "permissions":
destination = subjectPermissionsHref(workspaceId, subjectId);
break;
case "journal":
destination = subjectJournalHref(workspaceId, subjectId);
break;
case "conversation":
case "chat":
destination = subjectChatHref(workspaceId, subjectId);
break;
default:
destination = subjectMemoryHref(workspaceId, subjectId, decoded[4]);
}
} else return null;
return destination + url.search + url.hash;
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { subjectsHref, subjectHref } from '#lib/workspace/subjektiv/routes.ts';
import { onDestroy, onMount } from "svelte";
import BevelLine from "#lib/workspace/ui/BevelLine.svelte";
import type { TicketDetail, TicketAssignmentTrackingProjection, TicketAssignmentTrackingHistoryPage, TicketAssignmentsPage, TicketAssignmentPrincipalSnapshot } from "#lib/generated/ticket-api.ts";
@@ -156,7 +157,7 @@
{#if subjectsBusy}<p role="status">Loading Subjects…</p>{/if}
{#if subjectsError}<p class="workspace-callout is-error" role="alert">Subjects: {subjectsError}</p><button class="workspace-secondary-button" disabled={subjectsBusy} onclick={() => void readSubjects()}>Retry Subjects</button>{/if}
{#if subjectsCursor}<button class="workspace-secondary-button" disabled={subjectsBusy} onclick={() => void readSubjects(true)}>More Subjects</button>{/if}
{#if !subjectsBusy && !subjectsError && !subjects.length}<p>No Subjects available. Create or configure a Subject in <a href={`/w/${encodeURIComponent(workspaceId)}/memory`}>Memory</a>.</p>{/if}
{#if !subjectsBusy && !subjectsError && !subjects.length}<p>No Subjects available. Create or configure a Subject in <a href={subjectsHref(workspaceId)}>Subjects</a>.</p>{/if}
<p>Backend validates the Subject and current permissions. Assignment does not grant permissions or select or start a concrete Worker.</p>
</details>
{/if}
@@ -196,7 +197,7 @@
<details class="ticket-detail-section ticket-subject-worker">
<summary>Subject and current Worker</summary>
<p>Assignment follows the Subject across Worker replacement. Worker availability does not change handling status.</p>
<a href={`/w/${encodeURIComponent(workspaceId)}/memory/${encodeURIComponent(current.principal.subject_id)}`}>Open responsible Subject and current Worker</a>
<a href={subjectHref(workspaceId, current.principal.subject_id)}>Open responsible Subject and current Worker</a>
</details>
{:else if current}
<p class="workspace-callout">Legacy assignment retained for audit only. Select a Subject to create a new assignment; old receipts do not authorize new work.</p>
@@ -1,13 +1,5 @@
<script lang="ts">
import DeferredPage from '#lib/workspace/navigation/DeferredPage.svelte';
import PageContent from './PageContent.svelte';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
import LegacySubjectRoute from '#lib/workspace/subjektiv/LegacySubjectRoute.svelte';
</script>
<DeferredPage {data} title="Subjects">
{#snippet children(resolved)}
<PageContent data={resolved} />
{/snippet}
</DeferredPage>
<LegacySubjectRoute />
@@ -0,0 +1,5 @@
<script lang="ts">
import LegacySubjectRoute from '#lib/workspace/subjektiv/LegacySubjectRoute.svelte';
</script>
<LegacySubjectRoute />
@@ -0,0 +1,13 @@
<script lang="ts">
import DeferredPage from '#lib/workspace/navigation/DeferredPage.svelte';
import PageContent from './PageContent.svelte';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<DeferredPage {data} title="Subjects">
{#snippet children(resolved)}
<PageContent data={resolved} />
{/snippet}
</DeferredPage>
@@ -1,7 +1,8 @@
<script lang="ts">
import { subjectsHref, subjectHref as subjectDetailHref } from '#lib/workspace/subjektiv/routes.ts';
import { goto } from '$app/navigation';
import { tick } from 'svelte';
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
import { formatDate } from '#lib/workspace/api/http.ts';
import {
createSubjektivSubject,
SubjektivSubjectCreateError,
@@ -30,11 +31,11 @@
let behaviorInput = $state<HTMLTextAreaElement>();
function subjectHref(subjectId: string): string {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(subjectId)}`);
return subjectDetailHref(data.workspaceId, subjectId);
}
function pageHref(cursor?: string | null): string {
const path = workspaceRoute(data.workspaceId, '/memory');
const path = subjectsHref(data.workspaceId);
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
}
@@ -125,16 +126,15 @@
</script>
<svelte:head>
<title>Subjects · Memory · Yoi Workspace</title>
<meta name="description" content="Workspace Memory subjects" />
<title>Subjects · Yoi Workspace</title>
<meta name="description" content="Workspace Subjects" />
</svelte:head>
<section class="memory-page memory-subject-index" aria-labelledby="memory-subjects-heading" data-memory-view="subjects">
<header class="memory-page-header">
<div>
<p class="memory-eyebrow">Memory</p>
<h1 id="memory-subjects-heading">Subjects</h1>
<p>Durable context, organized by explicit subject.</p>
<p>Choose a Subject to view its activity, conversation, and Memory.</p>
</div>
<div class="memory-header-actions">
{#if data.subjects.data}
@@ -222,7 +222,7 @@
{#if data.cursor}<p><a href={pageHref()}>Return to the first page</a></p>{/if}
</div>
{:else}
<div class="subject-list" aria-label="Memory subjects">
<div class="subject-list" aria-label="Subjects">
{#each subjects as subject (subject.id)}
<a class="subject-row" href={subjectHref(subject.id)}>
<div class="subject-copy">
@@ -1,4 +1,5 @@
<script lang="ts">
import { subjectsHref as subjectListHref, subjectHref, subjectMemoryHref, subjectJournalHref, subjectPermissionsHref, subjectChatHref } from '#lib/workspace/subjektiv/routes.ts';
// identity、current Worker、Committed Memory、生成contextの利用可否を先に示す。
// current WorkerはBackend singleton projectionであり、過去のSession帰属から推測しない。
// behaviorは利用者が管理する本文、surfaceはMemoryからの生成物として分ける。
@@ -7,7 +8,7 @@
import { untrack } from 'svelte';
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
import SubjectDelete from '#lib/workspace/memory/SubjectDelete.svelte';
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
import { formatDate } from '#lib/workspace/api/http.ts';
import {
SubjektivSubjectCreateError,
updateSubjektivSubjectBehavior,
@@ -110,22 +111,16 @@
const memories = $derived(data.memories.data?.items ?? []);
function subjectsHref(): string {
return workspaceRoute(data.workspaceId, '/memory');
return subjectListHref(data.workspaceId);
}
function memoryHref(memoryId: string, changeId?: string): string {
const path = workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
);
const path = subjectMemoryHref(data.workspaceId, data.subjectId, memoryId);
return changeId ? `${path}?change_id=${encodeURIComponent(changeId)}` : path;
}
function memoryPageHref(cursor?: string | null): string {
const path = workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}`,
);
const path = subjectHref(data.workspaceId, data.subjectId);
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
}
@@ -157,7 +152,7 @@
</script>
<svelte:head>
<title>{subject?.role ?? data.subjectId} · Memory · Yoi Workspace</title>
<title>{subject?.role ?? data.subjectId} · Subjects · Yoi Workspace</title>
<meta name="description" content="Subject resident surface and current committed Memories" />
</svelte:head>
@@ -166,7 +161,7 @@
<header class="memory-page-header">
<div>
<p class="memory-eyebrow">Memory subject</p>
<p class="memory-eyebrow">Subject</p>
<h1 id="subject-heading">{subject?.role ?? data.subjectId}</h1>
<code class="subject-id" title={data.subjectId}>{data.subjectId}</code>
</div>
@@ -180,7 +175,11 @@
{/if}
{#if !deletionStarted}
<nav aria-label="Subjectの記録と管理">{#if subject?.state === 'active'}<a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}/conversation`)}>Open conversation</a> · {/if}<a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}/journal`)}>Journal · 最近の出来事とSessionを探す →</a> · <a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}/permissions`)}>Operation permissions · 直接操作と委譲上限 →</a></nav>
<nav class="subject-navigation" aria-label="Subjectの記録と管理">
{#if subject?.state === 'active'}<a href={subjectChatHref(data.workspaceId, data.subjectId)}>Open conversation</a>{/if}
<a href={subjectJournalHref(data.workspaceId, data.subjectId)}>Journal · 最近の出来事とSessionを探す →</a>
<a href={subjectPermissionsHref(data.workspaceId, data.subjectId)}>Operation permissions · 直接操作と委譲上限 →</a>
</nav>
{#if subject}
<section class="subject-overview" aria-label="Subject status">
@@ -402,6 +401,7 @@
</section>
<style>
.subject-navigation { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.memory-page {
display: grid;
flex: 0 0 auto;
@@ -1,6 +1,6 @@
<script lang="ts">
import { subjectHref } from '#lib/workspace/subjektiv/routes.ts';
import SubjectConversation from '#lib/workspace/memory/SubjectConversation.svelte';
import { workspaceRoute } from '#lib/workspace/api/http.ts';
import type { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
import type { PageData as RouteData } from './$types';
let { data }: { data: ResolvedPageData<RouteData> } = $props();
@@ -10,7 +10,7 @@
<svelte:head><title>{subject?.role ?? 'Subject'} · Conversation · Yoi Workspace</title></svelte:head>
<section class="conversation-page" aria-labelledby="conversation-subject-heading">
<a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`)}>← Subject details</a>
<a href={subjectHref(data.workspaceId, data.subjectId)}>← Subject details</a>
{#if subject}
<h1 id="conversation-subject-heading">{subject.role}</h1>
{#key JSON.stringify([data.accountId, data.workspaceId, subject.id])}
@@ -1,8 +1,9 @@
<script lang="ts">
import { subjectHref, subjectMemoryHref } from '#lib/workspace/subjektiv/routes.ts';
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
import { journalHref, type JournalEntry, type TextPage } from '#lib/workspace/memory/journal-api.ts';
import { formatDate, workspaceRoute, type ApiResult } from '#lib/workspace/api/http.ts';
import { formatDate, type ApiResult } from '#lib/workspace/api/http.ts';
import type { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
import type { PageData as RouteData } from './$types';
let { data }: { data: ResolvedPageData<RouteData> } = $props();
@@ -18,7 +19,7 @@
function entryHref(e: JournalEntry) { return journalHref(data.workspaceId, data.subjectId, { journal: e.journal_id, entry: e.id }); }
function nextText(key: string, page: TextPage) { return href({ [`${key}_byte`]: String(page.next_byte), [`${key}_token`]: page.content_token }); }
function memoryHref(memoryId: string, changeId: string) {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}?change_id=${encodeURIComponent(changeId)}`);
return `${subjectMemoryHref(data.workspaceId, data.subjectId, memoryId)}?change_id=${encodeURIComponent(changeId)}`;
}
</script>
<svelte:head><title>{entry?.title ?? 'Journal'} · Yoi Workspace</title></svelte:head>
@@ -47,7 +48,7 @@
{/snippet}
<section class="journal-page" aria-labelledby="journal-title" data-journal-view={data.mode}>
<a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`)}>← SubjectのMemory</a>
<a href={subjectHref(data.workspaceId, data.subjectId)}>← Subject details</a>
<header><h1 id="journal-title">{entry?.title ?? 'Journal'}</h1>{#if entry}<p>{entry.state} · 出来事 {entry.position} · <time datetime={entry.created_at}>{formatDate(entry.created_at)}</time></p>{/if}</header>
<p class="muted">Journalは活動の経緯を記録した文章です。原発言・tool結果そのものではなく、Memoryの直接の抽出元や主張の証明ではありません。</p>
{#if data.mode === 'detail'}
@@ -1,10 +1,11 @@
<script lang="ts">
import { subjectHref as subjectDetailHref, subjectMemoryHref } from '#lib/workspace/subjektiv/routes.ts';
// current/historicalはchange_idとcurrent_change_idの一致で区別する。
// 本文・evidenceの続きも同じimmutable changeへ固定し、現在版の内容を混ぜない。
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
import { journalHref } from '#lib/workspace/memory/journal-api.ts';
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
import { formatDate } from '#lib/workspace/api/http.ts';
import type {
MemoryEvidenceOrigin,
SubjektivMemoryChangeRef,
@@ -21,14 +22,11 @@
const changes = $derived(data.changes.data?.items ?? []);
function subjectHref(): string {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`);
return subjectDetailHref(data.workspaceId, data.subjectId);
}
function memoryPath(memoryId = data.memoryId): string {
return workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
);
return subjectMemoryHref(data.workspaceId, data.subjectId, memoryId);
}
function changeHref(changeId: string): string {
@@ -1,13 +1,13 @@
<script lang="ts">
import { subjectHref } from '#lib/workspace/subjektiv/routes.ts';
import SubjectPermissions from '#lib/workspace/memory/SubjectPermissions.svelte';
import { workspaceRoute } from '#lib/workspace/api/http.ts';
import type { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
import type { PageData } from './$types';
let { data }: { data: ResolvedPageData<PageData> } = $props();
</script>
<svelte:head><title>Permissions · {data.subject.data?.role ?? 'Subject'} · Yoi Workspace</title></svelte:head>
<section class="permission-page">
<a href={workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`)}>← Subject details</a>
<a href={subjectHref(data.workspaceId, data.subjectId)}>← Subject details</a>
<h1>{data.subject.data?.role ?? 'Subject'} permissions</h1>
{#if data.subject.data}
{#key JSON.stringify([data.workspaceId, data.subjectId])}<SubjectPermissions workspaceId={data.workspaceId} subjectId={data.subjectId} />{/key}
@@ -2,7 +2,7 @@
import { cleanup, render, screen, within } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import JournalPage from "./[subjectId]/journal/PageContent.svelte";
import MemoryPage from "./[subjectId]/[memoryId]/PageContent.svelte";
import MemoryPage from "./[subjectId]/memories/[memoryId]/PageContent.svelte";
afterEach(cleanup);
const result = <T>(data: T | null, error: string | null = null) => ({
@@ -205,7 +205,7 @@ test("deleted originals do not hide saved entry, correction links or titled immu
screen.getByRole("link", { name: "復旧経路を先に確認する →" }).getAttribute(
"href",
),
).toBe("/w/workspace-1/memory/subject-1/memory-1?change_id=change-1");
).toBe("/w/workspace-1/subjektiv/subject-1/memories/memory-1?change_id=change-1");
expect(
screen.getByRole("link", { name: "復旧経路を先に確認する →" }).getAttribute(
"target",
@@ -51,7 +51,7 @@ vi.mock("$app/navigation", () => ({ goto: gotoMock }));
import SubjectIndexPage from "./PageContent.svelte";
import SubjectPage from "./[subjectId]/PageContent.svelte";
import MemoryDetailPage from "./[subjectId]/[memoryId]/PageContent.svelte";
import MemoryDetailPage from "./[subjectId]/memories/[memoryId]/PageContent.svelte";
afterEach(() => {
cleanup();
@@ -114,7 +114,7 @@ test("renders the subject index as the Memory product entry", () => {
expect(screen.queryByText("Memory fingerprint", { exact: true })).toBeNull();
expect(screen.getByText("1 shown · more available")).not.toBeNull();
expect(screen.getByRole("link", { name: "First page" }).getAttribute("href"))
.toBe("/w/workspace-1/memory");
.toBe("/w/workspace-1/subjektiv");
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
.toContain("cursor=subject-next");
expect(screen.queryByText("Staging")).toBeNull();
@@ -165,7 +165,7 @@ test("creates one Subject with the exact role and navigates to its generated det
}, { status: 201 }));
await waitFor(() => {
expect(gotoMock).toHaveBeenCalledWith(
"/w/workspace-1/memory/generated-subject-1",
"/w/workspace-1/subjektiv/generated-subject-1",
);
});
@@ -1039,7 +1039,7 @@ test("renders committed Memory detail, provenance, derivation, and immutable cha
});
expect(historicalLink.getAttribute("aria-current")).toBe("page");
expect(historicalLink.getAttribute("href")).toBe(
"/w/workspace-1/memory/subject-1/memory-1?change_id=z%2Fearlier%20%26%20context",
"/w/workspace-1/subjektiv/subject-1/memories/memory-1?change_id=z%2Fearlier%20%26%20context",
);
const currentLink = screen.getByRole("link", {
name: /Memory change a\/current/,
@@ -1049,7 +1049,7 @@ test("renders committed Memory detail, provenance, derivation, and immutable cha
expect(
screen.getByRole("link", { name: /memory-parent/ }).getAttribute("href"),
).toBe(
"/w/workspace-1/memory/subject-1/memory-parent?change_id=parent%2Fsource%20%26%20context",
"/w/workspace-1/subjektiv/subject-1/memories/memory-parent?change_id=parent%2Fsource%20%26%20context",
);
const bodyHref = screen.getByRole("link", { name: "View next body segment" })
.getAttribute("href");
@@ -74,7 +74,7 @@ Deno.test({
await route.continue();
},
);
await page.goto(`${origin}/w/memory-review/memory/pending-subject`);
await page.goto(`${origin}/w/memory-review/subjektiv/pending-subject`);
await page.locator(".subject-delete-open").waitFor();
await page.screenshot({
path: join(artifact, `${theme}-${width}-initial.png`),
@@ -143,7 +143,7 @@ Deno.test({
requests.push(request.postDataJSON());
}
});
await page.goto(`${origin}/w/memory-review/memory/${subject}`);
await page.goto(`${origin}/w/memory-review/subjektiv/${subject}`);
await page.locator(".subject-delete-open").waitFor();
if (subject === "legacy-subject") {
assert(
@@ -186,7 +186,7 @@ Deno.test({
});}
},
);
await page.goto(`${origin}/w/memory-review/memory/pending-subject`);
await page.goto(`${origin}/w/memory-review/subjektiv/pending-subject`);
await confirm(page);
await page.locator('[data-delete-state="failed"]').waitFor();
await page.screenshot({ path: join(artifact, "rejected.png") });
@@ -217,7 +217,7 @@ Deno.test({
});
},
);
await page.goto(`${origin}/w/memory-review/memory/pending-subject`);
await page.goto(`${origin}/w/memory-review/subjektiv/pending-subject`);
await confirm(page);
await page.locator('[data-delete-state="running"]').waitFor();
await page.getByRole("link", { name: "← All subjects", exact: true })
@@ -241,7 +241,7 @@ Deno.test({
{
const page = await browser.newPage();
await page.goto(
`${origin}/w/memory-review/memory/legacy-subject?persona=reader`,
`${origin}/w/memory-review/subjektiv/legacy-subject?persona=reader`,
);
await page.locator(".subject-overview").waitFor();
assertEquals(await page.locator(".subject-deletion").count(), 0);
+1 -1
View File
@@ -221,7 +221,7 @@ Deno.test("Journal links retain exact selected entry and bounded URL offsets", (
}),
"https://example.com",
);
assertEquals(url.pathname, "/w/workspace/memory/subject%2Fa/journal");
assertEquals(url.pathname, "/w/workspace/subjektiv/subject%2Fa/journal");
assertEquals(url.searchParams.get("entry"), "entry&selected");
assertEquals(
boundedParam(new URLSearchParams({ q: "あ".repeat(5) }), "q", 14),