@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.
Files changed (91) hide show
  1. package/.parachute/module.json +14 -3
  2. package/README.md +7 -7
  3. package/core/src/core.test.ts +279 -26
  4. package/core/src/expand-visibility.test.ts +102 -0
  5. package/core/src/expand.ts +31 -3
  6. package/core/src/indexed-fields.ts +1 -1
  7. package/core/src/link-count.test.ts +301 -0
  8. package/core/src/links.ts +97 -2
  9. package/core/src/mcp.ts +201 -33
  10. package/core/src/notes.ts +44 -8
  11. package/core/src/obsidian-alignment.test.ts +375 -0
  12. package/core/src/obsidian.ts +234 -14
  13. package/core/src/portable-md.test.ts +40 -0
  14. package/core/src/portable-md.ts +142 -16
  15. package/core/src/schema.ts +58 -11
  16. package/core/src/store.ts +69 -22
  17. package/core/src/tag-expand-axis.test.ts +301 -0
  18. package/core/src/tag-hierarchy.ts +80 -0
  19. package/core/src/tag-schemas.ts +61 -46
  20. package/core/src/triggers-store.test.ts +100 -0
  21. package/core/src/triggers-store.ts +165 -0
  22. package/core/src/types.ts +68 -4
  23. package/core/src/vault-projection.ts +20 -0
  24. package/core/src/wikilinks.ts +2 -2
  25. package/package.json +2 -3
  26. package/src/admin-spa.test.ts +100 -10
  27. package/src/admin-spa.ts +48 -3
  28. package/src/auth-hub-jwt.test.ts +8 -1
  29. package/src/auth-status.ts +2 -2
  30. package/src/auth.test.ts +39 -3
  31. package/src/auth.ts +31 -2
  32. package/src/auto-transcribe.test.ts +51 -0
  33. package/src/auto-transcribe.ts +24 -6
  34. package/src/autostart.test.ts +75 -0
  35. package/src/autostart.ts +84 -0
  36. package/src/cli.ts +434 -140
  37. package/src/config.test.ts +109 -0
  38. package/src/config.ts +157 -10
  39. package/src/export-watch.test.ts +23 -0
  40. package/src/export-watch.ts +14 -0
  41. package/src/git-preflight.test.ts +70 -0
  42. package/src/git-preflight.ts +68 -0
  43. package/src/hub-jwt.test.ts +75 -2
  44. package/src/hub-jwt.ts +43 -6
  45. package/src/init-summary.test.ts +120 -5
  46. package/src/init-summary.ts +67 -25
  47. package/src/live-match.test.ts +198 -0
  48. package/src/live-match.ts +310 -0
  49. package/src/mcp-install.test.ts +93 -0
  50. package/src/mcp-install.ts +106 -0
  51. package/src/mcp-tools.ts +80 -7
  52. package/src/mirror-config.test.ts +14 -0
  53. package/src/mirror-config.ts +11 -0
  54. package/src/mirror-import.test.ts +110 -0
  55. package/src/mirror-import.ts +71 -13
  56. package/src/mirror-manager.test.ts +51 -0
  57. package/src/mirror-manager.ts +73 -11
  58. package/src/mirror-routes.test.ts +463 -1
  59. package/src/mirror-routes.ts +474 -4
  60. package/src/oauth-discovery.test.ts +55 -0
  61. package/src/oauth-discovery.ts +24 -5
  62. package/src/routes.ts +696 -121
  63. package/src/routing.test.ts +451 -5
  64. package/src/routing.ts +113 -5
  65. package/src/scopes.ts +1 -1
  66. package/src/server.ts +66 -4
  67. package/src/storage.test.ts +162 -0
  68. package/src/subscribe.test.ts +588 -0
  69. package/src/subscribe.ts +248 -0
  70. package/src/subscriptions.ts +295 -0
  71. package/src/tag-expand-routes.test.ts +45 -0
  72. package/src/tag-scope.ts +68 -1
  73. package/src/token-store.ts +7 -7
  74. package/src/transcription-worker.test.ts +471 -5
  75. package/src/transcription-worker.ts +212 -44
  76. package/src/triggers-api.test.ts +533 -0
  77. package/src/triggers-api.ts +295 -0
  78. package/src/triggers.ts +93 -7
  79. package/src/usage.test.ts +362 -0
  80. package/src/usage.ts +318 -0
  81. package/src/vault-create.test.ts +340 -12
  82. package/src/vault-name.test.ts +61 -3
  83. package/src/vault-name.ts +62 -14
  84. package/src/vault-remove.test.ts +187 -0
  85. package/src/vault-store.ts +10 -3
  86. package/src/vault.test.ts +1353 -62
  87. package/web/ui/dist/assets/index-CGL256oe.js +60 -0
  88. package/web/ui/dist/assets/index-J0pVP7I-.css +1 -0
  89. package/web/ui/dist/index.html +2 -2
  90. package/web/ui/dist/assets/index-DBe8Xiah.css +0 -1
  91. 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 { defaultMirrorConfig, type MirrorConfig } from "./mirror-config.ts";
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
+