fix subject authority and bounded memory browsing
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -37,7 +37,7 @@ export type SubjektivSubjectState = "active" | "retired";
|
||||
|
||||
export type SubjektivSubjectResponse = { id: string, role: string, state: SubjektivSubjectState, store_revision: number, created_at: string, updated_at: string, current_worker?: | import("./worker-launch-api").WorkerLaunchWorkerSummary | null, };
|
||||
|
||||
export type SubjektivSubjectListResponse = { limit: number, items: Array<SubjektivSubjectResponse>, has_more: boolean, };
|
||||
export type SubjektivSubjectListResponse = { limit: number, items: Array<SubjektivSubjectResponse>, next_cursor?: string | null, has_more: boolean, };
|
||||
|
||||
export type SubjektivMemoryState = "active" | "resolved" | "retracted";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
|
||||
// Input contract: 0.1.0 (sha256:318ce25c5d41452715ccb31e81798bfe2b894796a1f885afb37c83cdb7525098)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
@@ -228,7 +228,9 @@ function parseStagingEvidence(value: unknown): MemoryStagingEvidence {
|
||||
summary: nullableString(record, "summary"),
|
||||
};
|
||||
if ("origin" in record) {
|
||||
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
|
||||
result.origin = record.origin === null
|
||||
? null
|
||||
: parseEvidenceOrigin(record.origin);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -259,7 +261,9 @@ function parseSourceEvidenceRef(value: unknown): MemorySourceEvidenceRef {
|
||||
summary: nullableString(record, "summary"),
|
||||
};
|
||||
if ("origin" in record) {
|
||||
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
|
||||
result.origin = record.origin === null
|
||||
? null
|
||||
: parseEvidenceOrigin(record.origin);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -350,10 +354,12 @@ export function parseSubjektivSubjectResponse(
|
||||
updated_at: requiredString(record, "updated_at"),
|
||||
};
|
||||
if ("current_worker" in record) {
|
||||
result.current_worker = record.current_worker === null ? null : parseWorkerLaunchWorkerSummary(
|
||||
record.current_worker,
|
||||
"Subject current worker",
|
||||
);
|
||||
result.current_worker = record.current_worker === null
|
||||
? null
|
||||
: parseWorkerLaunchWorkerSummary(
|
||||
record.current_worker,
|
||||
"Subject current worker",
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -363,8 +369,9 @@ export function parseSubjektivSubjectListResponse(
|
||||
): SubjektivSubjectListResponse {
|
||||
const record = strictRecord(
|
||||
value,
|
||||
["limit", "items", "has_more"],
|
||||
["limit", "items", "next_cursor", "has_more"],
|
||||
"Subject list response",
|
||||
["next_cursor"],
|
||||
);
|
||||
const limit = positiveInteger(record.limit, "limit");
|
||||
if (limit > MAX_SUBJECTS) invalid("limit exceeds the Subject list bound");
|
||||
@@ -373,11 +380,16 @@ export function parseSubjektivSubjectListResponse(
|
||||
);
|
||||
if (items.length > limit) invalid("items exceeds the declared Subject limit");
|
||||
assertUnique(items.map((item) => item.id), "Subject ids");
|
||||
return {
|
||||
const hasMore = requiredBoolean(record, "has_more");
|
||||
const nextCursor = optionalNullableString(record, "next_cursor");
|
||||
assertCursorInvariant(hasMore, nextCursor, "Subject list");
|
||||
const result: SubjektivSubjectListResponse = {
|
||||
limit,
|
||||
items,
|
||||
has_more: requiredBoolean(record, "has_more"),
|
||||
has_more: hasMore,
|
||||
};
|
||||
if (nextCursor !== undefined) result.next_cursor = nextCursor;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseSubjektivResidentSurfaceResponse(
|
||||
@@ -562,7 +574,9 @@ export function parseSubjektivMemoryReadResponse(
|
||||
).map(parseMemoryEvidenceCandidate);
|
||||
if (
|
||||
sourceCandidateIds.length !== sourceCandidates.length ||
|
||||
sourceCandidateIds.some((id, index) => sourceCandidates[index]?.candidate_id !== id)
|
||||
sourceCandidateIds.some((id, index) =>
|
||||
sourceCandidates[index]?.candidate_id !== id
|
||||
)
|
||||
) {
|
||||
invalid("source candidate ids do not match source candidates");
|
||||
}
|
||||
@@ -690,7 +704,9 @@ function parseMemoryEvidence(value: unknown): SubjektivMemoryEvidence {
|
||||
: parseEntryRange(record.entry_range, "entry_range");
|
||||
}
|
||||
if ("origin" in record) {
|
||||
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
|
||||
result.origin = record.origin === null
|
||||
? null
|
||||
: parseEvidenceOrigin(record.origin);
|
||||
}
|
||||
if ("excerpt" in record) result.excerpt = nullableString(record, "excerpt");
|
||||
if ("summary" in record) result.summary = nullableString(record, "summary");
|
||||
@@ -735,7 +751,9 @@ function parseMemorySourceEvidenceRef(
|
||||
: parseEntryRange(record.entry_range, "entry_range");
|
||||
}
|
||||
if ("origin" in record) {
|
||||
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
|
||||
result.origin = record.origin === null
|
||||
? null
|
||||
: parseEvidenceOrigin(record.origin);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
function subjectHref(subjectId: string): string {
|
||||
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(subjectId)}`);
|
||||
}
|
||||
|
||||
function pageHref(cursor?: string | null): string {
|
||||
const path = workspaceRoute(data.workspaceId, '/memory');
|
||||
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -33,6 +38,7 @@
|
||||
<div class="memory-state" role="status" data-memory-state="empty">
|
||||
<strong>No Memory subjects.</strong>
|
||||
<p>Subjects will appear here after they are created for this Workspace.</p>
|
||||
{#if data.cursor}<p><a href={pageHref()}>Return to the first page</a></p>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="subject-list" aria-label="Memory subjects">
|
||||
@@ -51,9 +57,12 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.subjects.data.has_more}
|
||||
<p class="memory-note">Only the first {data.subjects.data.limit} subjects are shown.</p>
|
||||
{/if}
|
||||
<nav class="memory-pagination" aria-label="Subject pages">
|
||||
{#if data.cursor}<a href={pageHref()}>First page</a>{/if}
|
||||
{#if data.subjects.data.has_more && data.subjects.data.next_cursor}
|
||||
<a href={pageHref(data.subjects.data.next_cursor)}>Next page →</a>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
{:else if data.subjects.error}
|
||||
<div class="memory-state is-error" role="alert" data-memory-state="error">
|
||||
@@ -203,8 +212,15 @@
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.memory-state p,
|
||||
.memory-note {
|
||||
.memory-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--font-size-compact);
|
||||
}
|
||||
|
||||
.memory-state p {
|
||||
margin: var(--space-1) 0 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,25 @@ import {
|
||||
} from "$lib/workspace/memory/api";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const MAX_CURSOR_BYTES = 16_384;
|
||||
|
||||
function boundedCursor(value: string | null): string | null {
|
||||
if (value === null || value.length === 0) return null;
|
||||
return new TextEncoder().encode(value).byteLength <= MAX_CURSOR_BYTES
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params, url }) => {
|
||||
const cursor = boundedCursor(url.searchParams.get("cursor"));
|
||||
const query = new URLSearchParams({ limit: "100" });
|
||||
if (cursor) query.set("cursor", cursor);
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
cursor,
|
||||
subjects: await loadJson(
|
||||
fetch,
|
||||
`${workspaceApiPath(params.workspaceId, "/subjektiv/subjects")}?limit=100`,
|
||||
`${workspaceApiPath(params.workspaceId, "/subjektiv/subjects")}?${query}`,
|
||||
undefined,
|
||||
parseSubjektivSubjectListResponse,
|
||||
MEMORY_API_LOAD_POLICY,
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
);
|
||||
}
|
||||
|
||||
function memoryPageHref(cursor?: string | null): string {
|
||||
const path = workspaceRoute(
|
||||
data.workspaceId,
|
||||
`/memory/${encodeURIComponent(data.subjectId)}`,
|
||||
);
|
||||
return cursor ? `${path}?cursor=${encodeURIComponent(cursor)}` : path;
|
||||
}
|
||||
|
||||
function stateLabel(state: SubjektivMemoryState): string {
|
||||
return state[0].toUpperCase() + state.slice(1);
|
||||
}
|
||||
@@ -152,6 +160,7 @@
|
||||
<div class="memory-state" role="status" data-memories-empty>
|
||||
<strong>No committed Memories.</strong>
|
||||
<p>This subject has no current Memory revisions.</p>
|
||||
{#if data.cursor}<p><a href={memoryPageHref()}>Return to the first page</a></p>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="memory-list" aria-label="Current committed Memories">
|
||||
@@ -173,9 +182,12 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.memories.data.has_more}
|
||||
<p class="memory-note">More current Memories are available after this bounded page.</p>
|
||||
{/if}
|
||||
<nav class="memory-pagination" aria-label="Current Memory pages">
|
||||
{#if data.cursor}<a href={memoryPageHref()}>First page</a>{/if}
|
||||
{#if data.memories.data.has_more && data.memories.data.next_cursor}
|
||||
<a href={memoryPageHref(data.memories.data.next_cursor)}>Next page →</a>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
{:else if data.memories.error}
|
||||
<div class="memory-state is-error" role="alert">
|
||||
@@ -388,8 +400,7 @@
|
||||
font-size: var(--font-size-body);
|
||||
}
|
||||
|
||||
.memory-row p,
|
||||
.memory-note {
|
||||
.memory-row p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -417,6 +428,14 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.memory-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--font-size-compact);
|
||||
}
|
||||
|
||||
.memory-state strong {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,21 @@ import {
|
||||
} from "$lib/workspace/memory/api";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const MAX_CURSOR_BYTES = 16_384;
|
||||
|
||||
function boundedCursor(value: string | null): string | null {
|
||||
if (value === null || value.length === 0) return null;
|
||||
return new TextEncoder().encode(value).byteLength <= MAX_CURSOR_BYTES
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params, url }) => {
|
||||
const subjectId = params.subjectId;
|
||||
const subjectPath = `/subjektiv/subjects/${encodeURIComponent(subjectId)}`;
|
||||
const cursor = boundedCursor(url.searchParams.get("cursor"));
|
||||
const memoryQuery = new URLSearchParams({ limit: "100" });
|
||||
if (cursor) memoryQuery.set("cursor", cursor);
|
||||
const [subject, surface, memories] = await Promise.all([
|
||||
loadJson(
|
||||
fetch,
|
||||
@@ -27,7 +39,9 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||
),
|
||||
loadJson(
|
||||
fetch,
|
||||
`${workspaceApiPath(params.workspaceId, `${subjectPath}/memories`)}?limit=100`,
|
||||
`${
|
||||
workspaceApiPath(params.workspaceId, `${subjectPath}/memories`)
|
||||
}?${memoryQuery}`,
|
||||
undefined,
|
||||
parseSubjektivMemoryQueryResponse,
|
||||
MEMORY_API_LOAD_POLICY,
|
||||
@@ -37,6 +51,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
subjectId,
|
||||
cursor,
|
||||
subject,
|
||||
surface,
|
||||
memories,
|
||||
|
||||
+26
-2
@@ -28,6 +28,14 @@
|
||||
return `${memoryPath()}?revision=${revision}`;
|
||||
}
|
||||
|
||||
function revisionPageHref(cursor?: string | null): string {
|
||||
const query = new URLSearchParams();
|
||||
if (memory) query.set('revision', String(memory.revision));
|
||||
if (cursor) query.set('revision_cursor', cursor);
|
||||
const encoded = query.toString();
|
||||
return encoded ? `${memoryPath()}?${encoded}` : memoryPath();
|
||||
}
|
||||
|
||||
function derivedHref(reference: SubjektivMemoryRevisionRef): string {
|
||||
return `${memoryPath(reference.memory_id)}?revision=${reference.revision}`;
|
||||
}
|
||||
@@ -221,7 +229,10 @@
|
||||
<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>
|
||||
{#if data.revisions.data}
|
||||
{#if revisions.length === 0}
|
||||
<p class="empty-copy">No revision history is available.</p>
|
||||
<div>
|
||||
<p class="empty-copy">No revision history is available.</p>
|
||||
{#if data.revisionCursor}<p><a href={revisionPageHref()}>Return to the first page</a></p>{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<ol class="revision-list">
|
||||
{#each revisions as revision (revision.revision)}
|
||||
@@ -234,7 +245,12 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{#if data.revisions.data.has_more}<p class="bounded-note">More immutable revisions are available after this bounded page.</p>{/if}
|
||||
<nav class="memory-pagination" aria-label="Revision history pages">
|
||||
{#if data.revisionCursor}<a href={revisionPageHref()}>First page</a>{/if}
|
||||
{#if data.revisions.data.has_more && data.revisions.data.next_cursor}
|
||||
<a href={revisionPageHref(data.revisions.data.next_cursor)}>Next page →</a>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
{:else if data.revisions.error}
|
||||
<div class="memory-state is-error" role="alert"><strong>Revision history unavailable.</strong><p>{data.revisions.error}</p></div>
|
||||
@@ -433,6 +449,14 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.memory-pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--font-size-compact);
|
||||
}
|
||||
|
||||
.candidate-list {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
|
||||
@@ -8,6 +8,15 @@ import {
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
const SAFE_INTEGER_TEXT = /^(0|[1-9][0-9]{0,15})$/;
|
||||
const MAX_CURSOR_BYTES = 16_384;
|
||||
|
||||
function boundedCursor(
|
||||
value: string | null,
|
||||
maxBytes = MAX_CURSOR_BYTES,
|
||||
): string | null {
|
||||
if (value === null || value.length === 0) return null;
|
||||
return new TextEncoder().encode(value).byteLength <= maxBytes ? value : null;
|
||||
}
|
||||
|
||||
function safeIntegerQuery(
|
||||
source: URLSearchParams,
|
||||
@@ -39,13 +48,14 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
|
||||
const value = safeIntegerQuery(url.searchParams, key, positive);
|
||||
if (value !== null) detailQuery.set(key, value);
|
||||
}
|
||||
const evidenceCursor = url.searchParams.get("evidence_cursor");
|
||||
if (
|
||||
evidenceCursor !== null && evidenceCursor.length > 0 &&
|
||||
new TextEncoder().encode(evidenceCursor).byteLength <= 512
|
||||
) {
|
||||
detailQuery.set("evidence_cursor", evidenceCursor);
|
||||
}
|
||||
const evidenceCursor = boundedCursor(
|
||||
url.searchParams.get("evidence_cursor"),
|
||||
512,
|
||||
);
|
||||
if (evidenceCursor) detailQuery.set("evidence_cursor", evidenceCursor);
|
||||
const revisionCursor = boundedCursor(url.searchParams.get("revision_cursor"));
|
||||
const revisionQuery = new URLSearchParams({ limit: "100" });
|
||||
if (revisionCursor) revisionQuery.set("cursor", revisionCursor);
|
||||
|
||||
const [subject, memory, revisions] = await Promise.all([
|
||||
loadJson(
|
||||
@@ -64,7 +74,9 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
|
||||
),
|
||||
loadJson(
|
||||
fetch,
|
||||
`${workspaceApiPath(params.workspaceId, `${memoryPath}/revisions`)}?limit=100`,
|
||||
`${
|
||||
workspaceApiPath(params.workspaceId, `${memoryPath}/revisions`)
|
||||
}?${revisionQuery}`,
|
||||
undefined,
|
||||
(value) => parseSubjektivMemoryListRevisionsResponse(value, memoryId),
|
||||
MEMORY_API_LOAD_POLICY,
|
||||
@@ -75,6 +87,7 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
|
||||
workspaceId: params.workspaceId,
|
||||
subjectId,
|
||||
memoryId,
|
||||
revisionCursor,
|
||||
subject,
|
||||
memory,
|
||||
revisions,
|
||||
|
||||
@@ -28,7 +28,13 @@ test("renders the subject index as the Memory product entry", () => {
|
||||
render(SubjectIndexPage, {
|
||||
data: {
|
||||
workspaceId: "workspace-1",
|
||||
subjects: result({ limit: 100, items: [subject()], has_more: false }),
|
||||
cursor: "subject-current",
|
||||
subjects: result({
|
||||
limit: 100,
|
||||
items: [subject()],
|
||||
next_cursor: "subject-next",
|
||||
has_more: true,
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
|
||||
@@ -37,6 +43,10 @@ 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.getByRole("link", { name: "First page" }).getAttribute("href"))
|
||||
.toBe("/w/workspace-1/memory");
|
||||
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
|
||||
.toContain("cursor=subject-next");
|
||||
expect(screen.queryByText("Staging")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -45,6 +55,7 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
data: {
|
||||
workspaceId: "workspace-1",
|
||||
subjectId: "subject-1",
|
||||
cursor: "memory-current",
|
||||
subject: result(subject()),
|
||||
surface: result({
|
||||
subject_id: "subject-1",
|
||||
@@ -87,7 +98,8 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
updated_at: "2026-01-02T03:04:05Z",
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
next_cursor: "memory-next",
|
||||
has_more: true,
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
@@ -101,8 +113,12 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
|
||||
expect(screen.getAllByText("active", { exact: true }).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
expect(screen.getByText("resolved", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("retracted", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("Resolved", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByText("Retracted", { exact: true })).not.toBeNull();
|
||||
expect(screen.getByRole("navigation", { name: "Current Memory pages" }))
|
||||
.not.toBeNull();
|
||||
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
|
||||
.toContain("cursor=memory-next");
|
||||
});
|
||||
|
||||
test("distinguishes ready-empty, stale, failed, unavailable, and request error surfaces", async () => {
|
||||
@@ -177,6 +193,7 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
|
||||
workspaceId: "workspace-1",
|
||||
subjectId: "subject-1",
|
||||
memoryId: "memory-1",
|
||||
revisionCursor: "revision-current",
|
||||
subject: result(subject()),
|
||||
memory: result({
|
||||
memory_id: "memory-1",
|
||||
@@ -233,7 +250,8 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
|
||||
change_reason: "Resolved",
|
||||
updated_at: "2026-01-02T00:00:00Z",
|
||||
}],
|
||||
has_more: false,
|
||||
next_cursor: "revision-next",
|
||||
has_more: true,
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
@@ -248,4 +266,8 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
|
||||
expect(screen.getByRole("heading", { name: "Revision history" })).not
|
||||
.toBeNull();
|
||||
expect(screen.getByText("Historical revision")).not.toBeNull();
|
||||
expect(screen.getByRole("navigation", { name: "Revision history pages" }))
|
||||
.not.toBeNull();
|
||||
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
|
||||
.toContain("revision_cursor=revision-next");
|
||||
});
|
||||
|
||||
@@ -160,7 +160,8 @@ 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",
|
||||
@@ -170,6 +171,22 @@ Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bo
|
||||
Deno.test("Subject list parser preserves the exact bounded response", () => {
|
||||
const response = { limit: 100, items: [subject()], has_more: false };
|
||||
assertEquals(parseSubjektivSubjectListResponse(response), response);
|
||||
const nextPage = {
|
||||
limit: 1,
|
||||
items: [subject()],
|
||||
next_cursor: "subjektiv.subjects.next",
|
||||
has_more: true,
|
||||
};
|
||||
assertEquals(parseSubjektivSubjectListResponse(nextPage), nextPage);
|
||||
assertThrows(
|
||||
() =>
|
||||
parseSubjektivSubjectListResponse({
|
||||
limit: 1,
|
||||
items: [subject()],
|
||||
has_more: true,
|
||||
}),
|
||||
"cursor does not match has_more",
|
||||
);
|
||||
assertThrows(
|
||||
() =>
|
||||
parseSubjektivSubjectListResponse({
|
||||
@@ -278,7 +295,8 @@ 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",
|
||||
|
||||
Reference in New Issue
Block a user