prism-mcp-server 20.2.6 → 20.2.7
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.
- package/README.md +82 -104
- package/dist/session/sessionContext.js +113 -15
- package/dist/storage/configStorage.js +100 -0
- package/dist/tools/graphHandlers.js +5 -4
- package/dist/tools/handlers.js +2 -2
- package/dist/tools/ledgerHandlers.js +12 -16
- package/dist/tools/prismInferHandler.js +81 -7
- package/dist/tools/queryMemoryNaturalHandler.js +514 -0
- package/dist/tools/sessionMemoryDefinitions.js +23 -5
- package/dist/tools/v12Handlers.js +2 -46
- package/dist/utils/braveApi.js +31 -48
- package/dist/utils/codingQualityPolicy.js +400 -0
- package/dist/utils/layer1.js +63 -4
- package/dist/utils/nlQuery.js +6 -33
- package/dist/utils/qualityGate.js +16 -2
- package/dist/utils/synaluxSearch.js +12 -8
- package/package.json +3 -2
- package/dist/boundaries/__tests__/boundaries.test.js +0 -46
- package/dist/session/__tests__/sessionContext.test.js +0 -134
- package/dist/tools/__tests__/ingestHandler.test.js +0 -323
- package/dist/tools/__tests__/layer1Integration.test.js +0 -590
- package/dist/tools/__tests__/ledgerHandlers.test.js +0 -1281
package/dist/utils/nlQuery.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* This closes the gap where users have to learn tool syntax to query memories.
|
|
8
8
|
* Instead, they ask plain English questions and get structured results.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Parsing pipeline:
|
|
11
11
|
* 1. Intent classification (decision, todo, file, general)
|
|
12
12
|
* 2. Key phrase extraction
|
|
13
|
-
* 3.
|
|
14
|
-
*
|
|
13
|
+
* 3. Structured memory-query construction
|
|
14
|
+
*
|
|
15
|
+
* Execution, web fallback, and grounded prism_infer synthesis live in
|
|
16
|
+
* queryMemoryNaturalHandler.ts. Keeping network/model work out of this parser
|
|
17
|
+
* makes intent handling deterministic and independently testable.
|
|
15
18
|
*/
|
|
16
19
|
import { debugLog } from "./logger.js";
|
|
17
20
|
// ─── Intent Classification ───────────────────────────────────
|
|
@@ -151,34 +154,4 @@ export function parseNLQuery(query, project) {
|
|
|
151
154
|
suggestedArgs: args,
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Execute a natural language query end-to-end.
|
|
156
|
-
*
|
|
157
|
-
* Parses the query, executes the appropriate tool, and optionally
|
|
158
|
-
* synthesizes an answer using the local LLM.
|
|
159
|
-
*/
|
|
160
|
-
export async function executeNLQuery(query, project, synthesize = false) {
|
|
161
|
-
const parsed = parseNLQuery(query, project);
|
|
162
|
-
// Execute the search
|
|
163
|
-
let sources = [];
|
|
164
|
-
let answer = "";
|
|
165
|
-
try {
|
|
166
|
-
// Return the parsed query structure — the caller (handler) will
|
|
167
|
-
// execute the actual tool call using the suggestedTool and suggestedArgs.
|
|
168
|
-
answer =
|
|
169
|
-
`Intent: ${parsed.intent} (confidence: ${(parsed.confidence * 100).toFixed(0)}%)\n` +
|
|
170
|
-
`Keywords: ${parsed.extractedKeywords.join(", ")}\n` +
|
|
171
|
-
`Suggested tool: ${parsed.suggestedTool}\n` +
|
|
172
|
-
`Search query: "${parsed.searchQuery}"`;
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
debugLog(`NL query execution failed: ${err}`);
|
|
176
|
-
answer = `Unable to parse query: ${err}`;
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
...parsed,
|
|
180
|
-
answer: answer || "No relevant memories found.",
|
|
181
|
-
sources,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
157
|
debugLog("v12.2: Natural language query module loaded");
|
|
@@ -54,7 +54,11 @@ export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
|
54
54
|
.replace(/^#{1,6}\s+.*$/gm, "")
|
|
55
55
|
.replace(/^[\s*-]*\*{1,2}[^*]+\*{1,2}:?\s*$/gm, "");
|
|
56
56
|
const proseSentences = proseOnly.split(/[.!?\n]+/).map(s => s.trim()).filter(s => s.length > 10);
|
|
57
|
-
|
|
57
|
+
// Plain source code often repeats structural lines across methods. In code
|
|
58
|
+
// mode those are not prose loops, and splitting on "." corrupts member
|
|
59
|
+
// access (`node = self.root` becomes `node = self`). Pass B below still
|
|
60
|
+
// catches egregious repetition at the higher threshold.
|
|
61
|
+
if (mode !== "code" && proseSentences.length >= 6) {
|
|
58
62
|
const counts = new Map();
|
|
59
63
|
for (const s of proseSentences) {
|
|
60
64
|
const key = s.toLowerCase();
|
|
@@ -68,7 +72,17 @@ export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
|
68
72
|
// inside fake code blocks or other structural elements. Higher
|
|
69
73
|
// threshold avoids false positives on legitimate code patterns
|
|
70
74
|
// (e.g. `node = self.root` × 4 is fine, × 5 is suspicious).
|
|
71
|
-
|
|
75
|
+
// In code mode, periods are member-access operators, not sentence
|
|
76
|
+
// boundaries. Splitting `input.length` at "." made several different
|
|
77
|
+
// loops share the fragment `while (i < input` and falsely counted it as
|
|
78
|
+
// repeated output. Full-line matching still catches genuinely duplicated
|
|
79
|
+
// source while preserving valid repeated control-flow shapes.
|
|
80
|
+
const fullTextUnits = mode === "code"
|
|
81
|
+
? stripped.split(/\n+/)
|
|
82
|
+
: stripped.split(/[.!?\n]+/);
|
|
83
|
+
const allSentences = fullTextUnits
|
|
84
|
+
.map(s => s.trim())
|
|
85
|
+
.filter(s => s.length > 10);
|
|
72
86
|
if (allSentences.length >= 10) {
|
|
73
87
|
const counts = new Map();
|
|
74
88
|
for (const s of allSentences) {
|
|
@@ -69,7 +69,7 @@ async function portalPost(path, body, timeoutMs = 15_000) {
|
|
|
69
69
|
* the shape of performWebSearch() in braveApi.ts.
|
|
70
70
|
*/
|
|
71
71
|
export async function synaluxWebSearch(query, count = 10) {
|
|
72
|
-
debugLog(`[synaluxSearch] web search:
|
|
72
|
+
debugLog(`[synaluxSearch] web search: query_chars=${query.length}, limit=${count}`);
|
|
73
73
|
const data = await portalPost("/api/v1/prism/search", {
|
|
74
74
|
query,
|
|
75
75
|
limit: Math.min(count, 20),
|
|
@@ -92,7 +92,7 @@ export async function synaluxWebSearch(query, count = 10) {
|
|
|
92
92
|
* Used by code-mode handlers that pass raw data to the QuickJS sandbox.
|
|
93
93
|
*/
|
|
94
94
|
export async function synaluxWebSearchRaw(query, count = 10) {
|
|
95
|
-
debugLog(`[synaluxSearch] web search raw:
|
|
95
|
+
debugLog(`[synaluxSearch] web search raw: query_chars=${query.length}, limit=${count}`);
|
|
96
96
|
const data = await portalPost("/api/v1/prism/search", {
|
|
97
97
|
query,
|
|
98
98
|
limit: Math.min(count, 20),
|
|
@@ -118,7 +118,7 @@ export async function synaluxWebSearchRaw(query, count = 10) {
|
|
|
118
118
|
* Returns formatted text matching performLocalSearch() shape.
|
|
119
119
|
*/
|
|
120
120
|
export async function synaluxLocalSearch(query, count = 5) {
|
|
121
|
-
debugLog(`[synaluxSearch] local search:
|
|
121
|
+
debugLog(`[synaluxSearch] local search: query_chars=${query.length}, count=${count}`);
|
|
122
122
|
const data = await portalPost("/api/v1/prism/local-search", {
|
|
123
123
|
query,
|
|
124
124
|
count: Math.min(count, 20),
|
|
@@ -136,7 +136,7 @@ export async function synaluxLocalSearch(query, count = 5) {
|
|
|
136
136
|
* Local/POI search raw — returns JSON string for code-mode sandbox.
|
|
137
137
|
*/
|
|
138
138
|
export async function synaluxLocalSearchRaw(query, count = 5) {
|
|
139
|
-
debugLog(`[synaluxSearch] local search raw:
|
|
139
|
+
debugLog(`[synaluxSearch] local search raw: query_chars=${query.length}, count=${count}`);
|
|
140
140
|
const data = await portalPost("/api/v1/prism/local-search", {
|
|
141
141
|
query,
|
|
142
142
|
count: Math.min(count, 20),
|
|
@@ -162,7 +162,7 @@ export async function synaluxLocalSearchRaw(query, count = 5) {
|
|
|
162
162
|
* AI-grounded answers via Synalux portal.
|
|
163
163
|
*/
|
|
164
164
|
export async function synaluxBraveAnswers(query, model) {
|
|
165
|
-
debugLog(`[synaluxSearch] answers:
|
|
165
|
+
debugLog(`[synaluxSearch] answers: query_chars=${query.length}, model=${model || "default"}`);
|
|
166
166
|
const body = { query };
|
|
167
167
|
if (model)
|
|
168
168
|
body.model = model;
|
|
@@ -179,7 +179,7 @@ export async function synaluxBraveAnswers(query, model) {
|
|
|
179
179
|
* Scrape a URL via Synalux portal. Returns the extracted content string.
|
|
180
180
|
*/
|
|
181
181
|
export async function synaluxScrape(url, options) {
|
|
182
|
-
debugLog(`[synaluxSearch] scrape:
|
|
182
|
+
debugLog(`[synaluxSearch] scrape: url_chars=${url.length}`);
|
|
183
183
|
const body = { url };
|
|
184
184
|
if (options?.formats)
|
|
185
185
|
body.formats = options.formats;
|
|
@@ -187,9 +187,13 @@ export async function synaluxScrape(url, options) {
|
|
|
187
187
|
body.onlyMainContent = options.onlyMainContent;
|
|
188
188
|
if (options?.waitFor !== undefined)
|
|
189
189
|
body.waitFor = options.waitFor;
|
|
190
|
-
const data = await portalPost("/api/v1/prism/scrape", body);
|
|
190
|
+
const data = await portalPost("/api/v1/prism/scrape", body, options?.timeoutMs);
|
|
191
191
|
if (data.status === "error") {
|
|
192
192
|
throw new Error(`[synaluxSearch] scrape error: ${data.error || "unknown"}`);
|
|
193
193
|
}
|
|
194
|
-
return data.content ||
|
|
194
|
+
return data.content ||
|
|
195
|
+
data.data?.markdown ||
|
|
196
|
+
data.data?.html ||
|
|
197
|
+
data.data?.rawHtml ||
|
|
198
|
+
"";
|
|
195
199
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.2.
|
|
3
|
+
"version": "20.2.7",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -154,10 +154,11 @@
|
|
|
154
154
|
"jsdom": "^29.0.1",
|
|
155
155
|
"openai": "^6.32.0",
|
|
156
156
|
"p-limit": "^7.3.0",
|
|
157
|
-
"postcss": "8.5.
|
|
157
|
+
"postcss": "8.5.23",
|
|
158
158
|
"quickjs-emscripten": "^0.32.0",
|
|
159
159
|
"smol-toml": "^1.7.0",
|
|
160
160
|
"stream-json": "^2.0.0",
|
|
161
|
+
"tldts": "^7.0.27",
|
|
161
162
|
"turndown": "^7.2.2",
|
|
162
163
|
"zod": "^4.3.6"
|
|
163
164
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* boundaries.ts unit tests
|
|
3
|
-
*
|
|
4
|
-
* Verifies structural invariants of the safety declaration:
|
|
5
|
-
* - BOUNDARIES_VERSION is present and non-empty
|
|
6
|
-
* - BOUNDARIES_TEXT covers safety-critical concepts
|
|
7
|
-
* - Architecture/routing docs moved to server instructions (not per-call)
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect } from "vitest";
|
|
10
|
-
import { BOUNDARIES_VERSION, BOUNDARIES_TEXT } from "../../boundaries/boundaries.js";
|
|
11
|
-
describe("BOUNDARIES_VERSION", () => {
|
|
12
|
-
it("is a non-empty string", () => {
|
|
13
|
-
expect(typeof BOUNDARIES_VERSION).toBe("string");
|
|
14
|
-
expect(BOUNDARIES_VERSION.length).toBeGreaterThan(0);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
describe("BOUNDARIES_TEXT structure", () => {
|
|
18
|
-
it("is a non-empty string under 500 chars (compressed from ~2000)", () => {
|
|
19
|
-
expect(typeof BOUNDARIES_TEXT).toBe("string");
|
|
20
|
-
expect(BOUNDARIES_TEXT.length).toBeGreaterThan(50);
|
|
21
|
-
expect(BOUNDARIES_TEXT.length).toBeLessThan(600);
|
|
22
|
-
});
|
|
23
|
-
it("has no leading or trailing whitespace (trim() was applied)", () => {
|
|
24
|
-
expect(BOUNDARIES_TEXT).toBe(BOUNDARIES_TEXT.trim());
|
|
25
|
-
});
|
|
26
|
-
it("references BCBA reserved categories", () => {
|
|
27
|
-
expect(BOUNDARIES_TEXT).toMatch(/BCBA reserved/i);
|
|
28
|
-
});
|
|
29
|
-
it("references fail-closed refusal for reserved content", () => {
|
|
30
|
-
expect(BOUNDARIES_TEXT).toMatch(/refused/i);
|
|
31
|
-
});
|
|
32
|
-
it("states AAC access is never restricted as a consequence", () => {
|
|
33
|
-
expect(BOUNDARIES_TEXT).toMatch(/AAC access is never restricted/i);
|
|
34
|
-
});
|
|
35
|
-
it("references restraint as reserved", () => {
|
|
36
|
-
expect(BOUNDARIES_TEXT).toMatch(/restraint/i);
|
|
37
|
-
});
|
|
38
|
-
it("references crisis/self-harm interception", () => {
|
|
39
|
-
expect(BOUNDARIES_TEXT).toMatch(/crisis|self-harm/i);
|
|
40
|
-
});
|
|
41
|
-
it("does NOT contain architecture/routing docs (those moved to server instructions)", () => {
|
|
42
|
-
expect(BOUNDARIES_TEXT).not.toContain("session_save_ledger");
|
|
43
|
-
expect(BOUNDARIES_TEXT).not.toContain("inference routing");
|
|
44
|
-
expect(BOUNDARIES_TEXT).not.toMatch(/host note/i);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sessionContext.ts unit tests
|
|
3
|
-
*
|
|
4
|
-
* Covers: markContextLoaded, requireContextLoaded, noteInferenceForSession,
|
|
5
|
-
* getSessionState, TTL eviction, and fail-closed behaviour for unknown sessions.
|
|
6
|
-
*
|
|
7
|
-
* The module uses in-process Map state. Each test imports a fresh module instance
|
|
8
|
-
* via vi.resetModules() + dynamic re-import so tests are isolated without needing
|
|
9
|
-
* an exported reset function.
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
12
|
-
import { BOUNDARIES_VERSION } from "../../boundaries/boundaries.js";
|
|
13
|
-
// Reset module registry before each test so the in-memory Map starts empty.
|
|
14
|
-
let markContextLoaded;
|
|
15
|
-
let requireContextLoaded;
|
|
16
|
-
let noteInferenceForSession;
|
|
17
|
-
let getSessionState;
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
vi.resetModules();
|
|
20
|
-
const mod = await import("../../session/sessionContext.js");
|
|
21
|
-
markContextLoaded = mod.markContextLoaded;
|
|
22
|
-
requireContextLoaded = mod.requireContextLoaded;
|
|
23
|
-
noteInferenceForSession = mod.noteInferenceForSession;
|
|
24
|
-
getSessionState = mod.getSessionState;
|
|
25
|
-
});
|
|
26
|
-
describe("requireContextLoaded — fail-closed defaults", () => {
|
|
27
|
-
it("blocks an unknown conversation (never seen)", () => {
|
|
28
|
-
const result = requireContextLoaded("never-seen-id");
|
|
29
|
-
expect(result).not.toBeNull();
|
|
30
|
-
expect(result.blocked).toBe(true);
|
|
31
|
-
if (result && result.blocked)
|
|
32
|
-
expect(result.error).toContain("context_not_loaded");
|
|
33
|
-
});
|
|
34
|
-
it("allows (returns null) when conversation_id is undefined — gate is opt-in", () => {
|
|
35
|
-
// Callers without a conversation_id (auto-push hosts, resource readers,
|
|
36
|
-
// legacy clients) are not gated — they use the session-agnostic interface.
|
|
37
|
-
const result = requireContextLoaded(undefined);
|
|
38
|
-
expect(result).toBeNull();
|
|
39
|
-
});
|
|
40
|
-
it("blocks (hard) when conversation_id is empty string — empty string is not opt-in bypass", () => {
|
|
41
|
-
// "" is not the same as undefined. An empty string means the caller explicitly
|
|
42
|
-
// provided a conversation_id but it's invalid. The gate should block, not bypass.
|
|
43
|
-
const result = requireContextLoaded("");
|
|
44
|
-
expect(result).not.toBeNull();
|
|
45
|
-
expect(result.blocked).toBe(true);
|
|
46
|
-
if (result && result.blocked)
|
|
47
|
-
expect(result.error).toContain("context_not_loaded");
|
|
48
|
-
});
|
|
49
|
-
it("blocks a session by unknown id even if noteInference was called for it", () => {
|
|
50
|
-
// noteInferenceForSession no longer creates stubs, so an unregistered id
|
|
51
|
-
// is still unknown to the gate.
|
|
52
|
-
noteInferenceForSession("conv-telemetry-only", { backend: "local", usedCloud: false });
|
|
53
|
-
const result = requireContextLoaded("conv-telemetry-only");
|
|
54
|
-
expect(result).not.toBeNull();
|
|
55
|
-
expect(result.blocked).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe("markContextLoaded → requireContextLoaded lifecycle", () => {
|
|
59
|
-
it("returns null (pass) after markContextLoaded is called", () => {
|
|
60
|
-
markContextLoaded("conv-abc", "project-x", BOUNDARIES_VERSION);
|
|
61
|
-
expect(requireContextLoaded("conv-abc")).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
it("records project and boundariesVersion on the session", () => {
|
|
64
|
-
markContextLoaded("conv-meta", "my-project", "42");
|
|
65
|
-
const state = getSessionState("conv-meta");
|
|
66
|
-
expect(state).not.toBeNull();
|
|
67
|
-
expect(state.project).toBe("my-project");
|
|
68
|
-
expect(state.boundariesVersion).toBe("42");
|
|
69
|
-
expect(state.contextLoaded).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
it("is idempotent — calling twice does not break state", () => {
|
|
72
|
-
// Use the actual BOUNDARIES_VERSION so no drift warning fires.
|
|
73
|
-
markContextLoaded("conv-idem", "proj", BOUNDARIES_VERSION);
|
|
74
|
-
markContextLoaded("conv-idem", "proj-updated", BOUNDARIES_VERSION);
|
|
75
|
-
const state = getSessionState("conv-idem");
|
|
76
|
-
expect(state.project).toBe("proj-updated");
|
|
77
|
-
expect(state.boundariesVersion).toBe(BOUNDARIES_VERSION);
|
|
78
|
-
expect(requireContextLoaded("conv-idem")).toBeNull();
|
|
79
|
-
});
|
|
80
|
-
it("isolates sessions — loading one does not unblock another", () => {
|
|
81
|
-
markContextLoaded("conv-A", "proj", BOUNDARIES_VERSION);
|
|
82
|
-
expect(requireContextLoaded("conv-A")).toBeNull();
|
|
83
|
-
expect(requireContextLoaded("conv-B")).not.toBeNull();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("noteInferenceForSession", () => {
|
|
87
|
-
it("increments inferenceCalls on every call", () => {
|
|
88
|
-
markContextLoaded("conv-inf", "proj", BOUNDARIES_VERSION);
|
|
89
|
-
noteInferenceForSession("conv-inf", { backend: "local", usedCloud: false });
|
|
90
|
-
noteInferenceForSession("conv-inf", { backend: "local", usedCloud: false });
|
|
91
|
-
const state = getSessionState("conv-inf");
|
|
92
|
-
expect(state.inferenceCalls).toBe(2);
|
|
93
|
-
});
|
|
94
|
-
it("increments usedCloudCalls only for cloud calls", () => {
|
|
95
|
-
markContextLoaded("conv-cloud", "proj", BOUNDARIES_VERSION);
|
|
96
|
-
noteInferenceForSession("conv-cloud", { backend: "cloud", usedCloud: true });
|
|
97
|
-
noteInferenceForSession("conv-cloud", { backend: "local", usedCloud: false });
|
|
98
|
-
const state = getSessionState("conv-cloud");
|
|
99
|
-
expect(state.inferenceCalls).toBe(2);
|
|
100
|
-
expect(state.usedCloudCalls).toBe(1);
|
|
101
|
-
});
|
|
102
|
-
it("does NOT create a ghost stub for an unregistered session — only updates existing sessions", () => {
|
|
103
|
-
// noteInferenceForSession used to call getOrInit, creating stub entries
|
|
104
|
-
// with contextLoaded=false for every conversation_id that infers.
|
|
105
|
-
// Ghost stubs accumulate in the LRU and crowd out real sessions.
|
|
106
|
-
// The fix: no-op when the session doesn't exist yet.
|
|
107
|
-
noteInferenceForSession("conv-new-via-note", { backend: "local", usedCloud: false });
|
|
108
|
-
expect(getSessionState("conv-new-via-note")).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe("getSessionState", () => {
|
|
112
|
-
it("returns null for an unknown session", () => {
|
|
113
|
-
expect(getSessionState("does-not-exist")).toBeNull();
|
|
114
|
-
});
|
|
115
|
-
it("returns the current state object for a known session", () => {
|
|
116
|
-
markContextLoaded("conv-get", "proj", BOUNDARIES_VERSION);
|
|
117
|
-
const state = getSessionState("conv-get");
|
|
118
|
-
expect(state).not.toBeNull();
|
|
119
|
-
expect(state.contextLoaded).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
describe("lastSeen update", () => {
|
|
123
|
-
it("updates lastSeen on every requireContextLoaded call", async () => {
|
|
124
|
-
markContextLoaded("conv-ts", "proj", BOUNDARIES_VERSION);
|
|
125
|
-
const before = getSessionState("conv-ts").lastSeen;
|
|
126
|
-
// Advance time by mocking Date.now via vi.useFakeTimers
|
|
127
|
-
vi.useFakeTimers();
|
|
128
|
-
vi.advanceTimersByTime(5000);
|
|
129
|
-
requireContextLoaded("conv-ts");
|
|
130
|
-
const after = getSessionState("conv-ts").lastSeen;
|
|
131
|
-
vi.useRealTimers();
|
|
132
|
-
expect(after).toBeGreaterThanOrEqual(before);
|
|
133
|
-
});
|
|
134
|
-
});
|