mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
d031c3d13a
* feat(agentdb): add hierarchical-delete + causal-edge-delete + causal-node-delete MCP tools (#1784) The reporter (@Tovli) flagged that agentdb_hierarchical-store and agentdb_causal-edge had no delete counterparts. Net effect: /adr-index cant do a full re-index because deleted ADR files remain as stale nodes + dangling edges in adr-patterns / adr-edges forever. Architectural finding during scoping: neither @ruvector/graph-node native binding (createNode/createEdge/createHyperedge only) nor agentdbs HierarchicalMemory class exposes a public delete API. So the original proposed deleteNode/deleteEdge wrappers cant land — the storage layer genuinely has no delete primitive. Honest fix: surface delete for the SQL fallback path that bridge stores already use (memory_entries soft-delete via status flip + AttestationLog audit), and explicitly report controller="native-unsupported" when an entry lives in a backend without delete. No silent black holes. What ships: v3/@claude-flow/cli/src/memory/memory-bridge.ts: 3 new bridge functions - bridgeDeleteHierarchical({ key, tier? }): 1. Try hm.delete() if HM ever ships one 2. Try hm.remove() for stub HMs 3. SQL fallback: soft-delete on memory_entries WHERE namespace LIKE hierarchical% 4. native-unsupported when neither path matches but real HM is loaded - bridgeDeleteCausalEdge({ sourceId, targetId, relation? }): 1. Try causalGraph.removeEdge() if it ever exists 2. SQL fallback: soft-delete causal-edges row keyed by sourceId→targetId 3. native-unsupported when graph-node native is the canonical store - bridgeDeleteCausalNode({ nodeId }) — cascade: Soft-deletes the node row + every edge whose key matches nodeId→% or %→nodeId Returns deletedEdges count for observability All three flow through MutationGuard (fail-closed) and AttestationLog (audit). v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts: 3 new MCP tools - agentdb_hierarchical-delete (validates key + tier enum) - agentdb_causal-edge-delete (validates sourceId + targetId + optional relation) - agentdb_causal-node-delete (cascade) Registered in agentdbTools export array. Each tools description surfaces the native-backend caveat so callers can tell at a glance. v3/@claude-flow/cli/__tests__/agentdb-delete-tools.test.ts: 12 surface tests - All 3 tools registered in agentdbTools - Required-field validation - Tier enum rejection - Cascade contract shape - InputSchema completeness Validation: - tsc clean - 12 new delete-tool tests pass - 781 baseline cli tests still pass (parser, output, validate-input ×2, mcp-tools-deep, commands, cli, commands-deep, config-adapter ×2, p1-commands, router-bandit, neural-package-bridge) Closes #1784 (the SQL-fallback delete path). Out of scope (tracked for follow-up): - Tombstone tables for native graph-node storage (recall paths would filter against a shadow table). Bigger architectural change. - Updating plugins/ruflo-adr/scripts/import.mjs to invoke the new delete tools during re-index. Requires the sync semantics call. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(agentdb): wire bridge delete fns to agentdb@3.0.0-alpha.13 native APIs (#1784) Upstream agentic-flow#150 / agentic-flow#151 merged + agentdb@3.0.0-alpha.13 shipped with the missing delete API. Updates the bridge to use the native methods first, falling back to the SQL path only when the controller is older than alpha.13. What changed: v3/@claude-flow/cli/package.json: bumped agentdb dep range "agentdb": "^3.0.0-alpha.11" -> "^3.0.0-alpha.13" v3/pnpm-lock.yaml: regenerated with --no-frozen-lockfile v3/@claude-flow/cli/src/memory/memory-bridge.ts: 3 bridge fns updated. bridgeDeleteHierarchical: now tries reflexionMemory.deleteEpisode(key) FIRST. ReflexionMemory.deleteEpisode propagates through graph adapter + generic graph backend + vector backend AND purges SQL episodes + episode_embeddings rows in one call (durably consistent across backends). Falls back to old hm.delete()/hm.remove() probes + SQL soft-delete for pre-alpha.13 / non-reflexion controllers. bridgeDeleteCausalEdge: now tries causalGraph.deleteEdgesByEndpoints( sourceId, targetId, relation) FIRST — Cypher-routed (MATCH … DETACH DELETE). Cypher-injection-safe; labels validated /^[A-Za-z_][A-Za-z0-9_]*$/ upstream. Falls back to removeEdge() probe and SQL fallback. bridgeDeleteCausalNode: now tries causalGraph.deleteNode(id, {cascade: true}) FIRST. Returns native {deletedNode, deletedEdges} for accurate audit. Cascade counts incident edges before delete; works regardless of binding stats. Falls back to SQL multi-row soft-delete. Validation: - tsc clean - 12 delete-tool surface tests pass - 781 baseline cli tests still pass - Total 793 cli tests green - lockfile drift fixed (regenerated for new agentdb range) The "controller: native-unsupported" branch is now dead-code on alpha.13 backends — it only fires for pre-alpha.13 installs that explicitly load @ruvector/graph-node native binding without the Cypher routing. Closes #1784 (truly this time). Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
122 lines
4.9 KiB
TypeScript
122 lines
4.9 KiB
TypeScript
/**
|
|
* agentdb_*-delete MCP tool surface tests (#1784).
|
|
*
|
|
* Verifies the three new delete tools exist, validate inputs, and surface
|
|
* the right shapes for both supported (SQL fallback) and unsupported
|
|
* (native graph-node / HierarchicalMemory) backends.
|
|
*
|
|
* Behavioral integration tests (actually deleting rows from a live
|
|
* AgentDB) are out of scope here — they require a sqlite + agentdb
|
|
* instance which the cli's existing test scaffolding doesn't spin up
|
|
* for unit tests. The MCP-level shape tests below cover the contract
|
|
* the importer (plugins/ruflo-adr/scripts/import.mjs) depends on.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
agentdbHierarchicalDelete,
|
|
agentdbCausalEdgeDelete,
|
|
agentdbCausalNodeDelete,
|
|
agentdbTools,
|
|
} from '../src/mcp-tools/agentdb-tools.js';
|
|
|
|
describe('agentdb delete tools — surface (#1784)', () => {
|
|
it('exposes agentdb_hierarchical-delete in the tool registry', () => {
|
|
const names = agentdbTools.map((t) => t.name);
|
|
expect(names).toContain('agentdb_hierarchical-delete');
|
|
});
|
|
|
|
it('exposes agentdb_causal-edge-delete in the tool registry', () => {
|
|
const names = agentdbTools.map((t) => t.name);
|
|
expect(names).toContain('agentdb_causal-edge-delete');
|
|
});
|
|
|
|
it('exposes agentdb_causal-node-delete in the tool registry', () => {
|
|
const names = agentdbTools.map((t) => t.name);
|
|
expect(names).toContain('agentdb_causal-node-delete');
|
|
});
|
|
|
|
it('hierarchical-delete validates required key parameter', async () => {
|
|
const r = await agentdbHierarchicalDelete.handler({});
|
|
expect(r.success).toBe(false);
|
|
expect(r.deleted).toBe(false);
|
|
expect(typeof r.error).toBe('string');
|
|
});
|
|
|
|
it('hierarchical-delete rejects invalid tier', async () => {
|
|
const r = await agentdbHierarchicalDelete.handler({
|
|
key: 'test-key',
|
|
tier: 'not-a-real-tier',
|
|
});
|
|
expect(r.success).toBe(false);
|
|
expect(r.error).toMatch(/tier|Invalid|disallowed/i);
|
|
});
|
|
|
|
it('hierarchical-delete accepts valid tiers', async () => {
|
|
for (const tier of ['working', 'episodic', 'semantic']) {
|
|
const r = await agentdbHierarchicalDelete.handler({ key: 'test-key', tier });
|
|
// Either it works (success) or it surfaces a controller status — both are fine
|
|
expect(r).toBeDefined();
|
|
expect(typeof r.success).toBe('boolean');
|
|
}
|
|
});
|
|
|
|
it('causal-edge-delete validates required sourceId / targetId', async () => {
|
|
const r1 = await agentdbCausalEdgeDelete.handler({ targetId: 'B' });
|
|
expect(r1.success).toBe(false);
|
|
const r2 = await agentdbCausalEdgeDelete.handler({ sourceId: 'A' });
|
|
expect(r2.success).toBe(false);
|
|
});
|
|
|
|
it('causal-edge-delete handles a missing edge cleanly (returns native-unsupported or not-found)', async () => {
|
|
const r = await agentdbCausalEdgeDelete.handler({
|
|
sourceId: 'nonexistent-source',
|
|
targetId: 'nonexistent-target',
|
|
});
|
|
expect(r).toBeDefined();
|
|
expect(typeof r.success).toBe('boolean');
|
|
// If the bridge isn't available at all, the tool surfaces an error;
|
|
// if it is, controller will be 'native-unsupported' or 'sql-error'.
|
|
if (r.controller) {
|
|
expect(['native-unsupported', 'bridge-fallback', 'sql-error', 'guard']).toContain(r.controller);
|
|
}
|
|
});
|
|
|
|
it('causal-node-delete validates required nodeId', async () => {
|
|
const r = await agentdbCausalNodeDelete.handler({});
|
|
expect(r.success).toBe(false);
|
|
expect(r.deletedNode).toBe(false);
|
|
expect(r.deletedEdges).toBe(0);
|
|
});
|
|
|
|
it('causal-node-delete returns the cascade contract shape', async () => {
|
|
const r = await agentdbCausalNodeDelete.handler({ nodeId: 'nonexistent-node' });
|
|
expect(r).toBeDefined();
|
|
expect(typeof r.success).toBe('boolean');
|
|
expect(typeof r.deletedNode).toBe('boolean');
|
|
expect(typeof r.deletedEdges).toBe('number');
|
|
// nodeId is echoed back when the bridge returns a real result. When the
|
|
// bridge isn't available (test env without AgentDB), the handler returns
|
|
// its no-bridge fallback shape without nodeId — both are valid.
|
|
if (r.nodeId !== undefined) {
|
|
expect(r.nodeId).toBe('nonexistent-node');
|
|
} else {
|
|
expect(r.error).toMatch(/bridge not available/i);
|
|
}
|
|
});
|
|
|
|
it('all three delete tools have an inputSchema with required fields', () => {
|
|
for (const tool of [agentdbHierarchicalDelete, agentdbCausalEdgeDelete, agentdbCausalNodeDelete]) {
|
|
expect(tool.inputSchema.type).toBe('object');
|
|
expect(Array.isArray(tool.inputSchema.required)).toBe(true);
|
|
expect((tool.inputSchema.required as string[]).length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('all three delete tools return useful descriptions surfacing the native-backend caveat', () => {
|
|
expect(agentdbHierarchicalDelete.description).toMatch(/native|backend|delete/i);
|
|
expect(agentdbCausalEdgeDelete.description).toMatch(/native|backend|delete/i);
|
|
expect(agentdbCausalNodeDelete.description).toMatch(/cascade|delete/i);
|
|
});
|
|
});
|