diff --git a/web/hwlab-cloud-web/scripts/check.ts b/web/hwlab-cloud-web/scripts/check.ts index ab96942c..5ebd48b9 100644 --- a/web/hwlab-cloud-web/scripts/check.ts +++ b/web/hwlab-cloud-web/scripts/check.ts @@ -197,7 +197,8 @@ assertIncludes(workbenchRealtimePlanSource, "new Set(recovery.actions)", "Realti assertIncludes(workbenchRealtimePlanSource, "actions.has(\"schedule-session-list\")", "Realtime stream errors must schedule bounded session list refreshes only when transport requests that action"); assertIncludes(workbenchRealtimePlanSource, "authority: \"automatic-recovery\"", "Realtime recovery planner must classify transport recovery as automatic recovery authority"); assert.doesNotMatch(workbenchRealtimePlanSource, /force:\s*true/u, "Realtime recovery planner must not turn transport recovery into force-refresh work"); -assertIncludes(workbenchColadaSource, "options.force ? queryCache.refresh(entry) : queryCache.fetch(entry)", "Workbench force refresh must stay on the same Colada key path"); +assertIncludes(workbenchColadaSource, "const state = await queryCache.refresh(entry);", "Workbench reads must preserve Colada staleTime/min-interval governance"); +assert.doesNotMatch(workbenchColadaSource, /queryCache\.fetch\(entry/u, "Workbench reads must not call queryCache.fetch(entry), which bypasses Colada freshness governance"); assertIncludes(workbenchStoreSource, "runtimePolicy.sessionListRealtimeRefreshDelayMs", "Realtime recovery delay must come from runtime policy instead of store constants"); assertIncludes(workbenchStoreSource, "workbenchColadaQueries.fetchSession", "Realtime session detail recovery must enter Colada query facade"); assertIncludes(workbenchStoreSource, "runtimePolicy.workbenchSessionDetailMinRefreshMs", "Realtime session detail recovery budget must come from runtime policy"); diff --git a/web/hwlab-cloud-web/src/stores/workbench-colada-queries.ts b/web/hwlab-cloud-web/src/stores/workbench-colada-queries.ts index cb078d50..684dd667 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-colada-queries.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-colada-queries.ts @@ -72,7 +72,8 @@ async function runWorkbenchQuery(queryCache: QueryCache, key: EntryKey, enabled: false, staleTime: normalizeStaleTime(options.minIntervalMs) }); - const state = await (options.force ? queryCache.refresh(entry) : queryCache.fetch(entry)); + // refresh() preserves Colada staleTime/min-interval governance; fetch() would bypass it. + const state = await queryCache.refresh(entry); return state.data as TResult; } diff --git a/web/hwlab-cloud-web/src/stores/workbench-colada.test.ts b/web/hwlab-cloud-web/src/stores/workbench-colada.test.ts index b4112d1a..b18971e0 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-colada.test.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-colada.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { test } from "bun:test"; import { createApp } from "vue"; import { createPinia, setActivePinia } from "pinia"; @@ -8,6 +11,8 @@ import { createWorkbenchServerState, type WorkbenchServerState } from "./workben import { workbenchColadaKeys } from "./workbench-colada-keys"; import { useWorkbenchColadaReducer } from "./workbench-colada-reducer"; +const storeDir = path.dirname(fileURLToPath(import.meta.url)); + function installColadaContext(): ReturnType { const app = createApp({}); const pinia = createPinia(); @@ -39,3 +44,9 @@ test("workbench colada reducer stores projection state in the query cache", () = assert.equal(reducer.serverState.value.sessionsById.ses_colada?.sessionId, "ses_colada"); }); }); + +test("workbench colada read path preserves freshness governance", () => { + const source = fs.readFileSync(path.join(storeDir, "workbench-colada-queries.ts"), "utf8"); + assert.match(source, /const state = await queryCache\.refresh\(entry\);/u); + assert.doesNotMatch(source, /queryCache\.fetch\(entry/u); +});