chore: integrate Subject Memory information improvements

This commit is contained in:
2026-10-05 17:08:32 +09:00
11 changed files with 626 additions and 108 deletions
@@ -502,7 +502,9 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
subjectIndexLoad.includes("parseSubjektivSubjectListResponse") &&
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"'),
"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}/memories`") &&
subjectPage.includes("DocumentMarkdown from") &&
subjectPage.includes("Resident surface") &&
subjectPage.includes("Resident context") &&
subjectPage.includes("Subject store revision") &&
subjectPage.includes("Current Memories") &&
subjectPage.includes("surface?.availability === 'ready'") &&
subjectPage.includes("surface?.availability === 'ungenerated'") &&
subjectPage.includes("surface?.availability === 'stale'") &&
subjectPage.includes("surface?.availability === 'failed'") &&
subjectPage.includes("data-surface-ready-empty") &&
!subjectPage.includes("overflow-y"),
"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;
}
function subjectStateLabel(state: SubjektivSubjectResponse['state']): string {
return state === 'active' ? 'Available' : 'Retired';
}
async function openCreateForm(): Promise<void> {
createExpanded = true;
createError = null;
@@ -109,7 +113,7 @@
</div>
<div class="memory-header-actions">
{#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}
<button
bind:this={createButton}
@@ -184,14 +188,19 @@
{#each subjects as subject (subject.id)}
<a class="subject-row" href={subjectHref(subject.id)}>
<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>
<code title={subject.id}>{subject.id}</code>
</div>
<dl class="subject-meta">
<div><dt>Store revision</dt><dd>{subject.store_revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
<div><dt>Worker</dt><dd>{subject.current_worker?.display_name ?? 'None'}</dd></div>
<div>
<dt>Worker connection</dt>
<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>
</a>
{/each}
@@ -426,8 +435,8 @@
.subject-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-4);
align-self: center;
}
@@ -441,12 +450,19 @@
}
.subject-meta dd {
display: grid;
gap: var(--space-1);
margin-top: var(--space-1);
color: var(--text-muted);
font-size: var(--font-size-compact);
overflow-wrap: anywhere;
}
.subject-meta dd strong {
color: var(--text);
font-weight: 700;
}
.memory-state {
padding: var(--space-5) 0;
color: var(--text-muted);
@@ -1,7 +1,10 @@
<script lang="ts">
import DocumentMarkdown from '#lib/workspace/markdown/DocumentMarkdown.svelte';
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';
let { data }: PageProps = $props();
@@ -36,6 +39,24 @@
function kindLabel(kind: string): string {
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 surfaceSummary(availability: SubjektivResidentSurfaceAvailability): string {
if (availability !== 'ready') return 'No current generated context is available to use.';
return snapshot?.body_md.trim()
? 'Current generated context is available.'
: 'A current empty surface exists; there is no resident context to show.';
}
function subjectStateLabel(state: 'active' | 'retired'): string {
return state === 'active' ? 'Available' : 'Retired';
}
</script>
<svelte:head>
@@ -56,13 +77,50 @@
</header>
{#if data.subject.data}
<dl class="subject-facts" aria-label="Subject details">
<div><dt>Role</dt><dd>{data.subject.data.role}</dd></div>
<div><dt>State</dt><dd><span class="subject-state is-{data.subject.data.state}">{data.subject.data.state}</span></dd></div>
<div><dt>Store revision</dt><dd>{data.subject.data.store_revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div>
<div><dt>Current worker</dt><dd>{data.subject.data.current_worker?.display_name ?? 'None'}</dd></div>
</dl>
<section class="subject-overview" aria-label="Subject status">
<div>
<span>Subject status</span>
<strong class="subject-state is-{data.subject.data.state}">{subjectStateLabel(data.subject.data.state)}</strong>
<p>{data.subject.data.state === 'active' ? 'Available for new Worker connections.' : 'No longer available for new Worker connections.'}</p>
</div>
<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>{surfaceSummary(surface.availability)}</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}
<div class="memory-state is-error" role="alert">
<strong>Subject details unavailable.</strong>
@@ -75,38 +133,40 @@
<section class="surface-section" aria-labelledby="resident-surface-heading">
<header class="section-heading">
<div>
<p class="memory-eyebrow">Resident context</p>
<h2 id="resident-surface-heading">Resident surface</h2>
<p class="memory-eyebrow">Generated from committed Memory</p>
<h2 id="resident-surface-heading">Resident context</h2>
</div>
{#if surface}
<span class="availability is-{surface.availability}">{surface.availability}</span>
<span class="availability is-{surface.availability}">{surfaceLabel(surface.availability)}</span>
{/if}
</header>
{#if surface?.availability === 'ready' && snapshot}
<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>
<time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time>
<span aria-hidden="true">·</span>
<span>{snapshot.memory_refs.length} Memory ref{snapshot.memory_refs.length === 1 ? '' : 's'}</span>
<span>{snapshot.memory_refs.length} referenced Memory {snapshot.memory_refs.length === 1 ? 'record' : 'records'}</span>
</div>
{#if snapshot.body_md.trim().length === 0}
<div class="memory-state" role="status" data-surface-ready-empty>
<strong>Resident surface is ready and empty.</strong>
<p>This subject currently has no resident context to display.</p>
<strong>Resident context is current but empty.</strong>
<p>A generated surface exists, but it contains no context to show. Committed Memories, if any, remain listed below.</p>
</div>
{:else}
<article class="surface-document" aria-label="Resident surface">
<article class="surface-document" aria-label="Resident context">
<DocumentMarkdown text={snapshot.body_md} />
</article>
{/if}
<details class="surface-details">
<summary>Surface provenance</summary>
<summary>Surface sources and diagnostics</summary>
<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>
<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>
{#if snapshot.memory_refs.length === 0}
None
@@ -123,26 +183,26 @@
</details>
{:else if surface?.availability === 'ungenerated'}
<div class="memory-state" role="status">
<strong>Resident surface has not been generated.</strong>
<p>Committed Memories remain available below.</p>
<strong>Resident context has not been generated.</strong>
<p>Committed Memories, if any, remain available below.</p>
</div>
{:else if surface?.availability === 'stale'}
<div class="memory-state is-warning" role="status">
<strong>Resident surface is stale.</strong>
<p>The latest committed Memories are not represented by a publishable snapshot.</p>
<strong>Resident context needs to be refreshed.</strong>
<p>The latest committed Memories are not represented by a current surface, so no generated context is shown.</p>
</div>
{:else if surface?.availability === 'failed'}
<div class="memory-state is-error" role="alert">
<strong>Resident surface generation failed.</strong>
<p>No failed or partial snapshot is displayed.</p>
<strong>Resident context generation failed.</strong>
<p>No failed or partial surface is shown. Committed Memories remain available below.</p>
</div>
{:else if data.surface.error}
<div class="memory-state is-error" role="alert">
<strong>Resident surface unavailable.</strong>
<strong>Resident context status unavailable.</strong>
<p>{data.surface.error}</p>
</div>
{: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}
</section>
@@ -152,14 +212,14 @@
<p class="memory-eyebrow">Committed records</p>
<h2 id="current-memories-heading">Current Memories</h2>
</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>
{#if data.memories.data}
{#if memories.length === 0}
<div class="memory-state" role="status" data-memories-empty>
<strong>No committed Memories.</strong>
<p>This subject has no current Memory revisions.</p>
<strong>{data.cursor ? 'No committed Memories on this page.' : 'No committed Memories yet.'}</strong>
<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}
</div>
{:else}
@@ -175,9 +235,9 @@
<p>{memory.excerpt || 'No excerpt.'}</p>
</div>
<dl>
<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>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
<div><dt>Memory ID</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
<div><dt>Memory revision</dt><dd>{memory.revision}</dd></div>
<div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl>
</a>
{/each}
@@ -270,25 +330,79 @@
font-size: var(--font-size-compact);
}
.subject-facts {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
.subject-overview {
display: grid;
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 {
display: block;
.subject-overview > div {
display: grid;
align-content: start;
gap: var(--space-1);
min-width: 0;
}
.subject-facts dt {
white-space: normal;
.subject-overview span,
.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 {
margin-top: var(--space-1);
color: var(--text);
.subject-overview strong {
color: var(--text-strong);
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,
.availability,
.memory-pill,
@@ -455,7 +569,7 @@
}
@media (max-width: 900px) {
.subject-facts {
.subject-overview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -472,10 +586,16 @@
gap: var(--space-2);
}
.subject-facts {
.subject-overview,
.subject-technical-details dl,
.surface-details dl {
grid-template-columns: minmax(0, 1fr);
}
.subject-overview {
padding: var(--space-3);
}
.memory-row {
padding-inline: 0;
}
@@ -63,6 +63,11 @@
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 {
return range ? `${range[0]}–${range[1]}` : 'None';
}
@@ -98,8 +103,8 @@
<header class="memory-page-header">
<div>
<div class="pill-row">
<span class="memory-pill is-{memory.state}">{memory.state}</span>
<span class="memory-kind">{label(memory.kind)}</span>
<span class="memory-pill is-{memory.state}">{titleLabel(memory.state)}</span>
<span class="memory-kind">{titleLabel(memory.kind)}</span>
{#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if}
</div>
<h1 id="memory-detail-heading">{memory.claim}</h1>
@@ -109,11 +114,11 @@
</header>
<dl class="memory-facts" aria-label="Memory details">
<div><dt>Revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
<div><dt>State</dt><dd>{memory.state}</dd></div>
<div><dt>Kind</dt><dd>{label(memory.kind)}</dd></div>
<div><dt>Memory revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
<div><dt>Status</dt><dd>{titleLabel(memory.state)}</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>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>
<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">
<header class="section-heading">
<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>
{#if memory.source_candidates.length === 0}
@@ -226,7 +231,11 @@
</section>
<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 revisions.length === 0}
<div>
@@ -238,7 +247,7 @@
{#each revisions as revision (revision.revision)}
<li class:current={revision.revision === memory.revision}>
<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>
<small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small>
</a>
@@ -386,6 +395,7 @@
}
.memory-facts {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
@@ -414,6 +424,7 @@
.why-useful,
.staleness,
.change-reason,
.section-intro,
.empty-copy,
.bounded-note {
margin: 0;
@@ -73,6 +73,9 @@ test("renders the subject index as the Memory product entry", () => {
expect(screen.getByRole("link", { name: /Release coordinator/ })).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"))
.toBe("/w/workspace-1/memory");
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
@@ -214,7 +217,10 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
workspaceId: "workspace-1",
subjectId: "subject-1",
cursor: "memory-current",
subject: result(subject()),
subject: result({
...subject(),
current_worker: { display_name: "Release Worker" },
}),
surface: result({
subject_id: "subject-1",
availability: "ready" as const,
@@ -267,9 +273,15 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
expect(screen.getByRole("heading", { name: "Resident context", level: 1 }))
.not.toBeNull();
});
expect(screen.getByRole("article", { name: "Resident surface" })).not
expect(screen.getByRole("article", { name: "Resident context" })).not
.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,
);
expect(screen.getByText("Resolved", { exact: true })).not.toBeNull();
@@ -280,6 +292,34 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
.toContain("cursor=memory-next");
});
test("candidate-only Subjects show no committed Memories, not an inferred candidate count", () => {
// The store regression candidate_decisions_reject_conflicts_and_roll_back_partial_writes
// stages a candidate without committing a Memory. Its public read projection is
// the same empty committed list as a new Subject; candidates are not a page input.
render(SubjectPage, {
data: {
workspaceId: "workspace-1",
subjectId: "subject-1",
cursor: null,
subject: result({ ...subject(), store_revision: 0 }),
surface: result({ subject_id: "subject-1", availability: "ungenerated" }),
memories: result({ limit: 100, items: [], has_more: false }),
},
} as never);
expect(screen.getByText("None", { exact: true })).not.toBeNull();
expect(screen.getByText("No committed Memories yet.")).not.toBeNull();
expect(screen.getByText("Not connected", { exact: true })).not.toBeNull();
expect(screen.getAllByText("Not generated", { exact: true })).toHaveLength(2);
expect(screen.queryByRole("article", { name: "Resident context" }))
.toBeNull();
expect(
screen.queryByText(
/candidate count|all Memories lost|automatically updating/i,
),
).toBeNull();
});
test("distinguishes ready-empty, stale, failed, unavailable, and request error surfaces", async () => {
const base = {
workspaceId: "workspace-1",
@@ -303,8 +343,25 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
}),
},
} as never);
expect(screen.getByText("Resident surface is ready and empty.")).not
expect(screen.getByText("Resident context is current but empty.")).not
.toBeNull();
expect(screen.getByText(/current empty surface exists/)).not.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(
{
@@ -317,7 +374,8 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
},
} 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(
{
@@ -333,7 +391,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
expect(screen.getByRole("alert").textContent).toContain("generation failed");
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();
await view.rerender(
@@ -342,7 +400,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
} as never,
);
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);
if (!error.message.includes(expectedMessage)) {
throw new Error(
`expected ${JSON.stringify(error.message)} to include ${
JSON.stringify(expectedMessage)
}`,
`expected ${JSON.stringify(error.message)} to include ${JSON.stringify(expectedMessage)}`,
);
}
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") {
return {
id,
@@ -187,14 +209,35 @@ Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bo
() =>
parseSubjektivSubjectListResponse({
limit: 100,
items: Array.from({ length: 101 }, (_, index) =>
subject(`subject-${index}`)),
items: Array.from({ length: 101 }, (_, index) => subject(`subject-${index}`)),
has_more: false,
}),
"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", () => {
const response = { limit: 100, items: [subject()], has_more: false };
assertEquals(parseSubjektivSubjectListResponse(response), response);
@@ -328,8 +371,7 @@ Deno.test("Subject create client distinguishes rejected and unknown outcomes wit
throw new TypeError("connection reset");
}) as typeof fetch;
await assertSubjectCreateRejects(
() =>
createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
() => createSubjektivSubject(unknownFetch, "workspace-1", { role: "Reviewer" }),
"unknown_outcome",
"may have been created",
);
@@ -433,8 +475,7 @@ Deno.test("Current Memory list parser rejects unknown variants, unsafe integers,
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
items: Array.from({ length: 101 }, (_, index) =>
queryItem(`memory-${index}`)),
items: Array.from({ length: 101 }, (_, index) => queryItem(`memory-${index}`)),
has_more: false,
}),
"bounded array",