From 998db6a15b228c660a6aadd5f9792fe110091d17 Mon Sep 17 00:00:00 2001 From: "memos-autodev[bot]" Date: Mon, 20 Jul 2026 21:27:54 +0800 Subject: [PATCH 1/2] fix: viewer dashboard drifts to zero after a namespace flip (#2131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viewer's Overview/metrics/traces/session routes filtered reads through the core's mutable activeNamespace, which every turn/session rewrites from caller context hints (openclaw: profileId = ctx.agentId). When the gateway session or a sub-agent turn flipped the profile, all historical rows failed the visibility clauses and dashboard counts collapsed to zero. The reported root cause (missing chunks table) is a misdiagnosis: the only 2.0 reference to chunks is the legacy 1.0-import reader in server/routes/migrate.ts; the live pipeline queries traces/episodes/ sessions with vectors stored in BLOB columns, so no schema change is needed. Fix: complete the existing includeAllNamespaces viewer convention (already used by diag.ts/session.ts/memory.ts) — add the option to core.metrics() and core.listEpisodes(), and pass it from overview.ts, metrics.ts, trace.ts and session.ts. Scoped reads and turn-time retrieval isolation are unchanged; explicit owner filters still narrow. Co-Authored-By: Claude Fable 5 --- .../agent-contract/memory-core.ts | 4 +- .../core/pipeline/memory-core.ts | 10 ++- .../server/routes/metrics.ts | 5 +- .../server/routes/overview.ts | 16 +++-- .../server/routes/session.ts | 7 +- .../memos-local-plugin/server/routes/trace.ts | 6 ++ .../tests/unit/pipeline/memory-core.test.ts | 68 +++++++++++++++++++ .../tests/unit/server/http.test.ts | 4 +- 8 files changed, 107 insertions(+), 13 deletions(-) diff --git a/apps/memos-local-plugin/agent-contract/memory-core.ts b/apps/memos-local-plugin/agent-contract/memory-core.ts index ad1dd3f13..4f6f6bbe8 100644 --- a/apps/memos-local-plugin/agent-contract/memory-core.ts +++ b/apps/memos-local-plugin/agent-contract/memory-core.ts @@ -369,7 +369,7 @@ export interface MemoryCore { archiveWorldModel(id: string): Promise; /** Reverse of {@link archiveWorldModel}. */ unarchiveWorldModel(id: string): Promise; - listEpisodes(input: { sessionId?: SessionId; limit?: number; offset?: number }): Promise; + listEpisodes(input: { sessionId?: SessionId; limit?: number; offset?: number; includeAllNamespaces?: boolean }): Promise; /** * Like `listEpisodes` but returns rich per-row metadata the viewer * needs to render its task list without a second round trip @@ -504,7 +504,7 @@ export interface MemoryCore { * Aggregate counts for the viewer's Analytics tab. `days` controls * the window the `dailyWrites` histogram covers. */ - metrics(input?: { days?: number }): Promise<{ + metrics(input?: { days?: number; includeAllNamespaces?: boolean }): Promise<{ total: number; writesToday: number; sessions: number; diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index 96721cc0b..aedc7716a 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -3188,6 +3188,7 @@ export function createMemoryCore( sessionId?: SessionId; limit?: number; offset?: number; + includeAllNamespaces?: boolean; }): Promise { ensureLive(); const rows = handle.repos.episodes.list({ @@ -3196,7 +3197,7 @@ export function createMemoryCore( offset: input.offset ?? 0, }); return rows - .filter((r: EpisodeRow) => visibleToCurrent(r)) + .filter((r: EpisodeRow) => input.includeAllNamespaces || visibleToCurrent(r)) .map((r: EpisodeRow) => r.id as EpisodeId); } @@ -3763,7 +3764,7 @@ export function createMemoryCore( }); } - async function metrics(input?: { days?: number }): Promise<{ + async function metrics(input?: { days?: number; includeAllNamespaces?: boolean }): Promise<{ total: number; writesToday: number; sessions: number; @@ -3908,9 +3909,12 @@ export function createMemoryCore( // shows: 1 user turn = 1 memory (regardless of how many tool calls // / sub-steps were captured for that turn). // Apply namespace visibility so the count matches the filtered list. + // Viewer callers pass `includeAllNamespaces` — `activeNamespace` is + // rewritten by every turn/session, so binding this count to it made + // the dashboard total collapse whenever another profile's turn ran. const totalTurns = handle.repos.traces.countTurns( {}, - visibilityWhere(activeNamespace), + input?.includeAllNamespaces ? undefined : visibilityWhere(activeNamespace), ); return { diff --git a/apps/memos-local-plugin/server/routes/metrics.ts b/apps/memos-local-plugin/server/routes/metrics.ts index fdf6b583f..ce1c833e1 100644 --- a/apps/memos-local-plugin/server/routes/metrics.ts +++ b/apps/memos-local-plugin/server/routes/metrics.ts @@ -41,8 +41,11 @@ export function registerMetricsRoutes(routes: Routes, deps: ServerDeps): void { routes.set("GET /api/v1/metrics", async (ctx) => { const raw = ctx.url.searchParams.get("days"); const days = raw ? Number(raw) : undefined; + // Viewer analytics cover the whole local database — see overview.ts + // for why viewer reads must not follow the turn-scoped namespace. return await deps.core.metrics({ days: Number.isFinite(days) ? days : undefined, + includeAllNamespaces: true, }); }); @@ -112,7 +115,7 @@ export function registerMetricsRoutes(routes: Routes, deps: ServerDeps): void { // web / whatever the agent ran. Fold them in with the api_logs // rows so the panel answers "is anything slow?" regardless of // whether the slowness was internal or user-visible. - const traces = await deps.core.listTraces({ limit: 2_000, offset: 0 }); + const traces = await deps.core.listTraces({ limit: 2_000, offset: 0, includeAllNamespaces: true }); for (const tr of traces) { if (tr.ts < sinceMs) continue; for (const tc of tr.toolCalls ?? []) { diff --git a/apps/memos-local-plugin/server/routes/overview.ts b/apps/memos-local-plugin/server/routes/overview.ts index d19504e2b..df3085da1 100644 --- a/apps/memos-local-plugin/server/routes/overview.ts +++ b/apps/memos-local-plugin/server/routes/overview.ts @@ -27,19 +27,25 @@ export function registerOverviewRoutes(routes: Routes, deps: ServerDeps): void { // headless callers. Routing the ping through the viewer's mount // hook keeps the semantics honest (a browser actually opened // the page) and is naturally deduped by browser tab lifetime. + // The viewer is a local single-user admin surface: its aggregate + // counts must reflect the whole database, not the namespace of + // whichever agent profile processed the most recent turn. The core + // rewrites its active namespace on every turn/session, so scoped + // reads here made the dashboard "drift to zero" as soon as a + // message arrived (#2131). Same convention as diag.ts / session.ts. const [health, episodeIds, skills, policies, worldModels, metrics] = await Promise.all([ deps.core.health(), - deps.core.listEpisodes({ limit: 5_000 }), - deps.core.listSkills({ limit: 500 }), + deps.core.listEpisodes({ limit: 5_000, includeAllNamespaces: true }), + deps.core.listSkills({ limit: 500, includeAllNamespaces: true }), // Core only exposes `listPolicies({ status? })`; the viewer wants // the grand total + per-status so we request the biggest page and // break it down here. 500 is plenty — fresh installs have dozens. - deps.core.listPolicies({ limit: 500 }), - deps.core.listWorldModels({ limit: 500 }), + deps.core.listPolicies({ limit: 500, includeAllNamespaces: true }), + deps.core.listWorldModels({ limit: 500, includeAllNamespaces: true }), // `metrics.total` is the grand total of traces — cheaper than a // dedicated count RPC and already cached by the core. - deps.core.metrics({ days: 1 }), + deps.core.metrics({ days: 1, includeAllNamespaces: true }), ]); const skillStats = { diff --git a/apps/memos-local-plugin/server/routes/session.ts b/apps/memos-local-plugin/server/routes/session.ts index 135b55704..2bf3fc639 100644 --- a/apps/memos-local-plugin/server/routes/session.ts +++ b/apps/memos-local-plugin/server/routes/session.ts @@ -91,7 +91,12 @@ export function registerSessionRoutes(routes: Routes, deps: ServerDeps): void { ownerProfileId, includeAllNamespaces: true, }); - const episodeIds = await deps.core.listEpisodes({ sessionId, limit, offset }); + const episodeIds = await deps.core.listEpisodes({ + sessionId, + limit, + offset, + includeAllNamespaces: true, + }); return { episodeIds, limit, diff --git a/apps/memos-local-plugin/server/routes/trace.ts b/apps/memos-local-plugin/server/routes/trace.ts index ec1dca4c8..7a8e742dd 100644 --- a/apps/memos-local-plugin/server/routes/trace.ts +++ b/apps/memos-local-plugin/server/routes/trace.ts @@ -44,6 +44,10 @@ export function registerTraceRoutes(routes: Routes, deps: ServerDeps): void { const groupByTurn = params.get("groupByTurn") === "true"; const includeTotal = params.get("includeTotal") !== "false"; const listLimit = includeTotal ? limit : limit + 1; + // Viewer list: show every namespace's rows by default (explicit + // ownerAgentKind / ownerProfileId query filters still narrow) — + // see overview.ts for why viewer reads must not follow the + // turn-scoped active namespace. const rawTraces = await deps.core.listTraces({ limit: listLimit, offset, @@ -52,6 +56,7 @@ export function registerTraceRoutes(routes: Routes, deps: ServerDeps): void { ownerProfileId, q, groupByTurn, + includeAllNamespaces: true, }); const { traces, hasMore } = trimTracePage(rawTraces, limit, groupByTurn); const total = includeTotal @@ -61,6 +66,7 @@ export function registerTraceRoutes(routes: Routes, deps: ServerDeps): void { ownerProfileId, q, groupByTurn, + includeAllNamespaces: true, }) : undefined; // When grouping, `traces.length === limit` is no longer a reliable diff --git a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts index 62c18fa16..d25421d26 100644 --- a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts +++ b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts @@ -451,6 +451,74 @@ describe("MemoryCore façade", () => { expect(await core.listTraces({ limit: 10, groupByTurn: true })).toHaveLength(1); }); + // Regression for #2131: viewer dashboard counts must not "drift to + // zero" when a turn/session from a different sub-agent profile flips + // the core's active namespace. The viewer is a local single-user + // admin surface, so its aggregate reads pass `includeAllNamespaces` + // (same convention as diag.ts / session.ts routes) and must stay + // stable regardless of which namespace processed the last turn. + it("keeps metrics + listEpisodes stable across a namespace flip (includeAllNamespaces)", async () => { + pipeline = createPipeline(buildDeps(db!)); + core = createMemoryCore( + pipeline, + resolveHome("openclaw", "/tmp/memos-mc-test"), + "test", + ); + await core.init(); + + const mainNs = { agentKind: "openclaw", profileId: "main" }; + const subagentNs = { agentKind: "openclaw", profileId: "subagent-x" }; + + // 1. A turn under the boot namespace writes one memory. + const start = await core.onTurnStart({ + agent: "openclaw", + namespace: mainNs, + sessionId: "s-main", + userText: "remember the deploy checklist", + ts: 1_700_000_000_001, + }); + await core.onTurnEnd({ + agent: "openclaw", + namespace: mainNs, + sessionId: "s-main", + episodeId: start.query.episodeId!, + agentText: "stored the deploy checklist", + toolCalls: [], + ts: 1_700_000_000_002, + }); + + const before = await core.metrics({ days: 1, includeAllNamespaces: true }); + expect(before.total).toBe(1); + const episodesBefore = await core.listEpisodes({ + limit: 10, + includeAllNamespaces: true, + }); + expect(episodesBefore).toHaveLength(1); + + // 2. A session under a DIFFERENT profile flips activeNamespace + // (this is what the gateway/sub-agents do in production). + await core.openSession({ + agent: "openclaw", + sessionId: "s-sub", + namespace: subagentNs, + }); + + // Namespace-scoped reads hide the other profile's row (intended + // multi-profile isolation)… + const scoped = await core.metrics({ days: 1 }); + expect(scoped.total).toBe(0); + await expect(core.listEpisodes({ limit: 10 })).resolves.toHaveLength(0); + + // …but the viewer's all-namespace reads must NOT drift. + const after = await core.metrics({ days: 1, includeAllNamespaces: true }); + expect(after.total).toBe(1); + const episodesAfter = await core.listEpisodes({ + limit: 10, + includeAllNamespaces: true, + }); + expect(episodesAfter).toHaveLength(1); + }); + it("records visible subagent task and result in the parent episode", async () => { pipeline = createPipeline(buildDeps(db!)); core = createMemoryCore( diff --git a/apps/memos-local-plugin/tests/unit/server/http.test.ts b/apps/memos-local-plugin/tests/unit/server/http.test.ts index 9e4c2a1f8..616b9e852 100644 --- a/apps/memos-local-plugin/tests/unit/server/http.test.ts +++ b/apps/memos-local-plugin/tests/unit/server/http.test.ts @@ -470,7 +470,8 @@ describe("HTTP server — REST routes", () => { expect(Array.isArray(body.traces)).toBe(true); expect(body.traces[0]?.id).toBe("tr-1"); expect(body.traces[0]?.summary).toBe("greeted"); - // The route must forward the query string into the core call. + // The route must forward the query string into the core call, and + // pin the viewer to all-namespace reads (#2131 drift regression). expect(core.listTraces).toHaveBeenCalledWith({ limit: 25, offset: 0, @@ -479,6 +480,7 @@ describe("HTTP server — REST routes", () => { groupByTurn: false, ownerAgentKind: undefined, ownerProfileId: undefined, + includeAllNamespaces: true, }); }); From 67a48630014fb07f6665ff3ccacc65c2c476269d Mon Sep 17 00:00:00 2001 From: "memos-autodev[bot]" Date: Mon, 20 Jul 2026 21:57:58 +0800 Subject: [PATCH 2/2] fix: align listApiLogs namespace scoping with viewer metrics feeds Follow-up to #2131 (OCR round 1/2): - listApiLogs: accept includeAllNamespaces in the contract and core facade (contract symmetry with listTraces/listSkills/listPolicies); metrics/tools now passes it explicitly so both feeds of the bump() aggregation are scoped identically - metrics(): document that the traces fetch feeding sessions/ writesToday/embeddings/dailyWrites is intentionally cross-namespace; only totalTurns respects includeAllNamespaces - listEpisodes: apply limit/offset after the visibility filter on the namespace-scoped path so pages are not silently under-filled by other namespaces' rows Co-Authored-By: Claude Fable 5 --- .../agent-contract/memory-core.ts | 14 +++++ .../core/pipeline/memory-core.ts | 44 +++++++++++--- .../server/routes/metrics.ts | 11 +++- .../tests/unit/pipeline/memory-core.test.ts | 57 +++++++++++++++++++ .../tests/unit/server/http.test.ts | 14 +++++ 5 files changed, 132 insertions(+), 8 deletions(-) diff --git a/apps/memos-local-plugin/agent-contract/memory-core.ts b/apps/memos-local-plugin/agent-contract/memory-core.ts index 4f6f6bbe8..70fbb12d8 100644 --- a/apps/memos-local-plugin/agent-contract/memory-core.ts +++ b/apps/memos-local-plugin/agent-contract/memory-core.ts @@ -435,6 +435,20 @@ export interface MemoryCore { toolNames?: readonly string[]; limit?: number; offset?: number; + /** + * When true, ignore the (turn-scoped) active namespace and return + * rows from every namespace. Viewer callers set this so the Logs + * page and metrics aggregations stay stable across profile flips + * (#2131); matches the pattern already used by `listTraces` / + * `listSkills` / `listPolicies`. + * + * Note: the write path currently does not stamp per-namespace + * owner columns on `api_logs`, so this flag is a no-op today — + * every listing is effectively cross-namespace. Keeping the + * parameter formalises the contract now so a future per-namespace + * write can be added without breaking viewer callers. + */ + includeAllNamespaces?: boolean; }): Promise<{ logs: ApiLogDTO[]; total: number }>; // ── skills ── diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index aedc7716a..80398131b 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -3191,13 +3191,27 @@ export function createMemoryCore( includeAllNamespaces?: boolean; }): Promise { ensureLive(); - const rows = handle.repos.episodes.list({ - sessionId: input.sessionId, - limit: input.limit ?? 50, - offset: input.offset ?? 0, - }); - return rows - .filter((r: EpisodeRow) => input.includeAllNamespaces || visibleToCurrent(r)) + const limit = input.limit ?? 50; + const offset = input.offset ?? 0; + if (input.includeAllNamespaces) { + // Every row passes the visibility filter, so repo-level paging + // is both correct and cheap. + return handle.repos.episodes + .list({ sessionId: input.sessionId, limit, offset }) + .map((r: EpisodeRow) => r.id as EpisodeId); + } + // Namespace-scoped path: paging at the repo level would apply + // `limit` before the visibility filter runs, silently under-filling + // pages (callers can't tell a short page from end-of-data). Fetch + // the widest window the repo allows, filter, then page in memory — + // same idiom as `listEpisodeRows` / `countEpisodes`. The repo + // clamps the fetch window to 500 rows (`clampLimit`), so scoped + // paging is exact within the newest 500 episodes; beyond that the + // same shared limitation applies to the sibling list/count methods. + return handle.repos.episodes + .list({ sessionId: input.sessionId, limit: 100_000 }) + .filter((r: EpisodeRow) => visibleToCurrent(r)) + .slice(offset, offset + limit) .map((r: EpisodeRow) => r.id as EpisodeId); } @@ -3400,8 +3414,16 @@ export function createMemoryCore( toolNames?: readonly string[]; limit?: number; offset?: number; + includeAllNamespaces?: boolean; }): Promise<{ logs: ApiLogDTO[]; total: number }> { ensureLive(); + // `includeAllNamespaces` is accepted for contract symmetry with the + // other viewer list* methods (#2131). The `api_logs` write path does + // not currently stamp per-namespace owner columns, so every row is + // effectively cross-namespace regardless of this flag. Callers that + // fan into viewer aggregations still pass `true` so their intent is + // explicit if a per-namespace write path is added later. + void input?.includeAllNamespaces; const limit = Math.max(1, Math.min(500, input?.limit ?? 50)); const offset = Math.max(0, input?.offset ?? 0); const rows = handle.repos.apiLogs.list({ @@ -3802,6 +3824,14 @@ export function createMemoryCore( const oneDayMs = 86_400_000; const sinceMs = now - days * oneDayMs; + // NOTE: `traces` is fetched unfiltered (all namespaces) below so + // that `sessions` / `writesToday` / `embeddings` / `dailyWrites` + // populate the viewer chart even after a turn from a different + // profile flips the active namespace (#2131). Only `total` + // (totalTurns) respects `includeAllNamespaces`. If a per-namespace + // scoping is ever needed for these derived fields (e.g. per-agent + // views), thread `visibilityWhere(activeNamespace)` through the + // repo query the same way `countTurns` does. const traces = handle.repos.traces.list({ limit: 10_000 }); const sessions = new Set(); let writesToday = 0; diff --git a/apps/memos-local-plugin/server/routes/metrics.ts b/apps/memos-local-plugin/server/routes/metrics.ts index ce1c833e1..1c7ecb93d 100644 --- a/apps/memos-local-plugin/server/routes/metrics.ts +++ b/apps/memos-local-plugin/server/routes/metrics.ts @@ -104,7 +104,16 @@ export function registerMetricsRoutes(routes: Routes, deps: ServerDeps): void { // tools, and their timings reflect background work rather than // response latency. const PUBLIC_API_LOG_TOOLS = new Set(["memos_search", "memory_search", "memory_add"]); - const { logs } = await deps.core.listApiLogs({ limit: 5_000, offset: 0 }); + // Same convention as the `listTraces` call below (#2131): the tool + // panel folds api_logs entries and trace tool-calls into a single + // aggregation, so both feeds must be scoped identically. Passing + // `includeAllNamespaces: true` here keeps them aligned even if a + // future per-namespace write path lands on api_logs. + const { logs } = await deps.core.listApiLogs({ + limit: 5_000, + offset: 0, + includeAllNamespaces: true, + }); for (const lg of logs) { if (lg.calledAt < sinceMs) continue; if (!PUBLIC_API_LOG_TOOLS.has(lg.toolName)) continue; diff --git a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts index d25421d26..ad86ff751 100644 --- a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts +++ b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts @@ -519,6 +519,63 @@ describe("MemoryCore façade", () => { expect(episodesAfter).toHaveLength(1); }); + // Namespace-scoped listEpisodes must apply `limit` AFTER the + // visibility filter. Paging at the repo level under-fills pages when + // rows from other namespaces occupy the fetched window, which reads + // as a false end-of-data to paginating callers. + it("fills scoped listEpisodes pages past other namespaces' rows", async () => { + pipeline = createPipeline(buildDeps(db!)); + core = createMemoryCore( + pipeline, + resolveHome("openclaw", "/tmp/memos-mc-test"), + "test", + ); + await core.init(); + + const mainNs = { agentKind: "openclaw", profileId: "main" }; + const subNs = { agentKind: "openclaw", profileId: "subagent-x" }; + + const runTurn = async ( + ns: typeof mainNs, + sessionId: string, + ts: number, + ): Promise => { + const start = await core!.onTurnStart({ + agent: "openclaw", + namespace: ns, + sessionId, + userText: `note for ${sessionId}`, + ts, + }); + await core!.onTurnEnd({ + agent: "openclaw", + namespace: ns, + sessionId, + episodeId: start.query.episodeId!, + agentText: `stored for ${sessionId}`, + toolCalls: [], + ts: ts + 1, + }); + }; + + // Three episodes, newest-first order: main-2, sub-1, main-1 — the + // sub-profile row sits inside the first page window of size 2. + await runTurn(mainNs, "s-main-1", 1_700_000_000_010); + await runTurn(subNs, "s-sub-1", 1_700_000_000_020); + await runTurn(mainNs, "s-main-2", 1_700_000_000_030); + + // Active namespace is `main` after the last turn. A scoped page of + // 2 must contain BOTH main episodes, skipping the interleaved + // sub-profile row instead of consuming a page slot on it. + const page = await core.listEpisodes({ limit: 2 }); + expect(page).toHaveLength(2); + + // And the all-namespace read still sees everything. + await expect( + core.listEpisodes({ limit: 10, includeAllNamespaces: true }), + ).resolves.toHaveLength(3); + }); + it("records visible subagent task and result in the parent episode", async () => { pipeline = createPipeline(buildDeps(db!)); core = createMemoryCore( diff --git a/apps/memos-local-plugin/tests/unit/server/http.test.ts b/apps/memos-local-plugin/tests/unit/server/http.test.ts index 616b9e852..c3ba213e5 100644 --- a/apps/memos-local-plugin/tests/unit/server/http.test.ts +++ b/apps/memos-local-plugin/tests/unit/server/http.test.ts @@ -522,6 +522,20 @@ describe("HTTP server — REST routes", () => { expect(Array.isArray(body.dailyWrites)).toBe(true); }); + it("GET /api/v1/metrics/tools scopes both data feeds to all namespaces", async () => { + const r = await fetch(`${handle.url}/api/v1/metrics/tools?minutes=60`); + expect(r.status).toBe(200); + // The tool panel folds api_logs entries and trace tool-calls into + // one aggregation; both feeds must be pinned to all-namespace reads + // or the chart under-reports after a namespace flip (#2131). + expect(core.listApiLogs).toHaveBeenCalledWith( + expect.objectContaining({ includeAllNamespaces: true }), + ); + expect(core.listTraces).toHaveBeenCalledWith( + expect.objectContaining({ includeAllNamespaces: true }), + ); + }); + it("GET /api/v1/config returns resolved config", async () => { const r = await fetch(`${handle.url}/api/v1/config`); expect(r.status).toBe(200);