feat(web): clarify Subject Memory status

This commit is contained in:
2026-10-05 13:56:33 +09:00
parent cc97987482
commit fbb09e6597
10 changed files with 460 additions and 120 deletions
@@ -66,7 +66,13 @@ ${"detail-wide-code-".repeat(18)}
<script>alert("not rendered")</script>`; <script>alert("not rendered")</script>`;
const subjects = [ const subjects = [
subject(representativeSubjectId, "Release coordination", "active", 42), subject(
representativeSubjectId,
"Release coordination",
"active",
42,
connectedWorker("Release coordination Worker with a deliberately long display name"),
),
subject(emptySubjectId, "Empty ready subject", "active", 0), subject(emptySubjectId, "Empty ready subject", "active", 0),
subject(staleSubjectId, "Stale surface subject", "active", 19), subject(staleSubjectId, "Stale surface subject", "active", 19),
subject(failedSubjectId, "Failed surface subject", "active", 8), subject(failedSubjectId, "Failed surface subject", "active", 8),
@@ -75,7 +81,37 @@ const subjects = [
]; ];
const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", 1); const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", 1);
function subject(id: string, role: string, state: "active" | "retired", storeRevision: number) { function connectedWorker(displayName: string) {
return {
runtime_id: "embedded-worker-runtime",
worker_id: "release-coordination-worker",
host_id: "fixture-host",
display_name: displayName,
label: displayName,
profile: "builtin:companion",
singleton_key: `subjektiv:${representativeSubjectId}`,
tags: [],
workspace: {
visibility: "workspace",
identity: workspaceId,
workspace_id: workspaceId,
},
state: "idle",
last_seen_at: "2026-01-02T03:04:05Z",
pinned: false,
retention_state: "retained",
implementation: { kind: "embedded", display_hint: "Fixture Worker" },
diagnostics: [],
};
}
function subject(
id: string,
role: string,
state: "active" | "retired",
storeRevision: number,
currentWorker?: ReturnType<typeof connectedWorker>,
) {
return { return {
id, id,
role, role,
@@ -83,6 +119,7 @@ function subject(id: string, role: string, state: "active" | "retired", storeRev
store_revision: storeRevision, store_revision: storeRevision,
created_at: "2026-01-01T00:00:00Z", created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z", updated_at: "2026-01-02T03:04:05Z",
...(currentWorker ? { current_worker: currentWorker } : {}),
}; };
} }
@@ -125,7 +162,7 @@ function memoriesFor(subjectId: string) {
1, 1,
"working_assumption", "working_assumption",
"retracted", "retracted",
"T-672 UNIQUE END MARKER", "Memory list end marker for scroll validation",
), ),
]; ];
} }
@@ -235,7 +272,7 @@ function memoryDetail(memoryId: string, requestedRevision: number | null) {
evidence_id: "evidence-message-0001", evidence_id: "evidence-message-0001",
origin: { kind: "human_input", account_id: "account-fixture" }, origin: { kind: "human_input", account_id: "account-fixture" },
evidence_kind: "message", evidence_kind: "message",
label: "T-672 product direction", label: "Memory information design source",
summary: "Bounded source reference retained with the committed revision.", summary: "Bounded source reference retained with the committed revision.",
}], }],
source_refs_total: 1, source_refs_total: 1,
@@ -16,7 +16,7 @@ const memoryId =
const subjectPageCursor = "fixture-subject-page-2"; const subjectPageCursor = "fixture-subject-page-2";
const createInteractionArtifacts = join( const createInteractionArtifacts = join(
repositoryRoot, repositoryRoot,
"target/web-ux/t-689-browser-interactions", "target/web-ux/t-692-browser-interactions",
); );
async function freePort(): Promise<number> { async function freePort(): Promise<number> {
@@ -51,7 +51,7 @@ async function markerIsInsideMain(page: Page): Promise<boolean> {
const document = (globalThis as any).document; const document = (globalThis as any).document;
const main = document.querySelector("main"); const main = document.querySelector("main");
const marker = [...document.querySelectorAll("h3")].find((heading) => const marker = [...document.querySelectorAll("h3")].find((heading) =>
heading.textContent === "T-672 UNIQUE END MARKER" heading.textContent === "Memory list end marker for scroll validation"
); );
if (!main || !marker) return false; if (!main || !marker) return false;
const mainRect = main.getBoundingClientRect(); const mainRect = main.getBoundingClientRect();
@@ -262,7 +262,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
assertEquals(await page.locator('a[href="https://example.com"]').count(), 1); assertEquals(await page.locator('a[href="https://example.com"]').count(), 1);
assertEquals(await page.locator('a[href^="javascript:"]').count(), 0); assertEquals(await page.locator('a[href^="javascript:"]').count(), 0);
assertEquals( assertEquals(
await page.getByRole("article", { name: "Resident surface" }).locator("img, script") await page.getByRole("article", { name: "Resident context" }).locator("img, script")
.count(), .count(),
0, 0,
); );
@@ -282,7 +282,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await page.locator("main").hover(); await page.locator("main").hover();
await page.mouse.wheel(0, 100_000); await page.mouse.wheel(0, 100_000);
} else if (scenario.label === "keyboard") { } else if (scenario.label === "keyboard") {
await page.getByText("Surface provenance", { exact: true }).focus(); await page.getByText("Surface sources and diagnostics", { exact: true }).focus();
await page.keyboard.press("End"); await page.keyboard.press("End");
} else { } else {
await swipeMainToEnd(page); await swipeMainToEnd(page);
@@ -313,17 +313,17 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
level: 1, level: 1,
}).waitFor(); }).waitFor();
assertEquals(await detailPage.getByText("candidate-release-decision-0001").count(), 1); 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 information design source").count(), 1);
assertEquals(await detailPage.getByText("memory-derived-source-0002").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.getByRole("heading", { name: "Revision history" }).count(), 1);
assertEquals(await detailPage.getByText("Revision 3", { exact: true }).count(), 1); assertEquals(await detailPage.getByText("Memory revision 3", { exact: true }).count(), 1);
assertEquals(await detailPage.getByText("Revision 2", { exact: true }).count(), 1); assertEquals(await detailPage.getByText("Memory revision 2", { exact: true }).count(), 1);
assertEquals(await detailPage.getByText("Revision 1", { exact: true }).count(), 0); assertEquals(await detailPage.getByText("Memory revision 1", { exact: true }).count(), 0);
await detailPage.getByRole("navigation", { name: "Revision history pages" }).getByRole( await detailPage.getByRole("navigation", { name: "Revision history pages" }).getByRole(
"link", "link",
{ name: "Next page →" }, { name: "Next page →" },
).click(); ).click();
await detailPage.getByText("Revision 1", { exact: true }).waitFor(); await detailPage.getByText("Memory revision 1", { exact: true }).waitFor();
assert(new URL(detailPage.url()).searchParams.has("revision_cursor")); assert(new URL(detailPage.url()).searchParams.has("revision_cursor"));
assertEquals( assertEquals(
await detailPage.getByRole("navigation", { name: "Revision history pages" }).getByRole( await detailPage.getByRole("navigation", { name: "Revision history pages" }).getByRole(
@@ -352,6 +352,15 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
const paginationPage = await paginationContext.newPage(); const paginationPage = await paginationContext.newPage();
await paginationPage.goto(`${baseUrl}/w/${workspaceId}/memory`); await paginationPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor(); await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor();
assertEquals(
await paginationPage.getByText(
"Release coordination Worker with a deliberately long display name",
{ exact: true },
).count(),
1,
);
assertEquals(await paginationPage.getByText("Store revision", { exact: true }).count(), 0);
assert((await paginationPage.getByText("Not connected", { exact: true }).count()) > 0);
assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0); assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0);
await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole( await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole(
"link", "link",
@@ -451,10 +460,12 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
await createPage.locator("code.subject-id", { hasText: "created-subject-0001" }).count(), await createPage.locator("code.subject-id", { hasText: "created-subject-0001" }).count(),
1, 1,
); );
const currentWorker = createPage.locator(".subject-facts div").filter({ const currentWorker = createPage.getByText("Worker connection", { exact: true }).locator(
hasText: "Current worker", "..",
}); );
assert((await currentWorker.textContent())?.includes("None")); assert((await currentWorker.textContent())?.includes("Not connected"));
await createPage.getByText("Resident context has not been generated.").waitFor();
await createPage.getByText("No committed Memories yet.").waitFor();
assertEquals(mutationPaths, [ assertEquals(mutationPaths, [
`/api/w/${workspaceId}/subjektiv/subjects`, `/api/w/${workspaceId}/subjektiv/subjects`,
`/api/w/${workspaceId}/subjektiv/subjects`, `/api/w/${workspaceId}/subjektiv/subjects`,
@@ -472,21 +483,29 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
const stateCases = [ const stateCases = [
{ {
id: "empty-subject", id: "empty-subject",
text: "Resident surface is ready and empty.", text: "Resident context is current but empty.",
role: "status" as const,
},
{
id: "stale-subject",
text: "Resident context needs to be refreshed.",
role: "status" as const, role: "status" as const,
}, },
{ id: "stale-subject", text: "Resident surface is stale.", role: "status" as const },
{ {
id: "failed-subject", id: "failed-subject",
text: "Resident surface generation failed.", text: "Resident context generation failed.",
role: "alert" as const, role: "alert" as const,
}, },
{ {
id: "ungenerated-subject", id: "ungenerated-subject",
text: "Resident surface has not been generated.", text: "Resident context has not been generated.",
role: "status" as const, role: "status" as const,
}, },
{ id: "error-subject", text: "Resident surface unavailable.", role: "alert" as const }, {
id: "error-subject",
text: "Resident context status unavailable.",
role: "alert" as const,
},
]; ];
for (const stateCase of stateCases) { for (const stateCase of stateCases) {
const page = await stateContext.newPage(); const page = await stateContext.newPage();
@@ -494,8 +513,14 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
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));
if (stateCase.id === "stale-subject") {
assertEquals(await page.getByText("A resolved stale-surface lesson").count(), 1);
}
if (stateCase.id === "failed-subject") {
assertEquals(await page.getByText("A retracted failed-surface constraint").count(), 1);
}
if (stateCase.id !== "empty-subject") { if (stateCase.id !== "empty-subject") {
assertEquals(await page.getByRole("article", { name: "Resident surface" }).count(), 0); assertEquals(await page.getByRole("article", { name: "Resident context" }).count(), 0);
} }
await assertNoPageWideOverflow(page); await assertNoPageWideOverflow(page);
await page.close(); await page.close();
@@ -122,9 +122,33 @@
"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": {
"kind": "selector", "kind": "selector",
"selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident surface']" "selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident context']"
}, },
"capturePoints": [{ "id": "initial", "label": "Ready subject" }] "capturePoints": [
{ "id": "initial", "label": "Ready subject" },
{
"id": "subject-diagnostics",
"label": "Subject diagnostics",
"interaction": [
{ "action": "click", "selector": "details.subject-technical-details summary" }
],
"ready": {
"kind": "selector",
"selector": "details.subject-technical-details[open]"
}
},
{
"id": "surface-diagnostics",
"label": "Surface sources and diagnostics",
"interaction": [
{ "action": "click", "selector": "details.surface-details summary" }
],
"ready": {
"kind": "selector",
"selector": "details.surface-details[open]"
}
}
]
}, },
{ {
"id": "detail", "id": "detail",
@@ -150,6 +174,18 @@
}, },
"capturePoints": [{ "id": "initial", "label": "Ready empty" }] "capturePoints": [{ "id": "initial", "label": "Ready empty" }]
}, },
{
"id": "ungenerated",
"label": "Ungenerated subject context",
"path": "/w/memory-review/memory/ungenerated-subject",
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
"ready": {
"kind": "selector",
"selector": "[data-memory-view='subject'][data-memory-state='ungenerated']"
},
"capturePoints": [{ "id": "initial", "label": "Not generated" }]
},
{ {
"id": "stale", "id": "stale",
"label": "Stale subject surface", "label": "Stale subject surface",
@@ -122,9 +122,33 @@
"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": {
"kind": "selector", "kind": "selector",
"selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident surface']" "selector": "[data-memory-view='subject'][data-memory-state='ready'] article[aria-label='Resident context']"
}, },
"capturePoints": [{ "id": "initial", "label": "Ready subject" }] "capturePoints": [
{ "id": "initial", "label": "Ready subject" },
{
"id": "subject-diagnostics",
"label": "Subject diagnostics",
"interaction": [
{ "action": "click", "selector": "details.subject-technical-details summary" }
],
"ready": {
"kind": "selector",
"selector": "details.subject-technical-details[open]"
}
},
{
"id": "surface-diagnostics",
"label": "Surface sources and diagnostics",
"interaction": [
{ "action": "click", "selector": "details.surface-details summary" }
],
"ready": {
"kind": "selector",
"selector": "details.surface-details[open]"
}
}
]
}, },
{ {
"id": "detail", "id": "detail",
@@ -150,6 +174,18 @@
}, },
"capturePoints": [{ "id": "initial", "label": "Ready empty" }] "capturePoints": [{ "id": "initial", "label": "Ready empty" }]
}, },
{
"id": "ungenerated",
"label": "Ungenerated subject context",
"path": "/w/memory-review/memory/ungenerated-subject",
"goal": "Distinguish a Subject with no generated resident context from lost committed Memory.",
"dataState": "Ungenerated resident context and a successful empty committed Memory list.",
"ready": {
"kind": "selector",
"selector": "[data-memory-view='subject'][data-memory-state='ungenerated']"
},
"capturePoints": [{ "id": "initial", "label": "Not generated" }]
},
{ {
"id": "stale", "id": "stale",
"label": "Stale subject surface", "label": "Stale subject surface",
@@ -502,7 +502,9 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
subjectIndexLoad.includes('"/subjektiv/subjects"') && subjectIndexLoad.includes('"/subjektiv/subjects"') &&
subjectIndexLoad.includes("parseSubjektivSubjectListResponse") && subjectIndexLoad.includes("parseSubjektivSubjectListResponse") &&
subjectIndexPage.includes(">Subjects</h1>") && subjectIndexPage.includes(">Subjects</h1>") &&
subjectIndexPage.includes("subject.store_revision") && subjectIndexPage.includes("Worker connection") &&
subjectIndexPage.includes("subject.current_worker") &&
!subjectIndexPage.includes("subject.store_revision") &&
subjectIndexPage.includes('data-memory-view="subjects"'), subjectIndexPage.includes('data-memory-view="subjects"'),
"Memory product entry should list explicit typed subjects", "Memory product entry should list explicit typed subjects",
); );
@@ -510,10 +512,13 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
subjectPageLoad.includes("`${subjectPath}/surface`") && subjectPageLoad.includes("`${subjectPath}/surface`") &&
subjectPageLoad.includes("`${subjectPath}/memories`") && subjectPageLoad.includes("`${subjectPath}/memories`") &&
subjectPage.includes("DocumentMarkdown from") && subjectPage.includes("DocumentMarkdown from") &&
subjectPage.includes("Resident surface") && subjectPage.includes("Resident context") &&
subjectPage.includes("Subject store revision") &&
subjectPage.includes("Current Memories") && subjectPage.includes("Current Memories") &&
subjectPage.includes("surface?.availability === 'ready'") && subjectPage.includes("surface?.availability === 'ready'") &&
subjectPage.includes("surface?.availability === 'ungenerated'") &&
subjectPage.includes("surface?.availability === 'stale'") && subjectPage.includes("surface?.availability === 'stale'") &&
subjectPage.includes("surface?.availability === 'failed'") &&
subjectPage.includes("data-surface-ready-empty") && subjectPage.includes("data-surface-ready-empty") &&
!subjectPage.includes("overflow-y"), !subjectPage.includes("overflow-y"),
"Subject page should combine strict resident-surface and current-Memory reads without a nested vertical scroller", "Subject page should combine strict resident-surface and current-Memory reads without a nested vertical scroller",
@@ -31,6 +31,10 @@
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path; return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
} }
function subjectStateLabel(state: SubjektivSubjectResponse['state']): string {
return state === 'active' ? 'Available' : 'Retired';
}
async function openCreateForm(): Promise<void> { async function openCreateForm(): Promise<void> {
createExpanded = true; createExpanded = true;
createError = null; createError = null;
@@ -109,7 +113,7 @@
</div> </div>
<div class="memory-header-actions"> <div class="memory-header-actions">
{#if data.subjects.data} {#if data.subjects.data}
<span class="memory-count">{subjects.length}{data.subjects.data.has_more ? '+' : ''} subject{subjects.length === 1 ? '' : 's'}</span> <span class="memory-count">{subjects.length} shown{data.subjects.data.has_more ? ' · more available' : ''}</span>
{/if} {/if}
<button <button
bind:this={createButton} bind:this={createButton}
@@ -184,14 +188,19 @@
{#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">
<span class="memory-state-pill is-{subject.state}">{subject.state}</span> <span class="memory-state-pill is-{subject.state}">{subjectStateLabel(subject.state)}</span>
<h2>{subject.role}</h2> <h2>{subject.role}</h2>
<code title={subject.id}>{subject.id}</code> <code title={subject.id}>{subject.id}</code>
</div> </div>
<dl class="subject-meta"> <dl class="subject-meta">
<div><dt>Store revision</dt><dd>{subject.store_revision}</dd></div> <div>
<div><dt>Updated</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div> <dt>Worker connection</dt>
<div><dt>Worker</dt><dd>{subject.current_worker?.display_name ?? 'None'}</dd></div> <dd>
<strong>{subject.current_worker ? 'Connected' : 'Not connected'}</strong>
{#if subject.current_worker}<span>{subject.current_worker.display_name}</span>{/if}
</dd>
</div>
<div><dt>Last changed</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
</dl> </dl>
</a> </a>
{/each} {/each}
@@ -426,8 +435,8 @@
.subject-meta { .subject-meta {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3); gap: var(--space-4);
align-self: center; align-self: center;
} }
@@ -441,12 +450,19 @@
} }
.subject-meta dd { .subject-meta dd {
display: grid;
gap: var(--space-1);
margin-top: var(--space-1); margin-top: var(--space-1);
color: var(--text-muted); color: var(--text-muted);
font-size: var(--font-size-compact); font-size: var(--font-size-compact);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.subject-meta dd strong {
color: var(--text);
font-weight: 700;
}
.memory-state { .memory-state {
padding: var(--space-5) 0; padding: var(--space-5) 0;
color: var(--text-muted); color: var(--text-muted);
@@ -1,7 +1,10 @@
<script lang="ts"> <script lang="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, workspaceRoute } from '#lib/workspace/api/http.ts';
import type { SubjektivMemoryState } from '#lib/generated/memory-api.ts'; import type {
SubjektivMemoryState,
SubjektivResidentSurfaceAvailability,
} from '#lib/generated/memory-api.ts';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
@@ -36,6 +39,17 @@
function kindLabel(kind: string): string { function kindLabel(kind: string): string {
return kind.replaceAll('_', ' '); return kind.replaceAll('_', ' ');
} }
function surfaceLabel(availability: SubjektivResidentSurfaceAvailability): string {
if (availability === 'ready') return snapshot?.body_md.trim() ? 'Ready to use' : 'Ready, no context';
if (availability === 'ungenerated') return 'Not generated';
if (availability === 'stale') return 'Needs refresh';
return 'Generation failed';
}
function subjectStateLabel(state: 'active' | 'retired'): string {
return state === 'active' ? 'Available' : 'Retired';
}
</script> </script>
<svelte:head> <svelte:head>
@@ -56,13 +70,50 @@
</header> </header>
{#if data.subject.data} {#if data.subject.data}
<dl class="subject-facts" aria-label="Subject details"> <section class="subject-overview" aria-label="Subject status">
<div><dt>Role</dt><dd>{data.subject.data.role}</dd></div> <div>
<div><dt>State</dt><dd><span class="subject-state is-{data.subject.data.state}">{data.subject.data.state}</span></dd></div> <span>Subject status</span>
<div><dt>Store revision</dt><dd>{data.subject.data.store_revision}</dd></div> <strong class="subject-state is-{data.subject.data.state}">{subjectStateLabel(data.subject.data.state)}</strong>
<div><dt>Updated</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div> <p>{data.subject.data.state === 'active' ? 'Available for new Worker connections.' : 'No longer available for new Worker connections.'}</p>
<div><dt>Current worker</dt><dd>{data.subject.data.current_worker?.display_name ?? 'None'}</dd></div> </div>
</dl> <div>
<span>Worker connection</span>
<strong>{data.subject.data.current_worker ? 'Connected' : 'Not connected'}</strong>
<p>{data.subject.data.current_worker?.display_name ?? 'No Worker currently owns this Subject connection.'}</p>
</div>
<div>
<span>Committed Memories</span>
{#if data.memories.data}
<strong>{data.cursor ? `${memories.length} on this page` : memories.length === 0 ? 'None' : 'Available'}</strong>
<p>{memories.length} shown{data.memories.data.has_more ? ' · more available' : ''}</p>
{:else}
<strong>Unavailable</strong>
<p>The current Memory list could not be read.</p>
{/if}
</div>
<div>
<span>Resident context</span>
{#if surface}
<strong class="availability is-{surface.availability}">{surfaceLabel(surface.availability)}</strong>
<p>{surface.availability === 'ready' ? 'Current generated context is available.' : 'No current generated context is available to use.'}</p>
{:else}
<strong>Unavailable</strong>
<p>The resident context status could not be read.</p>
{/if}
</div>
</section>
<details class="subject-technical-details">
<summary>Technical details</summary>
<dl>
<div><dt>Subject ID</dt><dd><code>{data.subject.data.id}</code></dd></div>
<div>
<dt>Subject store revision</dt>
<dd>{data.subject.data.store_revision}<small>Internal change number for committed Memories; not a Memory count or content-quality score.</small></dd>
</div>
<div><dt>Last changed</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div>
</dl>
</details>
{:else if data.subject.error} {:else if data.subject.error}
<div class="memory-state is-error" role="alert"> <div class="memory-state is-error" role="alert">
<strong>Subject details unavailable.</strong> <strong>Subject details unavailable.</strong>
@@ -75,38 +126,40 @@
<section class="surface-section" aria-labelledby="resident-surface-heading"> <section class="surface-section" aria-labelledby="resident-surface-heading">
<header class="section-heading"> <header class="section-heading">
<div> <div>
<p class="memory-eyebrow">Resident context</p> <p class="memory-eyebrow">Generated from committed Memory</p>
<h2 id="resident-surface-heading">Resident surface</h2> <h2 id="resident-surface-heading">Resident context</h2>
</div> </div>
{#if surface} {#if surface}
<span class="availability is-{surface.availability}">{surface.availability}</span> <span class="availability is-{surface.availability}">{surfaceLabel(surface.availability)}</span>
{/if} {/if}
</header> </header>
{#if surface?.availability === 'ready' && snapshot} {#if surface?.availability === 'ready' && snapshot}
<div class="surface-meta"> <div class="surface-meta">
<span>Built from store revision {snapshot.built_from_store_revision}</span> <span>Generated <time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time></span>
<span aria-hidden="true">·</span> <span aria-hidden="true">·</span>
<time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time> <span>{snapshot.memory_refs.length} referenced Memory {snapshot.memory_refs.length === 1 ? 'record' : 'records'}</span>
<span aria-hidden="true">·</span>
<span>{snapshot.memory_refs.length} Memory ref{snapshot.memory_refs.length === 1 ? '' : 's'}</span>
</div> </div>
{#if snapshot.body_md.trim().length === 0} {#if snapshot.body_md.trim().length === 0}
<div class="memory-state" role="status" data-surface-ready-empty> <div class="memory-state" role="status" data-surface-ready-empty>
<strong>Resident surface is ready and empty.</strong> <strong>Resident context is current but empty.</strong>
<p>This subject currently has no resident context to display.</p> <p>A generated surface exists, but it contains no context to show. Committed Memories, if any, remain listed below.</p>
</div> </div>
{:else} {:else}
<article class="surface-document" aria-label="Resident surface"> <article class="surface-document" aria-label="Resident context">
<DocumentMarkdown text={snapshot.body_md} /> <DocumentMarkdown text={snapshot.body_md} />
</article> </article>
{/if} {/if}
<details class="surface-details"> <details class="surface-details">
<summary>Surface provenance</summary> <summary>Surface sources and diagnostics</summary>
<dl> <dl>
<div><dt>Snapshot id</dt><dd><code>{snapshot.snapshot_id}</code></dd></div> <div><dt>Snapshot ID</dt><dd><code>{snapshot.snapshot_id}</code></dd></div>
<div> <div>
<dt>Memory refs</dt> <dt>Generated from Subject revision</dt>
<dd>{snapshot.built_from_store_revision}<small>The Subject change number used for this surface, distinct from each Memory’s revision.</small></dd>
</div>
<div>
<dt>Referenced Memory revisions</dt>
<dd> <dd>
{#if snapshot.memory_refs.length === 0} {#if snapshot.memory_refs.length === 0}
None None
@@ -123,26 +176,26 @@
</details> </details>
{:else if surface?.availability === 'ungenerated'} {:else if surface?.availability === 'ungenerated'}
<div class="memory-state" role="status"> <div class="memory-state" role="status">
<strong>Resident surface has not been generated.</strong> <strong>Resident context has not been generated.</strong>
<p>Committed Memories remain available below.</p> <p>Committed Memories, if any, remain available below.</p>
</div> </div>
{:else if surface?.availability === 'stale'} {:else if surface?.availability === 'stale'}
<div class="memory-state is-warning" role="status"> <div class="memory-state is-warning" role="status">
<strong>Resident surface is stale.</strong> <strong>Resident context needs to be refreshed.</strong>
<p>The latest committed Memories are not represented by a publishable snapshot.</p> <p>The latest committed Memories are not represented by a current surface, so no generated context is shown.</p>
</div> </div>
{:else if surface?.availability === 'failed'} {:else if surface?.availability === 'failed'}
<div class="memory-state is-error" role="alert"> <div class="memory-state is-error" role="alert">
<strong>Resident surface generation failed.</strong> <strong>Resident context generation failed.</strong>
<p>No failed or partial snapshot is displayed.</p> <p>No failed or partial surface is shown. Committed Memories remain available below.</p>
</div> </div>
{:else if data.surface.error} {:else if data.surface.error}
<div class="memory-state is-error" role="alert"> <div class="memory-state is-error" role="alert">
<strong>Resident surface unavailable.</strong> <strong>Resident context status unavailable.</strong>
<p>{data.surface.error}</p> <p>{data.surface.error}</p>
</div> </div>
{:else} {:else}
<div class="memory-state" role="status"><p>Resident surface data is unavailable.</p></div> <div class="memory-state" role="status"><p>Resident context status is unavailable.</p></div>
{/if} {/if}
</section> </section>
@@ -152,14 +205,14 @@
<p class="memory-eyebrow">Committed records</p> <p class="memory-eyebrow">Committed records</p>
<h2 id="current-memories-heading">Current Memories</h2> <h2 id="current-memories-heading">Current Memories</h2>
</div> </div>
{#if data.memories.data}<span class="memory-count">{memories.length}{data.memories.data.has_more ? '+' : ''}</span>{/if} {#if data.memories.data}<span class="memory-count">{memories.length} shown{data.memories.data.has_more ? ' · more available' : ''}</span>{/if}
</header> </header>
{#if data.memories.data} {#if data.memories.data}
{#if memories.length === 0} {#if memories.length === 0}
<div class="memory-state" role="status" data-memories-empty> <div class="memory-state" role="status" data-memories-empty>
<strong>No committed Memories.</strong> <strong>{data.cursor ? 'No committed Memories on this page.' : 'No committed Memories yet.'}</strong>
<p>This subject has no current Memory revisions.</p> <p>{data.cursor ? 'Return to the first page to review earlier records.' : 'This Subject has no current Memory records.'}</p>
{#if data.cursor}<p><a href={memoryPageHref()}>Return to the first page</a></p>{/if} {#if data.cursor}<p><a href={memoryPageHref()}>Return to the first page</a></p>{/if}
</div> </div>
{:else} {:else}
@@ -175,9 +228,9 @@
<p>{memory.excerpt || 'No excerpt.'}</p> <p>{memory.excerpt || 'No excerpt.'}</p>
</div> </div>
<dl> <dl>
<div><dt>Memory id</dt><dd><code title={memory.id}>{memory.id}</code></dd></div> <div><dt>Memory ID</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
<div><dt>Revision</dt><dd>{memory.revision}</dd></div> <div><dt>Memory revision</dt><dd>{memory.revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div> <div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl> </dl>
</a> </a>
{/each} {/each}
@@ -270,25 +323,79 @@
font-size: var(--font-size-compact); font-size: var(--font-size-compact);
} }
.subject-facts { .subject-overview {
grid-template-columns: repeat(5, minmax(0, 1fr)); display: grid;
gap: var(--space-3); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-4);
padding: var(--space-4);
background: var(--bg-raised);
border-radius: var(--radius-soft);
} }
.subject-facts > div { .subject-overview > div {
display: block; display: grid;
align-content: start;
gap: var(--space-1);
min-width: 0;
} }
.subject-facts dt { .subject-overview span,
white-space: normal; .subject-overview p,
.subject-technical-details summary,
.subject-technical-details small,
.surface-details small {
color: var(--text-muted);
font-size: var(--font-size-compact);
} }
.subject-facts dd { .subject-overview strong {
margin-top: var(--space-1); color: var(--text-strong);
color: var(--text);
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.subject-overview p {
margin: 0;
overflow-wrap: anywhere;
}
.subject-technical-details {
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--line);
}
.subject-technical-details summary {
width: fit-content;
cursor: pointer;
font-weight: 700;
}
.subject-technical-details dl,
.surface-details dl {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-4);
margin-top: var(--space-3);
}
.subject-technical-details dl > div,
.surface-details dl > div {
display: block;
min-width: 0;
}
.subject-technical-details dd,
.surface-details dd {
margin-top: var(--space-1);
overflow-wrap: anywhere;
}
.subject-technical-details small,
.surface-details small {
display: block;
margin-top: var(--space-1);
line-height: 1.4;
}
.subject-state, .subject-state,
.availability, .availability,
.memory-pill, .memory-pill,
@@ -455,7 +562,7 @@
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.subject-facts { .subject-overview {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -472,10 +579,16 @@
gap: var(--space-2); gap: var(--space-2);
} }
.subject-facts { .subject-overview,
.subject-technical-details dl,
.surface-details dl {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
} }
.subject-overview {
padding: var(--space-3);
}
.memory-row { .memory-row {
padding-inline: 0; padding-inline: 0;
} }
@@ -63,6 +63,11 @@
return value.replaceAll('_', ' '); return value.replaceAll('_', ' ');
} }
function titleLabel(value: string): string {
const normalized = label(value);
return normalized[0].toUpperCase() + normalized.slice(1);
}
function rangeLabel(range: [number, number] | null | undefined): string { function rangeLabel(range: [number, number] | null | undefined): string {
return range ? `${range[0]}–${range[1]}` : 'None'; return range ? `${range[0]}–${range[1]}` : 'None';
} }
@@ -98,8 +103,8 @@
<header class="memory-page-header"> <header class="memory-page-header">
<div> <div>
<div class="pill-row"> <div class="pill-row">
<span class="memory-pill is-{memory.state}">{memory.state}</span> <span class="memory-pill is-{memory.state}">{titleLabel(memory.state)}</span>
<span class="memory-kind">{label(memory.kind)}</span> <span class="memory-kind">{titleLabel(memory.kind)}</span>
{#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if} {#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if}
</div> </div>
<h1 id="memory-detail-heading">{memory.claim}</h1> <h1 id="memory-detail-heading">{memory.claim}</h1>
@@ -109,11 +114,11 @@
</header> </header>
<dl class="memory-facts" aria-label="Memory details"> <dl class="memory-facts" aria-label="Memory details">
<div><dt>Revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div> <div><dt>Memory revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
<div><dt>State</dt><dd>{memory.state}</dd></div> <div><dt>Status</dt><dd>{titleLabel(memory.state)}</dd></div>
<div><dt>Kind</dt><dd>{label(memory.kind)}</dd></div> <div><dt>Type</dt><dd>{titleLabel(memory.kind)}</dd></div>
<div><dt>Created</dt><dd><time datetime={memory.created_at}>{formatDate(memory.created_at)}</time></dd></div> <div><dt>Created</dt><dd><time datetime={memory.created_at}>{formatDate(memory.created_at)}</time></dd></div>
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div> <div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl> </dl>
<section class="detail-section memory-body-section" aria-labelledby="memory-body-heading"> <section class="detail-section memory-body-section" aria-labelledby="memory-body-heading">
@@ -140,7 +145,7 @@
<section class="detail-section provenance-section" aria-labelledby="provenance-heading"> <section class="detail-section provenance-section" aria-labelledby="provenance-heading">
<header class="section-heading"> <header class="section-heading">
<div><p class="memory-eyebrow">Audit context</p><h2 id="provenance-heading">Candidate provenance</h2></div> <div><p class="memory-eyebrow">Audit context</p><h2 id="provenance-heading">Candidate provenance</h2></div>
<span>{memory.source_candidates.length} candidate{memory.source_candidates.length === 1 ? '' : 's'}</span> <span>{memory.source_candidates.length} shown</span>
</header> </header>
{#if memory.source_candidates.length === 0} {#if memory.source_candidates.length === 0}
@@ -226,7 +231,11 @@
</section> </section>
<section class="detail-section revision-section" aria-labelledby="revision-heading"> <section class="detail-section revision-section" aria-labelledby="revision-heading">
<header class="section-heading"><div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>{#if data.revisions.data}<span>{revisions.length}{data.revisions.data.has_more ? '+' : ''}</span>{/if}</header> <header class="section-heading">
<div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>
{#if data.revisions.data}<span>{revisions.length} shown{data.revisions.data.has_more ? ' · more available' : ''}</span>{/if}
</header>
<p class="section-intro">These are versions of this Memory record, separate from the Subject’s internal update number.</p>
{#if data.revisions.data} {#if data.revisions.data}
{#if revisions.length === 0} {#if revisions.length === 0}
<div> <div>
@@ -238,7 +247,7 @@
{#each revisions as revision (revision.revision)} {#each revisions as revision (revision.revision)}
<li class:current={revision.revision === memory.revision}> <li class:current={revision.revision === memory.revision}>
<a href={revisionHref(revision.revision)} aria-current={revision.revision === memory.revision ? 'page' : undefined}> <a href={revisionHref(revision.revision)} aria-current={revision.revision === memory.revision ? 'page' : undefined}>
<div><strong>Revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{revision.state}</span><span class="memory-kind">{label(revision.kind)}</span></div> <div><strong>Memory revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{titleLabel(revision.state)}</span><span class="memory-kind">{titleLabel(revision.kind)}</span></div>
<p>{revision.claim}</p> <p>{revision.claim}</p>
<small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small> <small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small>
</a> </a>
@@ -386,6 +395,7 @@
} }
.memory-facts { .memory-facts {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3); gap: var(--space-3);
} }
@@ -414,6 +424,7 @@
.why-useful, .why-useful,
.staleness, .staleness,
.change-reason, .change-reason,
.section-intro,
.empty-copy, .empty-copy,
.bounded-note { .bounded-note {
margin: 0; margin: 0;
@@ -1,12 +1,6 @@
// @vitest-environment happy-dom // @vitest-environment happy-dom
import { import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/svelte";
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/svelte";
import { afterEach, expect, test, vi } from "vitest"; import { afterEach, expect, test, vi } from "vitest";
const { gotoMock } = vi.hoisted(() => ({ const { gotoMock } = vi.hoisted(() => ({
@@ -73,6 +67,9 @@ test("renders the subject index as the Memory product entry", () => {
expect(screen.getByRole("link", { name: /Release coordinator/ })).not expect(screen.getByRole("link", { name: /Release coordinator/ })).not
.toBeNull(); .toBeNull();
expect(screen.getByText("subject-1")).not.toBeNull(); expect(screen.getByText("subject-1")).not.toBeNull();
expect(screen.getByText("Not connected")).not.toBeNull();
expect(screen.queryByText("Store revision", { exact: true })).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/memory");
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href")) expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
@@ -184,9 +181,7 @@ test("preserves invalid and rejected role drafts with accessible errors", async
}); });
test("warns about an unknown create outcome without replaying the POST", async () => { test("warns about an unknown create outcome without replaying the POST", async () => {
const fetchMock = vi.fn(() => const fetchMock = vi.fn(() => Promise.reject(new TypeError("connection reset")));
Promise.reject(new TypeError("connection reset"))
);
vi.stubGlobal("fetch", fetchMock); vi.stubGlobal("fetch", fetchMock);
render(SubjectIndexPage, { data: subjectIndexData() } as never); render(SubjectIndexPage, { data: subjectIndexData() } as never);
@@ -214,14 +209,16 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
workspaceId: "workspace-1", workspaceId: "workspace-1",
subjectId: "subject-1", subjectId: "subject-1",
cursor: "memory-current", cursor: "memory-current",
subject: result(subject()), subject: result({
...subject(),
current_worker: { display_name: "Release Worker" },
}),
surface: result({ surface: result({
subject_id: "subject-1", subject_id: "subject-1",
availability: "ready" as const, availability: "ready" as const,
snapshot: { snapshot: {
snapshot_id: "snapshot-1", snapshot_id: "snapshot-1",
body_md: body_md: "# Resident context\n\nUse the **current** committed record.",
"# Resident context\n\nUse the **current** committed record.",
memory_refs: [{ memory_id: "memory-1", revision: 3 }], memory_refs: [{ memory_id: "memory-1", revision: 3 }],
built_from_store_revision: 9, built_from_store_revision: 9,
created_at: "2026-01-02T03:04:05Z", created_at: "2026-01-02T03:04:05Z",
@@ -267,9 +264,15 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
expect(screen.getByRole("heading", { name: "Resident context", level: 1 })) expect(screen.getByRole("heading", { name: "Resident context", level: 1 }))
.not.toBeNull(); .not.toBeNull();
}); });
expect(screen.getByRole("article", { name: "Resident surface" })).not expect(screen.getByRole("article", { name: "Resident context" })).not
.toBeNull(); .toBeNull();
expect(screen.getAllByText("active", { exact: true }).length).toBeGreaterThan( expect(screen.getByText("Connected", { exact: true })).not.toBeNull();
expect(screen.getByText("Release Worker", { exact: true })).not.toBeNull();
expect(screen.getByText("3 on this page", { exact: true })).not.toBeNull();
expect(screen.getByText("Subject store revision", { exact: true })).not
.toBeNull();
expect(screen.getByText(/not a Memory count/)).not.toBeNull();
expect(screen.getAllByText("Active", { exact: true }).length).toBeGreaterThan(
0, 0,
); );
expect(screen.getByText("Resolved", { exact: true })).not.toBeNull(); expect(screen.getByText("Resolved", { exact: true })).not.toBeNull();
@@ -303,8 +306,24 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
}), }),
}, },
} as never); } as never);
expect(screen.getByText("Resident surface is ready and empty.")).not expect(screen.getByText("Resident context is current but empty.")).not
.toBeNull(); .toBeNull();
expect(screen.getByText("No committed Memories yet.")).not.toBeNull();
await view.rerender(
{
data: {
...base,
surface: result({
subject_id: "subject-1",
availability: "ungenerated" as const,
}),
},
} as never,
);
expect(screen.getByText("Resident context has not been generated.")).not
.toBeNull();
expect(screen.getAllByText("Not generated", { exact: true })).toHaveLength(2);
await view.rerender( await view.rerender(
{ {
@@ -317,7 +336,8 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
}, },
} as never, } as never,
); );
expect(screen.getByText("Resident surface is stale.")).not.toBeNull(); expect(screen.getByText("Resident context needs to be refreshed.")).not
.toBeNull();
await view.rerender( await view.rerender(
{ {
@@ -333,7 +353,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
expect(screen.getByRole("alert").textContent).toContain("generation failed"); expect(screen.getByRole("alert").textContent).toContain("generation failed");
await view.rerender({ data: { ...base, surface: result(null) } } as never); await view.rerender({ data: { ...base, surface: result(null) } } as never);
expect(screen.getByText("Resident surface data is unavailable.")).not expect(screen.getByText("Resident context status is unavailable.")).not
.toBeNull(); .toBeNull();
await view.rerender( await view.rerender(
@@ -342,7 +362,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
} as never, } as never,
); );
expect(screen.getByRole("alert").textContent).toContain( expect(screen.getByRole("alert").textContent).toContain(
"Resident surface unavailable", "Resident context status unavailable",
); );
}); });
+50 -9
View File
@@ -61,9 +61,7 @@ async function assertSubjectCreateRejects(
assertEquals(error.status, expectedStatus); assertEquals(error.status, expectedStatus);
if (!error.message.includes(expectedMessage)) { if (!error.message.includes(expectedMessage)) {
throw new Error( throw new Error(
`expected ${JSON.stringify(error.message)} to include ${ `expected ${JSON.stringify(error.message)} to include ${JSON.stringify(expectedMessage)}`,
JSON.stringify(expectedMessage)
}`,
); );
} }
return; return;
@@ -82,6 +80,30 @@ function subject(id = "subject-1") {
}; };
} }
function currentWorker() {
return {
runtime_id: "runtime-1",
worker_id: "worker-1",
host_id: "host-1",
display_name: "Release Worker",
label: "Release Worker",
profile: "builtin:companion",
singleton_key: "subjektiv:subject-1",
tags: [],
workspace: {
visibility: "workspace",
identity: "workspace-1",
workspace_id: "workspace-1",
},
state: "idle",
last_seen_at: "2026-09-02T00:00:00Z",
pinned: false,
retention_state: "retained",
implementation: { kind: "embedded", display_hint: "Embedded" },
diagnostics: [],
};
}
function queryItem(id = "memory-1") { function queryItem(id = "memory-1") {
return { return {
id, id,
@@ -187,14 +209,35 @@ Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bo
() => () =>
parseSubjektivSubjectListResponse({ parseSubjektivSubjectListResponse({
limit: 100, limit: 100,
items: Array.from({ length: 101 }, (_, index) => items: Array.from({ length: 101 }, (_, index) => subject(`subject-${index}`)),
subject(`subject-${index}`)),
has_more: false, has_more: false,
}), }),
"bounded array", "bounded array",
); );
}); });
Deno.test("Subject parser preserves live Worker connection state", () => {
assertEquals(
parseSubjektivSubjectResponse({
...subject(),
current_worker: currentWorker(),
}),
{ ...subject(), current_worker: currentWorker() },
);
assertEquals(
parseSubjektivSubjectResponse({ ...subject(), current_worker: null }),
{ ...subject(), current_worker: null },
);
assertThrows(
() =>
parseSubjektivSubjectResponse({
...subject(),
current_worker: { display_name: "Incomplete Worker" },
}),
"current worker.runtime_id",
);
});
Deno.test("Subject list parser preserves the exact bounded response", () => { Deno.test("Subject list parser preserves the exact bounded response", () => {
const response = { limit: 100, items: [subject()], has_more: false }; const response = { limit: 100, items: [subject()], has_more: false };
assertEquals(parseSubjektivSubjectListResponse(response), response); assertEquals(parseSubjektivSubjectListResponse(response), response);
@@ -328,8 +371,7 @@ Deno.test("Subject create client distinguishes rejected and unknown outcomes wit
throw new TypeError("connection reset"); throw new TypeError("connection reset");
}) as typeof fetch; }) as typeof fetch;
await assertSubjectCreateRejects( await assertSubjectCreateRejects(
() => () => createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
"unknown_outcome", "unknown_outcome",
"may have been created", "may have been created",
); );
@@ -433,8 +475,7 @@ Deno.test("Current Memory list parser rejects unknown variants, unsafe integers,
assertThrows( assertThrows(
() => () =>
parseSubjektivMemoryQueryResponse({ parseSubjektivMemoryQueryResponse({
items: Array.from({ length: 101 }, (_, index) => items: Array.from({ length: 101 }, (_, index) => queryItem(`memory-${index}`)),
queryItem(`memory-${index}`)),
has_more: false, has_more: false,
}), }),
"bounded array", "bounded array",