feat: move Subject navigation and routes into subjektiv
This commit is contained in:
@@ -17,10 +17,10 @@ import {
|
|||||||
|
|
||||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||||
const artifacts = join(root, "target/web-ux/t-737-browser-interactions");
|
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 journalPath = `${subjectPath}/journal`;
|
||||||
const memoryPath =
|
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 claim = "Keep subject Memory provenance typed and visible";
|
||||||
const entryTitle = "Release milestone 00 · release [a+b] %_";
|
const entryTitle = "Release milestone 00 · release [a+b] %_";
|
||||||
const detailPath = `${journalPath}?journal=journal-release&entry=entry-00`;
|
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();
|
.waitFor();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
new URL(page.url()).pathname,
|
new URL(page.url()).pathname,
|
||||||
`${subjectPath}/${fixture.memoryId}`,
|
`${subjectPath}/memories/${fixture.memoryId}`,
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
new URL(page.url()).searchParams.get("change_id"),
|
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 [
|
const state of [
|
||||||
{
|
{
|
||||||
path:
|
path:
|
||||||
`/w/${fixture.workspaceId}/memory/empty-subject/journal?view=sessions`,
|
`/w/${fixture.workspaceId}/subjektiv/empty-subject/journal?view=sessions`,
|
||||||
text: "Journalはまだありません。",
|
text: "Journalはまだありません。",
|
||||||
role: "status" as const,
|
role: "status" as const,
|
||||||
},
|
},
|
||||||
@@ -570,7 +570,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
|||||||
await assertNoPageWideOverflow(page);
|
await assertNoPageWideOverflow(page);
|
||||||
}
|
}
|
||||||
await page.goto(
|
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(
|
await page.locator("#related-journal").getByText(
|
||||||
"対応するJournalは未接続",
|
"対応するJournalは未接続",
|
||||||
@@ -591,7 +591,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
|||||||
r.payload.selector.candidate_id === fixture.candidateId
|
r.payload.selector.candidate_id === fixture.candidateId
|
||||||
));
|
));
|
||||||
await page.goto(
|
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({
|
await page.getByRole("alert").filter({
|
||||||
hasText: "Journal参照を取得できません。",
|
hasText: "Journal参照を取得できません。",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ if (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
|
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
|
||||||
const permissionsMode = Deno.args[3] === "permissions";
|
const navigationMode = Deno.args[3] === "navigation";
|
||||||
|
const permissionsMode = Deno.args[3] === "permissions" || navigationMode;
|
||||||
if (permissionsMode) {
|
if (permissionsMode) {
|
||||||
// 権限の証拠は親から渡された実製品buildを使って採取する。
|
// 権限の証拠は親から渡された実製品buildを使って採取する。
|
||||||
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
|
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
|
||||||
@@ -105,6 +106,12 @@ if (permissionsMode) {
|
|||||||
...permissionFixtureSubjects.map(([id, role]) => subject(id, role, "active", "fingerprint-0")),
|
...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");
|
const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", "fingerprint-1");
|
||||||
|
|
||||||
function connectedWorker(displayName: string) {
|
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`;
|
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
|
||||||
if (url.pathname === subjectsPath) {
|
if (url.pathname === subjectsPath) {
|
||||||
if (request.method === "POST") {
|
if (request.method === "POST") {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ async function openSubjectPage(
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
mobile = false,
|
mobile = false,
|
||||||
): Promise<void> {
|
): 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" });
|
const heading = page.locator("h1", { hasText: "Release coordination" });
|
||||||
await heading.waitFor({ state: "attached" });
|
await heading.waitFor({ state: "attached" });
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
@@ -189,10 +189,10 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
|||||||
await assertNoPageWideOverflow(page);
|
await assertNoPageWideOverflow(page);
|
||||||
await assertMainOwnsVerticalScroll(page);
|
await assertMainOwnsVerticalScroll(page);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await page.locator('aside a[href$="/memory"]', { hasText: "Subjects" }).count(),
|
await page.locator('aside a[href$="/subjektiv"]', { hasText: "Subjects" }).count(),
|
||||||
1,
|
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") {
|
if (scenario.label === "wheel") {
|
||||||
assertEquals(
|
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 detailContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||||
const detailPage = await detailContext.newPage();
|
const detailPage = await detailContext.newPage();
|
||||||
await detailPage.goto(
|
await detailPage.goto(
|
||||||
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}/${
|
`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}/memories/${
|
||||||
encodeURIComponent(memoryId)
|
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 paginationContext = await browser.newContext({ viewport: { width: 768, height: 900 } });
|
||||||
const paginationPage = await paginationContext.newPage();
|
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();
|
await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await paginationPage.getByText(
|
await paginationPage.getByText(
|
||||||
@@ -354,7 +354,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
|||||||
);
|
);
|
||||||
|
|
||||||
await paginationPage.goto(
|
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("heading", { name: "Current Memories", level: 2 }).waitFor();
|
||||||
await paginationPage.getByRole("navigation", { name: "Current Memory pages" }).getByRole(
|
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 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.getByText("No Memory subjects.").waitFor();
|
||||||
await emptyPage.getByRole("button", { name: "New Subject" }).click();
|
await emptyPage.getByRole("button", { name: "New Subject" }).click();
|
||||||
const emptyRole = emptyPage.getByRole("textbox", { name: "Role" });
|
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);
|
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.getByText("Subject on the next page").waitFor();
|
||||||
await createPage.getByRole("button", { name: "New Subject" }).click();
|
await createPage.getByRole("button", { name: "New Subject" }).click();
|
||||||
const createRole = createPage.getByRole("textbox", { name: "Role" });
|
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.",
|
"Challenge unsupported assumptions before release.",
|
||||||
);
|
);
|
||||||
await createPage.getByRole("button", { name: "Create Subject" }).click();
|
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();
|
await createPage.getByRole("heading", { name: "Release steward", level: 1 }).waitFor();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
await createPage.getByRole("article", { name: "User-managed Subject behavior" }).count(),
|
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) {
|
for (const stateCase of stateCases) {
|
||||||
const page = await stateContext.newPage();
|
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 });
|
const state = page.getByRole(stateCase.role).filter({ hasText: stateCase.text });
|
||||||
await state.waitFor();
|
await state.waitFor();
|
||||||
assert((await state.textContent())?.includes(stateCase.text));
|
assert((await state.textContent())?.includes(stateCase.text));
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ await Deno.mkdir(output);
|
|||||||
const port = await freePort();
|
const port = await freePort();
|
||||||
const base = `http://127.0.0.1:${port}`;
|
const base = `http://127.0.0.1:${port}`;
|
||||||
const subject = "empty-subject";
|
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 captures: unknown[] = [];
|
||||||
const failures: string[] = [];
|
const failures: string[] = [];
|
||||||
const faults: { kind: string; message: string; expected: boolean }[] = [];
|
const faults: { kind: string; message: string; expected: boolean }[] = [];
|
||||||
@@ -233,7 +233,7 @@ try {
|
|||||||
faultMode = "normal";
|
faultMode = "normal";
|
||||||
const secondTab = await context.newPage();
|
const secondTab = await context.newPage();
|
||||||
try {
|
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("textbox").fill("Independent input from another tab");
|
||||||
await secondTab.getByRole("button", { name: "Send message", exact: true }).click();
|
await secondTab.getByRole("button", { name: "Send message", exact: true }).click();
|
||||||
await secondTab.getByText(
|
await secondTab.getByText(
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ try {
|
|||||||
};
|
};
|
||||||
manifest.scenarios.push(scenario);
|
manifest.scenarios.push(scenario);
|
||||||
try {
|
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("heading", { name: "Subjects", level: 1, exact: true }).waitFor();
|
||||||
await page.getByRole("button", { name: "New Subject", exact: true }).click();
|
await page.getByRole("button", { name: "New Subject", exact: true }).click();
|
||||||
const role = page.getByRole("textbox", { name: "Role", exact: true });
|
const role = page.getByRole("textbox", { name: "Role", exact: true });
|
||||||
@@ -354,7 +354,7 @@ try {
|
|||||||
await page.getByRole("alert").scrollIntoViewIfNeeded();
|
await page.getByRole("alert").scrollIntoViewIfNeeded();
|
||||||
await capture(page, observation, `${prefix}-later-403-reopened-pending`);
|
await capture(page, observation, `${prefix}-later-403-reopened-pending`);
|
||||||
await page.getByRole("button", { name: "Create Subject", exact: true }).click();
|
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 })
|
await page.getByRole("heading", { name: roleText.trim(), level: 1, exact: true })
|
||||||
.waitFor();
|
.waitFor();
|
||||||
assertEquals(
|
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);
|
await assertNoPageWideOverflow(page);
|
||||||
}
|
}
|
||||||
async function open(page: Page, base: string, id: string, state = "ready") {
|
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();
|
await page.locator(`[data-permission-state=${state}][aria-busy=false]`).waitFor();
|
||||||
}
|
}
|
||||||
async function add(
|
async function add(
|
||||||
@@ -268,7 +268,7 @@ try {
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
observe(page);
|
observe(page);
|
||||||
// 通常のSubject詳細から管理導線へ進む。
|
// 通常の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(".subject-overview").waitFor();
|
||||||
await page.locator('main a[href$="/permissions"]').click();
|
await page.locator('main a[href$="/permissions"]').click();
|
||||||
await page.locator("[data-permission-state=ready]").waitFor();
|
await page.locator("[data-permission-state=ready]").waitFor();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "sessions",
|
"id": "sessions",
|
||||||
"label": "Session journals",
|
"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.",
|
"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.",
|
"dataState": "Two Sessions, one with unreflected sources; deleted versus unavailable originals; bounded next page.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
{
|
{
|
||||||
"id": "list",
|
"id": "list",
|
||||||
"label": "Literal search results",
|
"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.",
|
"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.",
|
"dataState": "Sixteen matching events across two Sessions with fixed/open states and a continuation.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail-missing",
|
"id": "detail-missing",
|
||||||
"label": "Saved Journal with deleted original",
|
"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.",
|
"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.",
|
"dataState": "Open event, three of eight related Memories, correcting entry; original conversation is deleted.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail-unavailable",
|
"id": "detail-unavailable",
|
||||||
"label": "Journal with unavailable original",
|
"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.",
|
"goal": "Distinguish inability to check the original from confirmed deletion.",
|
||||||
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
|
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
{
|
{
|
||||||
"id": "memory",
|
"id": "memory",
|
||||||
"label": "Memory links back to exact Journal events",
|
"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.",
|
"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.",
|
"dataState": "Three of eight persisted candidate links, including missing/unavailable originals and reconsideration.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
{
|
{
|
||||||
"id": "memory-unconnected",
|
"id": "memory-unconnected",
|
||||||
"label": "Unconnected Memory",
|
"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.",
|
"goal": "Recognize a missing persisted connection without fabricating a target or pending reason.",
|
||||||
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
|
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
{
|
{
|
||||||
"id": "no-journal",
|
"id": "no-journal",
|
||||||
"label": "No saved 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.",
|
"goal": "Distinguish no saved Journal from pending, no search results and read failure.",
|
||||||
"dataState": "Successful empty Session-journal page.",
|
"dataState": "Successful empty Session-journal page.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
{
|
{
|
||||||
"id": "no-search-result",
|
"id": "no-search-result",
|
||||||
"label": "No literal search matches",
|
"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.",
|
"goal": "Keep the query visible when search succeeds with zero results.",
|
||||||
"dataState": "Successful empty search, not an unavailable Journal.",
|
"dataState": "Successful empty search, not an unavailable Journal.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
"id": "denied",
|
"id": "denied",
|
||||||
"label": "Journal read 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.",
|
"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.",
|
"dataState": "Intentional fixture HTTP 403; verifies error presentation only, not backend authorization.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
{
|
{
|
||||||
"id": "request-failure",
|
"id": "request-failure",
|
||||||
"label": "Journal list unavailable",
|
"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.",
|
"goal": "Keep request failure distinct from zero matching entries.",
|
||||||
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
|
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "sessions",
|
"id": "sessions",
|
||||||
"label": "Session journals",
|
"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.",
|
"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.",
|
"dataState": "Two Sessions, one with unreflected sources; deleted versus unavailable originals; bounded next page.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
{
|
{
|
||||||
"id": "list",
|
"id": "list",
|
||||||
"label": "Literal search results",
|
"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.",
|
"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.",
|
"dataState": "Sixteen matching events across two Sessions with fixed/open states and a continuation.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail-missing",
|
"id": "detail-missing",
|
||||||
"label": "Saved Journal with deleted original",
|
"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.",
|
"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.",
|
"dataState": "Open event, three of eight related Memories, correcting entry; original conversation is deleted.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail-unavailable",
|
"id": "detail-unavailable",
|
||||||
"label": "Journal with unavailable original",
|
"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.",
|
"goal": "Distinguish inability to check the original from confirmed deletion.",
|
||||||
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
|
"dataState": "Fixed event remains readable, but original presence/permission is unavailable.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
{
|
{
|
||||||
"id": "memory",
|
"id": "memory",
|
||||||
"label": "Memory links back to exact Journal events",
|
"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.",
|
"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.",
|
"dataState": "Three of eight persisted candidate links, including missing/unavailable originals and reconsideration.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
{
|
{
|
||||||
"id": "memory-unconnected",
|
"id": "memory-unconnected",
|
||||||
"label": "Unconnected Memory",
|
"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.",
|
"goal": "Recognize a missing persisted connection without fabricating a target or pending reason.",
|
||||||
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
|
"dataState": "Immutable historical change with insufficient_source; no Journal anchors.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
{
|
{
|
||||||
"id": "no-journal",
|
"id": "no-journal",
|
||||||
"label": "No saved 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.",
|
"goal": "Distinguish no saved Journal from pending, no search results and read failure.",
|
||||||
"dataState": "Successful empty Session-journal page.",
|
"dataState": "Successful empty Session-journal page.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
{
|
{
|
||||||
"id": "no-search-result",
|
"id": "no-search-result",
|
||||||
"label": "No literal search matches",
|
"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.",
|
"goal": "Keep the query visible when search succeeds with zero results.",
|
||||||
"dataState": "Successful empty search, not an unavailable Journal.",
|
"dataState": "Successful empty search, not an unavailable Journal.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
"id": "denied",
|
"id": "denied",
|
||||||
"label": "Journal read 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.",
|
"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.",
|
"dataState": "Intentional fixture HTTP 403; verifies error presentation only, not backend authorization.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
{
|
{
|
||||||
"id": "request-failure",
|
"id": "request-failure",
|
||||||
"label": "Journal list unavailable",
|
"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.",
|
"goal": "Keep request failure distinct from zero matching entries.",
|
||||||
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
|
"dataState": "Intentional fixture HTTP 503 with the search query retained.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects",
|
"id": "subjects",
|
||||||
"label": "Subject index",
|
"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.",
|
"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.",
|
"dataState": "Active and retired subjects across a paginated list, plus create-form and permission-rejection states.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects-empty",
|
"id": "subjects-empty",
|
||||||
"label": "Empty Subject index",
|
"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.",
|
"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.",
|
"dataState": "Successful empty Subject page with a return-to-first-page recovery link.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects-error",
|
"id": "subjects-error",
|
||||||
"label": "Subject list 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.",
|
"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.",
|
"dataState": "Intentional Subject-list HTTP 503 with the independent create disclosure still available.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
{
|
{
|
||||||
"id": "representative",
|
"id": "representative",
|
||||||
"label": "Ready subject surface",
|
"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.",
|
"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.",
|
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail",
|
"id": "detail",
|
||||||
"label": "Committed Memory 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.",
|
"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.",
|
"dataState": "Current active change with bounded typed provenance and active/resolved/retracted immutable history.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
{
|
{
|
||||||
"id": "empty",
|
"id": "empty",
|
||||||
"label": "Ready-empty subject",
|
"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.",
|
"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.",
|
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
{
|
{
|
||||||
"id": "ungenerated",
|
"id": "ungenerated",
|
||||||
"label": "Ungenerated subject context",
|
"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.",
|
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
|
||||||
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
|
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
{
|
{
|
||||||
"id": "stale",
|
"id": "stale",
|
||||||
"label": "Stale subject surface",
|
"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.",
|
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
|
||||||
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
{
|
{
|
||||||
"id": "failed",
|
"id": "failed",
|
||||||
"label": "Failed subject surface",
|
"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.",
|
"goal": "See generation failure without displaying a failed or partial snapshot.",
|
||||||
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
{
|
{
|
||||||
"id": "error",
|
"id": "error",
|
||||||
"label": "Subject endpoint 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.",
|
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
|
||||||
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects",
|
"id": "subjects",
|
||||||
"label": "Subject index",
|
"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.",
|
"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.",
|
"dataState": "Active and retired subjects across a paginated list, plus create-form and permission-rejection states.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects-empty",
|
"id": "subjects-empty",
|
||||||
"label": "Empty Subject index",
|
"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.",
|
"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.",
|
"dataState": "Successful empty Subject page with a return-to-first-page recovery link.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subjects-error",
|
"id": "subjects-error",
|
||||||
"label": "Subject list 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.",
|
"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.",
|
"dataState": "Intentional Subject-list HTTP 503 with the independent create disclosure still available.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
{
|
{
|
||||||
"id": "representative",
|
"id": "representative",
|
||||||
"label": "Ready subject surface",
|
"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.",
|
"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.",
|
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
{
|
{
|
||||||
"id": "detail",
|
"id": "detail",
|
||||||
"label": "Committed Memory 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.",
|
"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.",
|
"dataState": "Current active change with bounded typed provenance and active/resolved/retracted immutable history.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
{
|
{
|
||||||
"id": "empty",
|
"id": "empty",
|
||||||
"label": "Ready-empty subject",
|
"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.",
|
"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.",
|
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
{
|
{
|
||||||
"id": "ungenerated",
|
"id": "ungenerated",
|
||||||
"label": "Ungenerated subject context",
|
"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.",
|
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
|
||||||
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
|
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
{
|
{
|
||||||
"id": "stale",
|
"id": "stale",
|
||||||
"label": "Stale subject surface",
|
"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.",
|
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
|
||||||
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
{
|
{
|
||||||
"id": "failed",
|
"id": "failed",
|
||||||
"label": "Failed subject surface",
|
"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.",
|
"goal": "See generation failure without displaying a failed or partial snapshot.",
|
||||||
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
{
|
{
|
||||||
"id": "error",
|
"id": "error",
|
||||||
"label": "Subject endpoint 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.",
|
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
|
||||||
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subject",
|
"id": "subject",
|
||||||
"label": "Subject detail",
|
"label": "Subject detail",
|
||||||
"path": "/w/memory-review/memory/pending-subject",
|
"path": "/w/memory-review/subjektiv/pending-subject",
|
||||||
"goal": "Review the Subject before irreversible deletion.",
|
"goal": "Review the Subject before irreversible deletion.",
|
||||||
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
|
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{
|
{
|
||||||
"id": "legacy",
|
"id": "legacy",
|
||||||
"label": "Legacy ended Subject",
|
"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.",
|
"goal": "Delete a legacy ended Subject without reactivating it.",
|
||||||
"dataState": "Saved retired wire state, owner.",
|
"dataState": "Saved retired wire state, owner.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
{
|
{
|
||||||
"id": "non-owner",
|
"id": "non-owner",
|
||||||
"label": "Nonowner Subject view",
|
"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.",
|
"goal": "Read Subject without presenting unauthorized deletion.",
|
||||||
"dataState": "Backend delete_subjects=false; no deletion controls.",
|
"dataState": "Backend delete_subjects=false; no deletion controls.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "subject",
|
"id": "subject",
|
||||||
"label": "Subject detail",
|
"label": "Subject detail",
|
||||||
"path": "/w/memory-review/memory/pending-subject",
|
"path": "/w/memory-review/subjektiv/pending-subject",
|
||||||
"goal": "Review the Subject before irreversible deletion.",
|
"goal": "Review the Subject before irreversible deletion.",
|
||||||
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
|
"dataState": "Active Subject, no current Worker; owner permissions from Backend projection.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{
|
{
|
||||||
"id": "legacy",
|
"id": "legacy",
|
||||||
"label": "Legacy ended Subject",
|
"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.",
|
"goal": "Delete a legacy ended Subject without reactivating it.",
|
||||||
"dataState": "Saved retired wire state, owner.",
|
"dataState": "Saved retired wire state, owner.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
{
|
{
|
||||||
"id": "non-owner",
|
"id": "non-owner",
|
||||||
"label": "Nonowner Subject view",
|
"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.",
|
"goal": "Read Subject without presenting unauthorized deletion.",
|
||||||
"dataState": "Backend delete_subjects=false; no deletion controls.",
|
"dataState": "Backend delete_subjects=false; no deletion controls.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-empty",
|
"id": "permissions-empty",
|
||||||
"label": "Unset permissions",
|
"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.",
|
"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.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unset permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-edit",
|
"id": "permissions-edit",
|
||||||
"label": "Configured permissions",
|
"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.",
|
"goal": "Read exact operation and target before editing.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-legacy",
|
"id": "permissions-legacy",
|
||||||
"label": "Preserved unsupported permission",
|
"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.",
|
"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.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Preserved unsupported permission.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-reader",
|
"id": "permissions-reader",
|
||||||
"label": "Read-only permissions",
|
"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.",
|
"goal": "Read policy without unauthorized editing controls.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-unavailable",
|
"id": "permissions-unavailable",
|
||||||
"label": "Unavailable permission service",
|
"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.",
|
"goal": "Recover without success claims.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-empty",
|
"id": "permissions-empty",
|
||||||
"label": "Unset permissions",
|
"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.",
|
"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.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unset permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-edit",
|
"id": "permissions-edit",
|
||||||
"label": "Configured permissions",
|
"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.",
|
"goal": "Read exact operation and target before editing.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Configured permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-legacy",
|
"id": "permissions-legacy",
|
||||||
"label": "Preserved unsupported permission",
|
"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.",
|
"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.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Preserved unsupported permission.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-reader",
|
"id": "permissions-reader",
|
||||||
"label": "Read-only permissions",
|
"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.",
|
"goal": "Read policy without unauthorized editing controls.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Read-only permissions.",
|
||||||
"ready": {
|
"ready": {
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
"id": "permissions-unavailable",
|
"id": "permissions-unavailable",
|
||||||
"label": "Unavailable permission service",
|
"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.",
|
"goal": "Recover without success claims.",
|
||||||
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
|
"dataState": "Synthetic HTTP only; no real Backend persistence, authorization, lifecycle or Worker evidence. Unavailable permission service.",
|
||||||
"ready": {
|
"ready": {
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
"dev": "deno run -A npm:vite@8.3.2 dev",
|
"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",
|
"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",
|
"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: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: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: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: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: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: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: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: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",
|
"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(
|
const subjectIndexLoad = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/+page.ts",
|
"./../../../routes/w/[workspaceId]/subjektiv/+page.ts",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const subjectIndexPage = await Deno.readTextFile(
|
const subjectIndexPage = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/PageContent.svelte",
|
"./../../../routes/w/[workspaceId]/subjektiv/PageContent.svelte",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const subjectPageLoad = await Deno.readTextFile(
|
const subjectPageLoad = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/[subjectId]/+page.ts",
|
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/+page.ts",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const subjectPage = await Deno.readTextFile(
|
const subjectPage = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte",
|
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/PageContent.svelte",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const memoryDetailLoad = await Deno.readTextFile(
|
const memoryDetailLoad = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/+page.ts",
|
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/memories/[memoryId]/+page.ts",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const memoryDetailPage = await Deno.readTextFile(
|
const memoryDetailPage = await Deno.readTextFile(
|
||||||
new URL(
|
new URL(
|
||||||
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/PageContent.svelte",
|
"./../../../routes/w/[workspaceId]/subjektiv/[subjectId]/memories/[memoryId]/PageContent.svelte",
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
memoryNav.includes('workspaceRoute(workspaceId, "/memory")') &&
|
memoryNav.includes('"/memory/staging"') && !memoryNav.includes('>Subjects</a>'),
|
||||||
memoryNav.includes(
|
"Memory navigation keeps staging separate from Subjects",
|
||||||
'<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",
|
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
|
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { subjectHref, subjectsHref } from "#lib/workspace/subjektiv/routes.ts";
|
||||||
|
|
||||||
export type WorkspaceBreadcrumb = {
|
export type WorkspaceBreadcrumb = {
|
||||||
label: string;
|
label: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
@@ -23,6 +25,31 @@ export function buildWorkspaceBreadcrumbs(
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(decodeURIComponent);
|
.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 (
|
if (
|
||||||
segments[0] === "workers" &&
|
segments[0] === "workers" &&
|
||||||
segments[1] &&
|
segments[1] &&
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectsHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { deleteSubjektivSubject, SubjektivSubjectDeleteError } from './api.ts';
|
import { deleteSubjektivSubject, SubjektivSubjectDeleteError } from './api.ts';
|
||||||
import { clearSubjectDeleteAttempt, readSubjectDeleteAttempt, saveSubjectDeleteAttempt, type SubjectDeleteAttempt } from './subject-delete.ts';
|
import { clearSubjectDeleteAttempt, readSubjectDeleteAttempt, saveSubjectDeleteAttempt, type SubjectDeleteAttempt } from './subject-delete.ts';
|
||||||
import type { SubjektivSubjectResponse } from '#lib/generated/memory-api.ts';
|
import type { SubjektivSubjectResponse } from '#lib/generated/memory-api.ts';
|
||||||
import { workspaceRoute } from '#lib/workspace/api/http.ts';
|
|
||||||
|
|
||||||
let { workspaceId, subjectId, subject, onstarted }: {
|
let { workspaceId, subjectId, subject, onstarted }: {
|
||||||
workspaceId: string;
|
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>
|
<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'}
|
{: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>
|
<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}
|
||||||
{#if error}<p class="workspace-callout is-error" role="alert">{error}</p>{/if}
|
{#if error}<p class="workspace-callout is-error" role="alert">{error}</p>{/if}
|
||||||
{#if attempt && phase !== 'completed'}
|
{#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";
|
import { loadJson, workspaceApiPath } from "#lib/workspace/api/http.ts";
|
||||||
|
|
||||||
export type JournalMetadata = {
|
export type JournalMetadata = {
|
||||||
@@ -411,9 +412,7 @@ export function journalHref(
|
|||||||
if (value != null && value !== "") params.set(key, value);
|
if (value != null && value !== "") params.set(key, value);
|
||||||
}
|
}
|
||||||
const suffix = params.toString();
|
const suffix = params.toString();
|
||||||
return `/w/${encodeURIComponent(workspaceId)}/memory/${
|
return `${subjectJournalHref(workspaceId, subjectId)}${suffix ? `?${suffix}` : ""}`;
|
||||||
encodeURIComponent(subjectId)
|
|
||||||
}/journal${suffix ? `?${suffix}` : ""}`;
|
|
||||||
}
|
}
|
||||||
export function boundedParam(
|
export function boundedParam(
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { workspaceRoute } from "#lib/workspace/api/http.ts";
|
import { workspaceRoute } from "#lib/workspace/api/http.ts";
|
||||||
|
import { ownsRoutePath } from './route-ownership.ts';
|
||||||
|
|
||||||
type Props = {
|
let { workspaceId, currentPath = '/' }: { workspaceId: string; currentPath?: string } = $props();
|
||||||
currentPath?: string;
|
const href = $derived(workspaceRoute(workspaceId, "/memory/staging"));
|
||||||
workspaceId: string;
|
const active = $derived(ownsRoutePath(href, currentPath));
|
||||||
};
|
|
||||||
|
|
||||||
let { currentPath = "/", workspaceId }: Props = $props();
|
|
||||||
let subjectsHref = $derived(
|
|
||||||
workspaceId ? workspaceRoute(workspaceId, "/memory") : "/",
|
|
||||||
);
|
|
||||||
let subjectsActive = $derived(
|
|
||||||
currentPath === subjectsHref || currentPath.startsWith(`${subjectsHref}/`),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="sidebar-nav-section sidebar-nav-section--category">
|
<section class="sidebar-nav-section sidebar-nav-section--category">
|
||||||
<h2 class="sidebar-nav-section__header">Memory</h2>
|
<h2 class="sidebar-nav-section__header">Memory</h2>
|
||||||
|
<a class="sidebar-link" class:active {href} aria-current={active ? 'page' : undefined}>Staging</a>
|
||||||
<a
|
|
||||||
class="sidebar-link"
|
|
||||||
class:active={subjectsActive}
|
|
||||||
href={subjectsHref}
|
|
||||||
aria-current={subjectsActive ? "page" : undefined}
|
|
||||||
>Subjects</a>
|
|
||||||
</section>
|
</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 { workspaceRoute } from '#lib/workspace/api/http.ts';
|
||||||
import { ownsRoutePath } from './route-ownership';
|
import { ownsRoutePath } from './route-ownership';
|
||||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
|
import SubjectsNavSection from './SubjectsNavSection.svelte';
|
||||||
import MemoryNavSection from './MemoryNavSection.svelte';
|
import MemoryNavSection from './MemoryNavSection.svelte';
|
||||||
import MergeRequestsNavSection from './MergeRequestsNavSection.svelte';
|
import MergeRequestsNavSection from './MergeRequestsNavSection.svelte';
|
||||||
import TicketsNavSection from './TicketsNavSection.svelte';
|
import TicketsNavSection from './TicketsNavSection.svelte';
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
<ObjectivesNavSection {currentPath} {workspaceId} />
|
<ObjectivesNavSection {currentPath} {workspaceId} />
|
||||||
<MergeRequestsNavSection {currentPath} {workspaceId} />
|
<MergeRequestsNavSection {currentPath} {workspaceId} />
|
||||||
<DriveNavSection {currentPath} {workspaceId} />
|
<DriveNavSection {currentPath} {workspaceId} />
|
||||||
|
<SubjectsNavSection {currentPath} {workspaceId} />
|
||||||
<MemoryNavSection {currentPath} {workspaceId} />
|
<MemoryNavSection {currentPath} {workspaceId} />
|
||||||
<WorkersNavSection {currentPath} {workspaceId} />
|
<WorkersNavSection {currentPath} {workspaceId} />
|
||||||
</nav>
|
</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">
|
<script lang="ts">
|
||||||
|
import { subjectsHref, subjectHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import BevelLine from "#lib/workspace/ui/BevelLine.svelte";
|
import BevelLine from "#lib/workspace/ui/BevelLine.svelte";
|
||||||
import type { TicketDetail, TicketAssignmentTrackingProjection, TicketAssignmentTrackingHistoryPage, TicketAssignmentsPage, TicketAssignmentPrincipalSnapshot } from "#lib/generated/ticket-api.ts";
|
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 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 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 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>
|
<p>Backend validates the Subject and current permissions. Assignment does not grant permissions or select or start a concrete Worker.</p>
|
||||||
</details>
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -196,7 +197,7 @@
|
|||||||
<details class="ticket-detail-section ticket-subject-worker">
|
<details class="ticket-detail-section ticket-subject-worker">
|
||||||
<summary>Subject and current Worker</summary>
|
<summary>Subject and current Worker</summary>
|
||||||
<p>Assignment follows the Subject across Worker replacement. Worker availability does not change handling status.</p>
|
<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>
|
</details>
|
||||||
{:else if current}
|
{: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>
|
<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">
|
<script lang="ts">
|
||||||
import DeferredPage from '#lib/workspace/navigation/DeferredPage.svelte';
|
import LegacySubjectRoute from '#lib/workspace/subjektiv/LegacySubjectRoute.svelte';
|
||||||
import PageContent from './PageContent.svelte';
|
|
||||||
import type { PageProps } from './$types';
|
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DeferredPage {data} title="Subjects">
|
<LegacySubjectRoute />
|
||||||
{#snippet children(resolved)}
|
|
||||||
<PageContent data={resolved} />
|
|
||||||
{/snippet}
|
|
||||||
</DeferredPage>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
+8
-8
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectsHref, subjectHref as subjectDetailHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
|
import { formatDate } from '#lib/workspace/api/http.ts';
|
||||||
import {
|
import {
|
||||||
createSubjektivSubject,
|
createSubjektivSubject,
|
||||||
SubjektivSubjectCreateError,
|
SubjektivSubjectCreateError,
|
||||||
@@ -30,11 +31,11 @@
|
|||||||
let behaviorInput = $state<HTMLTextAreaElement>();
|
let behaviorInput = $state<HTMLTextAreaElement>();
|
||||||
|
|
||||||
function subjectHref(subjectId: string): string {
|
function subjectHref(subjectId: string): string {
|
||||||
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(subjectId)}`);
|
return subjectDetailHref(data.workspaceId, subjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageHref(cursor?: string | null): string {
|
function pageHref(cursor?: string | null): string {
|
||||||
const path = workspaceRoute(data.workspaceId, '/memory');
|
const path = subjectsHref(data.workspaceId);
|
||||||
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,16 +126,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Subjects · Memory · Yoi Workspace</title>
|
<title>Subjects · Yoi Workspace</title>
|
||||||
<meta name="description" content="Workspace Memory subjects" />
|
<meta name="description" content="Workspace Subjects" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section class="memory-page memory-subject-index" aria-labelledby="memory-subjects-heading" data-memory-view="subjects">
|
<section class="memory-page memory-subject-index" aria-labelledby="memory-subjects-heading" data-memory-view="subjects">
|
||||||
<header class="memory-page-header">
|
<header class="memory-page-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="memory-eyebrow">Memory</p>
|
|
||||||
<h1 id="memory-subjects-heading">Subjects</h1>
|
<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>
|
||||||
<div class="memory-header-actions">
|
<div class="memory-header-actions">
|
||||||
{#if data.subjects.data}
|
{#if data.subjects.data}
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
{#if data.cursor}<p><a href={pageHref()}>Return to the first page</a></p>{/if}
|
{#if data.cursor}<p><a href={pageHref()}>Return to the first page</a></p>{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="subject-list" aria-label="Memory subjects">
|
<div class="subject-list" aria-label="Subjects">
|
||||||
{#each subjects as subject (subject.id)}
|
{#each subjects as subject (subject.id)}
|
||||||
<a class="subject-row" href={subjectHref(subject.id)}>
|
<a class="subject-row" href={subjectHref(subject.id)}>
|
||||||
<div class="subject-copy">
|
<div class="subject-copy">
|
||||||
+13
-13
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectsHref as subjectListHref, subjectHref, subjectMemoryHref, subjectJournalHref, subjectPermissionsHref, subjectChatHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
// identity、current Worker、Committed Memory、生成contextの利用可否を先に示す。
|
// identity、current Worker、Committed Memory、生成contextの利用可否を先に示す。
|
||||||
// current WorkerはBackend singleton projectionであり、過去のSession帰属から推測しない。
|
// current WorkerはBackend singleton projectionであり、過去のSession帰属から推測しない。
|
||||||
// behaviorは利用者が管理する本文、surfaceはMemoryからの生成物として分ける。
|
// behaviorは利用者が管理する本文、surfaceはMemoryからの生成物として分ける。
|
||||||
@@ -7,7 +8,7 @@
|
|||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
||||||
import SubjectDelete from '#lib/workspace/memory/SubjectDelete.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 {
|
import {
|
||||||
SubjektivSubjectCreateError,
|
SubjektivSubjectCreateError,
|
||||||
updateSubjektivSubjectBehavior,
|
updateSubjektivSubjectBehavior,
|
||||||
@@ -110,22 +111,16 @@
|
|||||||
const memories = $derived(data.memories.data?.items ?? []);
|
const memories = $derived(data.memories.data?.items ?? []);
|
||||||
|
|
||||||
function subjectsHref(): string {
|
function subjectsHref(): string {
|
||||||
return workspaceRoute(data.workspaceId, '/memory');
|
return subjectListHref(data.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function memoryHref(memoryId: string, changeId?: string): string {
|
function memoryHref(memoryId: string, changeId?: string): string {
|
||||||
const path = workspaceRoute(
|
const path = subjectMemoryHref(data.workspaceId, data.subjectId, memoryId);
|
||||||
data.workspaceId,
|
|
||||||
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
|
|
||||||
);
|
|
||||||
return changeId ? `${path}?change_id=${encodeURIComponent(changeId)}` : path;
|
return changeId ? `${path}?change_id=${encodeURIComponent(changeId)}` : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function memoryPageHref(cursor?: string | null): string {
|
function memoryPageHref(cursor?: string | null): string {
|
||||||
const path = workspaceRoute(
|
const path = subjectHref(data.workspaceId, data.subjectId);
|
||||||
data.workspaceId,
|
|
||||||
`/memory/${encodeURIComponent(data.subjectId)}`,
|
|
||||||
);
|
|
||||||
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +152,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<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" />
|
<meta name="description" content="Subject resident surface and current committed Memories" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
@@ -166,7 +161,7 @@
|
|||||||
|
|
||||||
<header class="memory-page-header">
|
<header class="memory-page-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="memory-eyebrow">Memory subject</p>
|
<p class="memory-eyebrow">Subject</p>
|
||||||
<h1 id="subject-heading">{subject?.role ?? data.subjectId}</h1>
|
<h1 id="subject-heading">{subject?.role ?? data.subjectId}</h1>
|
||||||
<code class="subject-id" title={data.subjectId}>{data.subjectId}</code>
|
<code class="subject-id" title={data.subjectId}>{data.subjectId}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +175,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !deletionStarted}
|
{#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}
|
{#if subject}
|
||||||
<section class="subject-overview" aria-label="Subject status">
|
<section class="subject-overview" aria-label="Subject status">
|
||||||
@@ -402,6 +401,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.subject-navigation { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
|
||||||
.memory-page {
|
.memory-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import SubjectConversation from '#lib/workspace/memory/SubjectConversation.svelte';
|
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 { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
|
||||||
import type { PageData as RouteData } from './$types';
|
import type { PageData as RouteData } from './$types';
|
||||||
let { data }: { data: ResolvedPageData<RouteData> } = $props();
|
let { data }: { data: ResolvedPageData<RouteData> } = $props();
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<svelte:head><title>{subject?.role ?? 'Subject'} · Conversation · Yoi Workspace</title></svelte:head>
|
<svelte:head><title>{subject?.role ?? 'Subject'} · Conversation · Yoi Workspace</title></svelte:head>
|
||||||
|
|
||||||
<section class="conversation-page" aria-labelledby="conversation-subject-heading">
|
<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}
|
{#if subject}
|
||||||
<h1 id="conversation-subject-heading">{subject.role}</h1>
|
<h1 id="conversation-subject-heading">{subject.role}</h1>
|
||||||
{#key JSON.stringify([data.accountId, data.workspaceId, subject.id])}
|
{#key JSON.stringify([data.accountId, data.workspaceId, subject.id])}
|
||||||
+4
-3
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectHref, subjectMemoryHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
||||||
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
|
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
|
||||||
import { journalHref, type JournalEntry, type TextPage } from '#lib/workspace/memory/journal-api.ts';
|
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 { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
|
||||||
import type { PageData as RouteData } from './$types';
|
import type { PageData as RouteData } from './$types';
|
||||||
let { data }: { data: ResolvedPageData<RouteData> } = $props();
|
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 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 nextText(key: string, page: TextPage) { return href({ [`${key}_byte`]: String(page.next_byte), [`${key}_token`]: page.content_token }); }
|
||||||
function memoryHref(memoryId: string, changeId: string) {
|
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>
|
</script>
|
||||||
<svelte:head><title>{entry?.title ?? 'Journal'} · Yoi Workspace</title></svelte:head>
|
<svelte:head><title>{entry?.title ?? 'Journal'} · Yoi Workspace</title></svelte:head>
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<section class="journal-page" aria-labelledby="journal-title" data-journal-view={data.mode}>
|
<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>
|
<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>
|
<p class="muted">Journalは活動の経緯を記録した文章です。原発言・tool結果そのものではなく、Memoryの直接の抽出元や主張の証明ではありません。</p>
|
||||||
{#if data.mode === 'detail'}
|
{#if data.mode === 'detail'}
|
||||||
+4
-6
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectHref as subjectDetailHref, subjectMemoryHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
// current/historicalはchange_idとcurrent_change_idの一致で区別する。
|
// current/historicalはchange_idとcurrent_change_idの一致で区別する。
|
||||||
// 本文・evidenceの続きも同じimmutable changeへ固定し、現在版の内容を混ぜない。
|
// 本文・evidenceの続きも同じimmutable changeへ固定し、現在版の内容を混ぜない。
|
||||||
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
|
import OriginalSource from '#lib/workspace/memory/OriginalSource.svelte';
|
||||||
import { journalHref } from '#lib/workspace/memory/journal-api.ts';
|
import { journalHref } from '#lib/workspace/memory/journal-api.ts';
|
||||||
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
|
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 {
|
import type {
|
||||||
MemoryEvidenceOrigin,
|
MemoryEvidenceOrigin,
|
||||||
SubjektivMemoryChangeRef,
|
SubjektivMemoryChangeRef,
|
||||||
@@ -21,14 +22,11 @@
|
|||||||
const changes = $derived(data.changes.data?.items ?? []);
|
const changes = $derived(data.changes.data?.items ?? []);
|
||||||
|
|
||||||
function subjectHref(): string {
|
function subjectHref(): string {
|
||||||
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`);
|
return subjectDetailHref(data.workspaceId, data.subjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function memoryPath(memoryId = data.memoryId): string {
|
function memoryPath(memoryId = data.memoryId): string {
|
||||||
return workspaceRoute(
|
return subjectMemoryHref(data.workspaceId, data.subjectId, memoryId);
|
||||||
data.workspaceId,
|
|
||||||
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeHref(changeId: string): string {
|
function changeHref(changeId: string): string {
|
||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { subjectHref } from '#lib/workspace/subjektiv/routes.ts';
|
||||||
import SubjectPermissions from '#lib/workspace/memory/SubjectPermissions.svelte';
|
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 { ResolvedPageData } from '#lib/workspace/navigation/deferred.ts';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
let { data }: { data: ResolvedPageData<PageData> } = $props();
|
let { data }: { data: ResolvedPageData<PageData> } = $props();
|
||||||
</script>
|
</script>
|
||||||
<svelte:head><title>Permissions · {data.subject.data?.role ?? 'Subject'} · Yoi Workspace</title></svelte:head>
|
<svelte:head><title>Permissions · {data.subject.data?.role ?? 'Subject'} · Yoi Workspace</title></svelte:head>
|
||||||
<section class="permission-page">
|
<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>
|
<h1>{data.subject.data?.role ?? 'Subject'} permissions</h1>
|
||||||
{#if data.subject.data}
|
{#if data.subject.data}
|
||||||
{#key JSON.stringify([data.workspaceId, data.subjectId])}<SubjectPermissions workspaceId={data.workspaceId} subjectId={data.subjectId} />{/key}
|
{#key JSON.stringify([data.workspaceId, data.subjectId])}<SubjectPermissions workspaceId={data.workspaceId} subjectId={data.subjectId} />{/key}
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
import { cleanup, render, screen, within } from "@testing-library/svelte";
|
import { cleanup, render, screen, within } from "@testing-library/svelte";
|
||||||
import { afterEach, expect, test } from "vitest";
|
import { afterEach, expect, test } from "vitest";
|
||||||
import JournalPage from "./[subjectId]/journal/PageContent.svelte";
|
import JournalPage from "./[subjectId]/journal/PageContent.svelte";
|
||||||
import MemoryPage from "./[subjectId]/[memoryId]/PageContent.svelte";
|
import MemoryPage from "./[subjectId]/memories/[memoryId]/PageContent.svelte";
|
||||||
|
|
||||||
afterEach(cleanup);
|
afterEach(cleanup);
|
||||||
const result = <T>(data: T | null, error: string | null = null) => ({
|
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(
|
screen.getByRole("link", { name: "復旧経路を先に確認する →" }).getAttribute(
|
||||||
"href",
|
"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(
|
expect(
|
||||||
screen.getByRole("link", { name: "復旧経路を先に確認する →" }).getAttribute(
|
screen.getByRole("link", { name: "復旧経路を先に確認する →" }).getAttribute(
|
||||||
"target",
|
"target",
|
||||||
+5
-5
@@ -51,7 +51,7 @@ vi.mock("$app/navigation", () => ({ goto: gotoMock }));
|
|||||||
|
|
||||||
import SubjectIndexPage from "./PageContent.svelte";
|
import SubjectIndexPage from "./PageContent.svelte";
|
||||||
import SubjectPage from "./[subjectId]/PageContent.svelte";
|
import SubjectPage from "./[subjectId]/PageContent.svelte";
|
||||||
import MemoryDetailPage from "./[subjectId]/[memoryId]/PageContent.svelte";
|
import MemoryDetailPage from "./[subjectId]/memories/[memoryId]/PageContent.svelte";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
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.queryByText("Memory fingerprint", { exact: true })).toBeNull();
|
||||||
expect(screen.getByText("1 shown · more available")).not.toBeNull();
|
expect(screen.getByText("1 shown · more available")).not.toBeNull();
|
||||||
expect(screen.getByRole("link", { name: "First page" }).getAttribute("href"))
|
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"))
|
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
|
||||||
.toContain("cursor=subject-next");
|
.toContain("cursor=subject-next");
|
||||||
expect(screen.queryByText("Staging")).toBeNull();
|
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 }));
|
}, { status: 201 }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(gotoMock).toHaveBeenCalledWith(
|
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("aria-current")).toBe("page");
|
||||||
expect(historicalLink.getAttribute("href")).toBe(
|
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", {
|
const currentLink = screen.getByRole("link", {
|
||||||
name: /Memory change a\/current/,
|
name: /Memory change a\/current/,
|
||||||
@@ -1049,7 +1049,7 @@ test("renders committed Memory detail, provenance, derivation, and immutable cha
|
|||||||
expect(
|
expect(
|
||||||
screen.getByRole("link", { name: /memory-parent/ }).getAttribute("href"),
|
screen.getByRole("link", { name: /memory-parent/ }).getAttribute("href"),
|
||||||
).toBe(
|
).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" })
|
const bodyHref = screen.getByRole("link", { name: "View next body segment" })
|
||||||
.getAttribute("href");
|
.getAttribute("href");
|
||||||
@@ -74,7 +74,7 @@ Deno.test({
|
|||||||
await route.continue();
|
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.locator(".subject-delete-open").waitFor();
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: join(artifact, `${theme}-${width}-initial.png`),
|
path: join(artifact, `${theme}-${width}-initial.png`),
|
||||||
@@ -143,7 +143,7 @@ Deno.test({
|
|||||||
requests.push(request.postDataJSON());
|
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();
|
await page.locator(".subject-delete-open").waitFor();
|
||||||
if (subject === "legacy-subject") {
|
if (subject === "legacy-subject") {
|
||||||
assert(
|
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 confirm(page);
|
||||||
await page.locator('[data-delete-state="failed"]').waitFor();
|
await page.locator('[data-delete-state="failed"]').waitFor();
|
||||||
await page.screenshot({ path: join(artifact, "rejected.png") });
|
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 confirm(page);
|
||||||
await page.locator('[data-delete-state="running"]').waitFor();
|
await page.locator('[data-delete-state="running"]').waitFor();
|
||||||
await page.getByRole("link", { name: "← All subjects", exact: true })
|
await page.getByRole("link", { name: "← All subjects", exact: true })
|
||||||
@@ -241,7 +241,7 @@ Deno.test({
|
|||||||
{
|
{
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.goto(
|
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();
|
await page.locator(".subject-overview").waitFor();
|
||||||
assertEquals(await page.locator(".subject-deletion").count(), 0);
|
assertEquals(await page.locator(".subject-deletion").count(), 0);
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ Deno.test("Journal links retain exact selected entry and bounded URL offsets", (
|
|||||||
}),
|
}),
|
||||||
"https://example.com",
|
"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(url.searchParams.get("entry"), "entry&selected");
|
||||||
assertEquals(
|
assertEquals(
|
||||||
boundedParam(new URLSearchParams({ q: "あ".repeat(5) }), "q", 14),
|
boundedParam(new URLSearchParams({ q: "あ".repeat(5) }), "q", 14),
|
||||||
|
|||||||
Reference in New Issue
Block a user