@kinqs/brainrouter-mcp-server 0.3.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.
- package/.env.example +144 -0
- package/README.md +56 -0
- package/agents/README.md +120 -0
- package/agents/code-reviewer.md +97 -0
- package/agents/security-auditor.md +101 -0
- package/agents/test-engineer.md +95 -0
- package/dist/__tests__/agent_mode.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.js +170 -0
- package/dist/__tests__/crypto.test.d.ts +1 -0
- package/dist/__tests__/crypto.test.js +28 -0
- package/dist/__tests__/host-integrations.test.d.ts +1 -0
- package/dist/__tests__/host-integrations.test.js +82 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +50 -0
- package/dist/__tests__/loader.test.d.ts +1 -0
- package/dist/__tests__/loader.test.js +89 -0
- package/dist/__tests__/neural-spark.test.d.ts +1 -0
- package/dist/__tests__/neural-spark.test.js +112 -0
- package/dist/__tests__/pagination.test.d.ts +1 -0
- package/dist/__tests__/pagination.test.js +23 -0
- package/dist/__tests__/redaction.test.d.ts +1 -0
- package/dist/__tests__/redaction.test.js +17 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +56 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +30 -0
- package/dist/__tests__/skill-activation.test.d.ts +1 -0
- package/dist/__tests__/skill-activation.test.js +112 -0
- package/dist/__tests__/working-memory.test.d.ts +1 -0
- package/dist/__tests__/working-memory.test.js +200 -0
- package/dist/__tests__/workspace-paths.test.d.ts +1 -0
- package/dist/__tests__/workspace-paths.test.js +56 -0
- package/dist/__tests__/writer.test.d.ts +1 -0
- package/dist/__tests__/writer.test.js +94 -0
- package/dist/api/auth/crypto.d.ts +4 -0
- package/dist/api/auth/crypto.js +54 -0
- package/dist/api/middleware/auth.d.ts +12 -0
- package/dist/api/middleware/auth.js +90 -0
- package/dist/api/pagination.d.ts +18 -0
- package/dist/api/pagination.js +32 -0
- package/dist/api/routes/auth.d.ts +1 -0
- package/dist/api/routes/auth.js +130 -0
- package/dist/api/routes/chat-completions.d.ts +7 -0
- package/dist/api/routes/chat-completions.js +474 -0
- package/dist/api/routes/contradictions.d.ts +1 -0
- package/dist/api/routes/contradictions.js +28 -0
- package/dist/api/routes/evidence.d.ts +1 -0
- package/dist/api/routes/evidence.js +59 -0
- package/dist/api/routes/governance.d.ts +1 -0
- package/dist/api/routes/governance.js +95 -0
- package/dist/api/routes/graph.d.ts +1 -0
- package/dist/api/routes/graph.js +25 -0
- package/dist/api/routes/hooks.d.ts +1 -0
- package/dist/api/routes/hooks.js +88 -0
- package/dist/api/routes/memories.d.ts +1 -0
- package/dist/api/routes/memories.js +92 -0
- package/dist/api/routes/persona.d.ts +1 -0
- package/dist/api/routes/persona.js +9 -0
- package/dist/api/routes/scenes.d.ts +1 -0
- package/dist/api/routes/scenes.js +35 -0
- package/dist/api/routes/skills.d.ts +1 -0
- package/dist/api/routes/skills.js +14 -0
- package/dist/api/routes/stats.d.ts +1 -0
- package/dist/api/routes/stats.js +8 -0
- package/dist/api/routes/users.d.ts +1 -0
- package/dist/api/routes/users.js +82 -0
- package/dist/api/routes/working.d.ts +1 -0
- package/dist/api/routes/working.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +492 -0
- package/dist/integrations/claude-code.d.ts +12 -0
- package/dist/integrations/claude-code.js +35 -0
- package/dist/integrations/codex.d.ts +12 -0
- package/dist/integrations/codex.js +34 -0
- package/dist/integrations/generic-mcp.d.ts +52 -0
- package/dist/integrations/generic-mcp.js +118 -0
- package/dist/loader.d.ts +29 -0
- package/dist/loader.js +200 -0
- package/dist/memory/capture.d.ts +35 -0
- package/dist/memory/capture.js +230 -0
- package/dist/memory/config.d.ts +2 -0
- package/dist/memory/config.js +3 -0
- package/dist/memory/engine.d.ts +203 -0
- package/dist/memory/engine.js +626 -0
- package/dist/memory/llm-semaphore.d.ts +41 -0
- package/dist/memory/llm-semaphore.js +81 -0
- package/dist/memory/memory-type-config.d.ts +11 -0
- package/dist/memory/memory-type-config.js +65 -0
- package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
- package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
- package/dist/memory/pipeline/cognitive-dedup.js +38 -0
- package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
- package/dist/memory/pipeline/cognitive-extractor.js +183 -0
- package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
- package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
- package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/focus-direction-shift.js +27 -0
- package/dist/memory/pipeline/graph-builder.d.ts +11 -0
- package/dist/memory/pipeline/graph-builder.js +88 -0
- package/dist/memory/pipeline/graph-recall.d.ts +13 -0
- package/dist/memory/pipeline/graph-recall.js +55 -0
- package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
- package/dist/memory/pipeline/identity-distiller.js +40 -0
- package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/l1-contradiction.js +66 -0
- package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
- package/dist/memory/pipeline/l1-dedup.js +39 -0
- package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
- package/dist/memory/pipeline/l1-extractor.js +180 -0
- package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/l2-direction-shift.js +27 -0
- package/dist/memory/pipeline/l2-scene.d.ts +15 -0
- package/dist/memory/pipeline/l2-scene.js +140 -0
- package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
- package/dist/memory/pipeline/l3-distiller.js +40 -0
- package/dist/memory/pipeline/neural-spark.d.ts +27 -0
- package/dist/memory/pipeline/neural-spark.js +78 -0
- package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
- package/dist/memory/pipeline/skill-prewarm.js +127 -0
- package/dist/memory/pipeline/task-queue.d.ts +54 -0
- package/dist/memory/pipeline/task-queue.js +117 -0
- package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
- package/dist/memory/prompts/cognitive-contradiction.js +25 -0
- package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
- package/dist/memory/prompts/cognitive-extraction.js +114 -0
- package/dist/memory/prompts/core-identity.d.ts +6 -0
- package/dist/memory/prompts/core-identity.js +60 -0
- package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/focus-direction-shift.js +32 -0
- package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/focus-scene-cluster.js +33 -0
- package/dist/memory/prompts/focus-scene.d.ts +7 -0
- package/dist/memory/prompts/focus-scene.js +40 -0
- package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
- package/dist/memory/prompts/graph-extraction-batch.js +54 -0
- package/dist/memory/prompts/graph-extraction.d.ts +2 -0
- package/dist/memory/prompts/graph-extraction.js +53 -0
- package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
- package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
- package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
- package/dist/memory/prompts/l1-contradiction.js +25 -0
- package/dist/memory/prompts/l1-extraction.d.ts +10 -0
- package/dist/memory/prompts/l1-extraction.js +114 -0
- package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/l2-direction-shift.js +32 -0
- package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/l2-scene-cluster.js +33 -0
- package/dist/memory/prompts/l2-scene.d.ts +7 -0
- package/dist/memory/prompts/l2-scene.js +40 -0
- package/dist/memory/prompts/l3-persona.d.ts +6 -0
- package/dist/memory/prompts/l3-persona.js +60 -0
- package/dist/memory/recall.d.ts +47 -0
- package/dist/memory/recall.js +427 -0
- package/dist/memory/redaction.d.ts +1 -0
- package/dist/memory/redaction.js +24 -0
- package/dist/memory/retry.d.ts +13 -0
- package/dist/memory/retry.js +53 -0
- package/dist/memory/scheduler.d.ts +9 -0
- package/dist/memory/scheduler.js +16 -0
- package/dist/memory/skill-hints-loader.d.ts +30 -0
- package/dist/memory/skill-hints-loader.js +100 -0
- package/dist/memory/store/embedding.d.ts +16 -0
- package/dist/memory/store/embedding.js +68 -0
- package/dist/memory/store/reranker.d.ts +24 -0
- package/dist/memory/store/reranker.js +83 -0
- package/dist/memory/store/sqlite.d.ts +167 -0
- package/dist/memory/store/sqlite.js +1816 -0
- package/dist/memory/store/types.d.ts +101 -0
- package/dist/memory/store/types.js +1 -0
- package/dist/memory/types.d.ts +207 -0
- package/dist/memory/types.js +7 -0
- package/dist/memory/validation.d.ts +441 -0
- package/dist/memory/validation.js +129 -0
- package/dist/memory/working/canvas.d.ts +5 -0
- package/dist/memory/working/canvas.js +43 -0
- package/dist/memory/working/offload.d.ts +71 -0
- package/dist/memory/working/offload.js +211 -0
- package/dist/memory/working/step-log.d.ts +16 -0
- package/dist/memory/working/step-log.js +35 -0
- package/dist/registry.d.ts +34 -0
- package/dist/registry.js +305 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.js +126 -0
- package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
- package/dist/scripts/validate-foreign-workspace-path.js +39 -0
- package/dist/tools/agent_memory_tools.d.ts +485 -0
- package/dist/tools/agent_memory_tools.js +793 -0
- package/dist/tools/create_skill.d.ts +46 -0
- package/dist/tools/create_skill.js +46 -0
- package/dist/tools/get_doc.d.ts +21 -0
- package/dist/tools/get_doc.js +24 -0
- package/dist/tools/get_persona.d.ts +15 -0
- package/dist/tools/get_persona.js +20 -0
- package/dist/tools/get_reference.d.ts +15 -0
- package/dist/tools/get_reference.js +20 -0
- package/dist/tools/get_skill.d.ts +34 -0
- package/dist/tools/get_skill.js +65 -0
- package/dist/tools/get_template_doc.d.ts +21 -0
- package/dist/tools/get_template_doc.js +24 -0
- package/dist/tools/list_docs.d.ts +15 -0
- package/dist/tools/list_docs.js +16 -0
- package/dist/tools/list_skills.d.ts +18 -0
- package/dist/tools/list_skills.js +17 -0
- package/dist/tools/list_template_docs.d.ts +15 -0
- package/dist/tools/list_template_docs.js +16 -0
- package/dist/tools/memory-engineering.d.ts +225 -0
- package/dist/tools/memory-engineering.js +284 -0
- package/dist/tools/memory-explain.d.ts +34 -0
- package/dist/tools/memory-explain.js +109 -0
- package/dist/tools/memory-governance.d.ts +171 -0
- package/dist/tools/memory-governance.js +224 -0
- package/dist/tools/memory-hooks.d.ts +67 -0
- package/dist/tools/memory-hooks.js +102 -0
- package/dist/tools/memory-working.d.ts +98 -0
- package/dist/tools/memory-working.js +101 -0
- package/dist/tools/memory_capture_turn.d.ts +66 -0
- package/dist/tools/memory_capture_turn.js +85 -0
- package/dist/tools/memory_consolidate.d.ts +55 -0
- package/dist/tools/memory_consolidate.js +176 -0
- package/dist/tools/memory_contradictions.d.ts +53 -0
- package/dist/tools/memory_contradictions.js +52 -0
- package/dist/tools/memory_graph_query.d.ts +51 -0
- package/dist/tools/memory_graph_query.js +35 -0
- package/dist/tools/memory_mark_cited.d.ts +43 -0
- package/dist/tools/memory_mark_cited.js +63 -0
- package/dist/tools/memory_recall.d.ts +77 -0
- package/dist/tools/memory_recall.js +81 -0
- package/dist/tools/memory_register_skill_hints.d.ts +49 -0
- package/dist/tools/memory_register_skill_hints.js +55 -0
- package/dist/tools/memory_resolve_session.d.ts +24 -0
- package/dist/tools/memory_resolve_session.js +133 -0
- package/dist/tools/memory_search.d.ts +146 -0
- package/dist/tools/memory_search.js +84 -0
- package/dist/tools/search_skills.d.ts +18 -0
- package/dist/tools/search_skills.js +17 -0
- package/dist/tools/update_doc.d.ts +24 -0
- package/dist/tools/update_doc.js +35 -0
- package/dist/tools/update_skill.d.ts +30 -0
- package/dist/tools/update_skill.js +80 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +4 -0
- package/dist/writer.d.ts +30 -0
- package/dist/writer.js +220 -0
- package/docs/TEMPLATE ONLY +1 -0
- package/docs/api/API.md +64 -0
- package/docs/api/security/SECURITY.md +58 -0
- package/docs/deployment/DockerDeployment.md +30 -0
- package/docs/design/Design.md +59 -0
- package/docs/design/themes/apple.md +101 -0
- package/docs/design/themes/dieter-grid.md +100 -0
- package/docs/design/themes/gallery-white.md +100 -0
- package/docs/design/themes/pinterest.md +101 -0
- package/docs/design/themes/realty-open-house.md +101 -0
- package/docs/design/themes/vodafone.md +101 -0
- package/docs/hooks/Hooks.md +30 -0
- package/docs/schema/Schema.md +35 -0
- package/docs/strategy/ScalingStrategy.md +19 -0
- package/package.json +88 -0
- package/references/accessibility-checklist.md +160 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +153 -0
- package/references/security-checklist.md +134 -0
- package/references/testing-patterns.md +236 -0
- package/skills/agent/adr-skill/SKILL.md +299 -0
- package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
- package/skills/agent/bootstrap-skill/SKILL.md +103 -0
- package/skills/agent/context-engineering/SKILL.md +307 -0
- package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
- package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
- package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
- package/skills/agent/handover-skill/SKILL.md +112 -0
- package/skills/agent/idea-refine-skill/SKILL.md +185 -0
- package/skills/agent/idea-refine-skill/examples.md +238 -0
- package/skills/agent/idea-refine-skill/frameworks.md +99 -0
- package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
- package/skills/agent/interview-skill/SKILL.md +226 -0
- package/skills/agent/planning-skill/SKILL.md +270 -0
- package/skills/agent/skill-authoring/SKILL.md +189 -0
- package/skills/agent/source-driven-skill/SKILL.md +197 -0
- package/skills/agent/spec-driven-skill/SKILL.md +221 -0
- package/skills/agent/sync-skill/SKILL.md +92 -0
- package/skills/agent/using-agent-skills/SKILL.md +189 -0
- package/skills/api/a11y-skill/SKILL.md +88 -0
- package/skills/api/api-skill/SKILL.md +123 -0
- package/skills/api/auth-skill/SKILL.md +80 -0
- package/skills/api/debug-skill/SKILL.md +535 -0
- package/skills/api/performance-skill/SKILL.md +100 -0
- package/skills/api/testing-skill/SKILL.md +100 -0
- package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
- package/skills/codebase/code-simplification/SKILL.md +352 -0
- package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
- package/skills/codebase/concerns-skill/SKILL.md +89 -0
- package/skills/codebase/conventions-skill/SKILL.md +95 -0
- package/skills/codebase/doc-management-skill/SKILL.md +47 -0
- package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
- package/skills/communication/1-3-1-rule/SKILL.md +120 -0
- package/skills/design/brutalist-skill/SKILL.md +131 -0
- package/skills/design/concept-diagrams/SKILL.md +387 -0
- package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/skills/design/concept-diagrams/templates/template.html +174 -0
- package/skills/design/gpt-tasteskill/SKILL.md +114 -0
- package/skills/design/minimalist-skill/SKILL.md +116 -0
- package/skills/design/output-skill/SKILL.md +87 -0
- package/skills/design/redesign-skill/SKILL.md +213 -0
- package/skills/design/soft-skill/SKILL.md +132 -0
- package/skills/design/stitch-skill/EXAMPLE.md +121 -0
- package/skills/design/stitch-skill/SKILL.md +222 -0
- package/skills/design/taste-skill/SKILL.md +269 -0
- package/skills/devops/ci-cd-skill/SKILL.md +402 -0
- package/skills/devops/docker-skill/SKILL.md +297 -0
- package/skills/devops/domain-skill/SKILL.md +234 -0
- package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
- package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
- package/skills/lifecycle/migration-skill/SKILL.md +218 -0
- package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
- package/skills/memory/agent-memory/SKILL.md +122 -0
- package/skills/qa/browser-testing-skill/SKILL.md +314 -0
- package/skills/ux/adversarial-ux-skill/SKILL.md +168 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
vi.mock("../memory/engine.js", () => ({
|
|
7
|
+
memoryEngine: {
|
|
8
|
+
capturePassiveL0: vi.fn((params) => ({
|
|
9
|
+
id: `l0-${params.sessionKey}`,
|
|
10
|
+
userId: "user-1",
|
|
11
|
+
sessionKey: params.sessionKey,
|
|
12
|
+
sessionId: "",
|
|
13
|
+
role: "tool",
|
|
14
|
+
messageText: "{}",
|
|
15
|
+
recordedAt: new Date().toISOString(),
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
skillTag: "host:claude-code",
|
|
18
|
+
})),
|
|
19
|
+
getUserByApiKey: vi.fn((apiKey) => {
|
|
20
|
+
if (apiKey === "br_admin") {
|
|
21
|
+
return {
|
|
22
|
+
userId: "admin",
|
|
23
|
+
isAdmin: true,
|
|
24
|
+
email: "admin@example.test",
|
|
25
|
+
status: "active",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (apiKey === "br_user") {
|
|
29
|
+
return {
|
|
30
|
+
userId: "user-1",
|
|
31
|
+
isAdmin: false,
|
|
32
|
+
email: "user@example.test",
|
|
33
|
+
status: "active",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}),
|
|
38
|
+
getUserById: vi.fn(() => ({
|
|
39
|
+
userId: "user-1",
|
|
40
|
+
isAdmin: false,
|
|
41
|
+
email: "user@example.test",
|
|
42
|
+
status: "active",
|
|
43
|
+
})),
|
|
44
|
+
store: {
|
|
45
|
+
deleteContextualFocus: vi.fn(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
async function createServer() {
|
|
50
|
+
const [{ workingRouter }, { hooksRouter }, { scenesRouter }] = await Promise.all([
|
|
51
|
+
import("../api/routes/working.js"),
|
|
52
|
+
import("../api/routes/hooks.js"),
|
|
53
|
+
import("../api/routes/scenes.js"),
|
|
54
|
+
]);
|
|
55
|
+
const app = express();
|
|
56
|
+
app.use(express.json());
|
|
57
|
+
app.use("/api/working", workingRouter);
|
|
58
|
+
app.use("/api/hooks", hooksRouter);
|
|
59
|
+
app.use("/api/scenes", scenesRouter);
|
|
60
|
+
const server = app.listen(0);
|
|
61
|
+
await new Promise((resolve) => server.once("listening", resolve));
|
|
62
|
+
const address = server.address();
|
|
63
|
+
if (!address || typeof address === "string")
|
|
64
|
+
throw new Error("No test server port");
|
|
65
|
+
return { server, baseUrl: `http://127.0.0.1:${address.port}` };
|
|
66
|
+
}
|
|
67
|
+
describe("Phase 4 and 5 API routes", () => {
|
|
68
|
+
let server;
|
|
69
|
+
let baseUrl = "";
|
|
70
|
+
beforeEach(async () => {
|
|
71
|
+
const created = await createServer();
|
|
72
|
+
server = created.server;
|
|
73
|
+
baseUrl = created.baseUrl;
|
|
74
|
+
});
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it("serves working context, offload, and reset endpoints", async () => {
|
|
81
|
+
const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-api-working-"));
|
|
82
|
+
const auth = { Authorization: "Bearer br_user" };
|
|
83
|
+
const offload = await fetch(`${baseUrl}/api/working/offload`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json", ...auth },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
workspacePath,
|
|
88
|
+
sessionKey: "api-working-session",
|
|
89
|
+
payload: "tool output",
|
|
90
|
+
title: "Tool output",
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
expect(offload.status).toBe(201);
|
|
94
|
+
const offloadJson = await offload.json();
|
|
95
|
+
expect(offloadJson.nodeId).toMatch(/^w/);
|
|
96
|
+
const context = await fetch(`${baseUrl}/api/working/context?sessionKey=api-working-session&workspacePath=${encodeURIComponent(workspacePath)}`, {
|
|
97
|
+
headers: auth,
|
|
98
|
+
});
|
|
99
|
+
expect(context.status).toBe(200);
|
|
100
|
+
const contextJson = await context.json();
|
|
101
|
+
expect(contextJson.canvas).toContain("flowchart TD");
|
|
102
|
+
expect(contextJson.steps).toHaveLength(1);
|
|
103
|
+
const reset = await fetch(`${baseUrl}/api/working/reset`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json", ...auth },
|
|
106
|
+
body: JSON.stringify({ workspacePath, sessionKey: "api-working-session" }),
|
|
107
|
+
});
|
|
108
|
+
expect(reset.status).toBe(200);
|
|
109
|
+
const resetJson = await reset.json();
|
|
110
|
+
expect(resetJson.deleted).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it("registers hooks and reports status", async () => {
|
|
113
|
+
const auth = { Authorization: "Bearer br_user" };
|
|
114
|
+
const register = await fetch(`${baseUrl}/api/hooks/register`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json", ...auth },
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
source: "claude-code",
|
|
119
|
+
events: ["PostToolUse"],
|
|
120
|
+
sessionKey: "api-hook-session",
|
|
121
|
+
event: "PostToolUse",
|
|
122
|
+
payload: { tool_name: "Bash", tool_input: { command: "echo ok" } },
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
expect(register.status).toBe(201);
|
|
126
|
+
const registerJson = await register.json();
|
|
127
|
+
expect(registerJson.captureResult.l0RecordedCount).toBe(1);
|
|
128
|
+
const status = await fetch(`${baseUrl}/api/hooks/status?source=claude-code`, { headers: auth });
|
|
129
|
+
expect(status.status).toBe(200);
|
|
130
|
+
const statusJson = await status.json();
|
|
131
|
+
expect(statusJson.hooks.some((hook) => hook.id === "user-1:claude-code:api-hook-session" && hook.userId === "user-1")).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it("rejects non-admin hook registration for another user", async () => {
|
|
134
|
+
const response = await fetch(`${baseUrl}/api/hooks/register`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer br_user" },
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
source: "codex",
|
|
139
|
+
userId: "other-user",
|
|
140
|
+
sessionKey: "blocked-session",
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
expect(response.status).toBe(403);
|
|
144
|
+
});
|
|
145
|
+
it("rejects non-admin working memory access for another user", async () => {
|
|
146
|
+
const workspacePath = mkdtempSync(join(tmpdir(), "brainrouter-api-working-rbac-"));
|
|
147
|
+
const response = await fetch(`${baseUrl}/api/working/context?sessionKey=blocked-session&userId=other-user&workspacePath=${encodeURIComponent(workspacePath)}`, {
|
|
148
|
+
headers: { Authorization: "Bearer br_user" },
|
|
149
|
+
});
|
|
150
|
+
expect(response.status).toBe(403);
|
|
151
|
+
});
|
|
152
|
+
it("rejects non-admin hook status reads for another user", async () => {
|
|
153
|
+
const response = await fetch(`${baseUrl}/api/hooks/status?userId=other-user`, {
|
|
154
|
+
headers: { Authorization: "Bearer br_user" },
|
|
155
|
+
});
|
|
156
|
+
expect(response.status).toBe(403);
|
|
157
|
+
});
|
|
158
|
+
it("evicts scenes via delete endpoint", async () => {
|
|
159
|
+
const auth = { Authorization: "Bearer br_user" };
|
|
160
|
+
const response = await fetch(`${baseUrl}/api/scenes/scene-123`, {
|
|
161
|
+
method: "DELETE",
|
|
162
|
+
headers: auth,
|
|
163
|
+
});
|
|
164
|
+
expect(response.status).toBe(200);
|
|
165
|
+
const json = await response.json();
|
|
166
|
+
expect(json.success).toBe(true);
|
|
167
|
+
const { memoryEngine } = await import("../memory/engine.js");
|
|
168
|
+
expect(memoryEngine.store.deleteContextualFocus).toHaveBeenCalledWith("user-1", ["scene-123"]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { hashPassword, signJwt, verifyJwt, verifyPassword } from "../api/auth/crypto.js";
|
|
3
|
+
describe("crypto auth helpers", () => {
|
|
4
|
+
it("hashPassword + verifyPassword round-trip", async () => {
|
|
5
|
+
const stored = await hashPassword("S3cret123!");
|
|
6
|
+
await expect(verifyPassword("S3cret123!", stored)).resolves.toBe(true);
|
|
7
|
+
});
|
|
8
|
+
it("verifyPassword fails with wrong password", async () => {
|
|
9
|
+
const stored = await hashPassword("S3cret123!");
|
|
10
|
+
await expect(verifyPassword("wrong", stored)).resolves.toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it("signJwt + verifyJwt round-trip", () => {
|
|
13
|
+
const token = signJwt({ userId: "u1", isAdmin: true }, "secret", 60);
|
|
14
|
+
const payload = verifyJwt(token, "secret");
|
|
15
|
+
expect(payload).not.toBeNull();
|
|
16
|
+
expect(payload?.userId).toBe("u1");
|
|
17
|
+
expect(payload?.isAdmin).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it("verifyJwt returns null for expired token", () => {
|
|
20
|
+
const token = signJwt({ userId: "u1" }, "secret", -1);
|
|
21
|
+
expect(verifyJwt(token, "secret")).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it("verifyJwt returns null for tampered signature", () => {
|
|
24
|
+
const token = signJwt({ userId: "u1" }, "secret", 60);
|
|
25
|
+
const tampered = `${token.slice(0, -1)}x`;
|
|
26
|
+
expect(verifyJwt(tampered, "secret")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { processClaudeCodeHook } from "../integrations/claude-code.js";
|
|
3
|
+
import { listHostHooks, registerHostHook } from "../integrations/generic-mcp.js";
|
|
4
|
+
describe("host integrations", () => {
|
|
5
|
+
it("captures a Claude Code PostToolUse event as redacted L0 memory", async () => {
|
|
6
|
+
const records = [];
|
|
7
|
+
const engine = {
|
|
8
|
+
capturePassiveL0(params) {
|
|
9
|
+
const record = {
|
|
10
|
+
id: "l0-test",
|
|
11
|
+
userId: "user-1",
|
|
12
|
+
sessionKey: "claude-session",
|
|
13
|
+
sessionId: "",
|
|
14
|
+
role: "tool",
|
|
15
|
+
messageText: params.content,
|
|
16
|
+
recordedAt: new Date().toISOString(),
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
skillTag: params.skillTag ?? "",
|
|
19
|
+
};
|
|
20
|
+
records.push(record);
|
|
21
|
+
return record;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const result = await processClaudeCodeHook(engine, {
|
|
25
|
+
event: "PostToolUse",
|
|
26
|
+
userId: "user-1",
|
|
27
|
+
sessionKey: "claude-session",
|
|
28
|
+
tool_name: "Bash",
|
|
29
|
+
tool_input: {
|
|
30
|
+
command: "curl -H 'Authorization: Bearer sk-test-key-value' https://example.test",
|
|
31
|
+
apiKey: "sk-test-key-value",
|
|
32
|
+
},
|
|
33
|
+
}, "default");
|
|
34
|
+
expect(result.l0RecordedCount).toBe(1);
|
|
35
|
+
expect(records).toHaveLength(1);
|
|
36
|
+
expect(records[0].skillTag).toBe("host:claude-code");
|
|
37
|
+
expect(records[0].messageText).toContain("PostToolUse");
|
|
38
|
+
expect(records[0].messageText).toContain("Bash");
|
|
39
|
+
expect(records[0].messageText).not.toContain("sk-test-key-value");
|
|
40
|
+
expect(records[0].messageText).toContain("[REDACTED]");
|
|
41
|
+
});
|
|
42
|
+
it("registers hooks and reports last-seen status", async () => {
|
|
43
|
+
registerHostHook({
|
|
44
|
+
userId: "user-1",
|
|
45
|
+
source: "claude-code",
|
|
46
|
+
events: ["PreToolUse", "PostToolUse", "Stop", "SubagentStop"],
|
|
47
|
+
sessionKey: "status-session",
|
|
48
|
+
});
|
|
49
|
+
const status = { hooks: listHostHooks("user-1").filter((hook) => hook.source === "claude-code") };
|
|
50
|
+
expect(status.hooks.some((hook) => hook.id === "user-1:claude-code:status-session")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("resolves workspacePath from registration database if omitted in a Stop event", async () => {
|
|
53
|
+
registerHostHook({
|
|
54
|
+
userId: "user-1",
|
|
55
|
+
source: "claude-code",
|
|
56
|
+
events: ["Stop"],
|
|
57
|
+
sessionKey: "session-without-path",
|
|
58
|
+
workspacePath: "/resolved/path/from/db",
|
|
59
|
+
});
|
|
60
|
+
const engine = {
|
|
61
|
+
capturePassiveL0(params) {
|
|
62
|
+
return {
|
|
63
|
+
id: "l0-test",
|
|
64
|
+
userId: params.userId,
|
|
65
|
+
sessionKey: params.sessionKey,
|
|
66
|
+
sessionId: params.sessionId ?? "",
|
|
67
|
+
role: params.role,
|
|
68
|
+
messageText: params.content,
|
|
69
|
+
recordedAt: new Date().toISOString(),
|
|
70
|
+
timestamp: params.timestamp ?? Date.now(),
|
|
71
|
+
skillTag: params.skillTag ?? "",
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const result = await processClaudeCodeHook(engine, {
|
|
76
|
+
event: "Stop",
|
|
77
|
+
userId: "user-1",
|
|
78
|
+
sessionKey: "session-without-path",
|
|
79
|
+
}, "default");
|
|
80
|
+
expect(result.flushedWorkingMemory).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Registry } from '../registry.js';
|
|
3
|
+
import { resolveRegistryConfig } from '../resolver.js';
|
|
4
|
+
import { loadSkillSection } from '../loader.js';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
describe('BrainRouter Skill Compliance', () => {
|
|
7
|
+
const config = resolveRegistryConfig();
|
|
8
|
+
const registry = new Registry(config);
|
|
9
|
+
registry.build();
|
|
10
|
+
const allSkills = registry.listSkills(); // No arguments = all categories, all scopes
|
|
11
|
+
it('should have indexed skills', () => {
|
|
12
|
+
expect(allSkills.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
// Dynamically create a test case for every single skill in the repo
|
|
15
|
+
allSkills.forEach((skill) => {
|
|
16
|
+
describe(`Skill: ${skill.name}`, () => {
|
|
17
|
+
it('should have valid frontmatter (name and description)', () => {
|
|
18
|
+
expect(skill.name).toBeDefined();
|
|
19
|
+
expect(skill.description).toBeDefined();
|
|
20
|
+
expect(skill.description.length).toBeGreaterThan(10);
|
|
21
|
+
});
|
|
22
|
+
it('should have a Workflow or Core Process section', () => {
|
|
23
|
+
const fragment = loadSkillSection(skill.filePath, 'workflow');
|
|
24
|
+
// If it returns the "Section not found" comment, it means it failed to find a workflow
|
|
25
|
+
expect(fragment.content).not.toContain('Section "workflow" not found');
|
|
26
|
+
expect(fragment.content.length).toBeGreaterThan(50);
|
|
27
|
+
});
|
|
28
|
+
it('should have an Overview section', () => {
|
|
29
|
+
const fragment = loadSkillSection(skill.filePath, 'overview');
|
|
30
|
+
expect(fragment.content).not.toContain('Section "overview" not found');
|
|
31
|
+
});
|
|
32
|
+
it('should produce a valid token estimate', () => {
|
|
33
|
+
const fragment = loadSkillSection(skill.filePath, 'full');
|
|
34
|
+
expect(fragment.tokenEstimate).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('BrainRouter Persona Compliance', () => {
|
|
40
|
+
const config = resolveRegistryConfig();
|
|
41
|
+
const registry = new Registry(config);
|
|
42
|
+
registry.build();
|
|
43
|
+
const personas = registry.listPersonas();
|
|
44
|
+
personas.forEach((persona) => {
|
|
45
|
+
it(`Persona "${persona.name}" should be a valid markdown file`, () => {
|
|
46
|
+
expect(existsSync(persona.filePath)).toBe(true);
|
|
47
|
+
expect(persona.filePath.endsWith('.md')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { loadSkillSection, loadDescription } from '../loader.js';
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
describe('loader.ts', () => {
|
|
7
|
+
const testDir = join(tmpdir(), 'brainrouter-loader-test');
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
10
|
+
mkdirSync(testDir, { recursive: true });
|
|
11
|
+
});
|
|
12
|
+
it('should extract frontmatter description', () => {
|
|
13
|
+
const filePath = join(testDir, 'SKILL.md');
|
|
14
|
+
writeFileSync(filePath, '---\nname: test-skill\ndescription: A test description\n---\n# Content');
|
|
15
|
+
const fragment = loadDescription(filePath);
|
|
16
|
+
expect(fragment.content).toBe('A test description');
|
|
17
|
+
});
|
|
18
|
+
it('should extract a named section', () => {
|
|
19
|
+
const filePath = join(testDir, 'SKILL.md');
|
|
20
|
+
const content = `---
|
|
21
|
+
name: test-skill
|
|
22
|
+
description: test
|
|
23
|
+
---
|
|
24
|
+
# Test Skill
|
|
25
|
+
## Overview
|
|
26
|
+
This is the overview.
|
|
27
|
+
## Workflow
|
|
28
|
+
1. Step 1
|
|
29
|
+
2. Step 2
|
|
30
|
+
`;
|
|
31
|
+
writeFileSync(filePath, content);
|
|
32
|
+
const fragment = loadSkillSection(filePath, 'workflow');
|
|
33
|
+
expect(fragment.content).toContain('## Workflow');
|
|
34
|
+
expect(fragment.content).toContain('1. Step 1');
|
|
35
|
+
expect(fragment.content).not.toContain('## Overview');
|
|
36
|
+
});
|
|
37
|
+
it('should handle section aliases', () => {
|
|
38
|
+
const filePath = join(testDir, 'SKILL.md');
|
|
39
|
+
const content = `---
|
|
40
|
+
name: test-skill
|
|
41
|
+
description: test
|
|
42
|
+
---
|
|
43
|
+
## Core Process
|
|
44
|
+
Specific process steps here.
|
|
45
|
+
`;
|
|
46
|
+
writeFileSync(filePath, content);
|
|
47
|
+
// "workflow" should match "Core Process"
|
|
48
|
+
const fragment = loadSkillSection(filePath, 'workflow');
|
|
49
|
+
expect(fragment.content).toContain('## Core Process');
|
|
50
|
+
expect(fragment.content).toContain('Specific process steps here.');
|
|
51
|
+
});
|
|
52
|
+
it('should extract phases (### Phase N)', () => {
|
|
53
|
+
const filePath = join(testDir, 'SKILL.md');
|
|
54
|
+
const content = `---
|
|
55
|
+
name: test-skill
|
|
56
|
+
description: test
|
|
57
|
+
---
|
|
58
|
+
## Steps
|
|
59
|
+
### Phase 1: Planning
|
|
60
|
+
Do this first.
|
|
61
|
+
### Phase 2: Execution
|
|
62
|
+
Do this second.
|
|
63
|
+
## Other Section
|
|
64
|
+
Noise.
|
|
65
|
+
`;
|
|
66
|
+
writeFileSync(filePath, content);
|
|
67
|
+
const fragment = loadSkillSection(filePath, 'phases');
|
|
68
|
+
expect(fragment.content).toContain('### Phase 1');
|
|
69
|
+
expect(fragment.content).toContain('### Phase 2');
|
|
70
|
+
expect(fragment.content).not.toContain('## Other Section');
|
|
71
|
+
});
|
|
72
|
+
it('should extract checklists', () => {
|
|
73
|
+
const filePath = join(testDir, 'SKILL.md');
|
|
74
|
+
const content = `---
|
|
75
|
+
name: test-skill
|
|
76
|
+
description: test
|
|
77
|
+
---
|
|
78
|
+
## Verification
|
|
79
|
+
- [ ] Task 1
|
|
80
|
+
- [x] Task 2
|
|
81
|
+
- Not a task.
|
|
82
|
+
`;
|
|
83
|
+
writeFileSync(filePath, content);
|
|
84
|
+
const fragment = loadSkillSection(filePath, 'checklist');
|
|
85
|
+
expect(fragment.content).toContain('- [ ] Task 1');
|
|
86
|
+
expect(fragment.content).toContain('- [x] Task 2');
|
|
87
|
+
expect(fragment.content).not.toContain('Not a task.');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { NeuralSparkEngine } from "../memory/pipeline/neural-spark.js";
|
|
3
|
+
class MockMemoryStore {
|
|
4
|
+
connections = [];
|
|
5
|
+
memories = new Map();
|
|
6
|
+
upsertConnection(userId, sourceId, targetId, weight) {
|
|
7
|
+
const existing = this.connections.find(c => c.sourceId === sourceId && c.targetId === targetId);
|
|
8
|
+
if (existing) {
|
|
9
|
+
existing.weight = weight;
|
|
10
|
+
existing.lastActivatedAt = new Date().toISOString();
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
this.connections.push({ sourceId, targetId, weight, lastActivatedAt: new Date().toISOString() });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
getConnectionsForSource(userId, sourceId) {
|
|
17
|
+
return this.connections
|
|
18
|
+
.filter(c => c.sourceId === sourceId)
|
|
19
|
+
.map(c => ({ targetId: c.targetId, weight: c.weight }));
|
|
20
|
+
}
|
|
21
|
+
strengthenConnectionsBatch(userId, pairs, delta) {
|
|
22
|
+
for (const pair of pairs) {
|
|
23
|
+
// Bi-directional
|
|
24
|
+
this.upsertConnection(userId, pair.source, pair.target, Math.min(1.0, this.getWeight(pair.source, pair.target) + delta));
|
|
25
|
+
this.upsertConnection(userId, pair.target, pair.source, Math.min(1.0, this.getWeight(pair.target, pair.source) + delta));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
decayConnections(userId, decayFactor) {
|
|
29
|
+
for (const conn of this.connections) {
|
|
30
|
+
conn.weight = Math.max(0.0, conn.weight * decayFactor);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
pruneConnections(userId, threshold) {
|
|
34
|
+
this.connections = this.connections.filter(c => c.weight >= threshold);
|
|
35
|
+
}
|
|
36
|
+
getWeight(sourceId, targetId) {
|
|
37
|
+
const c = this.connections.find(conn => conn.sourceId === sourceId && conn.targetId === targetId);
|
|
38
|
+
return c ? c.weight : 0.5; // Default baseline is 0.5
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
describe("Neural Spark Engine & Spreading Activation", () => {
|
|
42
|
+
it("should propagate potentials to neighbors correctly up to 2 hops", () => {
|
|
43
|
+
const store = new MockMemoryStore();
|
|
44
|
+
const engine = new NeuralSparkEngine(store);
|
|
45
|
+
// Setup network: A -> B -> C
|
|
46
|
+
// Weight A -> B: 0.8
|
|
47
|
+
// Weight B -> C: 0.9
|
|
48
|
+
store.upsertConnection("user-1", "node-A", "node-B", 0.8);
|
|
49
|
+
store.upsertConnection("user-1", "node-B", "node-C", 0.9);
|
|
50
|
+
// Initial inputs: node-A starts with potential 1.0 (fired)
|
|
51
|
+
const initialNodes = [
|
|
52
|
+
{ id: "node-A", potential: 1.0, fired: false },
|
|
53
|
+
{ id: "node-B", potential: 0.0, fired: false },
|
|
54
|
+
{ id: "node-C", potential: 0.0, fired: false },
|
|
55
|
+
];
|
|
56
|
+
const results = engine.propagateSparks("user-1", initialNodes);
|
|
57
|
+
// Node-B potential = 1.0 * 0.8 = 0.8 (since 0.8 >= 0.7, it fires)
|
|
58
|
+
// Node-C potential = 0.8 * 0.9 = 0.72 (since 0.72 >= 0.7, it fires)
|
|
59
|
+
const nodeA = results.find(n => n.id === "node-A");
|
|
60
|
+
const nodeB = results.find(n => n.id === "node-B");
|
|
61
|
+
const nodeC = results.find(n => n.id === "node-C");
|
|
62
|
+
expect(nodeA?.fired).toBe(true);
|
|
63
|
+
expect(nodeB?.fired).toBe(true);
|
|
64
|
+
expect(nodeB?.potential).toBeCloseTo(0.8);
|
|
65
|
+
expect(nodeC?.fired).toBe(true);
|
|
66
|
+
expect(nodeC?.potential).toBeCloseTo(0.72);
|
|
67
|
+
});
|
|
68
|
+
it("should respect refractory period and avoid double firing/infinite loops", () => {
|
|
69
|
+
const store = new MockMemoryStore();
|
|
70
|
+
const engine = new NeuralSparkEngine(store);
|
|
71
|
+
// Cycle network: A -> B -> A
|
|
72
|
+
store.upsertConnection("user-1", "node-A", "node-B", 0.9);
|
|
73
|
+
store.upsertConnection("user-1", "node-B", "node-A", 0.9);
|
|
74
|
+
const initialNodes = [
|
|
75
|
+
{ id: "node-A", potential: 1.0, fired: false },
|
|
76
|
+
];
|
|
77
|
+
const results = engine.propagateSparks("user-1", initialNodes);
|
|
78
|
+
const nodeA = results.find(n => n.id === "node-A");
|
|
79
|
+
const nodeB = results.find(n => n.id === "node-B");
|
|
80
|
+
expect(nodeA?.fired).toBe(true);
|
|
81
|
+
expect(nodeB?.fired).toBe(true);
|
|
82
|
+
// Node-A potential shouldn't loop indefinitely
|
|
83
|
+
expect(nodeA?.potential).toBe(1.0);
|
|
84
|
+
});
|
|
85
|
+
it("should strengthen co-cited pairs (Hebbian LTP)", () => {
|
|
86
|
+
const store = new MockMemoryStore();
|
|
87
|
+
const engine = new NeuralSparkEngine(store);
|
|
88
|
+
// Initial connection weight = 0.5
|
|
89
|
+
store.upsertConnection("user-1", "node-A", "node-B", 0.5);
|
|
90
|
+
store.upsertConnection("user-1", "node-B", "node-A", 0.5);
|
|
91
|
+
// Co-cite A and B
|
|
92
|
+
engine.strengthenSpines("user-1", ["node-A", "node-B"]);
|
|
93
|
+
const connections = store.getConnectionsForSource("user-1", "node-A");
|
|
94
|
+
const abConn = connections.find(c => c.targetId === "node-B");
|
|
95
|
+
// LTP step is 0.15, so 0.5 + 0.15 = 0.65
|
|
96
|
+
expect(abConn?.weight).toBeCloseTo(0.65);
|
|
97
|
+
});
|
|
98
|
+
it("should decay weights and prune weak connections (LTD)", () => {
|
|
99
|
+
const store = new MockMemoryStore();
|
|
100
|
+
const engine = new NeuralSparkEngine(store);
|
|
101
|
+
// node-A -> node-B: weight 0.8 (decays to 0.72)
|
|
102
|
+
// node-A -> node-C: weight 0.1 (decays to 0.09 and gets pruned < 0.10)
|
|
103
|
+
store.upsertConnection("user-1", "node-A", "node-B", 0.8);
|
|
104
|
+
store.upsertConnection("user-1", "node-A", "node-C", 0.1);
|
|
105
|
+
engine.decayAndPrune("user-1");
|
|
106
|
+
const connections = store.getConnectionsForSource("user-1", "node-A");
|
|
107
|
+
const abConn = connections.find(c => c.targetId === "node-B");
|
|
108
|
+
const acConn = connections.find(c => c.targetId === "node-C");
|
|
109
|
+
expect(abConn?.weight).toBeCloseTo(0.72);
|
|
110
|
+
expect(acConn).toBeUndefined(); // Pruned!
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { decodeCursor, encodeCursor, pageItems, PaginationQuerySchema } from "../api/pagination.js";
|
|
3
|
+
describe("cursor pagination", () => {
|
|
4
|
+
it("encodes and decodes opaque cursor payloads", () => {
|
|
5
|
+
const cursor = encodeCursor({ createdTime: "2026-05-19T00:00:00.000Z", recordId: "l1-1" });
|
|
6
|
+
expect(decodeCursor(cursor)).toEqual({ createdTime: "2026-05-19T00:00:00.000Z", recordId: "l1-1" });
|
|
7
|
+
expect(() => decodeCursor("not-a-cursor")).toThrow("Invalid cursor");
|
|
8
|
+
});
|
|
9
|
+
it("validates cursor query params and caps page size", () => {
|
|
10
|
+
expect(PaginationQuerySchema.parse({ limit: "2" })).toEqual({ limit: 2 });
|
|
11
|
+
expect(() => PaginationQuerySchema.parse({ limit: "101" })).toThrow();
|
|
12
|
+
});
|
|
13
|
+
it("returns a next cursor only when more items exist", () => {
|
|
14
|
+
const page = pageItems([{ id: "a" }, { id: "b" }, { id: "c" }], 2, (item) => ({ id: item.id }));
|
|
15
|
+
expect(page.items).toEqual([{ id: "a" }, { id: "b" }]);
|
|
16
|
+
expect(decodeCursor(page.nextCursor ?? undefined)).toEqual({ id: "b" });
|
|
17
|
+
});
|
|
18
|
+
it("omits next cursor when the result set fits in one page", () => {
|
|
19
|
+
const page = pageItems([{ id: "a" }], 2, (item) => ({ id: item.id }));
|
|
20
|
+
expect(page.items).toEqual([{ id: "a" }]);
|
|
21
|
+
expect(page.nextCursor).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { redactSensitiveMemoryText } from "../memory/redaction.js";
|
|
3
|
+
describe("memory redaction", () => {
|
|
4
|
+
it("redacts tokens and env-style secrets before capture", () => {
|
|
5
|
+
const redacted = redactSensitiveMemoryText("Bearer sk-test-key-value\nSECRET_TOKEN=abc123");
|
|
6
|
+
expect(redacted).not.toContain("sk-test-key-value");
|
|
7
|
+
expect(redacted).not.toContain("SECRET_TOKEN=abc123");
|
|
8
|
+
expect(redacted).toContain("[REDACTED]");
|
|
9
|
+
});
|
|
10
|
+
it("redacts database connection strings and IPv4 addresses", () => {
|
|
11
|
+
const redacted = redactSensitiveMemoryText("Connect to postgresql://admin:s3cret@10.0.0.5:5432/app from 192.168.1.10");
|
|
12
|
+
expect(redacted).not.toContain("s3cret");
|
|
13
|
+
expect(redacted).not.toContain("192.168.1.10");
|
|
14
|
+
expect(redacted).toContain("[REDACTED_CONN_STR]");
|
|
15
|
+
expect(redacted).toContain("[REDACTED_IP]");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { Registry } from '../registry.js';
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
describe('registry.ts', () => {
|
|
7
|
+
const globalRoot = join(tmpdir(), 'brainrouter-registry-test-global');
|
|
8
|
+
const localRoot = join(tmpdir(), 'brainrouter-registry-test-local');
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
rmSync(globalRoot, { recursive: true, force: true });
|
|
11
|
+
rmSync(localRoot, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(join(globalRoot, 'skills', 'agent', 'global-skill'), { recursive: true });
|
|
13
|
+
mkdirSync(join(localRoot, 'skills', 'agent', 'local-skill'), { recursive: true });
|
|
14
|
+
mkdirSync(join(localRoot, 'docs', 'api'), { recursive: true });
|
|
15
|
+
mkdirSync(join(globalRoot, 'agents'), { recursive: true });
|
|
16
|
+
mkdirSync(join(localRoot, 'agents'), { recursive: true });
|
|
17
|
+
writeFileSync(join(globalRoot, 'skills', 'agent', 'global-skill', 'SKILL.md'), '---\nname: global-skill\ndescription: global desc\n---\n');
|
|
18
|
+
writeFileSync(join(localRoot, 'skills', 'agent', 'local-skill', 'SKILL.md'), '---\nname: local-skill\ndescription: local desc\n---\n');
|
|
19
|
+
writeFileSync(join(localRoot, 'docs', 'api', 'API.md'), '# API');
|
|
20
|
+
writeFileSync(join(globalRoot, 'agents', 'global-persona.md'), 'global persona content');
|
|
21
|
+
writeFileSync(join(localRoot, 'agents', 'local-persona.md'), 'local persona content');
|
|
22
|
+
});
|
|
23
|
+
it('should index both global and local skills', () => {
|
|
24
|
+
const registry = new Registry({ globalRoot, localRoot });
|
|
25
|
+
registry.build();
|
|
26
|
+
const skills = registry.listSkills();
|
|
27
|
+
expect(skills.length).toBe(2);
|
|
28
|
+
expect(skills.find(s => s.name === 'global-skill')?.scope).toBe('global');
|
|
29
|
+
expect(skills.find(s => s.name === 'local-skill')?.scope).toBe('local');
|
|
30
|
+
});
|
|
31
|
+
it('should shadow global skills with local ones if names conflict', () => {
|
|
32
|
+
// Create a local skill with same name as global
|
|
33
|
+
mkdirSync(join(localRoot, 'skills', 'agent', 'global-skill'), { recursive: true });
|
|
34
|
+
writeFileSync(join(localRoot, 'skills', 'agent', 'global-skill', 'SKILL.md'), '---\nname: global-skill\ndescription: shadowed desc\n---\n');
|
|
35
|
+
const registry = new Registry({ globalRoot, localRoot });
|
|
36
|
+
registry.build();
|
|
37
|
+
const skill = registry.getSkill('global-skill');
|
|
38
|
+
expect(skill?.scope).toBe('local');
|
|
39
|
+
expect(skill?.description).toBe('shadowed desc');
|
|
40
|
+
});
|
|
41
|
+
it('should index local docs', () => {
|
|
42
|
+
const registry = new Registry({ globalRoot, localRoot });
|
|
43
|
+
registry.build();
|
|
44
|
+
const docs = registry.listDocs();
|
|
45
|
+
expect(docs.length).toBe(1);
|
|
46
|
+
expect(docs[0].name).toBe('api');
|
|
47
|
+
});
|
|
48
|
+
it('should index both global and local personas (agents)', () => {
|
|
49
|
+
const registry = new Registry({ globalRoot, localRoot });
|
|
50
|
+
registry.build();
|
|
51
|
+
const personas = registry.listPersonas();
|
|
52
|
+
expect(personas.length).toBe(2);
|
|
53
|
+
expect(personas.find(p => p.name === 'global-persona')).toBeDefined();
|
|
54
|
+
expect(personas.find(p => p.name === 'local-persona')).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|