Files
ruvnet__ruflo/v3/@claude-flow/cli/__tests__/agentdb-delete-tools.test.ts
rUv d031c3d13a feat(agentdb): hierarchical + causal-edge + causal-node delete MCP tools (#1784) (#1789)
* 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>
2026-05-06 10:55:04 -04:00

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);
});
});