Merge develop into T-693 and reconcile Subject behavior with Memory information

Normal merge of develop@b0bce50b retains T-692 state hierarchy, diagnostics disclosures, ready-empty and candidate-only regressions alongside T-693 behavior/CAS/route fences. ResidentContext load and refresh retain the five-second timeout from 685b8aa6, with a seven-request regression.

Validation: root cargo check; full worker(serial), session-store, workspace-server and server-api(typescript) suites; frontend check with zero diagnostics; 464 Web tests and 171 component tests (19 Memory); production Memory browser-shell test/build; fmt/diff/OpenAPI checks.
This commit is contained in:
2026-10-05 17:31:17 +09:00
49 changed files with 2998 additions and 299 deletions
@@ -33,6 +33,7 @@ import {
parseWorkerLaunchOptionsResponse,
parseWorkerSummary,
} from "./workers.ts";
import { loadJson } from "./http.ts";
const worker = {
runtime_id: "runtime-a",
@@ -111,6 +112,92 @@ Deno.test("Worker summary parser enforces observation freshness", () => {
assertEquals(observedStopped.state, "stopped");
});
const workerStates = [
{ kind: "idle" },
{ kind: "busy", state: { kind: "run", state: "running" } },
{ kind: "busy", state: { kind: "run", state: "pausing" } },
{ kind: "busy", state: { kind: "run", state: "paused" } },
{ kind: "busy", state: { kind: "run", state: "cancelling" } },
{ kind: "busy", state: { kind: "maintenance", state: "compacting" } },
] as const;
Deno.test("Worker summary parser preserves the optional submission completion fence in every state", () => {
for (const state of workerStates) {
for (const completion of [
{},
{ last_finished_submission_request_id: null },
{ last_finished_submission_request_id: "completed-submit" },
]) {
const snapshot = { last_command_id: 7, ...completion, state };
const parsed = parseWorkerSummary({
...worker,
resource_key: "W-1",
availability: "observed",
worker_state: snapshot,
});
assertEquals(parsed.worker_state, snapshot);
}
}
});
Deno.test("Worker summary parser validates submission completion fences without accepting unknown fields", () => {
for (const state of workerStates) {
for (const invalid of [0, false, {}, [], "x".repeat(65_537)]) {
assertThrows(
() => parseWorkerSummary({
...worker,
resource_key: "W-1",
availability: "observed",
worker_state: {
last_command_id: 7,
last_finished_submission_request_id: invalid,
state,
},
}),
Error,
"worker_state.last_finished_submission_request_id",
);
}
assertThrows(
() => parseWorkerSummary({
...worker,
resource_key: "W-1",
availability: "observed",
worker_state: {
last_command_id: 7,
last_finished_submission_request_id: "completed-submit",
state,
unexpected: true,
},
}),
Error,
"unknown field unexpected",
);
}
});
Deno.test("Console Worker API loading accepts a completed submission snapshot", async () => {
const snapshot = {
last_command_id: 0,
last_finished_submission_request_id: "completed-submit",
state: { kind: "idle" },
};
const result = await loadJson(
() => Promise.resolve(Response.json({
...worker,
resource_key: "W-1",
availability: "observed",
worker_state: snapshot,
})),
"/api/w/workspace-a/workers/W-1",
undefined,
parseWorkerSummary,
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
);
assertEquals(result.error, null);
assertEquals(result.data?.worker_state, snapshot);
});
Deno.test("Worker summary parser preserves attachment-effective External permissions", () => {
const attachment = {
alias: "sessions",
+12 -1
View File
@@ -262,7 +262,16 @@ function workerStateSnapshot(
label: string,
): WorkerStateSnapshot {
const snapshot = record(value, label);
exact(snapshot, ["last_command_id", "state"], label);
exact(
snapshot,
["last_command_id", "last_finished_submission_request_id", "state"],
label,
);
const lastFinishedSubmissionRequestId = optional(
snapshot.last_finished_submission_request_id,
`${label}.last_finished_submission_request_id`,
string,
);
const state = record(snapshot.state, `${label}.state`);
const kind = string(state.kind, `${label}.state.kind`);
if (kind === "idle") {
@@ -272,6 +281,7 @@ function workerStateSnapshot(
snapshot.last_command_id,
`${label}.last_command_id`,
),
last_finished_submission_request_id: lastFinishedSubmissionRequestId,
state: { kind: "idle" },
};
}
@@ -291,6 +301,7 @@ function workerStateSnapshot(
snapshot.last_command_id,
`${label}.last_command_id`,
),
last_finished_submission_request_id: lastFinishedSubmissionRequestId,
state: {
kind: "busy",
state: busyKind === "run"
@@ -0,0 +1,231 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, waitFor } from "@testing-library/svelte";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import ConfigSourceEditor from "./ConfigSourceEditor.svelte";
const toolchain = vi.hoisted(() => ({
construct: vi.fn(),
setSnapshot: vi.fn(),
analyze: vi.fn(),
complete: vi.fn(),
close: vi.fn(),
}));
vi.mock("./toolchain.ts", () => ({
ConfigSourceToolchain: class {
constructor() {
toolchain.construct();
}
setSnapshot = toolchain.setSnapshot;
analyze = toolchain.analyze;
complete = toolchain.complete;
close = toolchain.close;
},
}));
const schemaSource = "schema body\n".repeat(6000);
const tree = {
snapshot: {
revision: 7,
digest: "sha256:tree",
entries: {
"main.dcdl": {
path: "main.dcdl",
content_type: "decodal",
content: "WorkspaceConfigSchema({})\n",
content_digest: "sha256:main",
},
},
},
contract: {
contract_version: 1,
decodal_version: "0.4.0",
schema_version: 1,
entrypoints: ["main.dcdl"],
import_policy_version: 1,
schema_bundle: {
contributions: [{
provider_id: "builtin",
namespace: "workspace",
version: "1",
source: schemaSource,
source_digest: "sha256:contribution",
}],
source: schemaSource,
fingerprint: "sha256:schema",
},
fingerprint: "sha256:toolchain",
},
projection_digest: "sha256:projection",
};
const fetcher = vi.fn();
const response = () => new Response(JSON.stringify(tree));
beforeEach(() => {
vi.resetAllMocks();
vi.stubGlobal("fetch", fetcher);
fetcher.mockImplementation(async () => response());
toolchain.setSnapshot.mockResolvedValue(undefined);
toolchain.analyze.mockResolvedValue([]);
toolchain.complete.mockResolvedValue(null);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
function renderEditor(workspaceId = "w") {
return render(ConfigSourceEditor, { workspaceId });
}
function expectNoEditor(view: ReturnType<typeof renderEditor>) {
expect(view.queryByText("Select or create a source")).toBeNull();
expect(view.queryByRole("button", { name: "Create local source" }))
.toBeNull();
expect(view.queryByRole("button", { name: "Commit" })).toBeNull();
expect(view.container.querySelector(".cm-editor")).toBeNull();
}
async function expectReady(view: ReturnType<typeof renderEditor>) {
await waitFor(() =>
expect(view.getByRole("button", { name: "main.dcdl entrypoint" })).not
.toBeNull()
);
expect(view.container.querySelector('.cm-content[contenteditable="true"]'))
.not.toBeNull();
expect(view.container.textContent).toContain("WorkspaceConfigSchema({})");
expect(view.queryByRole("alert")).toBeNull();
}
test("loading is not an empty source tree; editing waits for schema installation", async () => {
let finishFetch!: (response: Response) => void;
let finishSetup!: () => void;
fetcher.mockReturnValueOnce(
new Promise<Response>((resolve) => {
finishFetch = resolve;
}),
);
toolchain.setSnapshot.mockReturnValueOnce(
new Promise<void>((resolve) => {
finishSetup = resolve;
}),
);
const view = render(ConfigSourceEditor, { workspaceId: "w/one" });
expect(view.getByRole("status").textContent).toContain("Loading source tree");
expectNoEditor(view);
finishFetch(response());
await waitFor(() =>
expect(toolchain.setSnapshot).toHaveBeenCalledWith(
tree.snapshot,
tree.contract.schema_bundle,
)
);
expectNoEditor(view);
finishSetup();
await expectReady(view);
expect(fetcher).toHaveBeenCalledWith(
"/api/w/w%2Fone/config/source-tree",
expect.anything(),
);
});
test.each(["http", "invalid"])(
"%s failure has a manual reload, not creation controls or automatic retries",
async (failure) => {
fetcher.mockResolvedValueOnce(
failure === "http"
? new Response("Backend unavailable", { status: 503 })
: new Response(JSON.stringify({ ...tree, unexpected: true })),
);
const view = renderEditor();
await waitFor(() =>
expect(view.getByRole("alert").textContent).toContain(
"Unable to load workspace configuration",
)
);
expect(view.getByRole("alert").textContent).toContain(
failure === "http" ? "Backend unavailable" : "invalid response",
);
expectNoEditor(view);
expect(toolchain.setSnapshot).not.toHaveBeenCalled();
await new Promise((resolve) => setTimeout(resolve, 300));
expect(fetcher).toHaveBeenCalledOnce();
await fireEvent.click(
view.getByRole("button", { name: "Reload source tree" }),
);
await expectReady(view);
expect(fetcher).toHaveBeenCalledTimes(2);
},
);
test.each(["construct", "setSnapshot"] as const)(
"toolchain %s failure is caught and manual reload initializes a fresh toolchain",
async (phase) => {
if (phase === "construct") {
toolchain.construct.mockImplementationOnce(() => {
throw new Error("Worker unavailable");
});
} else {toolchain.setSnapshot.mockRejectedValueOnce(
new Error("Schema setup failed"),
);}
const view = renderEditor();
await waitFor(() =>
expect(view.getByRole("alert").textContent).toContain(
phase === "construct" ? "Worker unavailable" : "Schema setup failed",
)
);
expectNoEditor(view);
await fireEvent.click(
view.getByRole("button", { name: "Reload source tree" }),
);
await expectReady(view);
expect(toolchain.construct).toHaveBeenCalledTimes(2);
},
);
test("a successfully loaded empty tree still offers source creation", async () => {
fetcher.mockResolvedValueOnce(
new Response(JSON.stringify({
...tree,
snapshot: { ...tree.snapshot, entries: {} },
})),
);
const view = renderEditor();
await waitFor(() =>
expect(view.getByText("Select or create a source")).not.toBeNull()
);
expect(view.getByRole("button", { name: "Create local source" })).not
.toBeNull();
expect(view.queryByRole("alert")).toBeNull();
});
test("unmount during schema setup closes the toolchain", async () => {
let failSetup!: (error: Error) => void;
toolchain.setSnapshot.mockReturnValueOnce(
new Promise<void>((_, reject) => {
failSetup = reject;
}),
);
toolchain.close.mockImplementationOnce(() =>
failSetup(new Error("toolchain closed"))
);
const view = renderEditor();
await waitFor(() => expect(toolchain.setSnapshot).toHaveBeenCalledOnce());
view.unmount();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(toolchain.close).toHaveBeenCalledOnce();
});
test("unmount during fetch does not start a toolchain", async () => {
let finishFetch!: (response: Response) => void;
fetcher.mockReturnValueOnce(
new Promise<Response>((resolve) => {
finishFetch = resolve;
}),
);
const view = renderEditor();
view.unmount();
finishFetch(response());
await new Promise((resolve) => setTimeout(resolve, 0));
expect(toolchain.construct).not.toHaveBeenCalled();
});
@@ -21,6 +21,8 @@
let newPath = $state("module.dcdl");
let diagnostics = $state<ConfigDiagnostic[]>([]);
let status = $state("Loading source tree…");
let loadState = $state<"loading" | "error" | "ready">("loading");
let mounted = false;
let busy = $state(false);
let workingChanges = $state<ConfigTreeChange[]>([]);
let baseRevision = $state(0);
@@ -42,9 +44,12 @@
const dirty = $derived(workingChanges.length > 0 || (selected ? source !== selected.content : source.length > 0));
onMount(() => {
toolchain = new ConfigSourceToolchain();
mounted = true;
void reload();
return () => toolchain?.close();
return () => {
mounted = false;
toolchain?.close();
};
});
$effect(() => {
@@ -70,9 +75,20 @@
});
async function reload() {
loadState = "loading";
status = "Loading source tree…";
analysisReady = false;
analysisGeneration += 1;
try {
treeState = await fetchConfigTree(workspaceId);
const remote = await fetchConfigTree(workspaceId);
if (!mounted) return;
toolchain?.close();
toolchain = null;
toolchain = new ConfigSourceToolchain();
await toolchain.setSnapshot(remote.snapshot, remote.contract.schema_bundle);
if (!mounted) return;
// Publish the new baseline only after both fetch and toolchain setup succeed.
treeState = remote;
if (!selectedPath || !treeState.snapshot.entries[selectedPath]) {
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
}
@@ -80,15 +96,19 @@
baseSnapshot = $state.snapshot(treeState.snapshot);
baseRevision = treeState.snapshot.revision;
baseDigest = treeState.snapshot.digest;
await toolchain?.setSnapshot(treeState.snapshot, treeState.contract.schema_bundle);
analysisReady = true;
workingChanges = [];
renamePath = selectedPath;
diagnostics = [];
conflict = false;
status = `Revision ${treeState.snapshot.revision} · ${treeState.snapshot.digest.slice(0, 20)}…`;
loadState = "ready";
} catch (error) {
if (!mounted) return;
toolchain?.close();
toolchain = null;
status = String(error);
loadState = "error";
}
}
@@ -213,8 +233,6 @@
}
async function discardAndReload() {
workingChanges = [];
source = "";
await reload();
}
@@ -287,7 +305,18 @@
}
</script>
<section class="config-source-shell" aria-label="Workspace configuration source tree">
<section class="config-source-shell" aria-label="Workspace configuration source tree" aria-busy={loadState === "loading"}>
{#if loadState === "loading"}
<div class="config-source-load" role="status">Loading source tree…</div>
{:else if loadState === "error"}
<div class="config-source-load">
<div role="alert">
<strong>Unable to load workspace configuration</strong>
<p>{status}</p>
</div>
<button type="button" onclick={reload}>Reload source tree</button>
</div>
{:else}
<aside class="config-source-tree">
<div class="config-source-tree__header">
<strong>Source tree</strong>
@@ -354,4 +383,12 @@
</ol>
{/if}
</div>
{/if}
</section>
<style>
.config-source-load {
grid-column: 1 / -1;
padding: 1rem;
}
</style>
@@ -11,6 +11,9 @@ import type {
} from "./types.ts";
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
// Schema bodies are documents, not short metadata. Keep them within the
// response budget; readJson also bounds the combined bundle and contributions.
const MAX_SCHEMA_SOURCE_BYTES = MAX_RESPONSE_BYTES;
const MAX_ERROR_BYTES = 4 * 1024;
const MAX_ENTRY_COUNT = 256;
const MAX_ENTRY_BYTES = 256 * 1024;
@@ -193,7 +196,7 @@ function parseContribution(value: unknown): ConfigSchemaContribution {
provider_id: boundedString(item.provider_id),
namespace: boundedString(item.namespace),
version: boundedString(item.version),
source: boundedString(item.source),
source: boundedString(item.source, MAX_SCHEMA_SOURCE_BYTES),
...(projection === undefined ? {} : { projection_validator: projection }),
source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES),
};
@@ -203,7 +206,7 @@ function parseSchemaBundle(value: unknown): WorkspaceConfigSchemaBundle {
const item = record(value, ["contributions", "source", "fingerprint"]);
return {
contributions: boundedArray(item.contributions).map(parseContribution),
source: boundedString(item.source),
source: boundedString(item.source, MAX_SCHEMA_SOURCE_BYTES),
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
};
}
@@ -6,6 +6,23 @@ import WorkerContextStatus from "./WorkerContextStatus.svelte";
afterEach(cleanup);
test("the three-dot Details control invokes the existing toggle and reflects its state", async () => {
let toggles = 0;
const view = render(WorkerContextStatus, {
metadata: null,
detailsOpen: false,
onToggleDetails: () => { toggles++; },
});
const button = view.getByRole("button", { name: "Details" });
expect(button.querySelectorAll("circle")).toHaveLength(3);
expect(button.textContent?.trim()).toBe("");
expect(button.getAttribute("aria-expanded")).toBe("false");
button.click();
expect(toggles).toBe(1);
await view.rerender({ detailsOpen: true });
expect(button.getAttribute("aria-expanded")).toBe("true");
});
test("renders resolved model, reasoning, and measured context accessibly", () => {
const view = render(WorkerContextStatus, {
metadata: {
@@ -19,13 +36,13 @@ test("renders resolved model, reasoning, and measured context accessibly", () =>
const status = view.getByLabelText("Worker model and context");
expect(status.textContent).toContain("gpt-6-astra · high");
expect(status.textContent).toContain("Context 142.0k / 272.0k (52%)");
expect(status.textContent).toContain("142.0k / 272.0k (52%)");
});
test("renders honest unavailable state instead of a fabricated zero percent", async () => {
const view = render(WorkerContextStatus, { metadata: null });
expect(view.getByText("Model unavailable")).not.toBeNull();
expect(view.getByText("Context unavailable")).not.toBeNull();
expect(view.getByText("unavailable")).not.toBeNull();
expect(view.container.textContent).not.toContain("0%");
await view.rerender({
@@ -38,7 +55,7 @@ test("renders honest unavailable state instead of a fabricated zero percent", as
},
});
expect(view.container.textContent).toContain("claude · 8.2k token budget");
expect(view.container.textContent).toContain("Context ~12.0k / 64.0k (19%)");
expect(view.container.textContent).toContain("~12.0k / 64.0k (19%)");
await view.rerender({
metadata: {
@@ -1,4 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { ConsoleWorkerMetadata } from "./model";
import {
formatContextSummary,
@@ -7,23 +8,51 @@
type Props = {
metadata: ConsoleWorkerMetadata | null;
controls?: Snippet;
detailsOpen?: boolean;
onToggleDetails?: () => void;
};
let { metadata }: Props = $props();
let { metadata, controls, detailsOpen = false, onToggleDetails }: Props = $props();
const modelSummary = $derived(formatModelSummary(metadata));
const contextSummary = $derived(formatContextSummary(metadata));
</script>
<div class="worker-context-status" aria-label="Worker model and context">
<span class="worker-model" title={modelSummary}>{modelSummary}</span>
<span class="separator" aria-hidden="true">|</span>
<span class="separator" aria-hidden="true">·</span>
<span class="worker-context" title={contextSummary}>{contextSummary}</span>
{#if controls || onToggleDetails}
<div class="worker-context-actions">
{@render controls?.()}
{#if onToggleDetails}
<button
type="button"
class="details-button"
aria-label="Details"
aria-expanded={detailsOpen}
onclick={onToggleDetails}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="5" cy="12" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="19" cy="12" r="2" />
</svg>
</button>
{/if}
</div>
{/if}
</div>
<style>
.worker-context-status {
display: flex;
flex: 0 0 auto;
flex-wrap: nowrap;
min-width: 0;
overflow: hidden;
white-space: nowrap;
padding-inline: var(--space-3);
align-items: center;
gap: var(--space-2);
color: var(--text-muted);
@@ -50,20 +79,44 @@
color: var(--line-strong);
}
@media (max-width: 640px) {
.worker-context-status {
width: 100%;
flex-wrap: wrap;
row-gap: 0;
}
.worker-context-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
.separator {
display: none;
}
.details-button {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: var(--space-5);
height: var(--space-5);
padding: 0;
border: 0;
border-radius: var(--space-1);
background: transparent;
color: var(--text-muted);
cursor: pointer;
}
.worker-model,
.worker-context {
flex: 1 1 100%;
}
.details-button:hover,
.details-button[aria-expanded="true"] {
background: var(--bg-subtle);
color: var(--text-strong);
}
.details-button:focus-visible {
color: var(--text-strong);
outline: 1px solid currentColor;
outline-offset: -2px;
}
.details-button svg {
width: 16px;
height: 16px;
fill: currentColor;
}
</style>
@@ -3769,6 +3769,29 @@ Deno.test("committed tool identities reconcile one live call block", () => {
);
});
Deno.test("image rejection correction updates the existing tool result without duplicate rows", () => {
const result: SessionSnapshotEntry = {
kind: "tool_result", entry_id: "result-image", timestamp: 2,
provenance: "tool_output", call_id: "image", summary: "Attached image", is_error: false,
attachments: [{ attachment_id: "image-body", media_type: "image/png", byte_len: 100 }],
};
const lines = projectConsole([
{ eventId: "call", event: { event: "session_entry_committed", data: { entry: {
kind: "tool_call", entry_id: "call-image", timestamp: 1, provenance: "model_output",
call_id: "image", name: "ViewImage", arguments: "{}",
} } } },
{ eventId: "original", event: { event: "session_entry_committed", data: { entry: result } } },
{ eventId: "correction", event: { event: "session_entry_committed", data: { entry: {
...result, timestamp: 3, summary: "Resize or crop and retry ViewImage", is_error: true, attachments: [],
} } } },
]).lines;
assertEquals(lines.length, 1);
assertEquals(lines[0].entryId, "result-image");
assertEquals(lines[0].toolCall?.state, "error");
assertEquals(lines[0].attachments ?? [], []);
assert(lines[0].body.includes("Resize or crop"), "show the model recovery instruction");
});
function readHistory(ids: string[], summary: string): ConsoleLine[] {
const entries = ids.flatMap((id): SessionSnapshotEntry[] => [{
kind: "tool_call", entry_id: `call-entry-${id}`, timestamp: 1,
@@ -45,7 +45,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
contextTokens: 142_000,
contextSource: "estimated",
}),
"Context ~142.0k / 272.0k (52%)",
"~142.0k / 272.0k (52%)",
);
assertEquals(formatModelSummary(null), "Model unavailable");
assertEquals(
@@ -64,7 +64,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
formatReasoning({ kind: "budget_tokens", budget_tokens: -2 }),
"reasoning unavailable",
);
assertEquals(formatContextSummary(null), "Context unavailable");
assertEquals(formatContextSummary(null), "unavailable");
assertEquals(
formatContextSummary({
model: null,
@@ -73,7 +73,7 @@ Deno.test("worker metadata formats effort, budget, estimate, and unknown safely"
contextTokens: 0,
contextSource: null,
}),
"Context unavailable",
"unavailable",
);
});
@@ -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",
@@ -738,6 +743,32 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
);
});
Deno.test("Model information stays below Composer in one compact monospace row", async () => {
const page = await Deno.readTextFile(new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
));
const composer = page.indexOf('<form class="console-composer"');
const composerEnd = page.indexOf("</form>", composer);
const status = page.indexOf("<WorkerContextStatus");
assert(composer >= 0 && composerEnd > composer && status > composerEnd, "Model information must follow Composer");
assert(page.includes("metadata={selectedConsoleProjection.workerMetadata}"), "Keep selected Worker metadata");
assert(page.indexOf("<WorkerContextStatus", status + 1) === -1, "Render model information only once");
const component = await Deno.readTextFile(new URL("./WorkerContextStatus.svelte", import.meta.url));
for (const rule of [
"flex-wrap: nowrap", "white-space: nowrap", "text-overflow: ellipsis",
"font-family: var(--font-mono)", "font-size: var(--font-size-compact)",
"line-height: var(--line-height-compact)", "padding-inline: var(--space-3)",
]) assert(component.includes(rule), rule);
assert(!component.includes("@media"), "Narrow layouts must retain the same single row");
const modes = page.indexOf('class="console-view-toggle"');
assert(modes > status && modes < page.indexOf("</WorkerContextStatus>", status), "Display toggle must be in the model information row");
assert(!page.includes("console-header"), "Remove the empty header and its styles");
assert(component.indexOf("{@render controls?.()}") < component.indexOf('class="details-button"'), "Display controls must precede Details");
assert(/\.worker-context-actions\s*\{[^}]*flex: 0 0 auto/.test(component), "Keep display controls from shrinking on narrow screens");
assert(component.includes("margin-left: auto"), "Align footer controls to the right");
});
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
const files = [
"ComposerInput.svelte",
@@ -1551,14 +1582,10 @@ Deno.test("Composer owns TUI-style completion and replaces Compact/Rewind header
const input = await Deno.readTextFile(
new URL("./ComposerInput.svelte", import.meta.url),
);
const header = page.slice(
page.indexOf('<section class="console-header'),
page.indexOf("{#if rewindTargets.length"),
);
assert(
!header.includes("Compact") && !header.includes("Rewind") &&
!page.includes('class="console-header') &&
!page.includes("requestRewindTargets"),
"Header must defer Compact/Rewind to commands",
"Compact/Rewind must remain commands without a dedicated header",
);
assert(
input.includes('role="listbox"') && input.includes('role="option"') &&
@@ -1588,12 +1615,8 @@ Deno.test("mini task summary owns pane toggling instead of the header", async ()
const component = await Deno.readTextFile(
new URL("./ConsoleTasks.svelte", import.meta.url),
);
const header = page.slice(
page.indexOf('<section class="console-header'),
page.indexOf("{#if rewindTargets.length"),
);
assert(
!header.includes("taskPaneOpen = !taskPaneOpen") &&
!page.includes('class="console-header') &&
page.includes("onTogglePane={() => {"),
"Only the mini summary should own Tasks toggling",
);
@@ -36,11 +36,11 @@ export function formatContextSummary(
used === null || used === undefined || limit === null ||
limit === undefined || !Number.isSafeInteger(used) ||
!Number.isSafeInteger(limit) || used < 0 || limit <= 0
) return "Context unavailable";
) return "unavailable";
const percent = Math.round((used / limit) * 100);
if (!Number.isFinite(percent)) return "Context unavailable";
if (!Number.isFinite(percent)) return "unavailable";
const estimate = metadata?.contextSource === "estimated" ? "~" : "";
return `Context ${estimate}${formatRunTokens(used)} / ${
return `${estimate}${formatRunTokens(used)} / ${
formatRunTokens(limit)
} (${percent}%)`;
}
@@ -0,0 +1,153 @@
// @vitest-environment happy-dom
import {
type CompletionResult,
completionStatus,
startCompletion,
} from "@codemirror/autocomplete";
import { EditorView } from "@codemirror/view";
import { cleanup, fireEvent, render, waitFor } from "@testing-library/svelte";
import { afterEach, expect, test, vi } from "vitest";
import DecodalSourceEditor from "./DecodalSourceEditor.svelte";
afterEach(cleanup);
function setup(
value: string,
props: {
readonly?: boolean;
fixedSchemaWrapper?: boolean;
onComplete?: (
source: string,
offset: number,
explicit: boolean,
) => Promise<CompletionResult | null>;
} = {},
) {
const onChange = vi.fn();
const result = render(DecodalSourceEditor, { value, onChange, ...props });
const content = result.container.querySelector<HTMLElement>(".cm-content")!;
const editor = EditorView.findFromDOM(content)!;
editor.focus();
return { ...result, content, editor, onChange };
}
test.each([
{ name: "plain line", value: "name = 1", anchor: 8, expected: "name = 1\n" },
{
name: "object body",
value: "{\n name = 1",
anchor: 12,
expected: "{\n name = 1\n ",
},
{ name: "empty object", value: "{}", anchor: 1, expected: "{\n \n}" },
{
name: "nested object",
value: "{\n nested = {}\n}",
anchor: 14,
expected: "{\n nested = {\n \n }\n}",
},
])(
"Enter inserts an indented newline in $name",
async ({ value, anchor, expected }) => {
const { content, editor, onChange } = setup(value);
editor.dispatch({ selection: { anchor } });
expect(await fireEvent.keyDown(content, { key: "Enter", code: "Enter" }))
.toBe(false);
expect(editor.state.doc.toString()).toBe(expected);
expect(onChange).toHaveBeenLastCalledWith(expected);
},
);
test("Enter indents inside the fixed wrapper without altering its assertion", async () => {
const { content, editor } = setup("{} as WorkspaceConfigSchema\n", {
fixedSchemaWrapper: true,
});
await fireEvent.keyDown(content, { key: "Enter", code: "Enter" });
expect(editor.state.doc.toString()).toBe(
"{\n \n} as WorkspaceConfigSchema\n",
);
expect(editor.state.selection.main.head).toBe(4);
});
test("Enter does not mutate readonly sources", async () => {
const value = "{\n name = 1";
const { content, editor, onChange } = setup(value, { readonly: true });
editor.dispatch({ selection: { anchor: value.length } });
await fireEvent.keyDown(content, { key: "Enter", code: "Enter" });
expect(editor.state.doc.toString()).toBe(value);
expect(onChange).not.toHaveBeenCalled();
});
test("Enter adds an indented newline instead of accepting a visible completion", async () => {
const value = "{\n na";
const { content, editor } = setup(value, {
onComplete: async () => ({
from: 4,
options: [{ label: "name", type: "property" }],
}),
});
editor.dispatch({ selection: { anchor: value.length } });
startCompletion(editor);
await waitFor(() => expect(completionStatus(editor.state)).toBe("active"));
// Wait past CodeMirror's completion interaction delay, so accepting a
// suggestion would be observable rather than ignored as an early keypress.
await new Promise((resolve) => setTimeout(resolve, 100));
await fireEvent.keyDown(content, { key: "Enter", code: "Enter" });
expect(editor.state.doc.toString()).toBe("{\n na\n ");
await waitFor(() => expect(completionStatus(editor.state)).toBeNull());
});
test("Tab indents the current line and Shift+Tab removes indentation", async () => {
const { content, editor, onChange } = setup("name = 1");
editor.dispatch({ selection: { anchor: 4 } });
expect(await fireEvent.keyDown(content, { key: "Tab", code: "Tab" })).toBe(
false,
);
expect(editor.state.doc.toString()).toBe(" name = 1");
expect(onChange).toHaveBeenLastCalledWith(" name = 1");
expect(editor.hasFocus).toBe(true);
await fireEvent.keyDown(content, { key: "Tab", code: "Tab", shiftKey: true });
expect(editor.state.doc.toString()).toBe("name = 1");
expect(onChange).toHaveBeenLastCalledWith("name = 1");
});
test("Tab and Shift+Tab indent every selected line", async () => {
const value = "first = 1\nsecond = 2";
const { content, editor } = setup(value);
editor.dispatch({ selection: { anchor: 0, head: value.length } });
await fireEvent.keyDown(content, { key: "Tab", code: "Tab" });
expect(editor.state.doc.toString()).toBe(" first = 1\n second = 2");
await fireEvent.keyDown(content, { key: "Tab", code: "Tab", shiftKey: true });
expect(editor.state.doc.toString()).toBe(value);
});
test("Tab inserts indentation on a blank line", async () => {
const { content, editor } = setup("");
await fireEvent.keyDown(content, { key: "Tab", code: "Tab" });
expect(editor.state.doc.toString()).toBe(" ");
});
test("Tab and Shift+Tab do not mutate readonly sources", async () => {
const value = " name = 1";
const { content, editor, onChange } = setup(value, { readonly: true });
await fireEvent.keyDown(content, { key: "Tab", code: "Tab" });
await fireEvent.keyDown(content, { key: "Tab", code: "Tab", shiftKey: true });
expect(editor.state.doc.toString()).toBe(value);
expect(onChange).not.toHaveBeenCalled();
});
test("Tab indents the schema body without modifying the fixed wrapper", async () => {
const value = "{\n name = 1\n} as WorkspaceConfigSchema\n";
const { content, editor } = setup(value, { fixedSchemaWrapper: true });
editor.dispatch({ selection: { anchor: value.indexOf("name") } });
await fireEvent.keyDown(content, { key: "Tab", code: "Tab" });
expect(editor.state.doc.toString()).toBe(
"{\n name = 1\n} as WorkspaceConfigSchema\n",
);
await fireEvent.keyDown(content, { key: "Tab", code: "Tab", shiftKey: true });
expect(editor.state.doc.toString()).toBe(value);
editor.dispatch({ selection: { anchor: 0 } });
await fireEvent.keyDown(content, { key: "Tab", code: "Tab" });
expect(editor.state.doc.toString()).toBe(value);
});
@@ -9,6 +9,7 @@
type CompletionContext,
type CompletionResult,
} from '@codemirror/autocomplete';
import { indentWithTab, insertNewlineAndIndent } from '@codemirror/commands';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection, type ViewUpdate } from '@codemirror/view';
@@ -129,6 +130,8 @@
decodal({ highlight: false }),
syntaxHighlighting(syntaxTheme),
autocompletion({
// Use the explicit keymap below so Enter never accepts a suggestion.
defaultKeymap: false,
activateOnTyping: false,
override: [
async (context: CompletionContext) => {
@@ -139,6 +142,10 @@
],
}),
keymap.of(completionKeymapWithoutEnter),
keymap.of([
{ key: 'Enter', run: insertNewlineAndIndent },
indentWithTab,
]),
fixedSchemaWrapperCompartment.of(
initialFixedSchemaWrapper ? fixedSchemaWrapperExtension() : [],
),
@@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy, type Snippet } from 'svelte';
import { onDestroy, onMount, type Snippet } from 'svelte';
import { on } from 'svelte/events';
import Bevel from '#lib/workspace/ui/Bevel.svelte';
import SidebarToggleIcon from './SidebarToggleIcon.svelte';
import './sidebar.css';
@@ -77,6 +78,19 @@
focusInside = false;
clearCloseTimer();
});
function closeAfterNavigation(event: MouseEvent) {
if (!mobile || !open || event.defaultPrevented || event.button !== 0 ||
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
if (!(link instanceof HTMLAnchorElement) || !frame.contains(link) ||
link.hasAttribute('download') || (link.target && link.target !== '_self')) return;
clearCloseTimer();
onOpenChange(false);
}
// Handle nested navigation links, including activation of the current page.
// Svelte's listener preserves ordering with delegated child click handlers.
onMount(() => on(frame, 'click', closeAfterNavigation));
onDestroy(clearCloseTimer);
</script>
@@ -0,0 +1,63 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render } from "@testing-library/svelte";
import { createRawSnippet } from "svelte";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import SidebarFrame from "./SidebarFrame.svelte";
// Stop happy-dom navigation after the component has handled the bubbling click.
const preventBrowserNavigation = (event: MouseEvent) => event.preventDefault();
beforeEach(() => document.addEventListener("click", preventBrowserNavigation));
afterEach(() => {
cleanup();
document.removeEventListener("click", preventBrowserNavigation);
});
function mount(mobile = true, mode: "pinned" | "hover" = "pinned") {
const onOpenChange = vi.fn();
const onModeChange = vi.fn();
const children = createRawSnippet(() => ({
render: () => `<nav>
<a href="/w/example"><span>Workspace home</span></a>
<a href="${location.href}">Current page</a>
<a href="#section">Section</a>
<a href="/account" target="_blank">New tab</a>
<a href="/export" download>Download</a>
<button type="button">Expand group</button>
</nav>`,
}));
return { ...render(SidebarFrame, {
mobile, mode, open: true, children, onOpenChange, onModeChange,
}), onOpenChange, onModeChange };
}
test.each(["pinned", "hover"] as const)("mobile %s closes on nested, current-page and hash links", async (mode) => {
const view = mount(true, mode);
for (const label of ["Workspace home", "Current page", "Section"]) {
view.onOpenChange.mockClear();
await fireEvent.click(view.getByText(label));
expect(view.onOpenChange).toHaveBeenCalledExactlyOnceWith(false);
expect(view.onModeChange).not.toHaveBeenCalled();
}
});
test("mobile ignores disclosure buttons, modified clicks, new tabs, downloads and cancelled navigation", async () => {
const view = mount();
for (const label of ["Expand group", "New tab", "Download"]) {
await fireEvent.click(view.getByText(label));
}
const link = view.getByText("Workspace home");
for (const options of [{ ctrlKey: true }, { metaKey: true }, { shiftKey: true }, { altKey: true }, { button: 1 }]) {
await fireEvent.click(link, options);
}
link.addEventListener("click", (event) => event.preventDefault(), { once: true });
await fireEvent.click(link);
expect(view.onOpenChange).not.toHaveBeenCalled();
});
test.each(["pinned", "hover"] as const)("desktop %s navigation does not change sidebar mode or open state", async (mode) => {
const view = mount(false, mode);
await fireEvent.click(view.getByText("Workspace home"));
expect(view.onOpenChange).not.toHaveBeenCalled();
expect(view.onModeChange).not.toHaveBeenCalled();
});
@@ -34,6 +34,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;
@@ -121,7 +125,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}
@@ -209,14 +213,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}
@@ -465,8 +474,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;
}
@@ -480,12 +489,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);
@@ -6,7 +6,7 @@
SubjektivSubjectCreateError,
updateSubjektivSubjectBehavior,
} from '#lib/workspace/memory/api.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();
@@ -116,6 +116,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 generated Memory context to show.';
}
function subjectStateLabel(state: 'active' | 'retired'): string {
return state === 'active' ? 'Available' : 'Retired';
}
</script>
<svelte:head>
@@ -136,13 +154,50 @@
</header>
{#if subject}
<dl class="subject-facts" aria-label="Subject details">
<div><dt>Role</dt><dd>{subject.role}</dd></div>
<div><dt>State</dt><dd><span class="subject-state is-{subject.state}">{subject.state}</span></dd></div>
<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>Current worker</dt><dd>{subject.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-{subject.state}">{subjectStateLabel(subject.state)}</strong>
<p>{subject.state === 'active' ? 'Available for new Worker connections.' : 'No longer available for new Worker connections.'}</p>
</div>
<div>
<span>Worker connection</span>
<strong>{subject.current_worker ? 'Connected' : 'Not connected'}</strong>
<p>{subject.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>Generated Memory 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>{subject.id}</code></dd></div>
<div>
<dt>Subject store revision</dt>
<dd>{subject.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={subject.updated_at}>{formatDate(subject.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>
@@ -189,38 +244,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
@@ -237,26 +294,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>
@@ -266,14 +323,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}
@@ -289,9 +346,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}
@@ -384,25 +441,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,
@@ -642,7 +753,7 @@
}
@media (max-width: 900px) {
.subject-facts {
.subject-overview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -659,10 +770,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;
@@ -75,6 +75,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"))
@@ -240,7 +243,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,
@@ -298,9 +304,15 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
.textContent,
)
.toContain("Prefer explicit evidence.");
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();
@@ -577,6 +589,93 @@ test("a refreshed loader snapshot fences old saves even after returning to the s
).toBeNull();
});
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.each(["ready", "ungenerated", "stale", "failed"] as const)(
"user behavior and diagnostic disclosures stay independent of %s generated Memory",
async (availability) => {
const record = { ...subject(), store_revision: 0 };
const view = render(SubjectPage, {
data: {
...subjectPageData(record),
surface: result({
subject_id: record.id,
availability,
...(availability === "ready"
? {
snapshot: {
snapshot_id: "empty-snapshot",
body_md: "",
memory_refs: [],
built_from_store_revision: 0,
created_at: "2026-01-02T03:04:05Z",
},
}
: {}),
}),
},
} as never);
expect(
screen.getByRole("article", { name: "User-managed Subject behavior" })
.textContent,
)
.toContain(record.behavior_md);
expect(screen.getByText("Generated Memory context", { exact: true })).not
.toBeNull();
expect(screen.getByText("No committed Memories yet.")).not.toBeNull();
expect(screen.getByText("Not connected", { exact: true })).not.toBeNull();
const technical = view.container.querySelector(
"details.subject-technical-details",
) as HTMLDetailsElement;
expect(technical.open).toBe(false);
expect(technical.textContent).toContain("Subject store revision");
if (availability === "ready") {
expect(screen.getByText("Resident context is current but empty.")).not
.toBeNull();
expect(screen.getByText(/no generated Memory context to show/)).not
.toBeNull();
expect(
view.container.querySelector("details.surface-details")?.textContent,
)
.toContain("Generated from Subject revision");
}
await fireEvent.click(screen.getByRole("button", { name: "Edit" }));
await fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(
screen.getByRole("article", { name: "User-managed Subject behavior" })
.textContent,
)
.toContain(record.behavior_md);
expect(technical.open).toBe(false);
},
);
test("distinguishes ready-empty, stale, failed, unavailable, and request error surfaces", async () => {
const base = {
workspaceId: "workspace-1",
@@ -600,8 +699,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(
{
@@ -614,7 +730,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(
{
@@ -630,7 +747,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(
@@ -639,7 +756,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",
);
});
@@ -1663,45 +1663,6 @@
</svelte:head>
<div class="console-shell worker-console-shell">
<section class="console-header card" aria-label="Worker controls">
<WorkerContextStatus metadata={selectedConsoleProjection.workerMetadata} />
<div class="console-header-actions">
<div
class="console-view-modes"
role="group"
aria-label="Console display mode"
>
<button
type="button"
class:active={consoleViewMode === "overview"}
aria-pressed={consoleViewMode === "overview"}
onclick={() => (consoleViewMode = "overview")}
>
Overview
</button>
<button
type="button"
class:active={consoleViewMode === "normal"}
aria-pressed={consoleViewMode === "normal"}
onclick={() => (consoleViewMode = "normal")}
>
Normal
</button>
</div>
<button
type="button"
class="secondary-button"
aria-expanded={workerDetailsOpen}
onclick={() => {
workerDetailsOpen = !workerDetailsOpen;
if (workerDetailsOpen) taskPaneOpen = false;
}}
>
Details
</button>
</div>
</section>
{#if rewindTargets.length > 0}
<section class="card rewind-targets" aria-label="Rewind targets">
<h3>Rewind targets</h3>
@@ -2106,6 +2067,28 @@
</div>
</div>
</form>
<WorkerContextStatus
metadata={selectedConsoleProjection.workerMetadata}
detailsOpen={workerDetailsOpen}
onToggleDetails={() => {
workerDetailsOpen = !workerDetailsOpen;
if (workerDetailsOpen) taskPaneOpen = false;
}}
>
{#snippet controls()}
<button
type="button"
class="console-view-toggle"
role="switch"
aria-checked={consoleViewMode === "overview"}
title="On: Overview · Off: Normal"
onclick={() => (consoleViewMode = consoleViewMode === "overview" ? "normal" : "overview")}
>
<span class="console-view-toggle-track" aria-hidden="true"></span>
<span>Overview</span>
</button>
{/snippet}
</WorkerContextStatus>
</div>
<style>
@@ -2131,14 +2114,6 @@
overscroll-behavior: contain;
}
.console-header {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.console-card {
display: grid;
align-content: start;
@@ -2146,46 +2121,59 @@
min-height: 100%;
}
.console-header-actions {
display: flex;
.console-view-toggle {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--space-2);
}
.console-view-modes {
display: inline-flex;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 0.55rem;
background: var(--bg-raised);
}
.console-view-modes button {
gap: var(--space-1);
min-height: var(--space-5);
padding: 0;
border: 0;
border-radius: var(--space-1);
background: transparent;
color: var(--text-muted);
padding: var(--space-1) var(--space-2);
font: inherit;
font-family: var(--font-sans);
font-size: var(--font-size-compact);
line-height: var(--line-height-compact);
font-weight: 700;
font-weight: 400;
cursor: pointer;
}
.console-view-modes button + button {
border-left: 1px solid var(--line);
}
.console-view-modes button:hover {
.console-view-toggle:hover {
color: var(--text-strong);
}
.console-view-modes button.active {
background: var(--accent);
color: var(--bg);
.console-view-toggle:focus-visible {
outline: 1px solid var(--text-strong);
outline-offset: -1px;
}
.console-view-toggle-track {
position: relative;
flex: 0 0 auto;
width: 24px;
height: 14px;
border-radius: 999px;
background: var(--line-strong);
}
.console-view-toggle-track::before {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--bg);
}
.console-view-toggle[aria-checked="true"] .console-view-toggle-track {
background: var(--accent-muted);
}
.console-view-toggle[aria-checked="true"] .console-view-toggle-track::before {
transform: translateX(10px);
}
.rewind-targets {
@@ -2684,14 +2672,5 @@
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
.console-header {
align-items: stretch;
flex-direction: column;
}
.console-header-actions {
align-self: flex-end;
}
}
</style>
@@ -261,13 +261,28 @@ test.each(["live_protocol", "retained_snapshot"] as const)("overlapping history
await screen.findByText("Start of conversation");
expect(view.container.querySelectorAll(".activity-summary")).toHaveLength(1);
expect(screen.getAllByText("searched 1 time・ran 1 command")).toHaveLength(1);
await fireEvent.click(screen.getByRole("button", { name: "Normal" }));
const footer = screen.getByLabelText("Worker model and context");
const overview = screen.getByRole("switch", { name: "Overview" });
expect(screen.queryByRole("button", { name: "Normal" })).toBeNull();
const details = screen.getByRole("button", { name: "Details" });
expect(view.container.querySelector(".console-header")).toBeNull();
expect(footer.contains(overview)).toBe(true);
expect(overview.nextElementSibling).toBe(details);
expect(overview.closest("form")).toBeNull();
expect(view.container.querySelector(".console-composer")?.nextElementSibling).toBe(footer);
expect(overview.getAttribute("aria-checked")).toBe("true");
await fireEvent.click(overview);
expect(overview.getAttribute("aria-checked")).toBe("false");
expect(overview.textContent?.trim()).toBe("Overview");
const transcript = screen.getByRole("article", { name: "main transcript" });
const ids = [...transcript.querySelectorAll("[data-console-line-id]")].map((line) => line.getAttribute("data-console-line-id"));
expect(ids).toHaveLength(3);
expect(new Set(ids).size).toBe(ids.length);
await fireEvent.click(screen.getByRole("button", { name: "Overview" }));
await fireEvent.click(overview);
expect(overview.getAttribute("aria-checked")).toBe("true");
expect(overview.textContent?.trim()).toBe("Overview");
expect(view.container.querySelectorAll(".activity-summary")).toHaveLength(1);
expect(multiplexer.sendWorkerMethod).not.toHaveBeenCalled();
});
test.each([
@@ -473,7 +488,7 @@ test("mini task summary opens and closes details without a header Tasks button",
latestListener().onFrame(subscribedFrame(session));
const summary = await screen.findByRole("button", { name: /1 task — pending/ });
expect(screen.queryByRole("complementary", { name: "Worker tasks" })).toBeNull();
expect(view.container.querySelector(".console-header")?.textContent).not.toContain("Tasks");
expect(view.container.querySelector(".console-header")).toBeNull();
await fireEvent.click(screen.getByRole("button", { name: "Details" }));
const details = await screen.findByRole("complementary", { name: "Worker detail" });
expect(details.textContent).not.toContain("Capabilities");
+89 -2
View File
@@ -71,18 +71,105 @@ Deno.test("config source API commits directly through the workspace scope", asyn
entrypoints: [],
}, fetcher);
assertEquals(calls.map((call) => call.url), [
assertEquals<unknown>(calls.map((call) => call.url), [
"/api/w/w%2Fone/config/source-tree",
"/api/w/w%2Fone/config/source-tree/revisions/7",
"/api/w/w%2Fone/config/source-tree/entries/profiles%2Fmain.dcdl",
"/api/w/w%2Fone/config/source-tree/commit",
]);
assertEquals(calls[3].init?.method, "POST");
assertEquals<unknown>(calls[3].init?.method, "POST");
assert(
String(calls[3].init?.body).includes('"base_digest":"sha256:base"'),
);
});
function withSchema(source: unknown, contributionSource: unknown = "builtin") {
return {
...tree,
contract: {
...tree.contract,
schema_bundle: {
...tree.contract.schema_bundle,
source,
contributions: [{
provider_id: "builtin",
namespace: "workspace",
version: "1",
source: contributionSource,
source_digest: "sha256:contribution",
}],
},
},
};
}
for (const field of ["bundle", "contribution"] as const) {
Deno.test(`config source API accepts ${field} schema bodies larger than metadata`, async () => {
const source = "schema body\n".repeat(6000);
const body = field === "bundle"
? withSchema(source)
: withSchema("builtin", source);
const fetcher = (() => Promise.resolve(response(body))) as typeof fetch;
assertEquals<unknown>(await fetchConfigTree("w", fetcher), body);
assertEquals<unknown>(
await commitConfigTree("w", {
base_revision: 7,
base_digest: snapshot.digest,
changes: [],
entrypoints: ["main.dcdl"],
}, fetcher),
body,
);
});
Deno.test(`config source API bounds ${field} schema bodies in UTF-8 bytes and checks types`, () => {
const body = (source: unknown) =>
field === "bundle" ? withSchema(source) : withSchema("builtin", source);
const limit = 8 * 1024 * 1024;
const atLimit = "é".repeat(limit / 2);
assertEquals<unknown>(
parseWorkspaceConfigTreeResponse(body(atLimit)),
body(atLimit),
);
for (const invalid of [atLimit + "x", null, 42, {}]) {
assertThrows(
() => parseWorkspaceConfigTreeResponse(body(invalid)),
ConfigSourceApiError,
);
}
});
}
Deno.test("config source API retains metadata and total response limits", async () => {
const invalid = withSchema("builtin");
invalid.contract.schema_bundle.contributions[0].provider_id = "x".repeat(
4097,
);
assertThrows(
() => parseWorkspaceConfigTreeResponse(invalid),
ConfigSourceApiError,
);
for (
const headers of [
new Headers(),
new Headers({ "content-length": String(8 * 1024 * 1024 + 1) }),
]
) {
const fetcher = (() =>
Promise.resolve(
new Response(
JSON.stringify(withSchema("x".repeat(8 * 1024 * 1024))),
{ headers },
),
)) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"too large",
);
}
});
Deno.test("config source API rejects unknown response fields", async () => {
const fetcher =
(() =>
+50 -9
View File
@@ -62,9 +62,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;
@@ -85,6 +83,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,
@@ -190,14 +212,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);
@@ -401,8 +444,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",
);
@@ -506,8 +548,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",