@openparachute/vault 0.6.0-rc.1 → 0.6.0
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/.parachute/module.json +14 -3
- package/README.md +7 -7
- package/core/src/core.test.ts +279 -26
- package/core/src/expand-visibility.test.ts +102 -0
- package/core/src/expand.ts +31 -3
- package/core/src/indexed-fields.ts +1 -1
- package/core/src/link-count.test.ts +301 -0
- package/core/src/links.ts +97 -2
- package/core/src/mcp.ts +201 -33
- package/core/src/notes.ts +44 -8
- package/core/src/obsidian-alignment.test.ts +375 -0
- package/core/src/obsidian.ts +234 -14
- package/core/src/portable-md.test.ts +40 -0
- package/core/src/portable-md.ts +142 -16
- package/core/src/schema.ts +58 -11
- package/core/src/store.ts +69 -22
- package/core/src/tag-expand-axis.test.ts +301 -0
- package/core/src/tag-hierarchy.ts +80 -0
- package/core/src/tag-schemas.ts +61 -46
- package/core/src/triggers-store.test.ts +100 -0
- package/core/src/triggers-store.ts +165 -0
- package/core/src/types.ts +68 -4
- package/core/src/vault-projection.ts +20 -0
- package/core/src/wikilinks.ts +2 -2
- package/package.json +2 -3
- package/src/admin-spa.test.ts +100 -10
- package/src/admin-spa.ts +48 -3
- package/src/auth-hub-jwt.test.ts +8 -1
- package/src/auth-status.ts +2 -2
- package/src/auth.test.ts +39 -3
- package/src/auth.ts +31 -2
- package/src/auto-transcribe.test.ts +51 -0
- package/src/auto-transcribe.ts +24 -6
- package/src/autostart.test.ts +75 -0
- package/src/autostart.ts +84 -0
- package/src/cli.ts +434 -140
- package/src/config.test.ts +109 -0
- package/src/config.ts +157 -10
- package/src/export-watch.test.ts +23 -0
- package/src/export-watch.ts +14 -0
- package/src/git-preflight.test.ts +70 -0
- package/src/git-preflight.ts +68 -0
- package/src/hub-jwt.test.ts +75 -2
- package/src/hub-jwt.ts +43 -6
- package/src/init-summary.test.ts +120 -5
- package/src/init-summary.ts +67 -25
- package/src/live-match.test.ts +198 -0
- package/src/live-match.ts +310 -0
- package/src/mcp-install.test.ts +93 -0
- package/src/mcp-install.ts +106 -0
- package/src/mcp-tools.ts +80 -7
- package/src/mirror-config.test.ts +14 -0
- package/src/mirror-config.ts +11 -0
- package/src/mirror-import.test.ts +110 -0
- package/src/mirror-import.ts +71 -13
- package/src/mirror-manager.test.ts +51 -0
- package/src/mirror-manager.ts +73 -11
- package/src/mirror-routes.test.ts +463 -1
- package/src/mirror-routes.ts +474 -4
- package/src/oauth-discovery.test.ts +55 -0
- package/src/oauth-discovery.ts +24 -5
- package/src/routes.ts +696 -121
- package/src/routing.test.ts +451 -5
- package/src/routing.ts +113 -5
- package/src/scopes.ts +1 -1
- package/src/server.ts +66 -4
- package/src/storage.test.ts +162 -0
- package/src/subscribe.test.ts +588 -0
- package/src/subscribe.ts +248 -0
- package/src/subscriptions.ts +295 -0
- package/src/tag-expand-routes.test.ts +45 -0
- package/src/tag-scope.ts +68 -1
- package/src/token-store.ts +7 -7
- package/src/transcription-worker.test.ts +471 -5
- package/src/transcription-worker.ts +212 -44
- package/src/triggers-api.test.ts +533 -0
- package/src/triggers-api.ts +295 -0
- package/src/triggers.ts +93 -7
- package/src/usage.test.ts +362 -0
- package/src/usage.ts +318 -0
- package/src/vault-create.test.ts +340 -12
- package/src/vault-name.test.ts +61 -3
- package/src/vault-name.ts +62 -14
- package/src/vault-remove.test.ts +187 -0
- package/src/vault-store.ts +10 -3
- package/src/vault.test.ts +1353 -62
- package/web/ui/dist/assets/index-CGL256oe.js +60 -0
- package/web/ui/dist/assets/index-J0pVP7I-.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-DBe8Xiah.css +0 -1
- package/web/ui/dist/assets/index-DDRo6F4u.js +0 -60
|
@@ -11,7 +11,12 @@ import fs from "node:fs";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
defaultMirrorConfig,
|
|
16
|
+
readMirrorConfigForVault,
|
|
17
|
+
writeMirrorConfigForVault,
|
|
18
|
+
type MirrorConfig,
|
|
19
|
+
} from "./mirror-config.ts";
|
|
15
20
|
import {
|
|
16
21
|
MirrorManager,
|
|
17
22
|
type MirrorDeps,
|
|
@@ -245,6 +250,37 @@ describe("handleMirrorPut", () => {
|
|
|
245
250
|
}
|
|
246
251
|
});
|
|
247
252
|
|
|
253
|
+
test("external + git not installed → 503 git_not_installed + actionable message", async () => {
|
|
254
|
+
// vault#415 nit — handleMirrorPut validates the external path via
|
|
255
|
+
// validateExternalPath, which shells `git`. On a git-less server it must
|
|
256
|
+
// return the friendly 503 (consistent with the import route), not let a
|
|
257
|
+
// raw "Executable not found" crash out. Force the preflight via the
|
|
258
|
+
// whichOverride seam against a REAL git repo so the only failure is the
|
|
259
|
+
// preflight.
|
|
260
|
+
home = tmp("mirror-put-nogit-installed-");
|
|
261
|
+
const { manager } = makeManager(home);
|
|
262
|
+
const external = tmp("mirror-put-nogit-target-");
|
|
263
|
+
initRepo(external);
|
|
264
|
+
try {
|
|
265
|
+
const req = new Request("http://x/admin/mirror", {
|
|
266
|
+
method: "PUT",
|
|
267
|
+
body: JSON.stringify({
|
|
268
|
+
enabled: true,
|
|
269
|
+
location: "external",
|
|
270
|
+
external_path: external,
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
const res = await handleMirrorPut(req, manager, () => null);
|
|
274
|
+
expect(res.status).toBe(503);
|
|
275
|
+
const body = (await res.json()) as { error_type: string; message: string };
|
|
276
|
+
expect(body.error_type).toBe("git_not_installed");
|
|
277
|
+
expect(body.message).toContain("git is required");
|
|
278
|
+
expect(body.message).toContain("dnf install git");
|
|
279
|
+
} finally {
|
|
280
|
+
fs.rmSync(external, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
248
284
|
test("accepts a valid external config, persists, restarts watch", async () => {
|
|
249
285
|
home = tmp("mirror-put-happy-");
|
|
250
286
|
const external = tmp("mirror-put-ext-");
|
|
@@ -1081,6 +1117,28 @@ const spawnCloneFail: GitSpawn = async () => ({
|
|
|
1081
1117
|
timedOut: false,
|
|
1082
1118
|
});
|
|
1083
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* vault#416 — a MirrorManager wired to the REAL per-vault config file (so
|
|
1122
|
+
* `handleMirrorImport`'s `readMirrorConfigForVault` agrees with what
|
|
1123
|
+
* `manager.reload()` wrote) + a no-op export. Passed to `handleMirrorImport`
|
|
1124
|
+
* as the `managerOverride` so the sync-enable step has a live manager without
|
|
1125
|
+
* standing up the registry factory. Bootstrap (git init of the internal
|
|
1126
|
+
* mirror) runs for real; push to a fake remote fails non-fatally (we assert
|
|
1127
|
+
* on persisted config + credentials, not on a landed push).
|
|
1128
|
+
*/
|
|
1129
|
+
function makeSyncManager(home: string): MirrorManager {
|
|
1130
|
+
process.env.PARACHUTE_HOME = home;
|
|
1131
|
+
process.env.HOME = home;
|
|
1132
|
+
const deps: MirrorDeps = {
|
|
1133
|
+
vaultName: "default",
|
|
1134
|
+
runExport: async () => ({ notes: 0 }),
|
|
1135
|
+
firstChangedNoteTitle: async () => "",
|
|
1136
|
+
readMirrorConfig: () => readMirrorConfigForVault("default"),
|
|
1137
|
+
writeMirrorConfig: (c) => writeMirrorConfigForVault("default", c),
|
|
1138
|
+
};
|
|
1139
|
+
return new MirrorManager(deps);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1084
1142
|
describe("handleMirrorImport", () => {
|
|
1085
1143
|
let home: string;
|
|
1086
1144
|
let fixture: string;
|
|
@@ -1258,6 +1316,35 @@ describe("handleMirrorImport", () => {
|
|
|
1258
1316
|
expect(body.message).toContain("vault.yaml");
|
|
1259
1317
|
});
|
|
1260
1318
|
|
|
1319
|
+
test("git not installed returns 503 + git_not_installed + actionable message", async () => {
|
|
1320
|
+
// vault#415 — live bug on a git-less Amazon Linux EC2 box. Force the
|
|
1321
|
+
// preflight (via the whichOverride seam) to see no git; the spawn seam
|
|
1322
|
+
// should never be reached.
|
|
1323
|
+
home = tmp("import-route-nogit-");
|
|
1324
|
+
await bootstrapVault(home);
|
|
1325
|
+
let spawnCalled = false;
|
|
1326
|
+
const spyingSpawn: GitSpawn = async () => {
|
|
1327
|
+
spawnCalled = true;
|
|
1328
|
+
return { exitCode: 0, stderr: "", timedOut: false };
|
|
1329
|
+
};
|
|
1330
|
+
const req = new Request("http://x/import", {
|
|
1331
|
+
method: "POST",
|
|
1332
|
+
body: JSON.stringify({
|
|
1333
|
+
remote_url: "https://github.com/a/b.git",
|
|
1334
|
+
mode: "merge",
|
|
1335
|
+
credentials: { kind: "none" },
|
|
1336
|
+
}),
|
|
1337
|
+
});
|
|
1338
|
+
const res = await handleMirrorImport(req, "default", spyingSpawn, () => null);
|
|
1339
|
+
expect(res.status).toBe(503);
|
|
1340
|
+
const body = (await res.json()) as { error_type: string; message: string };
|
|
1341
|
+
expect(body.error_type).toBe("git_not_installed");
|
|
1342
|
+
expect(body.message).toContain("git is required");
|
|
1343
|
+
expect(body.message).toContain("dnf install git");
|
|
1344
|
+
// Failed fast: the git spawn was never reached.
|
|
1345
|
+
expect(spawnCalled).toBe(false);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1261
1348
|
test("uses stored credentials when credentials: null (credentialsFile path)", async () => {
|
|
1262
1349
|
home = tmp("import-route-stored-creds-");
|
|
1263
1350
|
await bootstrapVault(home);
|
|
@@ -1334,3 +1421,378 @@ describe("handleMirrorImport", () => {
|
|
|
1334
1421
|
});
|
|
1335
1422
|
});
|
|
1336
1423
|
|
|
1424
|
+
// ---------------------------------------------------------------------------
|
|
1425
|
+
// vault#416 — auto-enable sync to the imported repo (default-on, opt-out).
|
|
1426
|
+
// ---------------------------------------------------------------------------
|
|
1427
|
+
|
|
1428
|
+
describe("handleMirrorImport — auto-enable sync (vault#416)", () => {
|
|
1429
|
+
let home: string;
|
|
1430
|
+
let fixture: string;
|
|
1431
|
+
let manager: MirrorManager;
|
|
1432
|
+
|
|
1433
|
+
afterEach(async () => {
|
|
1434
|
+
if (manager) await manager.stop();
|
|
1435
|
+
if (home) fs.rmSync(home, { recursive: true, force: true });
|
|
1436
|
+
if (fixture) fs.rmSync(fixture, { recursive: true, force: true });
|
|
1437
|
+
_resetImportInFlightForTest();
|
|
1438
|
+
clearVaultStoreCache();
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
test("enable_sync true + PAT auth → mirror configured, creds persisted, auto_push on, sync_enabled true", async () => {
|
|
1442
|
+
home = tmp("import-sync-pat-");
|
|
1443
|
+
await bootstrapVault(home);
|
|
1444
|
+
fixture = await buildExportFixture();
|
|
1445
|
+
manager = makeSyncManager(home);
|
|
1446
|
+
|
|
1447
|
+
const req = new Request("http://x/import", {
|
|
1448
|
+
method: "POST",
|
|
1449
|
+
body: JSON.stringify({
|
|
1450
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1451
|
+
mode: "merge",
|
|
1452
|
+
credentials: { kind: "pat", token: "ghp_import_token_abc" },
|
|
1453
|
+
enable_sync: true,
|
|
1454
|
+
}),
|
|
1455
|
+
});
|
|
1456
|
+
const res = await handleMirrorImport(
|
|
1457
|
+
req,
|
|
1458
|
+
"default",
|
|
1459
|
+
spawnCloneSuccess(fixture),
|
|
1460
|
+
undefined,
|
|
1461
|
+
manager,
|
|
1462
|
+
);
|
|
1463
|
+
expect(res.status).toBe(200);
|
|
1464
|
+
const body = (await res.json()) as {
|
|
1465
|
+
notes_imported: number;
|
|
1466
|
+
sync_enabled: boolean;
|
|
1467
|
+
sync_warning?: string;
|
|
1468
|
+
};
|
|
1469
|
+
expect(body.notes_imported).toBe(2);
|
|
1470
|
+
expect(body.sync_enabled).toBe(true);
|
|
1471
|
+
expect(body.sync_warning).toBeUndefined();
|
|
1472
|
+
|
|
1473
|
+
// Mirror config persisted with auto_push + enabled.
|
|
1474
|
+
const cfg = readMirrorConfigForVault("default");
|
|
1475
|
+
expect(cfg?.enabled).toBe(true);
|
|
1476
|
+
expect(cfg?.auto_push).toBe(true);
|
|
1477
|
+
|
|
1478
|
+
// Credentials persisted, pointing at the imported remote.
|
|
1479
|
+
const creds = readCredentials("default");
|
|
1480
|
+
expect(creds?.active_method).toBe("pat");
|
|
1481
|
+
expect(creds?.pat?.token).toBe("ghp_import_token_abc");
|
|
1482
|
+
expect(creds?.pat?.remote_url).toContain("github.com/aaron/my-vault.git");
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
test("enable_sync false → no mirror configured, sync_enabled false, no warning", async () => {
|
|
1486
|
+
home = tmp("import-sync-optout-");
|
|
1487
|
+
await bootstrapVault(home);
|
|
1488
|
+
fixture = await buildExportFixture();
|
|
1489
|
+
manager = makeSyncManager(home);
|
|
1490
|
+
|
|
1491
|
+
const req = new Request("http://x/import", {
|
|
1492
|
+
method: "POST",
|
|
1493
|
+
body: JSON.stringify({
|
|
1494
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1495
|
+
mode: "merge",
|
|
1496
|
+
credentials: { kind: "pat", token: "ghp_import_token_abc" },
|
|
1497
|
+
enable_sync: false,
|
|
1498
|
+
}),
|
|
1499
|
+
});
|
|
1500
|
+
const res = await handleMirrorImport(
|
|
1501
|
+
req,
|
|
1502
|
+
"default",
|
|
1503
|
+
spawnCloneSuccess(fixture),
|
|
1504
|
+
undefined,
|
|
1505
|
+
manager,
|
|
1506
|
+
);
|
|
1507
|
+
expect(res.status).toBe(200);
|
|
1508
|
+
const body = (await res.json()) as {
|
|
1509
|
+
sync_enabled: boolean;
|
|
1510
|
+
sync_warning?: string;
|
|
1511
|
+
};
|
|
1512
|
+
expect(body.sync_enabled).toBe(false);
|
|
1513
|
+
expect(body.sync_warning).toBeUndefined();
|
|
1514
|
+
|
|
1515
|
+
// Nothing configured.
|
|
1516
|
+
expect(readMirrorConfigForVault("default")).toBeUndefined();
|
|
1517
|
+
expect(readCredentials("default")).toBeNull();
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("enable_sync true + auth none → sync_enabled false + needs-write-creds warning; no broken mirror", async () => {
|
|
1521
|
+
home = tmp("import-sync-nocreds-");
|
|
1522
|
+
await bootstrapVault(home);
|
|
1523
|
+
fixture = await buildExportFixture();
|
|
1524
|
+
manager = makeSyncManager(home);
|
|
1525
|
+
|
|
1526
|
+
const req = new Request("http://x/import", {
|
|
1527
|
+
method: "POST",
|
|
1528
|
+
body: JSON.stringify({
|
|
1529
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1530
|
+
mode: "merge",
|
|
1531
|
+
credentials: { kind: "none" },
|
|
1532
|
+
enable_sync: true,
|
|
1533
|
+
}),
|
|
1534
|
+
});
|
|
1535
|
+
const res = await handleMirrorImport(
|
|
1536
|
+
req,
|
|
1537
|
+
"default",
|
|
1538
|
+
spawnCloneSuccess(fixture),
|
|
1539
|
+
undefined,
|
|
1540
|
+
manager,
|
|
1541
|
+
);
|
|
1542
|
+
expect(res.status).toBe(200);
|
|
1543
|
+
const body = (await res.json()) as {
|
|
1544
|
+
notes_imported: number;
|
|
1545
|
+
sync_enabled: boolean;
|
|
1546
|
+
sync_warning?: string;
|
|
1547
|
+
};
|
|
1548
|
+
// Import still succeeded.
|
|
1549
|
+
expect(body.notes_imported).toBe(2);
|
|
1550
|
+
expect(body.sync_enabled).toBe(false);
|
|
1551
|
+
expect(body.sync_warning).toContain("write credentials");
|
|
1552
|
+
|
|
1553
|
+
// No mirror left configured, no credentials written.
|
|
1554
|
+
expect(readMirrorConfigForVault("default")).toBeUndefined();
|
|
1555
|
+
expect(readCredentials("default")).toBeNull();
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
test("enable_sync defaults to true when omitted", async () => {
|
|
1559
|
+
home = tmp("import-sync-default-");
|
|
1560
|
+
await bootstrapVault(home);
|
|
1561
|
+
fixture = await buildExportFixture();
|
|
1562
|
+
manager = makeSyncManager(home);
|
|
1563
|
+
|
|
1564
|
+
const req = new Request("http://x/import", {
|
|
1565
|
+
method: "POST",
|
|
1566
|
+
body: JSON.stringify({
|
|
1567
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1568
|
+
mode: "merge",
|
|
1569
|
+
credentials: { kind: "pat", token: "ghp_default_on_token" },
|
|
1570
|
+
// enable_sync omitted — should default ON.
|
|
1571
|
+
}),
|
|
1572
|
+
});
|
|
1573
|
+
const res = await handleMirrorImport(
|
|
1574
|
+
req,
|
|
1575
|
+
"default",
|
|
1576
|
+
spawnCloneSuccess(fixture),
|
|
1577
|
+
undefined,
|
|
1578
|
+
manager,
|
|
1579
|
+
);
|
|
1580
|
+
expect(res.status).toBe(200);
|
|
1581
|
+
const body = (await res.json()) as { sync_enabled: boolean };
|
|
1582
|
+
expect(body.sync_enabled).toBe(true);
|
|
1583
|
+
expect(readMirrorConfigForVault("default")?.auto_push).toBe(true);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
test("existing mirror to a DIFFERENT remote → not clobbered, sync_enabled false + conflict warning", async () => {
|
|
1587
|
+
home = tmp("import-sync-conflict-");
|
|
1588
|
+
await bootstrapVault(home);
|
|
1589
|
+
fixture = await buildExportFixture();
|
|
1590
|
+
manager = makeSyncManager(home);
|
|
1591
|
+
|
|
1592
|
+
// Pre-existing mirror config (enabled) + credential pointing elsewhere.
|
|
1593
|
+
writeMirrorConfigForVault("default", {
|
|
1594
|
+
...defaultMirrorConfig(),
|
|
1595
|
+
enabled: true,
|
|
1596
|
+
auto_push: true,
|
|
1597
|
+
});
|
|
1598
|
+
writeCredentials("default", {
|
|
1599
|
+
active_method: "pat",
|
|
1600
|
+
github_oauth: null,
|
|
1601
|
+
pat: {
|
|
1602
|
+
token: "ghp_existing_other",
|
|
1603
|
+
remote_url:
|
|
1604
|
+
"https://x-access-token:ghp_existing_other@github.com/aaron/OTHER-repo.git",
|
|
1605
|
+
label: "existing backup",
|
|
1606
|
+
},
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
const req = new Request("http://x/import", {
|
|
1610
|
+
method: "POST",
|
|
1611
|
+
body: JSON.stringify({
|
|
1612
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1613
|
+
mode: "merge",
|
|
1614
|
+
credentials: { kind: "pat", token: "ghp_import_token_abc" },
|
|
1615
|
+
enable_sync: true,
|
|
1616
|
+
}),
|
|
1617
|
+
});
|
|
1618
|
+
const res = await handleMirrorImport(
|
|
1619
|
+
req,
|
|
1620
|
+
"default",
|
|
1621
|
+
spawnCloneSuccess(fixture),
|
|
1622
|
+
undefined,
|
|
1623
|
+
manager,
|
|
1624
|
+
);
|
|
1625
|
+
expect(res.status).toBe(200);
|
|
1626
|
+
const body = (await res.json()) as {
|
|
1627
|
+
sync_enabled: boolean;
|
|
1628
|
+
sync_warning?: string;
|
|
1629
|
+
};
|
|
1630
|
+
expect(body.sync_enabled).toBe(false);
|
|
1631
|
+
expect(body.sync_warning).toContain("already syncs to a different repo");
|
|
1632
|
+
|
|
1633
|
+
// The existing credential was NOT clobbered.
|
|
1634
|
+
const creds = readCredentials("default");
|
|
1635
|
+
expect(creds?.pat?.token).toBe("ghp_existing_other");
|
|
1636
|
+
expect(creds?.pat?.remote_url).toContain("OTHER-repo.git");
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
test("existing mirror to the SAME remote → no-op success (sync_enabled true)", async () => {
|
|
1640
|
+
home = tmp("import-sync-same-");
|
|
1641
|
+
await bootstrapVault(home);
|
|
1642
|
+
fixture = await buildExportFixture();
|
|
1643
|
+
manager = makeSyncManager(home);
|
|
1644
|
+
|
|
1645
|
+
writeMirrorConfigForVault("default", {
|
|
1646
|
+
...defaultMirrorConfig(),
|
|
1647
|
+
enabled: true,
|
|
1648
|
+
auto_push: true,
|
|
1649
|
+
});
|
|
1650
|
+
writeCredentials("default", {
|
|
1651
|
+
active_method: "pat",
|
|
1652
|
+
github_oauth: null,
|
|
1653
|
+
pat: {
|
|
1654
|
+
token: "ghp_same_token",
|
|
1655
|
+
remote_url:
|
|
1656
|
+
"https://x-access-token:ghp_same_token@github.com/aaron/my-vault.git",
|
|
1657
|
+
label: "existing same",
|
|
1658
|
+
},
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
const req = new Request("http://x/import", {
|
|
1662
|
+
method: "POST",
|
|
1663
|
+
body: JSON.stringify({
|
|
1664
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1665
|
+
mode: "merge",
|
|
1666
|
+
credentials: { kind: "pat", token: "ghp_same_token" },
|
|
1667
|
+
enable_sync: true,
|
|
1668
|
+
}),
|
|
1669
|
+
});
|
|
1670
|
+
const res = await handleMirrorImport(
|
|
1671
|
+
req,
|
|
1672
|
+
"default",
|
|
1673
|
+
spawnCloneSuccess(fixture),
|
|
1674
|
+
undefined,
|
|
1675
|
+
manager,
|
|
1676
|
+
);
|
|
1677
|
+
expect(res.status).toBe(200);
|
|
1678
|
+
const body = (await res.json()) as {
|
|
1679
|
+
sync_enabled: boolean;
|
|
1680
|
+
sync_warning?: string;
|
|
1681
|
+
};
|
|
1682
|
+
expect(body.sync_enabled).toBe(true);
|
|
1683
|
+
expect(body.sync_warning).toBeUndefined();
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
test("existing GitHub-connected mirror → PAT import doesn't clobber it (sync_enabled false + warning)", async () => {
|
|
1687
|
+
home = tmp("import-sync-oauth-conflict-");
|
|
1688
|
+
await bootstrapVault(home);
|
|
1689
|
+
fixture = await buildExportFixture();
|
|
1690
|
+
manager = makeSyncManager(home);
|
|
1691
|
+
|
|
1692
|
+
writeMirrorConfigForVault("default", {
|
|
1693
|
+
...defaultMirrorConfig(),
|
|
1694
|
+
enabled: true,
|
|
1695
|
+
auto_push: true,
|
|
1696
|
+
});
|
|
1697
|
+
writeCredentials("default", {
|
|
1698
|
+
active_method: "github_oauth",
|
|
1699
|
+
github_oauth: {
|
|
1700
|
+
access_token: "gho_existing_oauth",
|
|
1701
|
+
scope: "repo",
|
|
1702
|
+
authorized_at: "2026-05-28T00:00:00.000Z",
|
|
1703
|
+
user_login: "aaron",
|
|
1704
|
+
user_id: 1,
|
|
1705
|
+
},
|
|
1706
|
+
pat: null,
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
const req = new Request("http://x/import", {
|
|
1710
|
+
method: "POST",
|
|
1711
|
+
body: JSON.stringify({
|
|
1712
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1713
|
+
mode: "merge",
|
|
1714
|
+
credentials: { kind: "pat", token: "ghp_import_token_abc" },
|
|
1715
|
+
enable_sync: true,
|
|
1716
|
+
}),
|
|
1717
|
+
});
|
|
1718
|
+
const res = await handleMirrorImport(
|
|
1719
|
+
req,
|
|
1720
|
+
"default",
|
|
1721
|
+
spawnCloneSuccess(fixture),
|
|
1722
|
+
undefined,
|
|
1723
|
+
manager,
|
|
1724
|
+
);
|
|
1725
|
+
expect(res.status).toBe(200);
|
|
1726
|
+
const body = (await res.json()) as {
|
|
1727
|
+
sync_enabled: boolean;
|
|
1728
|
+
sync_warning?: string;
|
|
1729
|
+
};
|
|
1730
|
+
expect(body.sync_enabled).toBe(false);
|
|
1731
|
+
expect(body.sync_warning).toContain("connected GitHub account");
|
|
1732
|
+
|
|
1733
|
+
// The existing OAuth credential is untouched (not switched to a PAT).
|
|
1734
|
+
const creds = readCredentials("default");
|
|
1735
|
+
expect(creds?.active_method).toBe("github_oauth");
|
|
1736
|
+
expect(creds?.pat).toBeNull();
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
test("sync-setup failure after a successful import → import result still returned, sync_enabled false + warning", async () => {
|
|
1740
|
+
home = tmp("import-sync-setupfail-");
|
|
1741
|
+
await bootstrapVault(home);
|
|
1742
|
+
fixture = await buildExportFixture();
|
|
1743
|
+
manager = makeSyncManager(home);
|
|
1744
|
+
|
|
1745
|
+
// Force the sync-enable step to throw by stubbing the manager's reload.
|
|
1746
|
+
// The import itself has already succeeded by the time reload runs, so the
|
|
1747
|
+
// request must still return a 200 with the import counts intact.
|
|
1748
|
+
manager.reload = async () => {
|
|
1749
|
+
throw new Error("boom: simulated reload failure");
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
const req = new Request("http://x/import", {
|
|
1753
|
+
method: "POST",
|
|
1754
|
+
body: JSON.stringify({
|
|
1755
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1756
|
+
mode: "merge",
|
|
1757
|
+
credentials: { kind: "pat", token: "ghp_import_token_abc" },
|
|
1758
|
+
enable_sync: true,
|
|
1759
|
+
}),
|
|
1760
|
+
});
|
|
1761
|
+
const res = await handleMirrorImport(
|
|
1762
|
+
req,
|
|
1763
|
+
"default",
|
|
1764
|
+
spawnCloneSuccess(fixture),
|
|
1765
|
+
undefined,
|
|
1766
|
+
manager,
|
|
1767
|
+
);
|
|
1768
|
+
expect(res.status).toBe(200);
|
|
1769
|
+
const body = (await res.json()) as {
|
|
1770
|
+
notes_imported: number;
|
|
1771
|
+
sync_enabled: boolean;
|
|
1772
|
+
sync_warning?: string;
|
|
1773
|
+
};
|
|
1774
|
+
// Import NOT lost.
|
|
1775
|
+
expect(body.notes_imported).toBe(2);
|
|
1776
|
+
expect(body.sync_enabled).toBe(false);
|
|
1777
|
+
expect(body.sync_warning).toContain("enabling Sync failed");
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
test("invalid enable_sync type → 400 validation error", async () => {
|
|
1781
|
+
home = tmp("import-sync-badtype-");
|
|
1782
|
+
await bootstrapVault(home);
|
|
1783
|
+
const req = new Request("http://x/import", {
|
|
1784
|
+
method: "POST",
|
|
1785
|
+
body: JSON.stringify({
|
|
1786
|
+
remote_url: "https://github.com/aaron/my-vault.git",
|
|
1787
|
+
mode: "merge",
|
|
1788
|
+
credentials: { kind: "none" },
|
|
1789
|
+
enable_sync: "yes",
|
|
1790
|
+
}),
|
|
1791
|
+
});
|
|
1792
|
+
const res = await handleMirrorImport(req, "default");
|
|
1793
|
+
expect(res.status).toBe(400);
|
|
1794
|
+
const body = (await res.json()) as { field: string };
|
|
1795
|
+
expect(body.field).toBe("enable_sync");
|
|
1796
|
+
});
|
|
1797
|
+
});
|
|
1798
|
+
|