@openparachute/vault 0.4.6-rc.3 → 0.4.7-rc.1
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 +41 -0
- package/package.json +2 -2
- package/src/auth-hub-jwt.test.ts +161 -1
- package/src/auth.ts +57 -3
- package/src/cli.ts +420 -22
- package/src/config.ts +24 -0
- package/src/export-watch.test.ts +811 -0
- package/src/export-watch.ts +255 -0
- package/src/mcp-config.test.ts +260 -0
- package/src/mcp-install.test.ts +60 -0
- package/src/mcp-install.ts +61 -0
- package/src/mirror-config.test.ts +328 -0
- package/src/mirror-config.ts +470 -0
- package/src/mirror-deps.ts +88 -0
- package/src/mirror-manager.test.ts +550 -0
- package/src/mirror-manager.ts +521 -0
- package/src/mirror-registry.ts +26 -0
- package/src/mirror-routes.test.ts +380 -0
- package/src/mirror-routes.ts +152 -0
- package/src/routing.test.ts +76 -0
- package/src/routing.ts +46 -0
- package/src/server.ts +52 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the mirror config schema, parse/serialize, validation, and
|
|
3
|
+
* path resolution helpers (vault-sync Phase A1).
|
|
4
|
+
*
|
|
5
|
+
* All pure unit tests except the path-validation ones, which spawn `git`
|
|
6
|
+
* against tempdirs to exercise the real filesystem check.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
defaultMirrorConfig,
|
|
16
|
+
parseMirrorConfig,
|
|
17
|
+
resolveMirrorPath,
|
|
18
|
+
serializeMirrorConfig,
|
|
19
|
+
validateExternalPath,
|
|
20
|
+
validateMirrorConfigShape,
|
|
21
|
+
} from "./mirror-config.ts";
|
|
22
|
+
|
|
23
|
+
function tmp(prefix: string): string {
|
|
24
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function initRepo(dir: string): void {
|
|
28
|
+
Bun.spawnSync(["git", "init", "-q", "-b", "main"], { cwd: dir });
|
|
29
|
+
Bun.spawnSync(["git", "config", "user.email", "t@p.computer"], { cwd: dir });
|
|
30
|
+
Bun.spawnSync(["git", "config", "user.name", "T P"], { cwd: dir });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Defaults
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe("defaultMirrorConfig", () => {
|
|
38
|
+
test("defaults to enabled=false — upgrading vaults see zero behavior change", () => {
|
|
39
|
+
const d = defaultMirrorConfig();
|
|
40
|
+
expect(d.enabled).toBe(false);
|
|
41
|
+
expect(d.location).toBe("internal");
|
|
42
|
+
expect(d.external_path).toBeNull();
|
|
43
|
+
expect(d.watch).toBe(false);
|
|
44
|
+
expect(d.auto_commit).toBe(true);
|
|
45
|
+
expect(d.auto_push).toBe(false);
|
|
46
|
+
expect(d.commit_template).toContain("{{date}}");
|
|
47
|
+
expect(d.interval_seconds).toBe(5);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Parse + serialize
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe("parseMirrorConfig", () => {
|
|
56
|
+
test("returns undefined when no `mirror:` section is present", () => {
|
|
57
|
+
expect(parseMirrorConfig("port: 1940\n")).toBeUndefined();
|
|
58
|
+
expect(parseMirrorConfig("")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("parses a fully-specified mirror block", () => {
|
|
62
|
+
const yaml = [
|
|
63
|
+
"port: 1940",
|
|
64
|
+
"mirror:",
|
|
65
|
+
" enabled: true",
|
|
66
|
+
" location: external",
|
|
67
|
+
" external_path: /home/aaron/mirrors/gitcoin",
|
|
68
|
+
" watch: true",
|
|
69
|
+
" auto_commit: true",
|
|
70
|
+
" auto_push: true",
|
|
71
|
+
' commit_template: "vault: {{notes_changed}} note{{plural}}"',
|
|
72
|
+
" interval_seconds: 10",
|
|
73
|
+
].join("\n");
|
|
74
|
+
const m = parseMirrorConfig(yaml);
|
|
75
|
+
expect(m).toEqual({
|
|
76
|
+
enabled: true,
|
|
77
|
+
location: "external",
|
|
78
|
+
external_path: "/home/aaron/mirrors/gitcoin",
|
|
79
|
+
watch: true,
|
|
80
|
+
auto_commit: true,
|
|
81
|
+
auto_push: true,
|
|
82
|
+
commit_template: "vault: {{notes_changed}} note{{plural}}",
|
|
83
|
+
interval_seconds: 10,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("partial mirror block fills missing fields from defaults", () => {
|
|
88
|
+
const yaml = "mirror:\n enabled: true\n watch: true\n";
|
|
89
|
+
const m = parseMirrorConfig(yaml)!;
|
|
90
|
+
expect(m.enabled).toBe(true);
|
|
91
|
+
expect(m.watch).toBe(true);
|
|
92
|
+
expect(m.location).toBe("internal");
|
|
93
|
+
expect(m.auto_commit).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("external_path: null is interpreted as null", () => {
|
|
97
|
+
const m = parseMirrorConfig(
|
|
98
|
+
"mirror:\n enabled: true\n external_path: null\n",
|
|
99
|
+
)!;
|
|
100
|
+
expect(m.external_path).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("stops at next top-level key", () => {
|
|
104
|
+
const yaml = [
|
|
105
|
+
"mirror:",
|
|
106
|
+
" enabled: true",
|
|
107
|
+
" location: external",
|
|
108
|
+
" external_path: /a/b",
|
|
109
|
+
"port: 1940",
|
|
110
|
+
].join("\n");
|
|
111
|
+
const m = parseMirrorConfig(yaml)!;
|
|
112
|
+
expect(m.external_path).toBe("/a/b");
|
|
113
|
+
expect(m.enabled).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("serializeMirrorConfig", () => {
|
|
118
|
+
test("round-trips through parseMirrorConfig", () => {
|
|
119
|
+
const original = {
|
|
120
|
+
enabled: true,
|
|
121
|
+
location: "external" as const,
|
|
122
|
+
external_path: "/home/aaron/team-brain",
|
|
123
|
+
watch: true,
|
|
124
|
+
auto_commit: true,
|
|
125
|
+
auto_push: false,
|
|
126
|
+
commit_template: "export: {{date}} ({{notes_changed}} note{{plural}})",
|
|
127
|
+
interval_seconds: 5,
|
|
128
|
+
};
|
|
129
|
+
const yaml = serializeMirrorConfig(original).join("\n") + "\n";
|
|
130
|
+
const parsed = parseMirrorConfig(yaml);
|
|
131
|
+
expect(parsed).toEqual(original);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("serializes null external_path explicitly", () => {
|
|
135
|
+
const lines = serializeMirrorConfig({
|
|
136
|
+
...defaultMirrorConfig(),
|
|
137
|
+
enabled: true,
|
|
138
|
+
});
|
|
139
|
+
expect(lines.some((l) => l === " external_path: null")).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("quotes paths with colons or hashes", () => {
|
|
143
|
+
const lines = serializeMirrorConfig({
|
|
144
|
+
...defaultMirrorConfig(),
|
|
145
|
+
enabled: true,
|
|
146
|
+
location: "external",
|
|
147
|
+
external_path: "/path/with: colon",
|
|
148
|
+
});
|
|
149
|
+
expect(lines.some((l) => l.includes('"/path/with: colon"'))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Path resolution
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
describe("resolveMirrorPath", () => {
|
|
158
|
+
test("internal resolves to <vaultDataDir>/mirror", () => {
|
|
159
|
+
const p = resolveMirrorPath("/var/data/default", {
|
|
160
|
+
...defaultMirrorConfig(),
|
|
161
|
+
enabled: true,
|
|
162
|
+
location: "internal",
|
|
163
|
+
});
|
|
164
|
+
expect(p).toBe("/var/data/default/mirror");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("external returns the operator path verbatim", () => {
|
|
168
|
+
const p = resolveMirrorPath("/ignored", {
|
|
169
|
+
...defaultMirrorConfig(),
|
|
170
|
+
enabled: true,
|
|
171
|
+
location: "external",
|
|
172
|
+
external_path: "/home/aaron/notes",
|
|
173
|
+
});
|
|
174
|
+
expect(p).toBe("/home/aaron/notes");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("external + no path → null (manager treats as soft-disabled)", () => {
|
|
178
|
+
const p = resolveMirrorPath("/ignored", {
|
|
179
|
+
...defaultMirrorConfig(),
|
|
180
|
+
enabled: true,
|
|
181
|
+
location: "external",
|
|
182
|
+
external_path: null,
|
|
183
|
+
});
|
|
184
|
+
expect(p).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Shape validation
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
describe("validateMirrorConfigShape", () => {
|
|
193
|
+
test("accepts the minimal shape (empty object → defaults)", () => {
|
|
194
|
+
const r = validateMirrorConfigShape({});
|
|
195
|
+
expect(r.ok).toBe(true);
|
|
196
|
+
if (r.ok) {
|
|
197
|
+
expect(r.config).toEqual(defaultMirrorConfig());
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("rejects non-objects", () => {
|
|
202
|
+
expect(validateMirrorConfigShape(null).ok).toBe(false);
|
|
203
|
+
expect(validateMirrorConfigShape("hi").ok).toBe(false);
|
|
204
|
+
expect(validateMirrorConfigShape(42).ok).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects unknown location", () => {
|
|
208
|
+
const r = validateMirrorConfigShape({ location: "wherever" });
|
|
209
|
+
expect(r.ok).toBe(false);
|
|
210
|
+
if (!r.ok) expect(r.field).toBe("location");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("rejects external + missing external_path", () => {
|
|
214
|
+
const r = validateMirrorConfigShape({
|
|
215
|
+
enabled: true,
|
|
216
|
+
location: "external",
|
|
217
|
+
});
|
|
218
|
+
expect(r.ok).toBe(false);
|
|
219
|
+
if (!r.ok) expect(r.field).toBe("external_path");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("accepts external + missing external_path when disabled (operator turning off a broken mirror)", () => {
|
|
223
|
+
// Regression for the reviewer-flagged disable-only case: an operator
|
|
224
|
+
// PUTting `{enabled: false, location: "external"}` (no path) must
|
|
225
|
+
// succeed. Disable should never fail validation on path issues.
|
|
226
|
+
const r = validateMirrorConfigShape({
|
|
227
|
+
enabled: false,
|
|
228
|
+
location: "external",
|
|
229
|
+
});
|
|
230
|
+
expect(r.ok).toBe(true);
|
|
231
|
+
if (r.ok) {
|
|
232
|
+
expect(r.config.enabled).toBe(false);
|
|
233
|
+
expect(r.config.location).toBe("external");
|
|
234
|
+
expect(r.config.external_path).toBeNull();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("accepts external + external_path", () => {
|
|
239
|
+
const r = validateMirrorConfigShape({
|
|
240
|
+
enabled: true,
|
|
241
|
+
location: "external",
|
|
242
|
+
external_path: "/tmp/foo",
|
|
243
|
+
});
|
|
244
|
+
expect(r.ok).toBe(true);
|
|
245
|
+
if (r.ok) expect(r.config.external_path).toBe("/tmp/foo");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("rejects non-boolean enabled", () => {
|
|
249
|
+
const r = validateMirrorConfigShape({ enabled: "yes" });
|
|
250
|
+
expect(r.ok).toBe(false);
|
|
251
|
+
if (!r.ok) expect(r.field).toBe("enabled");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("rejects non-integer interval_seconds", () => {
|
|
255
|
+
const r = validateMirrorConfigShape({ interval_seconds: 0.5 });
|
|
256
|
+
expect(r.ok).toBe(false);
|
|
257
|
+
if (!r.ok) expect(r.field).toBe("interval_seconds");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("rejects empty commit_template", () => {
|
|
261
|
+
const r = validateMirrorConfigShape({ commit_template: " " });
|
|
262
|
+
expect(r.ok).toBe(false);
|
|
263
|
+
if (!r.ok) expect(r.field).toBe("commit_template");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("trims external_path whitespace; with internal location empty trim → null", () => {
|
|
267
|
+
const r = validateMirrorConfigShape({
|
|
268
|
+
enabled: false,
|
|
269
|
+
location: "internal",
|
|
270
|
+
external_path: " ",
|
|
271
|
+
});
|
|
272
|
+
expect(r.ok).toBe(true);
|
|
273
|
+
if (r.ok) expect(r.config.external_path).toBeNull();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("trims external_path whitespace on non-empty value", () => {
|
|
277
|
+
const r = validateMirrorConfigShape({
|
|
278
|
+
enabled: true,
|
|
279
|
+
location: "external",
|
|
280
|
+
external_path: " /tmp/foo ",
|
|
281
|
+
});
|
|
282
|
+
expect(r.ok).toBe(true);
|
|
283
|
+
if (r.ok) expect(r.config.external_path).toBe("/tmp/foo");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// validateExternalPath — filesystem-touching
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
describe("validateExternalPath", () => {
|
|
292
|
+
let dir: string;
|
|
293
|
+
afterEach(() => {
|
|
294
|
+
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("rejects missing path with actionable error", async () => {
|
|
298
|
+
dir = tmp("mirror-validate-");
|
|
299
|
+
const missing = path.join(dir, "nope");
|
|
300
|
+
const r = await validateExternalPath(missing);
|
|
301
|
+
expect(r.ok).toBe(false);
|
|
302
|
+
if (!r.ok) expect(r.error).toContain("doesn't exist");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("rejects path-is-a-file", async () => {
|
|
306
|
+
dir = tmp("mirror-validate-file-");
|
|
307
|
+
const file = path.join(dir, "f.txt");
|
|
308
|
+
fs.writeFileSync(file, "x");
|
|
309
|
+
const r = await validateExternalPath(file);
|
|
310
|
+
expect(r.ok).toBe(false);
|
|
311
|
+
if (!r.ok) expect(r.error).toContain("isn't a directory");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("rejects existing-dir but not a git repo", async () => {
|
|
315
|
+
dir = tmp("mirror-validate-nogit-");
|
|
316
|
+
const r = await validateExternalPath(dir);
|
|
317
|
+
expect(r.ok).toBe(false);
|
|
318
|
+
if (!r.ok) expect(r.error).toContain("isn't a git repository");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("accepts existing-dir git repo", async () => {
|
|
322
|
+
dir = tmp("mirror-validate-git-");
|
|
323
|
+
initRepo(dir);
|
|
324
|
+
const r = await validateExternalPath(dir);
|
|
325
|
+
expect(r.ok).toBe(true);
|
|
326
|
+
if (r.ok) expect(r.resolved_path).toBe(dir);
|
|
327
|
+
});
|
|
328
|
+
});
|