Skip to content

Commit 9433dad

Browse files
committed
test: pin tag round-trip through CognitiveMemoryManager encode/retrieve
Regression pin: encode({tags: ['bench-session:X', ...]}) must round-trip tags through ScoredMemoryTrace on retrieve. Covers: single-tag, multiple tags per trace, tags with colons/special chars in values. Any future change that drops tags breaks this test. Exercises the in-memory MemoryStore → VectorStore hydration path; real SqliteBrain round-trip coverage lives downstream in agentos-bench integration tests.
1 parent 5929380 commit 9433dad

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* @fileoverview Pins: encode({tags: [...]}) → retrieve() round-trip
3+
* must preserve the tag list on ScoredMemoryTrace.tags. Any future
4+
* change that drops tags during vector-store hydration breaks this
5+
* test.
6+
*
7+
* The real SqliteBrain round-trip is covered downstream in
8+
* agentos-bench integration tests; this spec pins the in-memory
9+
* MemoryStore → VectorStore hydration path.
10+
*/
11+
12+
import { describe, it, expect, vi, beforeEach } from 'vitest';
13+
import { CognitiveMemoryManager } from '../../src/memory/CognitiveMemoryManager';
14+
import type { CognitiveMemoryConfig, PADState } from '../../src/memory/core/config';
15+
import type { IVectorStore, VectorDocument, QueryResult } from '../../src/rag/IVectorStore';
16+
import type { IEmbeddingManager } from '../../src/rag/IEmbeddingManager';
17+
import type { IKnowledgeGraph } from '../../src/core/knowledge/IKnowledgeGraph';
18+
import type { IWorkingMemory } from '../../src/cognitive_substrate/memory/IWorkingMemory';
19+
20+
function createMockVectorStore(): IVectorStore {
21+
const collections = new Map<string, VectorDocument[]>();
22+
return {
23+
initialize: vi.fn().mockResolvedValue(undefined),
24+
createCollection: vi.fn().mockResolvedValue(undefined),
25+
deleteCollection: vi.fn().mockResolvedValue(undefined),
26+
collectionExists: vi.fn(async (n: string) => collections.has(n)),
27+
upsert: vi.fn(async (c: string, docs: VectorDocument[]) => {
28+
const existing = collections.get(c) ?? [];
29+
for (const d of docs) {
30+
const i = existing.findIndex((e) => e.id === d.id);
31+
if (i >= 0) existing[i] = d;
32+
else existing.push(d);
33+
}
34+
collections.set(c, existing);
35+
return { succeeded: docs.length, failed: 0 };
36+
}),
37+
query: vi.fn(async (c: string): Promise<QueryResult> => ({
38+
documents: (collections.get(c) ?? []).map((d) => ({ ...d, similarityScore: 0.85 })),
39+
})),
40+
deleteByIds: vi.fn().mockResolvedValue(undefined),
41+
getStats: vi.fn().mockResolvedValue({ documentCount: 0, vectorCount: 0 }),
42+
shutdown: vi.fn().mockResolvedValue(undefined),
43+
} as unknown as IVectorStore;
44+
}
45+
46+
function createMockEmbeddingManager(): IEmbeddingManager {
47+
return {
48+
generateEmbeddings: vi.fn(async () => ({ embeddings: [[0.1, 0.2, 0.3, 0.4]], model: 'mock', tokensUsed: 10 })),
49+
getDimension: vi.fn().mockReturnValue(4),
50+
} as unknown as IEmbeddingManager;
51+
}
52+
53+
function createMockKnowledgeGraph(): IKnowledgeGraph {
54+
return {
55+
initialize: vi.fn().mockResolvedValue(undefined),
56+
recordMemory: vi.fn().mockResolvedValue({ id: 'mem-1', createdAt: new Date().toISOString(), accessCount: 0, lastAccessedAt: new Date().toISOString() }),
57+
upsertEntity: vi.fn().mockResolvedValue({ id: 'e-1' }),
58+
getEntity: vi.fn().mockResolvedValue(undefined),
59+
queryEntities: vi.fn().mockResolvedValue([]),
60+
deleteEntity: vi.fn().mockResolvedValue(true),
61+
upsertRelation: vi.fn().mockResolvedValue({ id: 'r-1' }),
62+
getRelations: vi.fn().mockResolvedValue([]),
63+
deleteRelation: vi.fn().mockResolvedValue(true),
64+
getMemory: vi.fn().mockResolvedValue(undefined),
65+
queryMemories: vi.fn().mockResolvedValue([]),
66+
recallMemories: vi.fn().mockResolvedValue([]),
67+
traverse: vi.fn().mockResolvedValue({ root: {}, levels: [], totalEntities: 0, totalRelations: 0 }),
68+
findPath: vi.fn().mockResolvedValue(null),
69+
getNeighborhood: vi.fn().mockResolvedValue({ entities: [], relations: [] }),
70+
semanticSearch: vi.fn().mockResolvedValue([]),
71+
extractFromText: vi.fn().mockResolvedValue({ entities: [], relations: [] }),
72+
mergeEntities: vi.fn().mockResolvedValue({}),
73+
decayMemories: vi.fn().mockResolvedValue(0),
74+
getStats: vi.fn().mockResolvedValue({ totalEntities: 0, totalRelations: 0, totalMemories: 0 }),
75+
clear: vi.fn().mockResolvedValue(undefined),
76+
} as unknown as IKnowledgeGraph;
77+
}
78+
79+
function createMockWorkingMemory(): IWorkingMemory {
80+
const store = new Map<string, unknown>();
81+
return {
82+
id: 'mock-wm',
83+
initialize: vi.fn().mockResolvedValue(undefined),
84+
set: vi.fn(async (k: string, v: unknown) => { store.set(k, v); }),
85+
get: vi.fn(async (k: string) => store.get(k)),
86+
delete: vi.fn(async (k: string) => { store.delete(k); }),
87+
getAll: vi.fn(async () => Object.fromEntries(store)),
88+
clear: vi.fn(async () => { store.clear(); }),
89+
size: vi.fn(async () => store.size),
90+
has: vi.fn(async (k: string) => store.has(k)),
91+
close: vi.fn().mockResolvedValue(undefined),
92+
} as unknown as IWorkingMemory;
93+
}
94+
95+
describe('CognitiveMemoryManager tag round-trip', () => {
96+
let manager: CognitiveMemoryManager;
97+
const neutralMood: PADState = { valence: 0, arousal: 0, dominance: 0 };
98+
99+
beforeEach(async () => {
100+
manager = new CognitiveMemoryManager();
101+
await manager.initialize({
102+
vectorStore: createMockVectorStore(),
103+
embeddingManager: createMockEmbeddingManager(),
104+
knowledgeGraph: createMockKnowledgeGraph(),
105+
workingMemory: createMockWorkingMemory(),
106+
agentId: 'test-agent',
107+
traits: { openness: 0.7, conscientiousness: 0.6, emotionality: 0.5 },
108+
moodProvider: () => neutralMood,
109+
featureDetectionStrategy: 'keyword',
110+
collectionPrefix: 'test',
111+
} as CognitiveMemoryConfig);
112+
});
113+
114+
it('preserves tags from encode through retrieve', async () => {
115+
const encoded = await manager.encode(
116+
'the coffee machine broke on march 3',
117+
neutralMood,
118+
'neutral',
119+
{ tags: ['bench-session:session_42', 'foo-tag'] },
120+
);
121+
expect(encoded.tags).toContain('bench-session:session_42');
122+
expect(encoded.tags).toContain('foo-tag');
123+
124+
const result = await manager.retrieve('coffee machine', neutralMood, {
125+
scopes: [{ scope: 'user', scopeId: 'test-agent' }],
126+
});
127+
const retrieved = result.retrieved.find((t) => t.id === encoded.id);
128+
expect(retrieved, 'encoded trace should round-trip through retrieve').toBeDefined();
129+
expect(retrieved!.tags).toContain('bench-session:session_42');
130+
expect(retrieved!.tags).toContain('foo-tag');
131+
});
132+
133+
it('supports multiple distinct tags per trace without collision', async () => {
134+
const a = await manager.encode('trace A', neutralMood, 'neutral', {
135+
tags: ['bench-session:s1', 'category:a'],
136+
});
137+
const b = await manager.encode('trace B', neutralMood, 'neutral', {
138+
tags: ['bench-session:s2', 'category:b'],
139+
});
140+
141+
const result = await manager.retrieve('anything', neutralMood, {
142+
scopes: [{ scope: 'user', scopeId: 'test-agent' }],
143+
});
144+
const ra = result.retrieved.find((t) => t.id === a.id);
145+
const rb = result.retrieved.find((t) => t.id === b.id);
146+
expect(ra!.tags).toContain('bench-session:s1');
147+
expect(ra!.tags).toContain('category:a');
148+
expect(rb!.tags).toContain('bench-session:s2');
149+
expect(rb!.tags).toContain('category:b');
150+
});
151+
152+
it('round-trips tags with colons and special characters in values', async () => {
153+
const enc = await manager.encode('special', neutralMood, 'neutral', {
154+
tags: ['bench-session:project:123', 'path:/a/b/c'],
155+
});
156+
const result = await manager.retrieve('special', neutralMood, {
157+
scopes: [{ scope: 'user', scopeId: 'test-agent' }],
158+
});
159+
const retrieved = result.retrieved.find((t) => t.id === enc.id);
160+
expect(retrieved!.tags).toContain('bench-session:project:123');
161+
expect(retrieved!.tags).toContain('path:/a/b/c');
162+
});
163+
});

0 commit comments

Comments
 (0)