feat: cut over Memory to subjektiv product surfaces
This commit is contained in:
@@ -11,23 +11,263 @@ if (
|
||||
}
|
||||
|
||||
const workspaceId = "memory-review";
|
||||
const representativeSubjectId =
|
||||
"release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review";
|
||||
const representativeMemoryId =
|
||||
"memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001";
|
||||
const derivedMemoryId = "memory-derived-source-0002";
|
||||
const emptySubjectId = "empty-subject";
|
||||
const staleSubjectId = "stale-subject";
|
||||
const failedSubjectId = "failed-subject";
|
||||
const ungeneratedSubjectId = "ungenerated-subject";
|
||||
const errorSubjectId = "error-subject";
|
||||
|
||||
const paragraphs = Array.from(
|
||||
{ length: sectionCount },
|
||||
(_, index) =>
|
||||
`## Section ${
|
||||
index + 1
|
||||
}\n\nThis is representative Memory content with **important context**, an identifier \`memory-item-${
|
||||
}\n\nThis is representative resident Memory context with **important context**, an identifier \`memory-item-${
|
||||
index + 1
|
||||
}\`, and a long URL https://example.com/${"long-segment-".repeat(12)}${
|
||||
index + 1
|
||||
}.\n\n> A durable note for section ${index + 1}.\n\n- first detail\n- second detail`,
|
||||
).join("\n\n");
|
||||
const bodyMd =
|
||||
`# Workspace Memory\n\nThis document is read-only.\n\n| Very wide heading | Another heading | Decision |\n| --- | --- | --- |\n| ${
|
||||
const surfaceBodyMd =
|
||||
`# Resident subject context\n\nThis generated surface is read-only.\n\n| Very wide heading | Another heading | Decision |\n| --- | --- | --- |\n| ${
|
||||
"wide-value-".repeat(16)
|
||||
} | stable | keep scrolling local |\n\n##### Deep document heading\n\n###### Deepest document heading\n\n\`\`\`rust\nfn example() { println!("${
|
||||
} | stable | keep scrolling local |\n\n##### Deep surface heading\n\n###### Deepest surface heading\n\n\`\`\`rust\nfn example() { println!("${
|
||||
"wide-code-".repeat(18)
|
||||
}"); }\n\`\`\`\n\n[Safe link](https://example.com) [Unsafe link](javascript:alert(1))\n\n<img src=x onerror=alert(1)>\n\n${paragraphs}\n\n## T-661 UNIQUE END MARKER`;
|
||||
}"); }\n\`\`\`\n\n[Safe link](https://example.com) [Unsafe link](javascript:alert(1))\n\n<img src=x onerror=alert(1)>\n\n${paragraphs}\n\n## Surface content complete`;
|
||||
|
||||
const detailBodyMd = `# Committed Memory detail
|
||||
|
||||
This immutable revision preserves its candidate evidence, source refs, and derivation refs.
|
||||
|
||||
| Local table overflow | Value |
|
||||
| --- | --- |
|
||||
| ${"detail-wide-value-".repeat(14)} | stable |
|
||||
|
||||
\`\`\`text
|
||||
${"detail-wide-code-".repeat(18)}
|
||||
\`\`\`
|
||||
|
||||
[Safe evidence link](https://example.com/evidence) [Unsafe evidence link](javascript:alert(2))
|
||||
|
||||
<script>alert("not rendered")</script>`;
|
||||
|
||||
const subjects = [
|
||||
subject(representativeSubjectId, "Release coordination", "active", 42),
|
||||
subject(emptySubjectId, "Empty ready subject", "active", 0),
|
||||
subject(staleSubjectId, "Stale surface subject", "active", 19),
|
||||
subject(failedSubjectId, "Failed surface subject", "active", 8),
|
||||
subject(ungeneratedSubjectId, "Ungenerated subject", "active", 0),
|
||||
subject(errorSubjectId, "Unavailable subject", "retired", 3),
|
||||
];
|
||||
|
||||
function subject(id: string, role: string, state: "active" | "retired", storeRevision: number) {
|
||||
return {
|
||||
id,
|
||||
role,
|
||||
state,
|
||||
store_revision: storeRevision,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
};
|
||||
}
|
||||
|
||||
function memoriesFor(subjectId: string) {
|
||||
if (subjectId === emptySubjectId || subjectId === ungeneratedSubjectId) return [];
|
||||
if (subjectId === staleSubjectId) {
|
||||
return [
|
||||
memorySummary("stale-memory-1", 2, "lesson", "resolved", "A resolved stale-surface lesson"),
|
||||
];
|
||||
}
|
||||
if (subjectId === failedSubjectId) {
|
||||
return [
|
||||
memorySummary(
|
||||
"failed-memory-1",
|
||||
1,
|
||||
"constraint",
|
||||
"retracted",
|
||||
"A retracted failed-surface constraint",
|
||||
),
|
||||
];
|
||||
}
|
||||
return [
|
||||
memorySummary(
|
||||
representativeMemoryId,
|
||||
3,
|
||||
"decision",
|
||||
"active",
|
||||
"Keep subject Memory provenance typed and visible",
|
||||
),
|
||||
memorySummary(
|
||||
"memory-resolved-0002",
|
||||
2,
|
||||
"lesson",
|
||||
"resolved",
|
||||
"The bounded reader must preserve continuations",
|
||||
),
|
||||
memorySummary(
|
||||
"memory-retracted-0003",
|
||||
1,
|
||||
"working_assumption",
|
||||
"retracted",
|
||||
"T-672 UNIQUE END MARKER",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function memorySummary(
|
||||
id: string,
|
||||
revision: number,
|
||||
kind: string,
|
||||
state: string,
|
||||
claim: string,
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
revision,
|
||||
kind,
|
||||
state,
|
||||
claim,
|
||||
excerpt: `Read-only ${state} Memory with exact revision and provenance metadata.`,
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
};
|
||||
}
|
||||
|
||||
function surfaceFor(subjectId: string) {
|
||||
if (subjectId === representativeSubjectId) {
|
||||
return {
|
||||
subject_id: subjectId,
|
||||
availability: "ready",
|
||||
snapshot: {
|
||||
snapshot_id: "surface-snapshot-0042",
|
||||
body_md: surfaceBodyMd,
|
||||
memory_refs: [
|
||||
{ memory_id: representativeMemoryId, revision: 3 },
|
||||
{ memory_id: derivedMemoryId, revision: 4 },
|
||||
],
|
||||
built_from_store_revision: 42,
|
||||
created_at: "2026-01-02T03:04:05Z",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (subjectId === emptySubjectId) {
|
||||
return {
|
||||
subject_id: subjectId,
|
||||
availability: "ready",
|
||||
snapshot: {
|
||||
snapshot_id: "surface-snapshot-empty",
|
||||
body_md: "",
|
||||
memory_refs: [],
|
||||
built_from_store_revision: 0,
|
||||
created_at: "2026-01-02T03:04:05Z",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (subjectId === staleSubjectId) return { subject_id: subjectId, availability: "stale" };
|
||||
if (subjectId === failedSubjectId) return { subject_id: subjectId, availability: "failed" };
|
||||
return { subject_id: subjectId, availability: "ungenerated" };
|
||||
}
|
||||
|
||||
function memoryDetail(memoryId: string, requestedRevision: number | null) {
|
||||
const revision = requestedRevision && requestedRevision <= 3 ? requestedRevision : 3;
|
||||
const states = { 1: "retracted", 2: "resolved", 3: "active" } as const;
|
||||
return {
|
||||
memory_id: memoryId,
|
||||
revision,
|
||||
current_revision: 3,
|
||||
kind: "decision",
|
||||
state: states[revision as keyof typeof states],
|
||||
claim: revision === 3
|
||||
? "Keep subject Memory provenance typed and visible"
|
||||
: `Historical provenance decision, revision ${revision}`,
|
||||
body_md: detailBodyMd,
|
||||
why_useful:
|
||||
"Future readers can audit why a committed Memory exists without exposing unbounded session content.",
|
||||
staleness: null,
|
||||
change_reason: revision === 3
|
||||
? "Clarified the durable read contract."
|
||||
: "Immutable historical revision.",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: `2026-01-0${revision}T03:04:05Z`,
|
||||
body_offset: 0,
|
||||
body_byte_offset: 0,
|
||||
body_truncated: false,
|
||||
source_candidate_ids: ["candidate-release-decision-0001"],
|
||||
source_candidates: [{
|
||||
candidate_id: "candidate-release-decision-0001",
|
||||
evidence: [{
|
||||
id: "evidence-message-0001",
|
||||
kind: "message",
|
||||
entry_range: [12, 14],
|
||||
origin: {
|
||||
kind: "flow_instruction",
|
||||
workspace_id: workspaceId,
|
||||
runtime_id: "runtime-fixture",
|
||||
worker_id: "worker-fixture",
|
||||
flow_selector: "builtin:coder-review",
|
||||
flow_definition_id: "flow-definition-fixture",
|
||||
flow_definition_revision: 7,
|
||||
},
|
||||
excerpt: "Use normal explicit subject routes and preserve provenance.",
|
||||
summary: "The product contract was explicitly requested.",
|
||||
}],
|
||||
evidence_total: 1,
|
||||
evidence_truncated: false,
|
||||
source_refs: [{
|
||||
session_id: "session-fixture-0001",
|
||||
segment_id: "segment-fixture-0001",
|
||||
entry_range: [12, 14],
|
||||
evidence_id: "evidence-message-0001",
|
||||
origin: { kind: "human_input", account_id: "account-fixture" },
|
||||
evidence_kind: "message",
|
||||
label: "T-672 product direction",
|
||||
summary: "Bounded source reference retained with the committed revision.",
|
||||
}],
|
||||
source_refs_total: 1,
|
||||
source_refs_truncated: false,
|
||||
}],
|
||||
derived_from: [{ memory_id: derivedMemoryId, revision: 4 }],
|
||||
evidence_has_more: false,
|
||||
};
|
||||
}
|
||||
|
||||
function revisions(memoryId: string) {
|
||||
return {
|
||||
memory_id: memoryId,
|
||||
current_revision: 3,
|
||||
items: [
|
||||
{
|
||||
revision: 3,
|
||||
kind: "decision",
|
||||
state: "active",
|
||||
claim: "Keep subject Memory provenance typed and visible",
|
||||
change_reason: "Clarified the durable read contract.",
|
||||
updated_at: "2026-01-03T03:04:05Z",
|
||||
},
|
||||
{
|
||||
revision: 2,
|
||||
kind: "decision",
|
||||
state: "resolved",
|
||||
claim: "Historical provenance decision, revision 2",
|
||||
change_reason: "Resolved after endpoint integration.",
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
},
|
||||
{
|
||||
revision: 1,
|
||||
kind: "decision",
|
||||
state: "retracted",
|
||||
claim: "Historical provenance decision, revision 1",
|
||||
change_reason: "Retracted the legacy shape.",
|
||||
updated_at: "2026-01-01T03:04:05Z",
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
};
|
||||
}
|
||||
|
||||
const json = (value: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(value), {
|
||||
@@ -110,15 +350,34 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
|
||||
if (url.pathname === `/api/w/${workspaceId}/working-directories`) {
|
||||
return json({ workspace_id: workspaceId, items: [], diagnostics: [] });
|
||||
}
|
||||
if (url.pathname === `/api/w/${workspaceId}/memory`) {
|
||||
return json({
|
||||
body_md: bodyMd,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
bytes: new TextEncoder().encode(bodyMd).byteLength,
|
||||
record_source: "sqlite_workspace_authority.memory_document",
|
||||
});
|
||||
|
||||
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
|
||||
if (url.pathname === subjectsPath) {
|
||||
return json({ limit: 100, items: subjects, has_more: false });
|
||||
}
|
||||
if (url.pathname.startsWith(`${subjectsPath}/`)) {
|
||||
const suffix = url.pathname.slice(subjectsPath.length + 1);
|
||||
const parts = suffix.split("/").map(decodeURIComponent);
|
||||
const subjectId = parts[0];
|
||||
if (subjectId === errorSubjectId) return json({}, 503);
|
||||
const foundSubject = subjects.find((item) => item.id === subjectId);
|
||||
if (!foundSubject) return json({}, 404);
|
||||
|
||||
if (parts.length === 1) return json(foundSubject);
|
||||
if (parts.length === 2 && parts[1] === "surface") return json(surfaceFor(subjectId));
|
||||
if (parts.length === 2 && parts[1] === "memories") {
|
||||
return json({ items: memoriesFor(subjectId), has_more: false });
|
||||
}
|
||||
if (parts.length >= 3 && parts[1] === "memories") {
|
||||
const memoryId = parts[2];
|
||||
if (parts.length === 4 && parts[3] === "revisions") return json(revisions(memoryId));
|
||||
if (parts.length === 3) {
|
||||
const requested = url.searchParams.get("revision");
|
||||
return json(memoryDetail(memoryId, requested === null ? null : Number(requested)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname === `/api/w/${workspaceId}/protocol/ws` &&
|
||||
request.headers.get("upgrade") === "websocket"
|
||||
|
||||
@@ -9,6 +9,10 @@ const fixtureServer = join(
|
||||
"tools/web-ux/browser-tests/memory_document_fixture_server.ts",
|
||||
);
|
||||
const workspaceId = "memory-review";
|
||||
const subjectId =
|
||||
"release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review";
|
||||
const memoryId =
|
||||
"memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001";
|
||||
|
||||
async function freePort(): Promise<number> {
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
@@ -41,8 +45,8 @@ async function markerIsInsideMain(page: Page): Promise<boolean> {
|
||||
return await page.evaluate(() => {
|
||||
const document = (globalThis as any).document;
|
||||
const main = document.querySelector("main");
|
||||
const marker = [...document.querySelectorAll("h2")].find((heading) =>
|
||||
heading.textContent === "T-661 UNIQUE END MARKER"
|
||||
const marker = [...document.querySelectorAll("h3")].find((heading) =>
|
||||
heading.textContent === "T-672 UNIQUE END MARKER"
|
||||
);
|
||||
if (!main || !marker) return false;
|
||||
const mainRect = main.getBoundingClientRect();
|
||||
@@ -51,31 +55,86 @@ async function markerIsInsideMain(page: Page): Promise<boolean> {
|
||||
});
|
||||
}
|
||||
|
||||
async function openMemoryPage(
|
||||
async function assertNoPageWideOverflow(page: Page): Promise<void> {
|
||||
assertEquals(
|
||||
await page.evaluate(() => {
|
||||
const document = (globalThis as any).document;
|
||||
return document.documentElement.scrollWidth === document.documentElement.clientWidth;
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertMainOwnsVerticalScroll(page: Page): Promise<void> {
|
||||
const nested = await page.locator("main").evaluate((main) =>
|
||||
[...main.querySelectorAll("*")]
|
||||
.filter((element) => {
|
||||
const style = (globalThis as any).getComputedStyle(element);
|
||||
return ["auto", "scroll"].includes(style.overflowY) &&
|
||||
element.scrollHeight > element.clientHeight + 1;
|
||||
})
|
||||
.map((element) => `${element.tagName.toLowerCase()}.${element.className}`)
|
||||
);
|
||||
assertEquals(nested, []);
|
||||
}
|
||||
|
||||
async function openSubjectPage(
|
||||
page: Page,
|
||||
baseUrl: string,
|
||||
mobile = false,
|
||||
): Promise<void> {
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/memory`);
|
||||
const heading = page.locator("h1", { hasText: "Workspace Memory" });
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`);
|
||||
const heading = page.locator("h1", { hasText: "Release coordination" });
|
||||
await heading.waitFor({ state: "attached" });
|
||||
if (mobile) await page.getByRole("button", { name: "Hide sidebar" }).click();
|
||||
if (mobile) {
|
||||
const showSidebar = page.getByRole("button", { name: "Show sidebar" });
|
||||
await showSidebar.waitFor();
|
||||
assertEquals(await showSidebar.getAttribute("aria-expanded"), "false");
|
||||
assertEquals(
|
||||
await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert),
|
||||
false,
|
||||
);
|
||||
|
||||
await showSidebar.click();
|
||||
const hideSidebar = page.getByRole("button", { name: "Hide sidebar" });
|
||||
await hideSidebar.waitFor();
|
||||
assertEquals(await hideSidebar.getAttribute("aria-expanded"), "true");
|
||||
assertEquals(
|
||||
await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert),
|
||||
true,
|
||||
);
|
||||
|
||||
await hideSidebar.click();
|
||||
await showSidebar.waitFor();
|
||||
assertEquals(await showSidebar.getAttribute("aria-expanded"), "false");
|
||||
assertEquals(
|
||||
await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert),
|
||||
false,
|
||||
);
|
||||
}
|
||||
await heading.waitFor();
|
||||
}
|
||||
|
||||
async function swipeMainToEnd(page: Page): Promise<void> {
|
||||
const session = await page.context().newCDPSession(page);
|
||||
const viewport = page.viewportSize();
|
||||
if (!viewport) throw new Error("touch viewport is unavailable");
|
||||
const x = Math.max(20, Math.min(300, viewport.width - 20));
|
||||
const startY = Math.max(120, viewport.height - 100);
|
||||
const moveYs = [0.8, 0.62, 0.44, 0.26, 0.12].map((ratio) =>
|
||||
Math.max(20, Math.round(viewport.height * ratio))
|
||||
);
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const state = await mainScrollState(page);
|
||||
if (state.scrollTop + state.clientHeight >= state.scrollHeight - 1) return;
|
||||
await session.send("Input.dispatchTouchEvent", {
|
||||
type: "touchStart",
|
||||
touchPoints: [{ x: 300, y: 700, id: 1 }],
|
||||
touchPoints: [{ x, y: startY, id: 1 }],
|
||||
});
|
||||
for (const y of [580, 460, 340, 220, 100]) {
|
||||
for (const y of moveYs) {
|
||||
await session.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ x: 300, y, id: 1 }],
|
||||
touchPoints: [{ x, y, id: 1 }],
|
||||
});
|
||||
}
|
||||
await session.send("Input.dispatchTouchEvent", {
|
||||
@@ -84,10 +143,10 @@ async function swipeMainToEnd(page: Page): Promise<void> {
|
||||
});
|
||||
await page.waitForTimeout(20);
|
||||
}
|
||||
throw new Error("touch input did not reach the end of the Memory document");
|
||||
throw new Error("touch input did not reach the end of the resident Memory surface");
|
||||
}
|
||||
|
||||
Deno.test("production Memory shell renders safe Markdown and scrolls to its end by wheel, keyboard, and touch", async () => {
|
||||
Deno.test("production subject Memory shell renders exact states, safe Markdown, provenance, and one scroll owner", async () => {
|
||||
const build = await new Deno.Command(Deno.execPath(), {
|
||||
args: ["task", "build"],
|
||||
cwd: workspaceRoot,
|
||||
@@ -130,6 +189,13 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
colorScheme: "light" as const,
|
||||
mobile: true,
|
||||
},
|
||||
{
|
||||
label: "compact-mobile",
|
||||
width: 320,
|
||||
height: 700,
|
||||
colorScheme: "dark" as const,
|
||||
mobile: true,
|
||||
},
|
||||
]
|
||||
) {
|
||||
const context = await browser.newContext({
|
||||
@@ -144,21 +210,28 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
page.on("pageerror", (error) => errors.push(String(error)));
|
||||
await openMemoryPage(page, baseUrl, scenario.mobile);
|
||||
await openSubjectPage(page, baseUrl, scenario.mobile);
|
||||
|
||||
const initial = await mainScrollState(page);
|
||||
assert(initial.scrollHeight > initial.clientHeight);
|
||||
assertEquals(await markerIsInsideMain(page), false);
|
||||
await assertNoPageWideOverflow(page);
|
||||
await assertMainOwnsVerticalScroll(page);
|
||||
assertEquals(
|
||||
await page.evaluate(() => {
|
||||
const document = (globalThis as any).document;
|
||||
return document.documentElement.scrollWidth === document.documentElement.clientWidth;
|
||||
}),
|
||||
true,
|
||||
await page.locator('aside a[href$="/memory"]', { hasText: "Subjects" }).count(),
|
||||
1,
|
||||
);
|
||||
assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 0);
|
||||
|
||||
if (scenario.label === "wheel") {
|
||||
assertEquals(await page.locator("h1").allTextContents(), ["Workspace Memory"]);
|
||||
assertEquals(
|
||||
await page.getByRole("heading", {
|
||||
name: "Release coordination",
|
||||
level: 1,
|
||||
exact: true,
|
||||
}).count(),
|
||||
1,
|
||||
);
|
||||
assertEquals(
|
||||
await page.locator("strong").filter({ hasText: "important context" }).count() > 0,
|
||||
true,
|
||||
@@ -167,8 +240,8 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
assertEquals(await page.locator("ul li").count() > 0, true);
|
||||
for (
|
||||
const heading of [
|
||||
{ selector: "h5", text: "Deep document heading" },
|
||||
{ selector: "h6", text: "Deepest document heading" },
|
||||
{ selector: "h5", text: "Deep surface heading" },
|
||||
{ selector: "h6", text: "Deepest surface heading" },
|
||||
]
|
||||
) {
|
||||
const element = page.locator(heading.selector, { hasText: heading.text });
|
||||
@@ -182,8 +255,17 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
}
|
||||
assertEquals(await page.locator('a[href="https://example.com"]').count(), 1);
|
||||
assertEquals(await page.locator('a[href^="javascript:"]').count(), 0);
|
||||
assertEquals(await page.locator("article img, article script").count(), 0);
|
||||
assertEquals(await page.getByRole("heading", { name: "Memory Document" }).count(), 0);
|
||||
assertEquals(
|
||||
await page.getByRole("article", { name: "Resident surface" }).locator("img, script")
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
for (const state of ["Active", "Resolved", "Retracted"]) {
|
||||
assertEquals(
|
||||
await page.locator(".memory-pill", { hasText: state }).count(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
for (const label of ["Markdown table", "rust code block"]) {
|
||||
const region = page.getByRole("region", { name: label });
|
||||
@@ -194,7 +276,7 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
await page.locator("main").hover();
|
||||
await page.mouse.wheel(0, 100_000);
|
||||
} else if (scenario.label === "keyboard") {
|
||||
await page.getByText("Document details", { exact: true }).focus();
|
||||
await page.getByText("Surface provenance", { exact: true }).focus();
|
||||
await page.keyboard.press("End");
|
||||
} else {
|
||||
await swipeMainToEnd(page);
|
||||
@@ -213,33 +295,72 @@ Deno.test("production Memory shell renders safe Markdown and scrolls to its end
|
||||
await context.close();
|
||||
}
|
||||
|
||||
const stateContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const emptyPage = await stateContext.newPage();
|
||||
await emptyPage.route(`**/api/w/${workspaceId}/memory`, async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
body_md: " \n",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
bytes: 2,
|
||||
record_source: "fixture",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await emptyPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
|
||||
await emptyPage.getByText("Memory document is empty.").waitFor();
|
||||
assertEquals(await emptyPage.getByRole("alert").count(), 0);
|
||||
const detailContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const detailPage = await detailContext.newPage();
|
||||
await detailPage.goto(
|
||||
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}/${
|
||||
encodeURIComponent(memoryId)
|
||||
}`,
|
||||
);
|
||||
await detailPage.getByRole("heading", {
|
||||
name: "Keep subject Memory provenance typed and visible",
|
||||
level: 1,
|
||||
}).waitFor();
|
||||
assertEquals(await detailPage.getByText("candidate-release-decision-0001").count(), 1);
|
||||
assertEquals(await detailPage.getByText("T-672 product direction").count(), 1);
|
||||
assertEquals(await detailPage.getByText("memory-derived-source-0002").count(), 1);
|
||||
assertEquals(await detailPage.getByRole("heading", { name: "Revision history" }).count(), 1);
|
||||
assertEquals(await detailPage.getByText("Revision 3", { exact: true }).count(), 1);
|
||||
assertEquals(await detailPage.getByText("Revision 2", { exact: true }).count(), 1);
|
||||
assertEquals(await detailPage.getByText("Revision 1", { exact: true }).count(), 1);
|
||||
assertEquals(await detailPage.locator('a[href^="javascript:"]').count(), 0);
|
||||
assertEquals(
|
||||
await detailPage.getByRole("article", { name: "Committed Memory body" }).locator(
|
||||
"script, img",
|
||||
).count(),
|
||||
0,
|
||||
);
|
||||
for (const label of ["Markdown table", "text code block"]) {
|
||||
const region = detailPage.getByRole("region", { name: label });
|
||||
assertEquals(await region.getAttribute("tabindex"), "0");
|
||||
assert(await region.evaluate((element) => element.scrollWidth > element.clientWidth));
|
||||
}
|
||||
await assertNoPageWideOverflow(detailPage);
|
||||
await assertMainOwnsVerticalScroll(detailPage);
|
||||
await detailContext.close();
|
||||
|
||||
const errorPage = await stateContext.newPage();
|
||||
await errorPage.route(`**/api/w/${workspaceId}/memory`, async (route) => {
|
||||
await route.fulfill({ status: 503, contentType: "application/json", body: "{}" });
|
||||
});
|
||||
await errorPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
|
||||
const alert = errorPage.getByRole("alert");
|
||||
await alert.waitFor();
|
||||
assert((await alert.textContent())?.includes("Memory document unavailable."));
|
||||
assertEquals(await errorPage.getByText("Memory document is empty.").count(), 0);
|
||||
const stateContext = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
||||
const stateCases = [
|
||||
{
|
||||
id: "empty-subject",
|
||||
text: "Resident surface is ready and empty.",
|
||||
role: "status" as const,
|
||||
},
|
||||
{ id: "stale-subject", text: "Resident surface is stale.", role: "status" as const },
|
||||
{
|
||||
id: "failed-subject",
|
||||
text: "Resident surface generation failed.",
|
||||
role: "alert" as const,
|
||||
},
|
||||
{
|
||||
id: "ungenerated-subject",
|
||||
text: "Resident surface has not been generated.",
|
||||
role: "status" as const,
|
||||
},
|
||||
{ id: "error-subject", text: "Resident surface unavailable.", role: "alert" as const },
|
||||
];
|
||||
for (const stateCase of stateCases) {
|
||||
const page = await stateContext.newPage();
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${stateCase.id}`);
|
||||
const state = page.getByRole(stateCase.role).filter({ hasText: stateCase.text });
|
||||
await state.waitFor();
|
||||
assert((await state.textContent())?.includes(stateCase.text));
|
||||
if (stateCase.id !== "empty-subject") {
|
||||
assertEquals(await page.getByRole("article", { name: "Resident surface" }).count(), 0);
|
||||
}
|
||||
await assertNoPageWideOverflow(page);
|
||||
await page.close();
|
||||
}
|
||||
await stateContext.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "memory-subjects-dark",
|
||||
"title": "Memory subjects, surfaces, and committed records — dark",
|
||||
"baseUrl": "http://127.0.0.1:15286",
|
||||
"colorScheme": "dark",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [{
|
||||
"id": "fixture",
|
||||
"label": "Read-only Memory fixture",
|
||||
"auth": { "kind": "anonymous" }
|
||||
}],
|
||||
"viewports": [
|
||||
{ "label": "desktop", "width": 1440, "height": 900 },
|
||||
{ "label": "narrow", "width": 768, "height": 900 },
|
||||
{ "label": "mobile", "width": 390, "height": 844 },
|
||||
{ "label": "compact", "width": 320, "height": 700 }
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "subjects",
|
||||
"label": "Subject index",
|
||||
"path": "/w/memory-review/memory",
|
||||
"goal": "Choose an explicit Memory subject and inspect state, role, store revision, worker availability, and long identities.",
|
||||
"dataState": "Active and retired subjects spanning representative, empty, stale, failed, ungenerated, and unavailable states.",
|
||||
"ready": { "kind": "selector", "selector": "[data-memory-view='subjects'] .subject-list" },
|
||||
"capturePoints": [{ "id": "initial", "label": "Subjects" }]
|
||||
},
|
||||
{
|
||||
"id": "representative",
|
||||
"label": "Ready subject surface",
|
||||
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
|
||||
"goal": "Read the ready resident surface and scan active, resolved, and retracted committed Memories without nested vertical scrolling.",
|
||||
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident surface']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Ready subject" }]
|
||||
},
|
||||
{
|
||||
"id": "detail",
|
||||
"label": "Committed Memory detail",
|
||||
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
|
||||
"goal": "Audit immutable Memory body, candidate evidence, source refs, derivations, and revision history.",
|
||||
"dataState": "Current active revision with bounded typed provenance and active/resolved/retracted immutable history.",
|
||||
"ready": { "kind": "selector", "selector": "[data-memory-view='detail'] .revision-list" },
|
||||
"capturePoints": [{ "id": "initial", "label": "Memory detail" }]
|
||||
},
|
||||
{
|
||||
"id": "empty",
|
||||
"label": "Ready-empty subject",
|
||||
"path": "/w/memory-review/memory/empty-subject",
|
||||
"goal": "Distinguish a valid ready-empty snapshot and an empty committed Memory list from failure.",
|
||||
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='ready'] [data-surface-ready-empty]"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Ready empty" }]
|
||||
},
|
||||
{
|
||||
"id": "stale",
|
||||
"label": "Stale subject surface",
|
||||
"path": "/w/memory-review/memory/stale-subject",
|
||||
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
|
||||
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='stale']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Stale" }]
|
||||
},
|
||||
{
|
||||
"id": "failed",
|
||||
"label": "Failed subject surface",
|
||||
"path": "/w/memory-review/memory/failed-subject",
|
||||
"goal": "See generation failure without displaying a failed or partial snapshot.",
|
||||
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='failed']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Failed" }]
|
||||
},
|
||||
{
|
||||
"id": "error",
|
||||
"label": "Subject endpoint error",
|
||||
"path": "/w/memory-review/memory/error-subject",
|
||||
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
|
||||
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='error']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Unavailable" }]
|
||||
}
|
||||
],
|
||||
"processes": [{
|
||||
"id": "memory-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"browser-tests/memory_document_fixture_server.ts",
|
||||
"15286",
|
||||
"../../web/workspace/build"
|
||||
],
|
||||
"readyUrl": "http://127.0.0.1:15286/health"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "memory-subjects-light",
|
||||
"title": "Memory subjects, surfaces, and committed records — light",
|
||||
"baseUrl": "http://127.0.0.1:15285",
|
||||
"colorScheme": "light",
|
||||
"reducedMotion": "reduce",
|
||||
"personas": [{
|
||||
"id": "fixture",
|
||||
"label": "Read-only Memory fixture",
|
||||
"auth": { "kind": "anonymous" }
|
||||
}],
|
||||
"viewports": [
|
||||
{ "label": "desktop", "width": 1440, "height": 900 },
|
||||
{ "label": "narrow", "width": 768, "height": 900 },
|
||||
{ "label": "mobile", "width": 390, "height": 844 },
|
||||
{ "label": "compact", "width": 320, "height": 700 }
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"id": "subjects",
|
||||
"label": "Subject index",
|
||||
"path": "/w/memory-review/memory",
|
||||
"goal": "Choose an explicit Memory subject and inspect state, role, store revision, worker availability, and long identities.",
|
||||
"dataState": "Active and retired subjects spanning representative, empty, stale, failed, ungenerated, and unavailable states.",
|
||||
"ready": { "kind": "selector", "selector": "[data-memory-view='subjects'] .subject-list" },
|
||||
"capturePoints": [{ "id": "initial", "label": "Subjects" }]
|
||||
},
|
||||
{
|
||||
"id": "representative",
|
||||
"label": "Ready subject surface",
|
||||
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review",
|
||||
"goal": "Read the ready resident surface and scan active, resolved, and retracted committed Memories without nested vertical scrolling.",
|
||||
"dataState": "Ready generated Markdown surface with long content, unsafe Markdown, wide table/code, provenance refs, and three lifecycle states.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident surface']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Ready subject" }]
|
||||
},
|
||||
{
|
||||
"id": "detail",
|
||||
"label": "Committed Memory detail",
|
||||
"path": "/w/memory-review/memory/release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review/memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001",
|
||||
"goal": "Audit immutable Memory body, candidate evidence, source refs, derivations, and revision history.",
|
||||
"dataState": "Current active revision with bounded typed provenance and active/resolved/retracted immutable history.",
|
||||
"ready": { "kind": "selector", "selector": "[data-memory-view='detail'] .revision-list" },
|
||||
"capturePoints": [{ "id": "initial", "label": "Memory detail" }]
|
||||
},
|
||||
{
|
||||
"id": "empty",
|
||||
"label": "Ready-empty subject",
|
||||
"path": "/w/memory-review/memory/empty-subject",
|
||||
"goal": "Distinguish a valid ready-empty snapshot and an empty committed Memory list from failure.",
|
||||
"dataState": "Ready snapshot with empty Markdown and no refs; successful current list with no records.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='ready'] [data-surface-ready-empty]"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Ready empty" }]
|
||||
},
|
||||
{
|
||||
"id": "stale",
|
||||
"label": "Stale subject surface",
|
||||
"path": "/w/memory-review/memory/stale-subject",
|
||||
"goal": "Recognize that no snapshot is displayable while committed Memories remain readable.",
|
||||
"dataState": "Stale availability without snapshot and one resolved committed Memory.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='stale']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Stale" }]
|
||||
},
|
||||
{
|
||||
"id": "failed",
|
||||
"label": "Failed subject surface",
|
||||
"path": "/w/memory-review/memory/failed-subject",
|
||||
"goal": "See generation failure without displaying a failed or partial snapshot.",
|
||||
"dataState": "Failed availability without snapshot and one retracted committed Memory.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='failed']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Failed" }]
|
||||
},
|
||||
{
|
||||
"id": "error",
|
||||
"label": "Subject endpoint error",
|
||||
"path": "/w/memory-review/memory/error-subject",
|
||||
"goal": "Keep each read-only region explicit when subject, surface, and current Memory requests fail.",
|
||||
"dataState": "Intentional HTTP 503 from all subject-scoped endpoints.",
|
||||
"ready": {
|
||||
"kind": "selector",
|
||||
"selector": "[data-memory-view='subject'][data-memory-state='error']"
|
||||
},
|
||||
"capturePoints": [{ "id": "initial", "label": "Unavailable" }]
|
||||
}
|
||||
],
|
||||
"processes": [{
|
||||
"id": "memory-fixture",
|
||||
"command": "deno",
|
||||
"cwd": "..",
|
||||
"args": [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
"browser-tests/memory_document_fixture_server.ts",
|
||||
"15285",
|
||||
"../../web/workspace/build"
|
||||
],
|
||||
"readyUrl": "http://127.0.0.1:15285/health"
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user