hippocamp 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -346,3 +346,155 @@ test("parallel appends keep every event", async (t) => {
346
346
  assert.equal(new Set(ids).size, 20);
347
347
  assert.equal(new Set(results.map((result) => result.timestamp)).size, 20);
348
348
  });
349
+
350
+ test("search favors events from a month named in the query", async (t) => {
351
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
352
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-month-search-"));
353
+ const projectRoot = path.join(tempRoot, "month-project");
354
+
355
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
356
+ await fs.mkdir(projectRoot, { recursive: true });
357
+ t.after(async () => {
358
+ if (originalRoot === undefined) {
359
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
360
+ } else {
361
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
362
+ }
363
+ await fs.rm(tempRoot, { recursive: true, force: true });
364
+ });
365
+
366
+ for (const timestamp of ["2026-05-02T08:00:00.000Z", "2026-08-05T10:00:00.000Z", "2026-09-01T09:00:00.000Z"]) {
367
+ await memory.appendEvent({
368
+ content: "Published the Android build to Google Play production.",
369
+ cues: ["android-release"],
370
+ date: timestamp.slice(0, 10),
371
+ projectRoot,
372
+ scope: "project",
373
+ sync: false,
374
+ timestamp,
375
+ title: "Android release",
376
+ });
377
+ }
378
+
379
+ const byMonth = await memory.searchMemory({ projectRoot, query: "android build in august", scope: "project" });
380
+ const byDay = await memory.searchMemory({ projectRoot, query: "android build 2026-05-02", scope: "project" });
381
+
382
+ assert.equal(byMonth.results[0].id, "2026-08-05T10:00:00.000Z");
383
+ assert.equal(byDay.results[0].id, "2026-05-02T08:00:00.000Z");
384
+ });
385
+
386
+ test("search favors events from a relative date named in the query", async (t) => {
387
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
388
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-relative-date-"));
389
+ const projectRoot = path.join(tempRoot, "relative-project");
390
+
391
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
392
+ await fs.mkdir(projectRoot, { recursive: true });
393
+ t.after(async () => {
394
+ if (originalRoot === undefined) {
395
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
396
+ } else {
397
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
398
+ }
399
+ await fs.rm(tempRoot, { recursive: true, force: true });
400
+ });
401
+
402
+ // Seven days ago is always in last calendar week; one day ago is yesterday.
403
+ const daysAgo = (days) => new Date(Date.now() - days * 86_400_000).toISOString();
404
+ const timestamps = { yesterday: daysAgo(1), lastWeek: daysAgo(7), older: daysAgo(30) };
405
+
406
+ for (const [key, timestamp] of Object.entries(timestamps)) {
407
+ await memory.appendEvent({
408
+ // The old event matches the words better, so only the date words can rank the others first.
409
+ content: key === "older" ? "Deploy key rotation: rotated the deploy key again." : "Rotated the key for staging.",
410
+ cues: ["deploy-key"],
411
+ date: timestamp.slice(0, 10),
412
+ projectRoot,
413
+ scope: "project",
414
+ sync: false,
415
+ timestamp,
416
+ title: "Deploy key rotation",
417
+ });
418
+ }
419
+
420
+ const ids = async (phrase) =>
421
+ (await memory.searchMemory({ projectRoot, query: `deploy key rotation ${phrase}`, scope: "project" })).results.map(
422
+ (result) => result.id,
423
+ );
424
+
425
+ assert.deepEqual(await ids("yesterday"), [timestamps.yesterday]);
426
+ // On a Monday, yesterday is also in last week.
427
+ const lastWeek = await ids("last week");
428
+ assert.ok(lastWeek.includes(timestamps.lastWeek));
429
+ assert.ok(!lastWeek.includes(timestamps.older));
430
+ assert.deepEqual(await ids("today"), []);
431
+ });
432
+
433
+ test("search returns a snippet for a long event matched only by its date", async (t) => {
434
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
435
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-date-only-"));
436
+ const projectRoot = path.join(tempRoot, "date-only-project");
437
+
438
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
439
+ await fs.mkdir(projectRoot, { recursive: true });
440
+ t.after(async () => {
441
+ if (originalRoot === undefined) {
442
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
443
+ } else {
444
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
445
+ }
446
+ await fs.rm(tempRoot, { recursive: true, force: true });
447
+ });
448
+
449
+ await memory.appendEvent({
450
+ content: Array(30).fill("The weekly sync covered routine updates.").join("\n\n"),
451
+ cues: ["sync"],
452
+ date: "2026-08-05",
453
+ projectRoot,
454
+ scope: "project",
455
+ sync: false,
456
+ timestamp: "2026-08-05T10:00:00.000Z",
457
+ title: "Weekly sync",
458
+ });
459
+
460
+ const search = await memory.searchMemory({ projectRoot, query: "2026-08-05", scope: "project" });
461
+
462
+ assert.equal(search.results.length, 1);
463
+ assert.match(search.results[0].snippet, /^The weekly sync covered routine updates\./);
464
+ assert.ok(search.results[0].snippet.length <= 1200);
465
+ });
466
+
467
+ test("search returns nothing when no memory is about the question", async (t) => {
468
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
469
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-unrelated-"));
470
+ const projectRoot = path.join(tempRoot, "unrelated-project");
471
+
472
+ process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
473
+ await fs.mkdir(projectRoot, { recursive: true });
474
+ t.after(async () => {
475
+ if (originalRoot === undefined) {
476
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
477
+ } else {
478
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
479
+ }
480
+ await fs.rm(tempRoot, { recursive: true, force: true });
481
+ });
482
+
483
+ for (const [title, content] of [
484
+ ["Backend deploy", "The backend deploys to Cloud Run after the build passes."],
485
+ ["Deploy rollback", "A failed backend deploy rolls back to the previous revision."],
486
+ ["Billing note", "Invoices are generated monthly."],
487
+ ]) {
488
+ await memory.appendEvent({ content, cues: ["ops"], projectRoot, scope: "project", sync: false, title });
489
+ }
490
+
491
+ const unrelated = await memory.searchMemory({ projectRoot, query: "which kubernetes namespace does the backend deploy to", scope: "project" });
492
+ const related = await memory.searchMemory({ projectRoot, query: "where does the backend deploy", scope: "project" });
493
+
494
+ const monthOnly = await memory.searchMemory({ projectRoot, query: "deploy in august", scope: "project" });
495
+
496
+ assert.deepEqual(unrelated.results, []);
497
+ assert.equal(related.results[0].heading, "Backend deploy");
498
+ // No event is from August; the month name must not count as a missing topic word.
499
+ assert.ok(monthOnly.results.length > 0);
500
+ });
@@ -0,0 +1,67 @@
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { execFileSync } = require("node:child_process");
4
+ const fs = require("node:fs/promises");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const memory = require("../scripts/hippocamp-memory.cjs");
8
+
9
+ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
10
+ const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
11
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-symlink-"));
12
+ const realLagoon = path.join(tempRoot, "real-lagoon");
13
+ const linkedLagoon = path.join(tempRoot, "linked-lagoon");
14
+ const projectRoot = path.join(tempRoot, "linked-project");
15
+ const git = (args) => execFileSync("git", args, { cwd: realLagoon }).toString().trim();
16
+
17
+ // CI runners have no Git identity; set one for this test only.
18
+ const identity = { GIT_AUTHOR_NAME: "Test", GIT_AUTHOR_EMAIL: "test@example.com", GIT_COMMITTER_NAME: "Test", GIT_COMMITTER_EMAIL: "test@example.com" };
19
+ const saved = Object.fromEntries(Object.keys(identity).map((key) => [key, process.env[key]]));
20
+ Object.assign(process.env, identity);
21
+ t.after(() => {
22
+ for (const [key, value] of Object.entries(saved)) {
23
+ if (value === undefined) {
24
+ delete process.env[key];
25
+ } else {
26
+ process.env[key] = value;
27
+ }
28
+ }
29
+ });
30
+
31
+ await fs.mkdir(realLagoon);
32
+ await fs.mkdir(projectRoot);
33
+ await fs.symlink(realLagoon, linkedLagoon);
34
+ git(["init", "-q", "-b", "main"]);
35
+ git(["commit", "-q", "--allow-empty", "-m", "init"]);
36
+ process.env.HIPPOCAMP_GLOBAL_ROOT = linkedLagoon;
37
+ t.after(async () => {
38
+ if (originalRoot === undefined) {
39
+ delete process.env.HIPPOCAMP_GLOBAL_ROOT;
40
+ } else {
41
+ process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
42
+ }
43
+ await fs.rm(tempRoot, { recursive: true, force: true, maxRetries: 5 });
44
+ });
45
+
46
+ const result = await memory.appendEvent({
47
+ content: "Use advisory locks.",
48
+ cues: ["locks"],
49
+ projectRoot,
50
+ scope: "project",
51
+ title: "Queue decision",
52
+ });
53
+
54
+ assert.equal(result.sync.committed, true);
55
+ assert.match(git(["log", "--format=%s", "-1"]), /append project event/);
56
+
57
+ // A symlink inside the Lagoon pointing at another project must not be synced as this one.
58
+ const otherProject = path.join(tempRoot, "other-project");
59
+ await fs.mkdir(otherProject);
60
+ await fs.mkdir(path.join(realLagoon, "projects", "other-project"), { recursive: true });
61
+ await fs.symlink(path.join(realLagoon, "projects", "linked-project", "events"), path.join(realLagoon, "projects", "other-project", "events"));
62
+
63
+ await assert.rejects(
64
+ memory.appendEvent({ content: "Cross write.", cues: ["x"], projectRoot: otherProject, scope: "project", title: "Escape" }),
65
+ /escapes the memory root/,
66
+ );
67
+ });