151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
/**
|
|
* Skills 2.0 gap K — write-path conflict detection.
|
|
*
|
|
* When save_memory writes a new frame that contradicts an existing frame
|
|
* (detectContradiction), the save still succeeds but:
|
|
* - the return string carries a [flag: contradicts_existing …] marker
|
|
* - a correction-category improvement signal is recorded
|
|
*
|
|
* Previously detectContradiction existed but was never called on the write
|
|
* path — write-path conflicts silently overwrote reality, degrading the
|
|
* evolution-signal loop.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
MindDB,
|
|
IdentityLayer,
|
|
AwarenessLayer,
|
|
FrameStore,
|
|
SessionStore,
|
|
KnowledgeGraph,
|
|
HybridSearch,
|
|
ImprovementSignalStore,
|
|
} from '@waggle/core';
|
|
import { createMindTools } from '../src/tools.js';
|
|
import type { ToolDefinition } from '../src/tools.js';
|
|
import { MockEmbedder } from '../../hive-mind-core/tests/mind/helpers/mock-embedder.js';
|
|
|
|
describe('save_memory — gap K (write-path conflict detection)', () => {
|
|
let db: MindDB;
|
|
let frames: FrameStore;
|
|
let sessions: SessionStore;
|
|
let tools: ToolDefinition[];
|
|
let improvementSignals: ImprovementSignalStore;
|
|
|
|
function getSaveMemory(): ToolDefinition {
|
|
const tool = tools.find(t => t.name === 'save_memory');
|
|
if (!tool) throw new Error('save_memory tool not found');
|
|
return tool;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
db = new MindDB(':memory:');
|
|
const embedder = new MockEmbedder();
|
|
frames = new FrameStore(db);
|
|
sessions = new SessionStore(db);
|
|
const knowledge = new KnowledgeGraph(db);
|
|
const search = new HybridSearch(db, embedder);
|
|
improvementSignals = new ImprovementSignalStore(db);
|
|
|
|
tools = createMindTools({
|
|
db,
|
|
identity: new IdentityLayer(db),
|
|
awareness: new AwarenessLayer(db),
|
|
frames,
|
|
sessions,
|
|
search,
|
|
knowledge,
|
|
improvementSignals,
|
|
// Intentionally NO cognify → exercises the raw-frame fallback path
|
|
// (no LLM-dependent entity extraction in unit tests).
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
it('flags contradictions and records a correction signal', async () => {
|
|
const save = getSaveMemory();
|
|
|
|
// Prime with an affirmative decision frame.
|
|
// Using "enable/disable feature flag rollout users" — the existing
|
|
// detectContradiction algorithm needs asymmetric sentiment to fire;
|
|
// inputs with "will" in both sides add positive noise that can mask
|
|
// the polarity swing. Keep inputs tight and unambiguous.
|
|
const first = await save.execute({
|
|
content: 'Decision: enable feature flag rollout for users; proceed and confirmed.',
|
|
importance: 'important',
|
|
target: 'personal',
|
|
source: 'user_stated',
|
|
});
|
|
expect(first).toContain('Memory saved');
|
|
expect(first).not.toContain('contradicts_existing');
|
|
|
|
// Contradicting decision — disable/reject/cancel instead of enable/approve
|
|
const second = await save.execute({
|
|
content: 'Decision: disable feature flag rollout for users; cancel and reject.',
|
|
importance: 'important',
|
|
target: 'personal',
|
|
source: 'user_stated',
|
|
});
|
|
|
|
// Save still succeeded — conflict is a flag, not an abort
|
|
expect(second).toContain('Memory saved');
|
|
// But the return message is annotated
|
|
expect(second).toContain('[flag: contradicts_existing');
|
|
|
|
// And a correction signal is recorded
|
|
const top = improvementSignals.getByCategory('correction');
|
|
expect(top.length).toBeGreaterThan(0);
|
|
const writeConflict = top.find(s => s.pattern_key.startsWith('write-conflict:'));
|
|
expect(writeConflict).toBeDefined();
|
|
expect(writeConflict!.detail).toMatch(/contradicts existing frame/i);
|
|
expect(writeConflict!.category).toBe('correction');
|
|
});
|
|
|
|
it('non-contradicting saves do not flag or emit a signal', async () => {
|
|
const save = getSaveMemory();
|
|
await save.execute({
|
|
content: 'Decision: we chose PostgreSQL for analytics; approved and confirmed.',
|
|
target: 'personal',
|
|
source: 'user_stated',
|
|
});
|
|
const result = await save.execute({
|
|
content: 'Meeting notes from standup — discussed roadmap for Q2 and sprint planning.',
|
|
target: 'personal',
|
|
source: 'user_stated',
|
|
});
|
|
expect(result).not.toContain('contradicts_existing');
|
|
|
|
const top = improvementSignals.getByCategory('correction');
|
|
expect(top.find(s => s.pattern_key.startsWith('write-conflict:'))).toBeUndefined();
|
|
});
|
|
|
|
it('works without improvementSignals dep (backwards-compatible)', async () => {
|
|
// Rebuild tools without signals dep
|
|
const db2 = new MindDB(':memory:');
|
|
const embedder = new MockEmbedder();
|
|
const frames2 = new FrameStore(db2);
|
|
const sessions2 = new SessionStore(db2);
|
|
const knowledge2 = new KnowledgeGraph(db2);
|
|
const search2 = new HybridSearch(db2, embedder);
|
|
const tools2 = createMindTools({
|
|
db: db2,
|
|
identity: new IdentityLayer(db2),
|
|
awareness: new AwarenessLayer(db2),
|
|
frames: frames2,
|
|
sessions: sessions2,
|
|
search: search2,
|
|
knowledge: knowledge2,
|
|
// No improvementSignals — should not throw
|
|
});
|
|
const save = tools2.find(t => t.name === 'save_memory')!;
|
|
await save.execute({ content: 'Decision: enable feature flag rollout for users; proceed.', target: 'personal' });
|
|
const result = await save.execute({ content: 'Decision: disable feature flag rollout for users; cancel.', target: 'personal' });
|
|
// Flag still appears (detectContradiction doesn't need the signal store)
|
|
expect(result).toContain('contradicts_existing');
|
|
db2.close();
|
|
});
|
|
});
|