pi-vault-mind 0.14.2 → 0.16.3
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/CHANGELOG.md +293 -0
- package/README.md +5 -5
- package/agents/broadcaster.agent.md +2 -2
- package/agents/heavy-lifter.agent.md +1 -1
- package/agents/main.agent.md +29 -0
- package/agents/manager.agent.md +1 -1
- package/agents/miner.agent.md +1 -1
- package/dist/packages/obsidian/src/client.js +331 -0
- package/dist/packages/obsidian/src/types.js +1 -0
- package/dist/src/activity.d.ts +49 -0
- package/dist/src/activity.js +175 -0
- package/dist/src/agent-bus/Agent.d.ts +9 -1
- package/dist/src/agent-bus/Agent.js +18 -2
- package/dist/src/agent-bus/index.d.ts +5 -5
- package/dist/src/agent-bus/index.js +1 -1
- package/dist/src/agent-queue.js +12 -3
- package/dist/src/agents/BroadcasterAgent.d.ts +1 -1
- package/dist/src/agents/BroadcasterAgent.js +2 -2
- package/dist/src/agents/HeavyLifterAgent.d.ts +1 -1
- package/dist/src/agents/HeavyLifterAgent.js +2 -2
- package/dist/src/agents/ManagerAgent.js +1 -1
- package/dist/src/agents/MinerAgent.d.ts +1 -1
- package/dist/src/agents/MinerAgent.js +2 -2
- package/dist/src/agents/index.d.ts +2 -2
- package/dist/src/agents/index.js +2 -2
- package/dist/src/auth.d.ts +17 -15
- package/dist/src/auth.js +26 -37
- package/dist/src/autosync.js +2 -2
- package/dist/src/commands.d.ts +2 -19
- package/dist/src/commands.js +301 -231
- package/dist/src/config-keys.d.ts +16 -0
- package/dist/src/config-keys.js +45 -0
- package/dist/src/discover-schema.js +4 -4
- package/dist/src/edits.d.ts +21 -0
- package/dist/src/edits.js +75 -0
- package/dist/src/embedding-probe.d.ts +34 -0
- package/dist/src/embedding-probe.js +142 -0
- package/dist/src/embedding-providers.d.ts +133 -0
- package/dist/src/embedding-providers.js +173 -0
- package/dist/src/embedding-secrets.d.ts +38 -0
- package/dist/src/embedding-secrets.js +291 -0
- package/dist/src/engine.d.ts +6 -0
- package/dist/src/engine.js +78 -6
- package/dist/src/events.js +7 -10
- package/dist/src/extension-packages.d.ts +9 -0
- package/dist/src/extension-packages.js +14 -0
- package/dist/src/git.d.ts +19 -0
- package/dist/src/git.js +78 -0
- package/dist/src/graph.d.ts +2 -1
- package/dist/src/graph.js +47 -13
- package/dist/src/identities-config.d.ts +6 -0
- package/dist/src/identities-config.js +39 -0
- package/dist/src/identity-injector.js +1 -1
- package/dist/src/index.js +9 -26
- package/dist/src/lance.d.ts +9 -0
- package/dist/src/lance.js +143 -203
- package/dist/src/modal-client.d.ts +16 -0
- package/dist/src/modal-client.js +16 -0
- package/dist/src/modal-config.d.ts +37 -14
- package/dist/src/modal-config.js +59 -32
- package/dist/src/model-router.d.ts +27 -0
- package/dist/src/model-router.js +67 -0
- package/dist/src/models.d.ts +42 -0
- package/dist/src/models.js +78 -0
- package/dist/src/personalize.js +17 -3
- package/dist/src/scaffold.d.ts +48 -0
- package/dist/src/scaffold.js +246 -0
- package/dist/src/server.d.ts +47 -4
- package/dist/src/server.js +988 -221
- package/dist/src/session-search.d.ts +63 -0
- package/dist/src/session-search.js +379 -0
- package/dist/src/settings-ui.d.ts +2 -2
- package/dist/src/settings-ui.js +76 -135
- package/dist/src/sync.d.ts +3 -3
- package/dist/src/sync.js +19 -15
- package/dist/src/tool-catalog.d.ts +18 -0
- package/dist/src/tool-catalog.js +232 -0
- package/dist/src/tools/marksman.js +1 -1
- package/dist/src/tools.js +111 -65
- package/dist/src/types.d.ts +127 -71
- package/dist/src/utils.d.ts +57 -6
- package/dist/src/utils.js +114 -28
- package/dist/src/vault-tools.d.ts +34 -0
- package/dist/src/vault-tools.js +99 -0
- package/dist/src/vault-writer.js +4 -1
- package/dist/src/vm-handlers.js +140 -43
- package/dist/src/watcher.js +9 -9
- package/dist/test/activity-tool.test.js +54 -0
- package/dist/test/agent-bus/Agent.test.js +174 -0
- package/dist/test/agent-bus/AgentLoader.test.js +180 -0
- package/dist/test/agent-bus/AgentRegistry.test.js +86 -0
- package/dist/test/agent-bus/LLMProvider.test.js +162 -0
- package/dist/test/agent-bus/TaskQueue.test.js +256 -0
- package/dist/test/agent-dispatch-context.test.js +73 -0
- package/dist/test/agent-queue.test.js +1 -1
- package/dist/test/agents/Agents.test.js +282 -0
- package/dist/test/auth.test.js +57 -56
- package/dist/test/bridge-identity.test.js +34 -3
- package/dist/test/classify-search-mode.test.js +51 -0
- package/dist/test/commands.test.js +410 -0
- package/dist/test/config-merge.test.js +98 -0
- package/dist/test/context-capture.test.js +56 -0
- package/dist/test/discover-schema.test.js +8 -5
- package/dist/test/dispatch.test.js +45 -91
- package/dist/test/edits.test.js +144 -0
- package/dist/test/embedding-probe.test.js +424 -0
- package/dist/test/embedding-providers.test.js +157 -0
- package/dist/test/embedding-secrets.test.js +210 -0
- package/dist/test/engine-identity.test.js +1 -1
- package/dist/test/events.test.js +675 -0
- package/dist/test/git-bridge.test.js +160 -0
- package/dist/test/graph.test.js +464 -0
- package/dist/test/helpers/server.js +81 -0
- package/dist/test/identity-injector.test.js +197 -0
- package/dist/test/index.test.js +5 -7
- package/dist/test/lance-modal.test.js +67 -2
- package/dist/test/lance.test.js +429 -0
- package/dist/test/modal-client.test.js +1 -1
- package/dist/test/modal-config.test.js +92 -79
- package/dist/test/model-readiness.test.js +122 -0
- package/dist/test/model-router.test.js +99 -0
- package/dist/test/obsidian-client.test.js +42 -0
- package/dist/test/personalize.test.js +1 -1
- package/dist/test/rest-models.test.js +153 -0
- package/dist/test/rest-queue.test.js +135 -87
- package/dist/test/rest-setup.test.js +625 -115
- package/dist/test/rest-vm.test.js +355 -94
- package/dist/test/server-cors.test.js +48 -0
- package/dist/test/server-ws.test.js +61 -34
- package/dist/test/session-crud.test.js +323 -0
- package/dist/test/session-search.test.js +139 -0
- package/dist/test/settings-ui.test.js +113 -4
- package/dist/test/sync.test.js +12 -12
- package/dist/test/tombstone.test.js +8 -10
- package/dist/test/tool-catalog.test.js +86 -0
- package/dist/test/utils.test.js +588 -0
- package/dist/test/vault-tools.test.js +103 -12
- package/dist/test/vault-writer.test.js +371 -0
- package/dist/test/vm-handlers.test.js +620 -0
- package/extension-packages.json +9 -0
- package/package.json +107 -90
- package/pi-vault-mind.config.example.json +24 -2
- package/scripts/check-deps.sh +35 -32
- package/scripts/cli-e2e.mjs +215 -0
- package/scripts/configuration-e2e.mjs +783 -0
- package/scripts/e2e-commands.mjs +391 -0
- package/scripts/e2e-ws.mjs +3 -2
- package/scripts/fetch-modal-token.sh +4 -3
- package/scripts/generate-config-keys-json.mjs +45 -0
- package/scripts/generate-extension-packages-json.mjs +55 -0
- package/scripts/pi-session-query +0 -0
- package/scripts/publish-obsidian-plugin.sh +0 -0
- package/scripts/reset-test-vault.sh +2 -2
- package/scripts/setup-test-config.sh +0 -0
- package/scripts/setup-vault-pi.sh +48 -9
- package/scripts/test-slash-commands.mjs +2 -2
- package/scripts/vault-pi.sh +12 -3
- package/skills/github-release-notes-file/SKILL.md +42 -0
- package/skills/obsidian-guided-test-session/SKILL.md +110 -0
- package/skills/obsidian-interactive-test/SKILL.md +88 -0
- package/skills/obsidian-plugin-publish/SKILL.md +58 -0
- package/skills/pi-vault-mind-release/SKILL.md +141 -0
- package/skills/pre-test-release/SKILL.md +116 -0
- package/dist/test/auto-indexer.test.js +0 -2
|
@@ -1,95 +1,121 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
2
5
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import {
|
|
6
|
+
import { createModalClient, getModalToken, isModalConfigured, modalTokenEnvPath, modalUrl, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, resolveWorkspace, } from "../src/modal-config.js";
|
|
4
7
|
const makeCfg = (over = {}) => ({
|
|
5
8
|
dataDir: ".lancedb",
|
|
6
9
|
embedding: { remoteUrl: "https://pvm.modal.run" },
|
|
7
10
|
...over,
|
|
8
11
|
});
|
|
12
|
+
const REMOTE_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
|
|
13
|
+
const BRIDGE_ENV = "PVM_API_TOKEN";
|
|
14
|
+
const PROVIDER_ENV_KEYS = [
|
|
15
|
+
REMOTE_ENV,
|
|
16
|
+
BRIDGE_ENV,
|
|
17
|
+
"PVM_MODAL_READ_TOKEN",
|
|
18
|
+
"PVM_MODAL_WRITE_TOKEN",
|
|
19
|
+
];
|
|
9
20
|
describe("modal-config", () => {
|
|
10
|
-
let
|
|
11
|
-
let
|
|
12
|
-
let
|
|
13
|
-
beforeEach(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
fs.renameSync(MODAL_TOKEN_ENV_PATH, dotenvBak);
|
|
21
|
+
let originalCwd;
|
|
22
|
+
let testDir;
|
|
23
|
+
let savedEnvironment;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
originalCwd = process.cwd();
|
|
26
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "pvm-modal-config-"));
|
|
27
|
+
process.chdir(testDir);
|
|
28
|
+
savedEnvironment = {};
|
|
29
|
+
for (const key of PROVIDER_ENV_KEYS) {
|
|
30
|
+
savedEnvironment[key] = process.env[key];
|
|
31
|
+
delete process.env[key];
|
|
22
32
|
}
|
|
23
33
|
});
|
|
24
|
-
afterEach(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
fs.renameSync(dotenvBak, MODAL_TOKEN_ENV_PATH);
|
|
32
|
-
dotenvExisted = false;
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
for (const key of PROVIDER_ENV_KEYS) {
|
|
36
|
+
const value = savedEnvironment[key];
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
delete process.env[key];
|
|
39
|
+
else
|
|
40
|
+
process.env[key] = value;
|
|
33
41
|
}
|
|
42
|
+
process.chdir(originalCwd);
|
|
43
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
34
44
|
});
|
|
35
|
-
it("resolveModalToken: env wins over config
|
|
36
|
-
process.env[
|
|
45
|
+
it("resolveModalToken: remote provider env wins over legacy config", () => {
|
|
46
|
+
process.env[REMOTE_ENV] = "REMOTE_ENV_SENTINEL";
|
|
37
47
|
const cfg = makeCfg({
|
|
38
48
|
embedding: {
|
|
39
49
|
remoteUrl: "https://pvm.modal.run",
|
|
40
|
-
|
|
50
|
+
remoteApiKey: "REMOTE_LEGACY_SENTINEL",
|
|
41
51
|
},
|
|
42
52
|
});
|
|
43
|
-
assert.equal(resolveModalToken(cfg), "
|
|
53
|
+
assert.equal(resolveModalToken(cfg), "REMOTE_ENV_SENTINEL");
|
|
44
54
|
});
|
|
45
|
-
it("resolveModalToken: config
|
|
46
|
-
delete process.env[MODAL_TOKEN_ENV];
|
|
55
|
+
it("resolveModalToken: legacy config is the fallback", () => {
|
|
47
56
|
const cfg = makeCfg({
|
|
48
57
|
embedding: {
|
|
49
58
|
remoteUrl: "https://pvm.modal.run",
|
|
50
|
-
|
|
59
|
+
remoteApiKey: "REMOTE_LEGACY_SENTINEL",
|
|
51
60
|
},
|
|
52
61
|
});
|
|
53
|
-
assert.equal(resolveModalToken(cfg), "
|
|
54
|
-
});
|
|
55
|
-
it("
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
assert.equal(resolveModalToken(cfg), "REMOTE_LEGACY_SENTINEL");
|
|
63
|
+
});
|
|
64
|
+
it("PVM_API_TOKEN never resolves a remote provider credential", () => {
|
|
65
|
+
const cfg = makeCfg();
|
|
66
|
+
process.env[BRIDGE_ENV] = "BRIDGE_ONLY_SENTINEL";
|
|
67
|
+
const fromProcess = [
|
|
68
|
+
resolveModalToken(cfg),
|
|
69
|
+
getModalToken(cfg, "read"),
|
|
70
|
+
getModalToken(cfg, "write"),
|
|
71
|
+
];
|
|
72
|
+
delete process.env[BRIDGE_ENV];
|
|
73
|
+
fs.mkdirSync(path.dirname(modalTokenEnvPath()), { recursive: true });
|
|
74
|
+
fs.writeFileSync(modalTokenEnvPath(), 'PVM_API_TOKEN="BRIDGE_ONLY_SENTINEL"\n', "utf-8");
|
|
75
|
+
const fromStore = [
|
|
76
|
+
resolveModalToken(cfg),
|
|
77
|
+
getModalToken(cfg, "read"),
|
|
78
|
+
getModalToken(cfg, "write"),
|
|
79
|
+
];
|
|
80
|
+
const result = { fromProcess, fromStore };
|
|
81
|
+
assert.deepEqual(result, {
|
|
82
|
+
fromProcess: [undefined, undefined, undefined],
|
|
83
|
+
fromStore: [undefined, undefined, undefined],
|
|
59
84
|
});
|
|
60
|
-
assert.equal(
|
|
85
|
+
assert.equal(JSON.stringify(result).includes("BRIDGE_ONLY_SENTINEL"), false);
|
|
86
|
+
assert.equal(JSON.stringify(result).includes("REMOTE_PROVIDER_SENTINEL"), false);
|
|
61
87
|
});
|
|
62
88
|
it("createModalClient: null when not configured (no baseUrl or token)", () => {
|
|
63
|
-
delete process.env[
|
|
89
|
+
delete process.env[REMOTE_ENV];
|
|
64
90
|
assert.equal(createModalClient(makeCfg()), null);
|
|
65
91
|
assert.equal(createModalClient(makeCfg({
|
|
66
|
-
embedding: { remoteUrl: "https://pvm.modal.run"
|
|
92
|
+
embedding: { remoteUrl: "https://pvm.modal.run" },
|
|
67
93
|
})), null);
|
|
68
94
|
});
|
|
69
95
|
it("createModalClient: builds a client when baseUrl + token present", () => {
|
|
70
|
-
process.env[
|
|
96
|
+
process.env[REMOTE_ENV] = "tok";
|
|
71
97
|
const c = createModalClient(makeCfg({
|
|
72
|
-
embedding: { remoteUrl: "https://pvm.modal.run"
|
|
98
|
+
embedding: { remoteUrl: "https://pvm.modal.run" },
|
|
73
99
|
}));
|
|
74
100
|
assert.ok(c);
|
|
75
101
|
});
|
|
76
102
|
it("isModalConfigured: requires provider modal + baseUrl + token", () => {
|
|
77
|
-
delete process.env[
|
|
103
|
+
delete process.env[REMOTE_ENV];
|
|
78
104
|
assert.equal(isModalConfigured(makeCfg()), false);
|
|
79
|
-
process.env[
|
|
105
|
+
process.env[REMOTE_ENV] = "tok";
|
|
80
106
|
assert.equal(isModalConfigured(makeCfg({
|
|
81
|
-
embedding: { remoteUrl: "https://pvm.modal.run"
|
|
107
|
+
embedding: { remoteUrl: "https://pvm.modal.run" },
|
|
82
108
|
})), true);
|
|
83
109
|
// ollama provider is not modal-configured
|
|
84
110
|
assert.equal(isModalConfigured(makeCfg({
|
|
85
|
-
embedding: { localUrl: "http://127.0.0.1:11434/v1"
|
|
111
|
+
embedding: { localUrl: "http://127.0.0.1:11434/v1" },
|
|
86
112
|
})), false);
|
|
87
113
|
});
|
|
88
|
-
it("resolveModel: per-collection override wins, then
|
|
114
|
+
it("resolveModel: per-collection override wins, then embedding.model, default embeddinggemma", () => {
|
|
89
115
|
const cfg = makeCfg({
|
|
90
116
|
embedding: {
|
|
91
117
|
remoteUrl: "https://pvm.modal.run",
|
|
92
|
-
|
|
118
|
+
model: "global-model",
|
|
93
119
|
collectionModels: { special: { model: "special-model", dim: 128 } },
|
|
94
120
|
},
|
|
95
121
|
});
|
|
@@ -98,11 +124,11 @@ describe("modal-config", () => {
|
|
|
98
124
|
assert.equal(resolveModel(cfg, "other"), "global-model");
|
|
99
125
|
assert.equal(resolveModel(makeCfg()), "embeddinggemma");
|
|
100
126
|
});
|
|
101
|
-
it("resolveDim: per-collection override wins, then
|
|
127
|
+
it("resolveDim: per-collection override wins, then embedding.dim", () => {
|
|
102
128
|
const cfg = makeCfg({
|
|
103
129
|
embedding: {
|
|
104
130
|
remoteUrl: "https://pvm.modal.run",
|
|
105
|
-
|
|
131
|
+
dim: 512,
|
|
106
132
|
collectionModels: { special: { model: "m", dim: 128 } },
|
|
107
133
|
},
|
|
108
134
|
});
|
|
@@ -110,63 +136,50 @@ describe("modal-config", () => {
|
|
|
110
136
|
assert.equal(resolveDim(cfg, "special"), 128);
|
|
111
137
|
assert.equal(resolveDim(makeCfg()), undefined);
|
|
112
138
|
});
|
|
113
|
-
it("resolveModalToken: dotenv file is a fallback when env is unset",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
fs.writeFileSync(MODAL_TOKEN_ENV_PATH, 'PVM_API_TOKEN="dotenv-token"\n', "utf-8");
|
|
121
|
-
try {
|
|
122
|
-
const cfg = makeCfg({
|
|
123
|
-
embedding: { remoteUrl: "https://pvm.modal.run", modal: { baseUrl: "https://x" } },
|
|
124
|
-
});
|
|
125
|
-
assert.equal(resolveModalToken(cfg), "dotenv-token");
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
fs.rmSync(MODAL_TOKEN_ENV_PATH, { force: true });
|
|
129
|
-
}
|
|
139
|
+
it("resolveModalToken: dotenv file is a fallback when env is unset", () => {
|
|
140
|
+
fs.mkdirSync(path.dirname(modalTokenEnvPath()), { recursive: true });
|
|
141
|
+
fs.writeFileSync(modalTokenEnvPath(), 'PVM_REMOTE_EMBEDDING_API_KEY="dotenv-token"\n', "utf-8");
|
|
142
|
+
const cfg = makeCfg({
|
|
143
|
+
embedding: { remoteUrl: "https://pvm.modal.run" },
|
|
144
|
+
});
|
|
145
|
+
assert.equal(resolveModalToken(cfg), "dotenv-token");
|
|
130
146
|
});
|
|
131
147
|
it("modalUrl derives the canonical service URL from workspace", () => {
|
|
132
148
|
assert.equal(modalUrl("kylebrodeur"), "https://kylebrodeur--pi-vault-mind-embed-embeddingservice-fastapi-app.modal.run");
|
|
133
149
|
});
|
|
134
|
-
it("resolveBaseUrl: explicit
|
|
150
|
+
it("resolveBaseUrl: explicit remoteUrl wins over derived workspace URL", () => {
|
|
135
151
|
const cfg = makeCfg({
|
|
136
152
|
embedding: {
|
|
137
|
-
remoteUrl: "https://
|
|
138
|
-
|
|
139
|
-
baseUrl: "https://explicit.example.com",
|
|
140
|
-
workspace: "kylebrodeur",
|
|
141
|
-
},
|
|
153
|
+
remoteUrl: "https://explicit.example.com",
|
|
154
|
+
workspace: "kylebrodeur",
|
|
142
155
|
},
|
|
143
156
|
});
|
|
144
157
|
assert.equal(resolveBaseUrl(cfg), "https://explicit.example.com");
|
|
145
158
|
});
|
|
146
|
-
it("resolveBaseUrl: derives URL from workspace when
|
|
159
|
+
it("resolveBaseUrl: derives URL from workspace when remoteUrl is missing", () => {
|
|
147
160
|
const cfg = makeCfg({
|
|
148
|
-
embedding: {
|
|
161
|
+
embedding: { workspace: "kylebrodeur" },
|
|
149
162
|
});
|
|
150
163
|
assert.equal(resolveBaseUrl(cfg), modalUrl("kylebrodeur"));
|
|
151
164
|
});
|
|
152
165
|
it("resolveWorkspace returns the configured workspace slug", () => {
|
|
153
166
|
const cfg = makeCfg({
|
|
154
|
-
embedding: { remoteUrl: "https://pvm.modal.run",
|
|
167
|
+
embedding: { remoteUrl: "https://pvm.modal.run", workspace: "kylebrodeur" },
|
|
155
168
|
});
|
|
156
169
|
assert.equal(resolveWorkspace(cfg), "kylebrodeur");
|
|
157
170
|
assert.equal(resolveWorkspace(makeCfg()), undefined);
|
|
158
171
|
});
|
|
159
172
|
it("createModalClient: builds a client from workspace-derived URL", () => {
|
|
160
|
-
process.env[
|
|
173
|
+
process.env[REMOTE_ENV] = "tok";
|
|
161
174
|
const c = createModalClient(makeCfg({
|
|
162
|
-
embedding: {
|
|
175
|
+
embedding: { workspace: "kylebrodeur" },
|
|
163
176
|
}));
|
|
164
177
|
assert.ok(c);
|
|
165
178
|
});
|
|
166
|
-
it("isModalConfigured: true when
|
|
167
|
-
process.env[
|
|
179
|
+
it("isModalConfigured: true when remoteUrl + token are present (no explicit workspace)", () => {
|
|
180
|
+
process.env[REMOTE_ENV] = "tok";
|
|
168
181
|
assert.equal(isModalConfigured(makeCfg({
|
|
169
|
-
embedding: { remoteUrl: "https://pvm.modal.run",
|
|
182
|
+
embedding: { remoteUrl: "https://pvm.modal.run", workspace: "kylebrodeur" },
|
|
170
183
|
})), true);
|
|
171
184
|
});
|
|
172
185
|
it("namespacedTableName mirrors the server's ADR-3 scheme", () => {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { after, before, beforeEach, describe, it } from "node:test";
|
|
5
|
+
import { _resetAuthCache } from "../src/auth.js";
|
|
6
|
+
import { fakePi, fetchJson, makeVault, startTestServer, TEST_TOKEN, } from "./helpers/server.js";
|
|
7
|
+
const envVar = "PVM_API_TOKEN";
|
|
8
|
+
const readyResponse = {
|
|
9
|
+
status: "ready",
|
|
10
|
+
model: { id: "test-model-id", name: "Test Model Name", provider: "test-provider" },
|
|
11
|
+
};
|
|
12
|
+
const allStatuses = [
|
|
13
|
+
{ status: "model-registry-error" },
|
|
14
|
+
{ status: "model-registry-empty" },
|
|
15
|
+
{ status: "provider-authentication-required", authSource: "models_json_key" },
|
|
16
|
+
{ status: "configured-provider-unavailable", providerId: "test-provider-down" },
|
|
17
|
+
{ status: "model-selection-required" },
|
|
18
|
+
{ status: "current-model-unavailable", modelId: "test-current-model" },
|
|
19
|
+
readyResponse,
|
|
20
|
+
];
|
|
21
|
+
describe("GET /vault-mind/model-readiness", () => {
|
|
22
|
+
let vaultPath;
|
|
23
|
+
let port;
|
|
24
|
+
let srv;
|
|
25
|
+
let savedEnv;
|
|
26
|
+
let savedCwd;
|
|
27
|
+
let callbackCalls;
|
|
28
|
+
const responseSentinel = "CALLBACK_RESPONSE_SENTINEL_42";
|
|
29
|
+
const baseCallback = async () => {
|
|
30
|
+
callbackCalls++;
|
|
31
|
+
return { ...readyResponse, model: { ...readyResponse.model, name: responseSentinel } };
|
|
32
|
+
};
|
|
33
|
+
before(async () => {
|
|
34
|
+
savedEnv = process.env[envVar];
|
|
35
|
+
savedCwd = process.cwd();
|
|
36
|
+
process.env[envVar] = TEST_TOKEN;
|
|
37
|
+
vaultPath = makeVault("model-readiness");
|
|
38
|
+
process.chdir(vaultPath);
|
|
39
|
+
fs.mkdirSync(path.join(vaultPath, ".vault-mind"), { recursive: true });
|
|
40
|
+
srv = await startTestServer(fakePi, baseCallback);
|
|
41
|
+
port = srv.port;
|
|
42
|
+
});
|
|
43
|
+
after(() => {
|
|
44
|
+
try {
|
|
45
|
+
srv.stop();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// ignore double-stop
|
|
49
|
+
}
|
|
50
|
+
if (savedEnv === undefined)
|
|
51
|
+
delete process.env[envVar];
|
|
52
|
+
else
|
|
53
|
+
process.env[envVar] = savedEnv;
|
|
54
|
+
process.chdir(savedCwd);
|
|
55
|
+
if (fs.existsSync(vaultPath))
|
|
56
|
+
fs.rmSync(vaultPath, { recursive: true, force: true });
|
|
57
|
+
_resetAuthCache();
|
|
58
|
+
});
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
callbackCalls = 0;
|
|
61
|
+
});
|
|
62
|
+
it("requires bearer auth and rejects requests without a token", async () => {
|
|
63
|
+
const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness");
|
|
64
|
+
assert.equal(status, 401);
|
|
65
|
+
assert.ok(typeof body === "object" && body !== null);
|
|
66
|
+
assert.equal(body.error, "unauthorized");
|
|
67
|
+
assert.equal(callbackCalls, 0, "callback must not run for unauthenticated request");
|
|
68
|
+
assert.equal(JSON.stringify(body).includes(responseSentinel), false, "sentinel must not leak");
|
|
69
|
+
});
|
|
70
|
+
it("rejects non-GET methods with 405", async () => {
|
|
71
|
+
const { status, body } = await fetchJson(port, "POST", "/vault-mind/model-readiness", {
|
|
72
|
+
token: TEST_TOKEN,
|
|
73
|
+
});
|
|
74
|
+
assert.equal(status, 405);
|
|
75
|
+
assert.ok(typeof body === "object" && body !== null);
|
|
76
|
+
assert.equal(body.error, "Method not allowed");
|
|
77
|
+
assert.equal(callbackCalls, 0, "callback must not run for disallowed method");
|
|
78
|
+
assert.equal(JSON.stringify(body).includes(responseSentinel), false, "sentinel must not leak");
|
|
79
|
+
});
|
|
80
|
+
it("returns 500 when the readiness callback is not installed", async () => {
|
|
81
|
+
const previous = srv.serverState.getModelReadiness;
|
|
82
|
+
srv.serverState.getModelReadiness = undefined;
|
|
83
|
+
try {
|
|
84
|
+
const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
|
|
85
|
+
token: TEST_TOKEN,
|
|
86
|
+
});
|
|
87
|
+
assert.equal(status, 500);
|
|
88
|
+
assert.ok(typeof body === "object" && body !== null);
|
|
89
|
+
assert.equal(body.error, "Readiness callback not installed");
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
srv.serverState.getModelReadiness = previous;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
it("returns every valid readiness status unchanged", async () => {
|
|
96
|
+
for (const expected of allStatuses) {
|
|
97
|
+
srv.serverState.getModelReadiness = async () => expected;
|
|
98
|
+
const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
|
|
99
|
+
token: TEST_TOKEN,
|
|
100
|
+
});
|
|
101
|
+
assert.equal(status, 200, `expected 200 for status ${expected.status}`);
|
|
102
|
+
assert.deepEqual(body, expected, `status ${expected.status} must round-trip unchanged`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it("redacts callback failures to model-registry-error without leaking exception details", async () => {
|
|
106
|
+
const sentinel = "SENTINEL_EXCEPTION_TEXT_98765";
|
|
107
|
+
srv.serverState.getModelReadiness = async () => {
|
|
108
|
+
throw new Error(sentinel);
|
|
109
|
+
};
|
|
110
|
+
const { status, body } = await fetchJson(port, "GET", "/vault-mind/model-readiness", {
|
|
111
|
+
token: TEST_TOKEN,
|
|
112
|
+
});
|
|
113
|
+
assert.equal(status, 200);
|
|
114
|
+
assert.deepEqual(body, { status: "model-registry-error" });
|
|
115
|
+
assert.equal(JSON.stringify(body).includes(sentinel), false, "sentinel must not leak");
|
|
116
|
+
});
|
|
117
|
+
it("clears the readiness callback on server stop", () => {
|
|
118
|
+
assert.equal(typeof srv.serverState.getModelReadiness, "function");
|
|
119
|
+
srv.stop();
|
|
120
|
+
assert.equal(srv.serverState.getModelReadiness, undefined);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { defaultModelRouterChoices, scaffoldModelRouterConfig } from "../src/model-router.js";
|
|
8
|
+
import { resolveVaultMindPaths } from "../src/utils.js";
|
|
9
|
+
const savedAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
10
|
+
const savedHome = process.env.HOME;
|
|
11
|
+
describe("defaultModelRouterChoices", () => {
|
|
12
|
+
it("returns the tuned primary model and a 4-entry fallback sequence", () => {
|
|
13
|
+
const { primary, fallbackSequence } = defaultModelRouterChoices();
|
|
14
|
+
assert.equal(primary, "ollama/gemma4:31b-cloud");
|
|
15
|
+
assert.ok(Array.isArray(fallbackSequence));
|
|
16
|
+
assert.equal(fallbackSequence.length, 4);
|
|
17
|
+
});
|
|
18
|
+
it("includes the common baseline entries regardless of platform", () => {
|
|
19
|
+
const { fallbackSequence } = defaultModelRouterChoices();
|
|
20
|
+
for (const entry of ["ollama/gemma4:e4b", "ollama/gemma3:4b", "ollama/*"]) {
|
|
21
|
+
assert.ok(fallbackSequence.includes(entry), `expected fallbackSequence to include ${entry}`);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
it("includes exactly one of the platform-specific mid-tier variants", () => {
|
|
25
|
+
const { fallbackSequence } = defaultModelRouterChoices();
|
|
26
|
+
const macVariant = fallbackSequence.includes("ollama/gemma4:12b-mlx");
|
|
27
|
+
const otherVariant = fallbackSequence.includes("ollama/gemma4:12b");
|
|
28
|
+
// Exactly one of the two platform-specific entries is present — never both,
|
|
29
|
+
// never neither — without hardcoding which platform CI happens to run on.
|
|
30
|
+
assert.notEqual(macVariant, otherVariant);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("scaffoldModelRouterConfig", () => {
|
|
34
|
+
let testDir;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
testDir = path.join(tmpdir(), `model-router-test-${randomUUID()}`);
|
|
37
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
38
|
+
// Isolate the resolved agent dir under the temp vault so this test never
|
|
39
|
+
// touches the real ~/.pi/agent (getAgentDir() falls back to homedir()
|
|
40
|
+
// when PI_CODING_AGENT_DIR is unset).
|
|
41
|
+
process.env.PI_CODING_AGENT_DIR = path.join(testDir, ".vault-mind", ".pi", "agent");
|
|
42
|
+
process.env.HOME = testDir;
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (savedAgentDir === undefined) {
|
|
46
|
+
Reflect.deleteProperty(process.env, "PI_CODING_AGENT_DIR");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
process.env.PI_CODING_AGENT_DIR = savedAgentDir;
|
|
50
|
+
}
|
|
51
|
+
if (savedHome === undefined) {
|
|
52
|
+
Reflect.deleteProperty(process.env, "HOME");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
process.env.HOME = savedHome;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
it("resolves to resolveVaultMindPaths(cwd).modelRouter", () => {
|
|
65
|
+
const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
|
|
66
|
+
const result = scaffoldModelRouterConfig(testDir);
|
|
67
|
+
assert.equal(result.path, expectedPath);
|
|
68
|
+
});
|
|
69
|
+
it("creates model-router.json with the tuned defaults on first call", () => {
|
|
70
|
+
const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
|
|
71
|
+
const result = scaffoldModelRouterConfig(testDir);
|
|
72
|
+
assert.deepEqual(result, { created: true, path: expectedPath });
|
|
73
|
+
assert.ok(fs.existsSync(expectedPath), "expected model-router.json to be written");
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
|
|
75
|
+
assert.equal(parsed.defaultProfile, "auto");
|
|
76
|
+
assert.equal(typeof parsed.features, "object");
|
|
77
|
+
assert.ok(parsed.features);
|
|
78
|
+
assert.equal(typeof parsed.rateLimitFallback, "object");
|
|
79
|
+
assert.ok(Array.isArray(parsed.rateLimitFallback.fallbackSequence));
|
|
80
|
+
for (const tier of ["high", "medium", "low"]) {
|
|
81
|
+
const profile = parsed.profiles.auto[tier];
|
|
82
|
+
assert.ok(profile, `expected profiles.auto.${tier} to be defined`);
|
|
83
|
+
assert.equal(typeof profile.model, "string");
|
|
84
|
+
assert.equal(typeof profile.thinking, "string");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it("never overwrites an existing model-router.json (idempotent)", () => {
|
|
88
|
+
const expectedPath = resolveVaultMindPaths(testDir).modelRouter;
|
|
89
|
+
const first = scaffoldModelRouterConfig(testDir);
|
|
90
|
+
assert.equal(first.created, true);
|
|
91
|
+
const written = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
|
|
92
|
+
written.defaultProfile = "custom";
|
|
93
|
+
fs.writeFileSync(expectedPath, `${JSON.stringify(written, null, 2)}\n`, "utf-8");
|
|
94
|
+
const second = scaffoldModelRouterConfig(testDir);
|
|
95
|
+
assert.deepEqual(second, { created: false, path: expectedPath });
|
|
96
|
+
const unchanged = JSON.parse(fs.readFileSync(expectedPath, "utf-8"));
|
|
97
|
+
assert.equal(unchanged.defaultProfile, "custom");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import { VaultMindClient } from "../packages/obsidian/src/client.js";
|
|
4
|
+
describe("VaultMindClient", () => {
|
|
5
|
+
let originalFetch;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
originalFetch = globalThis.fetch;
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
globalThis.fetch = originalFetch;
|
|
11
|
+
});
|
|
12
|
+
it("listActivity encodes filters and returns activity events", async () => {
|
|
13
|
+
const jobId = "job/with spaces&symbols";
|
|
14
|
+
const responseBody = [
|
|
15
|
+
{
|
|
16
|
+
id: 1,
|
|
17
|
+
jobId,
|
|
18
|
+
ts: "2026-07-12T00:00:00.000Z",
|
|
19
|
+
kind: "status",
|
|
20
|
+
label: "status changed",
|
|
21
|
+
tone: "neutral",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
let captured;
|
|
25
|
+
globalThis.fetch = async (input, init) => {
|
|
26
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
27
|
+
captured = { url: new URL(rawUrl), init: init ?? {} };
|
|
28
|
+
return new Response(JSON.stringify(responseBody), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const client = new VaultMindClient({ host: "127.0.0.1", port: 11435, token: "client-token" });
|
|
34
|
+
const result = await client.listActivity({ jobId, limit: 7 });
|
|
35
|
+
assert.deepEqual(result, responseBody);
|
|
36
|
+
assert.ok(captured);
|
|
37
|
+
assert.equal(captured.url.pathname, "/agent/activity");
|
|
38
|
+
assert.equal(captured.url.searchParams.get("jobId"), jobId);
|
|
39
|
+
assert.equal(captured.url.searchParams.get("limit"), "7");
|
|
40
|
+
assert.equal(captured.init.method, "GET");
|
|
41
|
+
});
|
|
42
|
+
});
|