diff --git a/tools/web-ux/browser-tests/journal_browser_shell_test.ts b/tools/web-ux/browser-tests/journal_browser_shell_test.ts index fc683d28f..ab0e3c82d 100644 --- a/tools/web-ux/browser-tests/journal_browser_shell_test.ts +++ b/tools/web-ux/browser-tests/journal_browser_shell_test.ts @@ -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参照を取得できません。", diff --git a/tools/web-ux/browser-tests/memory_document_fixture_server.ts b/tools/web-ux/browser-tests/memory_document_fixture_server.ts index 85456e973..0581f0566 100644 --- a/tools/web-ux/browser-tests/memory_document_fixture_server.ts +++ b/tools/web-ux/browser-tests/memory_document_fixture_server.ts @@ -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") { diff --git a/tools/web-ux/browser-tests/memory_document_shell_test.ts b/tools/web-ux/browser-tests/memory_document_shell_test.ts index 79535ed31..10143bb5f 100644 --- a/tools/web-ux/browser-tests/memory_document_shell_test.ts +++ b/tools/web-ux/browser-tests/memory_document_shell_test.ts @@ -45,7 +45,7 @@ async function openSubjectPage( baseUrl: string, mobile = false, ): Promise { - 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)); diff --git a/tools/web-ux/browser-tests/subject_conversation_review.ts b/tools/web-ux/browser-tests/subject_conversation_review.ts index ad096edd1..e552da3ca 100644 --- a/tools/web-ux/browser-tests/subject_conversation_review.ts +++ b/tools/web-ux/browser-tests/subject_conversation_review.ts @@ -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( diff --git a/tools/web-ux/browser-tests/subject_creation_review.ts b/tools/web-ux/browser-tests/subject_creation_review.ts index ed1b26334..e977dbde5 100644 --- a/tools/web-ux/browser-tests/subject_creation_review.ts +++ b/tools/web-ux/browser-tests/subject_creation_review.ts @@ -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( diff --git a/tools/web-ux/browser-tests/subject_navigation_review.ts b/tools/web-ux/browser-tests/subject_navigation_review.ts new file mode 100644 index 000000000..3c1077a6a --- /dev/null +++ b/tools/web-ux/browser-tests/subject_navigation_review.ts @@ -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 ", + ); +} +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[] = [], + failures: string[] = [], + faults: Record[] = []; +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, + }), +); diff --git a/tools/web-ux/browser-tests/subject_permissions_review.ts b/tools/web-ux/browser-tests/subject_permissions_review.ts index 5af94bd8f..352bdf00c 100644 --- a/tools/web-ux/browser-tests/subject_permissions_review.ts +++ b/tools/web-ux/browser-tests/subject_permissions_review.ts @@ -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(); diff --git a/tools/web-ux/scenarios/journal-browser-dark.json b/tools/web-ux/scenarios/journal-browser-dark.json index fd5c3ce7b..a2d91d710 100644 --- a/tools/web-ux/scenarios/journal-browser-dark.json +++ b/tools/web-ux/scenarios/journal-browser-dark.json @@ -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": { diff --git a/tools/web-ux/scenarios/journal-browser-light.json b/tools/web-ux/scenarios/journal-browser-light.json index df719dd7a..8d1e83b1c 100644 --- a/tools/web-ux/scenarios/journal-browser-light.json +++ b/tools/web-ux/scenarios/journal-browser-light.json @@ -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": { diff --git a/tools/web-ux/scenarios/memory-subjects-dark.json b/tools/web-ux/scenarios/memory-subjects-dark.json index 633df5dab..4803e7e23 100644 --- a/tools/web-ux/scenarios/memory-subjects-dark.json +++ b/tools/web-ux/scenarios/memory-subjects-dark.json @@ -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": { diff --git a/tools/web-ux/scenarios/memory-subjects-light.json b/tools/web-ux/scenarios/memory-subjects-light.json index 54018c541..74910dee8 100644 --- a/tools/web-ux/scenarios/memory-subjects-light.json +++ b/tools/web-ux/scenarios/memory-subjects-light.json @@ -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": { diff --git a/tools/web-ux/scenarios/subject-delete-dark.json b/tools/web-ux/scenarios/subject-delete-dark.json index 12e9f5583..4215c8c53 100644 --- a/tools/web-ux/scenarios/subject-delete-dark.json +++ b/tools/web-ux/scenarios/subject-delete-dark.json @@ -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": { diff --git a/tools/web-ux/scenarios/subject-delete-light.json b/tools/web-ux/scenarios/subject-delete-light.json index b40e43656..6370b891a 100644 --- a/tools/web-ux/scenarios/subject-delete-light.json +++ b/tools/web-ux/scenarios/subject-delete-light.json @@ -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": { diff --git a/tools/web-ux/scenarios/subject-permissions-dark.json b/tools/web-ux/scenarios/subject-permissions-dark.json index df9570f29..7cab0626b 100644 --- a/tools/web-ux/scenarios/subject-permissions-dark.json +++ b/tools/web-ux/scenarios/subject-permissions-dark.json @@ -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": { diff --git a/tools/web-ux/scenarios/subject-permissions-light.json b/tools/web-ux/scenarios/subject-permissions-light.json index 56146c71a..655801666 100644 --- a/tools/web-ux/scenarios/subject-permissions-light.json +++ b/tools/web-ux/scenarios/subject-permissions-light.json @@ -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": { diff --git a/web/workspace/deno.json b/web/workspace/deno.json index b56315e6c..3411f10b0 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -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", diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 366b41a1a..99cbdfd74 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -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( - '', - ) && - memoryNav.includes("Subjects") && - memoryNav.includes("currentPath.startsWith(`${subjectsHref}/`)") && - memoryNav.includes("sidebar-nav-section--category") && - memoryNav.includes('class="sidebar-link"') && - !memoryNav.includes("Staging") && - !memoryNav.includes("Document"), - "Memory sidebar section should expose one Subjects link for the full nested route tree", + memoryNav.includes('"/memory/staging"') && !memoryNav.includes('>Subjects'), + "Memory navigation keeps staging separate from Subjects", ); assert( subjectIndexLoad.includes('"/subjektiv/subjects"') && diff --git a/web/workspace/src/lib/workspace/header/breadcrumb-model.ts b/web/workspace/src/lib/workspace/header/breadcrumb-model.ts index 97228b0fb..397823778 100644 --- a/web/workspace/src/lib/workspace/header/breadcrumb-model.ts +++ b/web/workspace/src/lib/workspace/header/breadcrumb-model.ts @@ -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] && diff --git a/web/workspace/src/lib/workspace/memory/SubjectDelete.svelte b/web/workspace/src/lib/workspace/memory/SubjectDelete.svelte index 577dc135c..05292b23b 100644 --- a/web/workspace/src/lib/workspace/memory/SubjectDelete.svelte +++ b/web/workspace/src/lib/workspace/memory/SubjectDelete.svelte @@ -1,9 +1,9 @@ diff --git a/web/workspace/src/lib/workspace/sidebar/SubjectsNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/SubjectsNavSection.svelte new file mode 100644 index 000000000..4335ca721 --- /dev/null +++ b/web/workspace/src/lib/workspace/sidebar/SubjectsNavSection.svelte @@ -0,0 +1,12 @@ + + + diff --git a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte index 7a0f9cff7..85b52cc5a 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkspaceSidebar.svelte @@ -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 @@ + diff --git a/web/workspace/src/lib/workspace/subjektiv/LegacySubjectRoute.svelte b/web/workspace/src/lib/workspace/subjektiv/LegacySubjectRoute.svelte new file mode 100644 index 000000000..e5b070cf3 --- /dev/null +++ b/web/workspace/src/lib/workspace/subjektiv/LegacySubjectRoute.svelte @@ -0,0 +1,34 @@ + + +Subject URL moved · Yoi Workspace +
+

Subject URL moved

+ {#if destination} +

{navigationFailed ? 'Automatic navigation failed.' : 'Opening the new Subject URL…'}

+ Continue to the new URL → + {:else} +

This legacy path has no corresponding Subject page.

+ Open Subjects → + {/if} +
+ + diff --git a/web/workspace/src/lib/workspace/subjektiv/navigation.browser.spec.ts b/web/workspace/src/lib/workspace/subjektiv/navigation.browser.spec.ts new file mode 100644 index 000000000..1e4f158fd --- /dev/null +++ b/web/workspace/src/lib/workspace/subjektiv/navigation.browser.spec.ts @@ -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(); +}); diff --git a/web/workspace/src/lib/workspace/subjektiv/routes.test.ts b/web/workspace/src/lib/workspace/subjektiv/routes.test.ts new file mode 100644 index 000000000..a3b4de0ed --- /dev/null +++ b/web/workspace/src/lib/workspace/subjektiv/routes.test.ts @@ -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 }, + ], + ); + } +}); diff --git a/web/workspace/src/lib/workspace/subjektiv/routes.ts b/web/workspace/src/lib/workspace/subjektiv/routes.ts new file mode 100644 index 000000000..b4224d68f --- /dev/null +++ b/web/workspace/src/lib/workspace/subjektiv/routes.ts @@ -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, +): 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; +} diff --git a/web/workspace/src/lib/workspace/tickets/TicketHandling.svelte b/web/workspace/src/lib/workspace/tickets/TicketHandling.svelte index 3c8b7db9f..434f28195 100644 --- a/web/workspace/src/lib/workspace/tickets/TicketHandling.svelte +++ b/web/workspace/src/lib/workspace/tickets/TicketHandling.svelte @@ -1,4 +1,5 @@ - - {#snippet children(resolved)} - - {/snippet} - + diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/[...legacy]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/memory/[...legacy]/+page.svelte new file mode 100644 index 000000000..614e2c602 --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/memory/[...legacy]/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/web/workspace/src/routes/w/[workspaceId]/subjektiv/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/subjektiv/+page.svelte new file mode 100644 index 000000000..ec81cda4a --- /dev/null +++ b/web/workspace/src/routes/w/[workspaceId]/subjektiv/+page.svelte @@ -0,0 +1,13 @@ + + + + {#snippet children(resolved)} + + {/snippet} + diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/+page.ts b/web/workspace/src/routes/w/[workspaceId]/subjektiv/+page.ts similarity index 100% rename from web/workspace/src/routes/w/[workspaceId]/memory/+page.ts rename to web/workspace/src/routes/w/[workspaceId]/subjektiv/+page.ts diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/PageContent.svelte b/web/workspace/src/routes/w/[workspaceId]/subjektiv/PageContent.svelte similarity index 96% rename from web/workspace/src/routes/w/[workspaceId]/memory/PageContent.svelte rename to web/workspace/src/routes/w/[workspaceId]/subjektiv/PageContent.svelte index 0a27469d8..f0890c556 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/PageContent.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/subjektiv/PageContent.svelte @@ -1,7 +1,8 @@ - Subjects · Memory · Yoi Workspace - + Subjects · Yoi Workspace +
-

Memory

Subjects

-

Durable context, organized by explicit subject.

+

Choose a Subject to view its activity, conversation, and Memory.

{#if data.subjects.data} @@ -222,7 +222,7 @@ {#if data.cursor}

Return to the first page

{/if}
{:else} -
+
{#each subjects as subject (subject.id)}
diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/+page.svelte similarity index 100% rename from web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/+page.svelte rename to web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/+page.svelte diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/+page.ts similarity index 100% rename from web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/+page.ts rename to web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/+page.ts diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte b/web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/PageContent.svelte similarity index 95% rename from web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte rename to web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/PageContent.svelte index 171cede8f..928830b34 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/subjektiv/[subjectId]/PageContent.svelte @@ -1,4 +1,5 @@ - {subject?.role ?? data.subjectId} · Memory · Yoi Workspace + {subject?.role ?? data.subjectId} · Subjects · Yoi Workspace @@ -166,7 +161,7 @@
-

Memory subject

+

Subject

{subject?.role ?? data.subjectId}

{data.subjectId}
@@ -180,7 +175,11 @@ {/if} {#if !deletionStarted} -
+ {#if subject}
@@ -402,6 +401,7 @@