Repository navigation
Expand file tree
/
Copy pathtest_stats.cpp
More file actions
225 lines (203 loc) · 8.48 KB
/
Copy pathtest_stats.cpp
File metadata and controls
225 lines (203 loc) · 8.48 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
/**
* StatsLogger tests — opt-in retrieval instrumentation (RAGGER_STATS).
*
* The ENTIRE test body is wrapped in `#ifdef RAGGER_STATS`. When the flag is
* off, stats_logger.{h,cpp} compile to nothing and there is no StatsLogger to
* test, so main() prints a skip notice and returns success. The CMake target
* is also only built+registered when RAGGER_STATS is ON (see CMakeLists.txt),
* so in a default build this file is never even compiled — the guard here is
* belt-and-suspenders so the file is always self-consistent.
*
* Strategy: point the logger at a THROWAWAY db (/tmp), log fabricated lookups,
* then reopen the db read-only and assert the rows landed correctly. No live
* data, no embedder, no daemon — pure unit test of the logging path.
*/
#ifdef RAGGER_STATS
#include "stats_logger.h"
#include "storage_types.h"
#include <sqlite3.h>
#include "check.h"
#include <cmath>
#include <filesystem>
#include <limits>
#include <print>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using ragger::SearchResult;
using ragger::StatsLogger;
static const std::string DB = "/tmp/ragger_test_stats.db";
static void cleanup() {
fs::remove(DB);
fs::remove(DB + "-wal");
fs::remove(DB + "-shm");
}
// Tiny read-only helper: run a single-int aggregate query against the db.
static int scalar(const std::string& sql) {
sqlite3* db = nullptr;
CHECK(sqlite3_open(DB.c_str(), &db) == SQLITE_OK);
sqlite3_stmt* st = nullptr;
CHECK(sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) == SQLITE_OK);
int out = -1;
if (sqlite3_step(st) == SQLITE_ROW) out = sqlite3_column_int(st, 0);
sqlite3_finalize(st);
sqlite3_close(db);
return out;
}
// Read a single text cell.
static std::string scalar_text(const std::string& sql) {
sqlite3* db = nullptr;
CHECK(sqlite3_open(DB.c_str(), &db) == SQLITE_OK);
sqlite3_stmt* st = nullptr;
CHECK(sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) == SQLITE_OK);
std::string out;
if (sqlite3_step(st) == SQLITE_ROW) {
auto* p = reinterpret_cast<const char*>(sqlite3_column_text(st, 0));
if (p) out = p;
}
sqlite3_finalize(st);
sqlite3_close(db);
return out;
}
// Read a single REAL cell; returns NaN when the cell is NULL.
static double scalar_real(const std::string& sql) {
sqlite3* db = nullptr;
CHECK(sqlite3_open(DB.c_str(), &db) == SQLITE_OK);
sqlite3_stmt* st = nullptr;
CHECK(sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) == SQLITE_OK);
double out = std::numeric_limits<double>::quiet_NaN();
if (sqlite3_step(st) == SQLITE_ROW) {
if (sqlite3_column_type(st, 0) != SQLITE_NULL)
out = sqlite3_column_double(st, 0);
}
sqlite3_finalize(st);
sqlite3_close(db);
return out;
}
static SearchResult mk(int id, const std::string& text, float score,
ragger::json meta = {}) {
SearchResult r;
r.id = id;
r.text = text;
r.score = score;
r.metadata = std::move(meta);
r.timestamp = "";
return r;
}
int main() {
std::println("Running stats logger tests:");
// --- 1. Construction creates the db + schema -----------------------
std::println(" test_open_creates_schema...");
cleanup();
{
StatsLogger s(DB);
CHECK(s.enabled());
}
CHECK(fs::exists(DB));
// Both tables exist.
CHECK(scalar("SELECT COUNT(*) FROM sqlite_master WHERE type='table' "
"AND name IN ('lookups','hits')") == 2);
std::println(" OK");
// --- 2. A lookup writes one lookups row + top-N hits ---------------
std::println(" test_log_lookup_writes_rows...");
cleanup();
{
StatsLogger s(DB);
std::vector<SearchResult> results = {
mk(101, "the white whale breached", 0.91f, {{"collection", "doc"}}),
mk(102, "obsession and the sea", 0.77f, {{"kind", "summary"}}),
mk(103, "a quiet grief", 0.55f, {{"level", "decision"}}),
mk(104, "unrelated chatter", 0.40f),
mk(105, "more noise", 0.33f),
};
s.log_lookup("the cost of obsession", 5, 0.3f, results, 12.5, /*top_n=*/3);
}
CHECK(scalar("SELECT COUNT(*) FROM lookups") == 1);
// top_n=3 → only 3 hits stored even though 5 results were returned.
CHECK(scalar("SELECT COUNT(*) FROM hits") == 3);
// result_count records the FULL set (5), not the capped hit count.
CHECK(scalar("SELECT result_count FROM lookups") == 5);
// Query text round-trips.
CHECK(scalar_text("SELECT query FROM lookups") == "the cost of obsession");
// Ranks are 1..3 in score order.
CHECK(scalar("SELECT memory_id FROM hits WHERE rank=1") == 101);
CHECK(scalar("SELECT memory_id FROM hits WHERE rank=3") == 103);
// Collection label extracted from metadata (collection/kind/level fallback).
CHECK(scalar_text("SELECT collection FROM hits WHERE rank=1") == "doc");
CHECK(scalar_text("SELECT collection FROM hits WHERE rank=2") == "summary");
CHECK(scalar_text("SELECT collection FROM hits WHERE rank=3") == "decision");
std::println(" OK");
// --- 3. top_n<=0 stores ALL results --------------------------------
std::println(" test_top_n_unlimited...");
cleanup();
{
StatsLogger s(DB);
std::vector<SearchResult> results = {
mk(1, "a", 0.9f), mk(2, "b", 0.8f), mk(3, "c", 0.7f), mk(4, "d", 0.6f),
};
s.log_lookup("q", 10, 0.0f, results, -1.0, /*top_n=*/0);
}
CHECK(scalar("SELECT COUNT(*) FROM hits") == 4);
// elapsed_ms negative → stored as NULL.
CHECK(scalar("SELECT COUNT(*) FROM lookups WHERE elapsed_ms IS NULL") == 1);
std::println(" OK");
// --- 4. Empty result set: lookup row, zero hits --------------------
std::println(" test_empty_results...");
cleanup();
{
StatsLogger s(DB);
s.log_lookup("no hits", 5, 0.9f, {}, 3.0);
}
CHECK(scalar("SELECT COUNT(*) FROM lookups") == 1);
CHECK(scalar("SELECT result_count FROM lookups") == 0);
CHECK(scalar("SELECT COUNT(*) FROM hits") == 0);
std::println(" OK");
// --- 5. Multiple lookups accumulate & FK links hold ----------------
std::println(" test_multiple_lookups...");
cleanup();
{
StatsLogger s(DB);
s.log_lookup("first", 3, 0.0f, {mk(1, "x", 0.5f)}, 1.0);
s.log_lookup("second", 3, 0.0f, {mk(2, "y", 0.6f), mk(3, "z", 0.4f)}, 2.0);
}
CHECK(scalar("SELECT COUNT(*) FROM lookups") == 2);
CHECK(scalar("SELECT COUNT(*) FROM hits") == 3);
// Every hit points at a real lookup row (no orphans).
CHECK(scalar("SELECT COUNT(*) FROM hits h "
"LEFT JOIN lookups l ON h.lookup_id = l.id "
"WHERE l.id IS NULL") == 0);
std::println(" OK");
// --- 6. Per-signal breakdown columns (vec/bm25/phon/blended) --------
std::println(" test_per_signal_breakdown...");
cleanup();
{
StatsLogger s(DB);
// Two hits: one with all three signals active, one where phon was
// inactive (-1 sentinel → NULL) to prove the active/inactive distinction.
SearchResult a = mk(201, "all three active", 0.88f, {{"collection", "doc"}});
a.vec_score = 0.9f; a.bm25_score = 0.5f; a.phon_score = 0.2f; a.blended = 8.9f;
SearchResult b = mk(202, "phon inactive", 0.60f, {{"collection", "doc"}});
b.vec_score = 0.4f; b.bm25_score = 0.3f; b.phon_score = -1.0f; b.blended = 4.4f;
s.log_lookup("signal probe", 5, 0.0f, {a, b}, 5.0, /*top_n=*/3);
}
// Active signals round-trip as their stored values.
CHECK(std::abs(scalar_real("SELECT vec_score FROM hits WHERE memory_id=201") - 0.9) < 1e-6);
CHECK(std::abs(scalar_real("SELECT bm25_score FROM hits WHERE memory_id=201") - 0.5) < 1e-6);
CHECK(std::abs(scalar_real("SELECT phon_score FROM hits WHERE memory_id=201") - 0.2) < 1e-6);
CHECK(std::abs(scalar_real("SELECT blended FROM hits WHERE memory_id=201") - 8.9) < 1e-6);
// Inactive phon (-1) is stored as NULL, not -1.
CHECK(scalar("SELECT phon_score IS NULL FROM hits WHERE memory_id=202") == 1);
// ...while its active vec/bm25 still land.
CHECK(std::abs(scalar_real("SELECT vec_score FROM hits WHERE memory_id=202") - 0.4) < 1e-6);
std::println(" OK");
cleanup();
std::println("All stats logger tests passed.");
return 0;
}
#else // !RAGGER_STATS
#include <print>
int main() {
std::println("stats logger tests skipped (built without RAGGER_STATS)");
return 0;
}
#endif // RAGGER_STATS