6cc9b9f0fe
* fix: env hydration, indexing, consolidation, connectors, hardening - config: hydrate ~/.agentmemory/.env into process.env at boot so all modules see it - search: shared indexRecords() so export-import and replay populate BM25 and vector (#1072) - snapshot: wire the periodic timer (#1006), clamp non-positive intervals, add a reentrancy guard - schema: CJK-aware jaccard dedup plus exact-match fallback for short memories - embeddings: shared resolveDimensions() so openrouter stops hardcoding 1536 (#1002) - viewer: buffer request bodies before decoding to fix multibyte corruption (#930) - providers: retry 429/503 with Retry-After under a total-elapsed budget cap - consolidation: fire on session stop (#1087), gate keyless installs, debounce the per-turn stop hook, drop the client-side double-fire - evict: bound stale-session recovery to one consolidation pass - api/patterns: bound session fan-out (#1100) - connect: write a memory-usage guideline into each hook-less agent's native rules file (12 agents, doc-verified paths, --no-guidelines opt-out) - graph: import graphify's graph.json via mem::graph::import-graphify + POST /agentmemory/graph/import-graphify; shared persistGraphDelta with endpoint remap so merged nodes never leave dangling or duplicate edges - fs-watcher: stat roots before fs.watch so missing roots fail deterministically on Node 24+ - test: regression tests for every fix * fix: address review findings on import, debounce, and connect paths - guidelines: refuse to touch files with a lone or reversed marker pair - export-import/replay: indexing after committed writes is best-effort, logged instead of failing the import; flatten the nested runChunked so replace-mode deletes stay bounded to one chunk - graph: persist the snapshot when merge-only batches mutate cached topNodes/topEdges entries - graph-import: async fs, typeof validation on path/cwd; REST handler whitelists the payload and 400s non-string values - fetch: cancel discarded response bodies before retrying - events: serialize the consolidation cooldown check so concurrent stops cannot both pass the read-check-write window - evict: gate recovered-session consolidation on isConsolidationEnabled and mirror the stop path's force flag - search: rebuild indexes per session chunk to bound peak memory - test: regression coverage for each (malformed markers, concurrent stops, snapshot persistence, AMBIGUOUS/default mappings, env isolation)
97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { loadSnapshotConfig, __resetEnvFileCache } from "../src/config.js";
|
|
|
|
// loadSnapshotConfig reads getMergedEnv(), which merges the on-disk
|
|
// ~/.agentmemory/.env under process.env. The tests below delete the
|
|
// process.env keys, so a developer machine with SNAPSHOT_* in its real .env
|
|
// would leak into the defaults; point HOME at an empty temp dir and reset
|
|
// the env-file cache so the file layer is deterministic.
|
|
//
|
|
// Regression (P1): a zero/negative SNAPSHOT_INTERVAL flowed straight into
|
|
// setInterval(fn, interval * 1000). Node clamps a non-positive delay to ~1ms,
|
|
// so the git-snapshot timer would fire on nearly every event-loop tick,
|
|
// saturating the worker with back-to-back full-state snapshots + commits.
|
|
// Non-positive values must fall back to the documented 3600s default.
|
|
|
|
const KEYS = ["SNAPSHOT_INTERVAL", "SNAPSHOT_ENABLED", "SNAPSHOT_DIR"] as const;
|
|
const DEFAULT_INTERVAL = 3600;
|
|
|
|
describe("loadSnapshotConfig interval validation", () => {
|
|
const saved: Record<string, string | undefined> = {};
|
|
let sandboxHome: string;
|
|
let savedHome: string | undefined;
|
|
let savedUserProfile: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
sandboxHome = mkdtempSync(join(tmpdir(), "am-snapcfg-"));
|
|
savedHome = process.env["HOME"];
|
|
savedUserProfile = process.env["USERPROFILE"];
|
|
process.env["HOME"] = sandboxHome;
|
|
process.env["USERPROFILE"] = sandboxHome;
|
|
__resetEnvFileCache();
|
|
for (const k of KEYS) {
|
|
saved[k] = process.env[k];
|
|
delete process.env[k];
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const k of KEYS) {
|
|
if (saved[k] === undefined) delete process.env[k];
|
|
else process.env[k] = saved[k];
|
|
}
|
|
if (savedHome === undefined) delete process.env["HOME"];
|
|
else process.env["HOME"] = savedHome;
|
|
if (savedUserProfile === undefined) delete process.env["USERPROFILE"];
|
|
else process.env["USERPROFILE"] = savedUserProfile;
|
|
__resetEnvFileCache();
|
|
rmSync(sandboxHome, { recursive: true, force: true });
|
|
});
|
|
|
|
it("falls back to the default when interval is zero", () => {
|
|
process.env["SNAPSHOT_INTERVAL"] = "0";
|
|
expect(loadSnapshotConfig().interval).toBe(DEFAULT_INTERVAL);
|
|
});
|
|
|
|
it("falls back to the default when interval is negative", () => {
|
|
process.env["SNAPSHOT_INTERVAL"] = "-5";
|
|
expect(loadSnapshotConfig().interval).toBe(DEFAULT_INTERVAL);
|
|
});
|
|
|
|
it("falls back to the default when interval is non-numeric", () => {
|
|
process.env["SNAPSHOT_INTERVAL"] = "not-a-number";
|
|
expect(loadSnapshotConfig().interval).toBe(DEFAULT_INTERVAL);
|
|
});
|
|
|
|
it("uses the default when interval is unset", () => {
|
|
expect(loadSnapshotConfig().interval).toBe(DEFAULT_INTERVAL);
|
|
});
|
|
|
|
it("accepts a valid positive interval unchanged", () => {
|
|
process.env["SNAPSHOT_INTERVAL"] = "120";
|
|
expect(loadSnapshotConfig().interval).toBe(120);
|
|
});
|
|
|
|
it("accepts the minimum floor of 1 second", () => {
|
|
process.env["SNAPSHOT_INTERVAL"] = "1";
|
|
expect(loadSnapshotConfig().interval).toBe(1);
|
|
});
|
|
|
|
it("never yields an interval that would clamp setInterval to sub-second", () => {
|
|
for (const bad of ["0", "-1", "-3600", "0.5"]) {
|
|
process.env["SNAPSHOT_INTERVAL"] = bad;
|
|
expect(loadSnapshotConfig().interval).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it("reports enabled state from SNAPSHOT_ENABLED", () => {
|
|
process.env["SNAPSHOT_ENABLED"] = "true";
|
|
expect(loadSnapshotConfig().enabled).toBe(true);
|
|
process.env["SNAPSHOT_ENABLED"] = "false";
|
|
expect(loadSnapshotConfig().enabled).toBe(false);
|
|
});
|
|
});
|