fix(web): gate compaction status by worker state
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
formatRunElapsed,
|
||||
formatRunElapsedCompact,
|
||||
formatRunTokens,
|
||||
visibleCompactionProgress,
|
||||
} from "./run-status.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
@@ -98,3 +99,42 @@ Deno.test("run status formatting matches the compact TUI shape", () => {
|
||||
assertEquals(formatRunTokens(3_000), "3.0k");
|
||||
assertEquals(formatRunTokens(999), "999");
|
||||
});
|
||||
|
||||
Deno.test("visible compaction requires a matching busy worker state", () => {
|
||||
const manual = {
|
||||
phase: "preparing",
|
||||
started_at_ms: 1_700_000_000_000,
|
||||
trigger: "manual",
|
||||
} as const;
|
||||
const automatic = {
|
||||
phase: "summarizing",
|
||||
started_at_ms: 1_700_000_000_000,
|
||||
trigger: "request_threshold",
|
||||
} as const;
|
||||
const maintenance = {
|
||||
last_command_id: 7,
|
||||
state: {
|
||||
kind: "busy",
|
||||
state: { kind: "maintenance", state: "compacting" },
|
||||
},
|
||||
} as const;
|
||||
const run = {
|
||||
last_command_id: 7,
|
||||
state: {
|
||||
kind: "busy",
|
||||
state: { kind: "run", state: "running" },
|
||||
},
|
||||
} as const;
|
||||
const idle = {
|
||||
last_command_id: 7,
|
||||
state: { kind: "idle" },
|
||||
} as const;
|
||||
|
||||
assertEquals(visibleCompactionProgress(manual, maintenance), manual);
|
||||
assertEquals(visibleCompactionProgress(automatic, run), automatic);
|
||||
assertEquals(visibleCompactionProgress(manual, run), null);
|
||||
assertEquals(visibleCompactionProgress(automatic, maintenance), null);
|
||||
assertEquals(visibleCompactionProgress(manual, idle), null);
|
||||
assertEquals(visibleCompactionProgress(manual, null), null);
|
||||
assertEquals(visibleCompactionProgress(null, maintenance), null);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Event as ProtocolEvent } from "$lib/generated/protocol";
|
||||
import type {
|
||||
Event as ProtocolEvent,
|
||||
InFlightCompaction,
|
||||
WorkerStateSnapshot,
|
||||
} from "$lib/generated/protocol";
|
||||
|
||||
export type RunActivityStats = {
|
||||
startedAtMs: number | null;
|
||||
@@ -71,3 +75,20 @@ export function formatRunTokens(tokens: number): string {
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
||||
return String(tokens);
|
||||
}
|
||||
|
||||
export function visibleCompactionProgress(
|
||||
compaction: InFlightCompaction | null,
|
||||
workerState: WorkerStateSnapshot | null,
|
||||
): InFlightCompaction | null {
|
||||
if (!compaction || workerState?.state.kind !== "busy") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const busyState = workerState.state.state;
|
||||
const triggerMatches = compaction.trigger === "manual"
|
||||
? busyState.kind === "maintenance" &&
|
||||
busyState.state === "compacting"
|
||||
: busyState.kind === "run";
|
||||
|
||||
return triggerMatches ? compaction : null;
|
||||
}
|
||||
|
||||
@@ -1116,3 +1116,28 @@ Deno.test("Web Console uses Notify while running and exposes durable pending con
|
||||
"ordinary text must remain Submit instead of being implicitly converted to Notify",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Web Console gates compaction progress before rendering run status", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const runStatus = await Deno.readTextFile(
|
||||
new URL("./WorkerRunStatus.svelte", import.meta.url),
|
||||
);
|
||||
|
||||
assert(
|
||||
consolePage.includes("visibleCompactionProgress(") &&
|
||||
consolePage.includes("consoleProjection.compaction") &&
|
||||
consolePage.includes("consoleProjection.workerState") &&
|
||||
consolePage.includes("compaction={compactionProgress}"),
|
||||
"Console page should pass only state-consistent compaction progress to the status component",
|
||||
);
|
||||
assert(
|
||||
runStatus.includes("compaction?.started_at_ms ?? startedAtMs") &&
|
||||
runStatus.includes("Compacting · {compaction.phase}"),
|
||||
"run status should render the compaction phase and derive elapsed time from the authoritative start timestamp",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type ComposerCompletionToken,
|
||||
} from "$lib/workspace/console/composer-completion";
|
||||
import WorkerRunStatus from "$lib/workspace/console/WorkerRunStatus.svelte";
|
||||
import { visibleCompactionProgress } from "$lib/workspace/console/run-status";
|
||||
import { resolveWorkerControlShortcut } from "$lib/workspace/console/worker-control-shortcuts";
|
||||
import {
|
||||
consoleWorkerViews,
|
||||
@@ -248,18 +249,12 @@
|
||||
liveWorkerState ?? (worker?.state === "stopped" ? "stopped" : "loading"),
|
||||
);
|
||||
const workerRunning = $derived(workerState === "running");
|
||||
const compactionProgress = $derived.by(() => {
|
||||
const state = consoleProjection.workerState?.state;
|
||||
if (!state || typeof state !== "object" || !("busy" in state)) return null;
|
||||
const progress = consoleProjection.compaction;
|
||||
if (!progress) return null;
|
||||
const busy = state.busy;
|
||||
if (!busy || typeof busy !== "object") return null;
|
||||
const valid = progress.trigger === "manual"
|
||||
? "maintenance" in busy && busy.maintenance === "compacting"
|
||||
: "run" in busy;
|
||||
return valid ? progress : null;
|
||||
});
|
||||
const compactionProgress = $derived(
|
||||
visibleCompactionProgress(
|
||||
consoleProjection.compaction,
|
||||
consoleProjection.workerState,
|
||||
),
|
||||
);
|
||||
const workerPaused = $derived(workerState === "paused");
|
||||
const composerEditable = $derived(protocolState === "open" && !sending);
|
||||
const draftHasText = $derived(draft.content.trim().length > 0);
|
||||
|
||||
Reference in New Issue
Block a user