Repository navigation
Expand file tree
/
Copy pathtest_memory.cpp
More file actions
298 lines (251 loc) · 9.6 KB
/
Copy pathtest_memory.cpp
File metadata and controls
298 lines (251 loc) · 9.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/**
* RaggerMemory facade tests
*
* Requires ONNX model files at the configured model_dir.
*/
#include "config.h"
#include "config_access.h"
#include "embedder.h"
#include "import.h"
#include "memory.h"
#include "sqlite/backend.h"
#include "test_env.h"
#include <sqlite3.h>
#include <algorithm>
#include <cassert>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <print>
#include <stdexcept>
namespace fs = std::filesystem;
static const std::string TEMP_DB1 = "/tmp/ragger_test_mem1.db";
static const std::string TEMP_DB2 = "/tmp/ragger_test_mem2.db";
static void cleanup(const std::string& path) {
fs::remove(path);
fs::remove(path + "-wal");
fs::remove(path + "-shm");
}
static void cleanup_all() {
cleanup(TEMP_DB1);
cleanup(TEMP_DB2);
}
void test_store_and_search() {
std::println(" test_store_and_search...");
cleanup_all();
ragger::RaggerMemory mem(TEMP_DB1);
mem.store("The Eiffel Tower is located in Paris, France.");
mem.store("Photosynthesis converts sunlight into energy in plants.");
auto resp = mem.search("Where is the Eiffel Tower?", 5, 0.0f);
assert(!resp.results.empty());
assert(resp.results[0].text.find("Eiffel") != std::string::npos);
mem.close();
cleanup_all();
std::println(" OK");
}
void test_store_with_tags() {
std::println(" test_store_with_tags...");
cleanup_all();
ragger::RaggerMemory mem(TEMP_DB1);
mem.store("Reference: HTTP status 200 means OK.", {{"tags", {"reference"}}});
mem.store("Memory: I had coffee this morning.", {{"tags", {"diary"}}});
// Lean v2 has no collection filter; verify tags round-trip. With only two
// near-equidistant candidates, min-max score normalization can be
// volatile enough to reorder results on a tiny raw-cosine gap (a known
// property of the blend, not specific to this doc pair) -- so find the
// HTTP row by content rather than assuming it lands at rank 0, and check
// its tags round-tripped correctly.
auto resp = mem.search("HTTP status", 5, 0.0f);
assert(!resp.results.empty());
auto it = std::find_if(resp.results.begin(), resp.results.end(),
[](const auto& r) { return r.text.find("HTTP") != std::string::npos; });
assert(it != resp.results.end());
assert((*it).metadata["tags"] == "reference");
mem.close();
cleanup_all();
std::println(" OK");
}
void test_count() {
std::println(" test_count...");
cleanup_all();
ragger::RaggerMemory mem(TEMP_DB1);
assert(mem.count() == 0);
mem.store("First item.");
mem.store("Second item.");
mem.store("Third item.");
assert(mem.count() == 3);
mem.close();
cleanup_all();
std::println(" OK");
}
void test_delete() {
std::println(" test_delete...");
cleanup_all();
ragger::RaggerMemory mem(TEMP_DB1);
std::string id1 = mem.store("To be deleted.");
mem.store("To be kept.");
assert(mem.count() == 2);
bool deleted = mem.delete_memory(std::stoi(id1));
assert(deleted);
assert(mem.count() == 1);
mem.close();
cleanup_all();
std::println(" OK");
}
void test_empty_db_path_throws() {
std::println(" test_empty_db_path_throws...");
bool threw = false;
try {
ragger::RaggerMemory mem("");
} catch (const std::invalid_argument&) {
threw = true;
}
assert(threw);
std::println(" OK");
}
// PHASE5 D.3: recipe Episode layer emits newest-session episodes
// chronologically; a project_summary recipe parses, emits nothing, no error.
void p5_recipe_context(const std::string& model_dir) {
std::println(" p5_recipe_context...");
cleanup_all();
auto r1 = ragger::set_config_persisted("capture_turns", "true");
auto r2 = ragger::set_config_persisted("build_context", "true");
assert(r1.has_value() && r2.has_value());
ragger::RaggerMemory mem(TEMP_DB1);
ragger::Embedder emb(model_dir);
ragger::sqlite::Backend db(emb, TEMP_DB1);
const std::string guid = "p5rc";
std::vector<int> m1, m2;
auto seed = [&](std::vector<int>& out, const std::string& stamp) {
const int t = db.store_turn("p5rc q " + stamp, "p5rc answer " + std::string(60, 'r') + stamp,
"test-model", false, guid, stamp);
assert(db.finalize_turn_summary(t, "p5rc summary " + stamp, "summarizer-model"));
sqlite3* raw = nullptr;
assert(sqlite3_open(TEMP_DB1.c_str(), &raw) == SQLITE_OK);
sqlite3_stmt* st = nullptr;
const std::string sql = "SELECT turn_summary_id FROM turn_summaries WHERE turn_id = " + std::to_string(t);
assert(sqlite3_prepare_v2(raw, sql.c_str(), -1, &st, nullptr) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW);
out.push_back(sqlite3_column_int(st, 0));
sqlite3_finalize(st);
sqlite3_close(raw);
};
seed(m1, "2026-07-01 10:00:00");
seed(m2, "2026-07-01 11:00:00");
auto close_ep = [&](const std::string& text, const std::vector<int>& members,
const std::string& stamp) {
ragger::EpisodeClose ec;
ec.text = text;
ec.model_name = "summarizer-model";
ec.session_guid = guid;
std::tm tm{};
strptime(stamp.c_str(), "%Y-%m-%d %H:%M:%S", &tm);
tm.tm_isdst = -1;
ec.span_start = ec.span_end = static_cast<long long>(mktime(&tm));
ec.member_turn_summary_ids = members;
assert(db.store_episode_close(ec) > 0);
};
close_ep("p5rc episode one", m1, "2026-07-01 10:00:00");
close_ep("p5rc episode two", m2, "2026-07-01 11:00:00");
// Custom recipe files under the (isolated) base recipes dir.
std::error_code ec;
const std::string rdir = ragger::config().resolved_recipes_dir();
fs::create_directories(rdir, ec);
{
std::ofstream f(rdir + "/p5rc_two.json");
f << R"({"name":"p5rc_two","layers":[{"kind":"episode","limit":2}],"max_tokens":8000})";
}
{
std::ofstream f(rdir + "/p5rc_proj.json");
f << R"({"name":"p5rc_proj","layers":[{"kind":"project_summary","limit":3}]})";
}
auto ctx = ragger::build_context(mem, guid, "p5rc_two");
assert(ctx.enabled);
assert(ctx.chunks.size() == 2);
// Chronological (oldest first) per SessionContext contract.
assert(ctx.chunks[0].kind == "episode");
assert(ctx.chunks[0].text == "p5rc episode one");
assert(ctx.chunks[1].text == "p5rc episode two");
auto ctx2 = ragger::build_context(mem, guid, "p5rc_proj");
assert(ctx2.enabled);
assert(ctx2.chunks.empty()); // parses, emits nothing
// "session_summary" alias resolves to the episode layer.
{
std::ofstream f(rdir + "/p5rc_alias.json");
f << R"({"name":"p5rc_alias","layers":[{"kind":"session_summary","limit":2}]})";
}
auto ctx3 = ragger::build_context(mem, guid, "p5rc_alias");
assert(ctx3.enabled);
assert(ctx3.chunks.size() == 2);
assert(ctx3.chunks[0].kind == "episode");
mem.close();
db.close();
cleanup_all();
std::println(" OK");
}
// PHASE8 D.4: the summary importer writes knowledge notes tagged
// `imported` + caller tags; re-importing the same JSONL skips both.
void p8_import_notes() {
std::println(" p8_import_notes...");
cleanup_all();
const std::string jsonl = "/tmp/ragger_test_mem1_notes.jsonl";
{
std::ofstream f(jsonl);
f << R"({"text":"Imported note one.","tags":"hand","timestamp":"2026-05-01 10:00:00"})" << "\n";
f << R"({"text":"Imported note two.","tags":"hand,second","timestamp":"2026-05-02 10:00:00"})" << "\n";
}
ragger::RaggerMemory mem(TEMP_DB1);
const auto items = ragger::load_summary_jsonl(jsonl);
assert(items.size() == 2);
int n = 0, n_skip = 0;
auto run_import = [&]() {
for (const auto& s : items) {
if (!s.timestamp.empty() && mem.note_exists_exact(s.text, s.timestamp)) { ++n_skip; continue; }
mem.store_note(s.text, s.tags, s.timestamp);
++n;
}
};
run_import();
assert(n == 2 && n_skip == 0);
n = 0;
run_import();
assert(n == 0 && n_skip == 2); // idempotent re-import
sqlite3* raw = nullptr;
assert(sqlite3_open(TEMP_DB1.c_str(), &raw) == SQLITE_OK);
sqlite3_stmt* st = nullptr;
auto scalar_text = [&](const std::string& sql) -> std::string {
assert(sqlite3_prepare_v2(raw, sql.c_str(), -1, &st, nullptr) == SQLITE_OK);
assert(sqlite3_step(st) == SQLITE_ROW);
std::string v = reinterpret_cast<const char*>(sqlite3_column_text(st, 0));
sqlite3_finalize(st); st = nullptr;
return v;
};
assert(scalar_text("SELECT COUNT(*) FROM knowledge WHERE kind='note'") == "2");
assert(scalar_text("SELECT tags FROM knowledge WHERE text='Imported note one.'") == "hand,imported");
assert(scalar_text("SELECT tags FROM knowledge WHERE text='Imported note two.'") == "hand,second,imported");
sqlite3_close(raw);
fs::remove(jsonl);
mem.close();
cleanup_all();
std::println(" OK");
}
int main() {
TestEnv env("memory"); // must be first: isolates from ~/.ragger
auto model_dir = env.model_dir();
if (!fs::exists(model_dir + "/model.onnx") && !fs::exists(model_dir + "/onnx/model.onnx")) {
std::cerr << "Skipping memory tests: model not found at " << model_dir << "\n";
std::println("test_memory: SKIPPED (no model)");
return 0;
}
std::println("Running memory facade tests:");
p5_recipe_context(model_dir);
p8_import_notes();
test_store_and_search();
test_store_with_tags();
test_count();
test_delete();
test_empty_db_path_throws();
std::println("test_memory: all passed");
return 0;
}