diff --git a/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts b/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts index 4981879c7a..2513d8e0ee 100644 --- a/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts +++ b/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts @@ -168,6 +168,61 @@ describe('#2226 — pattern store and search share a backend', () => { }, 60_000); }); +describe('#3288 — pattern store/search surface degraded:true on every non-reasoningBank path', () => { + // `controller: 'reasoningBank'` is the ONLY label that means the healthy + // path ran. Every other label (`bridge-fallback`, `memory-store-fallback`, + // and any future addition) is a degraded response — assert the invariant + // by controller identity, not by enumerating specific fallback labels, so + // a new label can't silently reopen this gap the way `bridge-fallback` did + // (bridgeStorePattern/bridgeSearchPatterns return it non-null, which used + // to skip the fallback blocks entirely and return undegraded). + const HEALTHY_CONTROLLER = 'reasoningBank'; + + it('agentdb_pattern-store sets degraded:true on every path except controller=reasoningBank', async () => { + const marker = `degraded-flag-store-${process.pid}-${process.hrtime.bigint()}`; + const stored = await agentdbPatternStore.handler({ + pattern: `Use ${marker} for secure session renewal`, + type: 'auth-pattern', + confidence: 0.9, + }); + if (!stored || stored.success !== true) return; // both bridge and fallback unavailable — skip + + if (stored.controller === HEALTHY_CONTROLLER) { + expect(stored.degraded).toBeUndefined(); + } else { + // The whole point of #3288: a caller must be able to detect degradation + // from response SHAPE, not by parsing a free-text `note` string — + // covers bridge-fallback (registry present, reasoningBank unusable) + // and memory-store-fallback (registry entirely absent) alike. + expect(stored.degraded).toBe(true); + expect(typeof stored.reason).toBe('string'); + } + }, 60_000); + + it('agentdb_pattern-search sets degraded:true on every path except controller=reasoningBank', async () => { + const marker = `degraded-flag-search-${process.pid}-${process.hrtime.bigint()}`; + await agentdbPatternStore.handler({ + pattern: `Use ${marker} for secure session renewal`, + type: 'auth-pattern', + confidence: 0.9, + }); + + const found = await agentdbPatternSearch.handler({ + query: marker, + topK: 5, + minConfidence: 0.1, + }); + if (!found || !Array.isArray(found.results)) return; // backend unavailable in isolation — skip + + if (found.controller === HEALTHY_CONTROLLER) { + expect(found.degraded).toBeUndefined(); + } else { + expect(found.degraded).toBe(true); + expect(typeof found.reason).toBe('string'); + } + }, 60_000); +}); + /** * 3.10.8 routing-learning fixes (follow-ups to the intelligence audit): * Bug B — Q-router cached a stale route decision and only invalidated the diff --git a/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts index 1ab11a42f6..151fd37139 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts @@ -166,7 +166,21 @@ export const agentdbPatternStore: MCPTool = { const bridge = await getBridge(); const result = await bridge.bridgeStorePattern({ pattern, type, confidence }); - if (result) return result; + if (result) { + // #3288: `controller: 'reasoningBank'` is the ONLY label that means + // the healthy path ran. Every other label bridgeStorePattern can + // return (`bridge-fallback` today; more may be added later) is a + // degraded write that still reports {success: true} — flag it at + // this one boundary instead of chasing each fallback label + // individually, so a new label can't silently reopen this gap. + if (result.controller === 'reasoningBank') return result; + return { + ...result, + degraded: true, + reason: `reasoningBank-unavailable:${result.controller}`, + note: `ReasoningBank controller unavailable (controller=${result.controller}). Pattern persisted via the fallback path. Run \`agentdb_health\` to inspect controller registration.`, + }; + } // ADR-093 F4: when the ReasoningBank controller registry returns // null (the cause of audit-reported "AgentDB bridge not available" @@ -186,6 +200,12 @@ export const agentdbPatternStore: MCPTool = { }); return { success: true, + // #3288: a caller must not have to already know to distrust a + // "successful" response — degraded:true is the structural signal + // (matching agentbbs-tools.ts's degradedResult() convention), + // `note` stays for the human-readable detail. + degraded: true, + reason: 'reasoningBank-unavailable:registry-null', patternId, controller: 'memory-store-fallback', note: 'ReasoningBank controller registry unavailable. Pattern persisted via memory_store. Run `agentdb_health` to inspect controller registration.', @@ -230,7 +250,19 @@ export const agentdbPatternSearch: MCPTool = { const bridge = await getBridge(); const result = await bridge.bridgeSearchPatterns({ query, topK, minConfidence }); if (result && Array.isArray(result.results) && result.results.length > 0) { - return result; + // #3288: `controller: 'reasoningBank'` is the only label meaning the + // healthy path ran and actually found results. Any other label + // (`bridge-fallback` today; more may be added later) is a degraded + // response even though it has results — flag it here, since this + // early return bypasses the tier1/tier2 fallback block below + // entirely (and its degraded:true) whenever there ARE results. + if (result.controller === 'reasoningBank') return result; + return { + ...result, + degraded: true, + reason: `reasoningBank-unavailable:${result.controller}`, + note: `ReasoningBank controller unavailable (controller=${result.controller}); results returned via the fallback path. Run \`agentdb_health\` to inspect controller registration.`, + }; } // #1889 — symmetric fallback. pattern-store writes to the `pattern` @@ -310,8 +342,13 @@ export const agentdbPatternSearch: MCPTool = { // round-trip sees both ends agree. The store reports // `memory-store-fallback`; we use the same name + a `tier` field // to expose which sub-strategy fired. + // #3288: degraded:true is the structural signal a caller can check + // without already knowing to distrust an apparently-successful + // response — matches agentbbs-tools.ts's degradedResult() convention. return { results, + degraded: true, + reason: result ? `reasoningBank-empty:${result.controller ?? 'unknown'}` : 'reasoningBank-unavailable:registry-null', controller: 'memory-store-fallback', tier, note: result