@indigoai-us/hq-cloud 6.14.18 → 6.14.19

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 (94) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/dist/bin/sync-runner-company.d.ts +7 -0
  3. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  4. package/dist/bin/sync-runner-company.js +21 -1
  5. package/dist/bin/sync-runner-company.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-loop.d.ts +10 -2
  7. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.js +201 -25
  9. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.d.ts +1 -1
  11. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-watch-routes.js +14 -2
  13. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  14. package/dist/bin/sync-runner.d.ts +17 -2
  15. package/dist/bin/sync-runner.d.ts.map +1 -1
  16. package/dist/bin/sync-runner.js +22 -4
  17. package/dist/bin/sync-runner.js.map +1 -1
  18. package/dist/bin/sync-runner.test.js +495 -3
  19. package/dist/bin/sync-runner.test.js.map +1 -1
  20. package/dist/cli/rescue-core.js +27 -1
  21. package/dist/cli/rescue-core.js.map +1 -1
  22. package/dist/cli/rescue-drop-dir-symlink.test.d.ts +2 -0
  23. package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +1 -0
  24. package/dist/cli/rescue-drop-dir-symlink.test.js +206 -0
  25. package/dist/cli/rescue-drop-dir-symlink.test.js.map +1 -0
  26. package/dist/cli/share.d.ts +15 -0
  27. package/dist/cli/share.d.ts.map +1 -1
  28. package/dist/cli/share.js +100 -55
  29. package/dist/cli/share.js.map +1 -1
  30. package/dist/cli/share.test.js +119 -6
  31. package/dist/cli/share.test.js.map +1 -1
  32. package/dist/cli/sync.d.ts.map +1 -1
  33. package/dist/cli/sync.js +19 -25
  34. package/dist/cli/sync.js.map +1 -1
  35. package/dist/cli/sync.test.js +121 -0
  36. package/dist/cli/sync.test.js.map +1 -1
  37. package/dist/journal.d.ts +3 -1
  38. package/dist/journal.d.ts.map +1 -1
  39. package/dist/journal.js +13 -2
  40. package/dist/journal.js.map +1 -1
  41. package/dist/journal.test.js +26 -1
  42. package/dist/journal.test.js.map +1 -1
  43. package/dist/personal-vault.d.ts +12 -0
  44. package/dist/personal-vault.d.ts.map +1 -1
  45. package/dist/personal-vault.js +20 -0
  46. package/dist/personal-vault.js.map +1 -1
  47. package/dist/qmd-reindex.d.ts +88 -11
  48. package/dist/qmd-reindex.d.ts.map +1 -1
  49. package/dist/qmd-reindex.js +354 -65
  50. package/dist/qmd-reindex.js.map +1 -1
  51. package/dist/qmd-reindex.test.d.ts +4 -0
  52. package/dist/qmd-reindex.test.d.ts.map +1 -1
  53. package/dist/qmd-reindex.test.js +285 -3
  54. package/dist/qmd-reindex.test.js.map +1 -1
  55. package/dist/s3.d.ts +13 -0
  56. package/dist/s3.d.ts.map +1 -1
  57. package/dist/s3.js +34 -1
  58. package/dist/s3.js.map +1 -1
  59. package/dist/s3.test.js +60 -1
  60. package/dist/s3.test.js.map +1 -1
  61. package/dist/telemetry.d.ts +22 -0
  62. package/dist/telemetry.d.ts.map +1 -1
  63. package/dist/telemetry.js +79 -0
  64. package/dist/telemetry.js.map +1 -1
  65. package/dist/telemetry.test.js +117 -1
  66. package/dist/telemetry.test.js.map +1 -1
  67. package/dist/watcher.d.ts +26 -1
  68. package/dist/watcher.d.ts.map +1 -1
  69. package/dist/watcher.js +75 -12
  70. package/dist/watcher.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/bin/sync-runner-company.ts +29 -1
  73. package/src/bin/sync-runner-watch-loop.ts +322 -32
  74. package/src/bin/sync-runner-watch-routes.ts +14 -1
  75. package/src/bin/sync-runner.test.ts +591 -6
  76. package/src/bin/sync-runner.ts +50 -5
  77. package/src/cli/rescue-core.ts +26 -1
  78. package/src/cli/rescue-drop-dir-symlink.test.ts +224 -0
  79. package/src/cli/share.test.ts +150 -6
  80. package/src/cli/share.ts +143 -48
  81. package/src/cli/sync.test.ts +150 -1
  82. package/src/cli/sync.ts +25 -29
  83. package/src/journal.test.ts +45 -0
  84. package/src/journal.ts +18 -1
  85. package/src/personal-vault.ts +23 -0
  86. package/src/qmd-reindex.test.ts +324 -4
  87. package/src/qmd-reindex.ts +414 -76
  88. package/src/s3.test.ts +79 -0
  89. package/src/s3.ts +48 -1
  90. package/src/telemetry.test.ts +128 -0
  91. package/src/telemetry.ts +80 -0
  92. package/src/watcher.ts +111 -14
  93. package/test/e2e/watcher-real-chokidar.test.ts +62 -2
  94. package/test/e2e/watcher-recursive-backend.test.ts +67 -1
@@ -12,9 +12,9 @@ import * as os from "os";
12
12
  import * as path from "path";
13
13
  import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
14
14
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
- import { FakeClock } from "../watcher.js";
16
- import { adaptivePollMs } from "./sync-runner-watch-loop.js";
17
- import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
15
+ import { FakeClock, } from "../watcher.js";
16
+ import { adaptivePollMs, journalEntriesForWatcherDelete, } from "./sync-runner-watch-loop.js";
17
+ import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
18
18
  import { HQ_CLOUD_VERSION } from "../version.js";
19
19
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
20
20
  import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
@@ -154,6 +154,9 @@ function makeDeps(overrides = {}) {
154
154
  updated: true,
155
155
  embedded: false,
156
156
  pendingDirty: false,
157
+ lockBusy: false,
158
+ timedOut: false,
159
+ corruptionQuarantined: false,
157
160
  })),
158
161
  reconcileManifest: vi
159
162
  .fn()
@@ -1800,6 +1803,9 @@ describe("all-complete aggregate", () => {
1800
1803
  updated: true,
1801
1804
  embedded: false,
1802
1805
  pendingDirty: false,
1806
+ lockBusy: false,
1807
+ timedOut: false,
1808
+ corruptionQuarantined: false,
1803
1809
  }));
1804
1810
  const deps = makeDeps({
1805
1811
  createVaultClient: () => makeVaultStub({
@@ -4656,6 +4662,90 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
4656
4662
  shutdown();
4657
4663
  await loop;
4658
4664
  });
4665
+ it("does not publish an exit-zero deletion that the scoped push refused", async () => {
4666
+ const publishBatch = vi.fn();
4667
+ const startEventSync = vi.fn().mockResolvedValue({
4668
+ publishBatch,
4669
+ receiver: { start: async () => { }, dispose: async () => { }, connected: true },
4670
+ ownDeviceId: "dev-test",
4671
+ dispose: vi.fn().mockResolvedValue(undefined),
4672
+ });
4673
+ const runPass = vi
4674
+ .fn()
4675
+ .mockResolvedValueOnce(0)
4676
+ .mockResolvedValueOnce({
4677
+ exitCode: 0,
4678
+ result: {
4679
+ pushPathResults: [{
4680
+ relativePath: "companies/indigo/deleted.md",
4681
+ status: "refused",
4682
+ operation: "delete",
4683
+ reason: "missing-delete-intent",
4684
+ }],
4685
+ },
4686
+ })
4687
+ .mockResolvedValue(0);
4688
+ const { loop, watcher, tick, shutdown } = runLoop({
4689
+ claims: ENROLLED_CLAIMS,
4690
+ startEventSync,
4691
+ runPass,
4692
+ });
4693
+ await microtasks();
4694
+ watcher.emit("companies/indigo/deleted.md", {
4695
+ paths: new Map([[
4696
+ "/tmp/hq/companies/indigo/deleted.md",
4697
+ "companies/indigo/deleted.md",
4698
+ ]]),
4699
+ });
4700
+ tick();
4701
+ await microtasks();
4702
+ expect(publishBatch).not.toHaveBeenCalled();
4703
+ shutdown();
4704
+ await loop;
4705
+ });
4706
+ it("does not publish unlinkDir when a covered descendant delete was refused", async () => {
4707
+ const publishBatch = vi.fn();
4708
+ const startEventSync = vi.fn().mockResolvedValue({
4709
+ publishBatch,
4710
+ receiver: { start: async () => { }, dispose: async () => { }, connected: true },
4711
+ ownDeviceId: "dev-test",
4712
+ dispose: vi.fn().mockResolvedValue(undefined),
4713
+ });
4714
+ const runPass = vi
4715
+ .fn()
4716
+ .mockResolvedValueOnce(0)
4717
+ .mockResolvedValueOnce({
4718
+ exitCode: 0,
4719
+ result: {
4720
+ pushPathResults: [{
4721
+ relativePath: "companies/indigo/archive/deleted.md",
4722
+ status: "refused",
4723
+ operation: "delete",
4724
+ reason: "missing-delete-intent",
4725
+ }],
4726
+ },
4727
+ })
4728
+ .mockResolvedValue(0);
4729
+ const { loop, watcher, tick, shutdown } = runLoop({
4730
+ claims: ENROLLED_CLAIMS,
4731
+ startEventSync,
4732
+ runPass,
4733
+ });
4734
+ await microtasks();
4735
+ const absolutePath = "/tmp/hq/companies/indigo/archive";
4736
+ watcher.emit("companies/indigo/archive", {
4737
+ paths: new Map([[
4738
+ absolutePath,
4739
+ "companies/indigo/archive",
4740
+ ]]),
4741
+ changes: new Map([[absolutePath, { kind: "unlinkDir" }]]),
4742
+ });
4743
+ tick();
4744
+ await microtasks();
4745
+ expect(publishBatch).not.toHaveBeenCalled();
4746
+ shutdown();
4747
+ await loop;
4748
+ });
4659
4749
  it("F04: mixed-route watcher batches publish only routes that were pushed", async () => {
4660
4750
  const publishBatch = vi.fn();
4661
4751
  const startEventSync = vi.fn().mockResolvedValue({
@@ -5694,4 +5784,406 @@ describe("US-002 — manifest reconciliation wiring", () => {
5694
5784
  expect(deps.stderr.raw()).not.toContain("manifest-reconciled");
5695
5785
  });
5696
5786
  });
5787
+ describe("watcher delete intents without pre-seeded authorization", () => {
5788
+ function startDeleteHarness(keys, options = {}) {
5789
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watch-delete-"));
5790
+ const companyRoot = path.join(hqRoot, "companies", "indigo");
5791
+ fs.mkdirSync(companyRoot, { recursive: true });
5792
+ const remote = new Map();
5793
+ const files = {};
5794
+ for (const key of keys) {
5795
+ const absolutePath = path.join(companyRoot, key);
5796
+ const content = `synced:${key}`;
5797
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
5798
+ fs.writeFileSync(absolutePath, content);
5799
+ remote.set(key, content);
5800
+ files[key] = {
5801
+ hash: `hash:${key}`,
5802
+ size: Buffer.byteLength(content),
5803
+ syncedAt: new Date().toISOString(),
5804
+ direction: "down",
5805
+ remoteEtag: `etag:${key}`,
5806
+ kind: "file",
5807
+ };
5808
+ }
5809
+ writeJournal("indigo", {
5810
+ version: "2", lastSync: new Date().toISOString(), files, pulls: [],
5811
+ });
5812
+ const watcher = makeBatchWatcherStub();
5813
+ const sleep = makeSteppableSleep();
5814
+ let capture;
5815
+ let triggerShutdown = () => { };
5816
+ let downloads = 0;
5817
+ let releaseInitialPull = () => { };
5818
+ const initialPullGate = new Promise((resolve) => {
5819
+ releaseInitialPull = resolve;
5820
+ });
5821
+ let initialPullGated = options.gateInitialPull === true;
5822
+ const runPass = vi.fn(async (argv) => {
5823
+ const direction = argv[argv.indexOf("--direction") + 1];
5824
+ if (direction === "push") {
5825
+ const journal = readJournal("indigo");
5826
+ const pathResults = [];
5827
+ const scopes = argv.flatMap((value, index) => argv[index - 1] === "--scope-path" ? [value] : []);
5828
+ for (const scope of scopes) {
5829
+ for (const key of Object.keys(journal.files)) {
5830
+ if (key !== scope && !key.startsWith(`${scope}/`))
5831
+ continue;
5832
+ const absolutePath = path.join(companyRoot, key);
5833
+ const entry = journal.files[key];
5834
+ if (fs.existsSync(absolutePath)) {
5835
+ remote.set(key, fs.readFileSync(absolutePath, "utf8"));
5836
+ continue;
5837
+ }
5838
+ const intent = entry.localDeleteIntent;
5839
+ const current = intent?.remoteEtag === entry.remoteEtag &&
5840
+ intent?.localHash === entry.hash &&
5841
+ intent?.localKind === entry.kind;
5842
+ if (current) {
5843
+ remote.delete(key);
5844
+ delete journal.files[key];
5845
+ pathResults.push({
5846
+ relativePath: `companies/indigo/${key}`,
5847
+ status: "accepted",
5848
+ operation: "delete",
5849
+ });
5850
+ }
5851
+ else {
5852
+ pathResults.push({
5853
+ relativePath: `companies/indigo/${key}`,
5854
+ status: "refused",
5855
+ operation: "delete",
5856
+ reason: "missing-delete-intent",
5857
+ });
5858
+ }
5859
+ }
5860
+ const added = path.join(companyRoot, scope);
5861
+ if (fs.existsSync(added) && fs.lstatSync(added).isFile()) {
5862
+ remote.set(scope, fs.readFileSync(added, "utf8"));
5863
+ }
5864
+ }
5865
+ writeJournal("indigo", journal);
5866
+ return { exitCode: 0, result: { pushPathResults: pathResults } };
5867
+ }
5868
+ if (direction === "pull" || direction === "both") {
5869
+ if (initialPullGated) {
5870
+ initialPullGated = false;
5871
+ await initialPullGate;
5872
+ }
5873
+ const journal = readJournal("indigo");
5874
+ for (const [key, content] of remote) {
5875
+ const absolutePath = path.join(companyRoot, key);
5876
+ if (fs.existsSync(absolutePath))
5877
+ continue;
5878
+ const entry = journal.files[key];
5879
+ const intent = entry?.localDeleteIntent;
5880
+ const currentIntent = intent?.remoteEtag === entry?.remoteEtag &&
5881
+ intent?.localHash === entry?.hash &&
5882
+ intent?.localKind === entry?.kind;
5883
+ if (currentIntent)
5884
+ continue;
5885
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
5886
+ fs.writeFileSync(absolutePath, content);
5887
+ downloads++;
5888
+ }
5889
+ }
5890
+ return 0;
5891
+ });
5892
+ const loop = runRunnerWithLoop([
5893
+ "--companies", "--watch", "--event-push", "--direction", "both",
5894
+ "--hq-root", hqRoot, "--poll-remote-ms", "60000",
5895
+ ], {
5896
+ runPass,
5897
+ createWatcher: (opts) => {
5898
+ capture = opts.captureLocalDeleteSnapshots;
5899
+ return watcher;
5900
+ },
5901
+ sleep: sleep.sleep,
5902
+ onShutdownSignal: (handler) => {
5903
+ triggerShutdown = handler;
5904
+ return () => { };
5905
+ },
5906
+ });
5907
+ return {
5908
+ hqRoot, companyRoot, remote, watcher, loop, tick: sleep.tick,
5909
+ capture: () => capture, downloads: () => downloads,
5910
+ releaseInitialPull,
5911
+ shutdown: () => triggerShutdown(),
5912
+ };
5913
+ }
5914
+ async function settle() {
5915
+ await flushLoopMicrotasks(12);
5916
+ }
5917
+ async function stopHarness(h) {
5918
+ h.shutdown();
5919
+ h.tick();
5920
+ await h.loop;
5921
+ fs.rmSync(h.hqRoot, { recursive: true, force: true });
5922
+ }
5923
+ it("live unlink binds the pre-delete revision, deletes remote, and survives immediate plus later full pulls", async () => {
5924
+ const h = startDeleteHarness(["knowledge/gone.md"]);
5925
+ await settle();
5926
+ const absolutePath = path.join(h.companyRoot, "knowledge/gone.md");
5927
+ fs.unlinkSync(absolutePath);
5928
+ const snapshots = h.capture()("companies/indigo/knowledge/gone.md", "unlink");
5929
+ expect(snapshots).toHaveLength(1);
5930
+ expect(readJournal("indigo").files["knowledge/gone.md"]?.localDeleteIntent)
5931
+ .toMatchObject({
5932
+ remoteEtag: "etag:knowledge/gone.md",
5933
+ localHash: "hash:knowledge/gone.md",
5934
+ localKind: "file",
5935
+ });
5936
+ h.watcher.emit("companies/indigo/knowledge/gone.md", {
5937
+ paths: new Map([[absolutePath, "companies/indigo/knowledge/gone.md"]]),
5938
+ changes: new Map([
5939
+ [absolutePath, { kind: "unlink", deleteSnapshots: snapshots }],
5940
+ ]),
5941
+ });
5942
+ h.tick();
5943
+ await settle();
5944
+ expect(h.remote.has("knowledge/gone.md")).toBe(false);
5945
+ expect(fs.existsSync(absolutePath)).toBe(false);
5946
+ expect(h.downloads()).toBe(0);
5947
+ for (let i = 0; i < 8; i++) {
5948
+ h.tick();
5949
+ await settle();
5950
+ }
5951
+ expect(fs.existsSync(absolutePath)).toBe(false);
5952
+ expect(h.downloads()).toBe(0);
5953
+ await stopHarness(h);
5954
+ });
5955
+ it("persists the intent before an in-flight pull plans the missing path", async () => {
5956
+ const h = startDeleteHarness(["knowledge/in-flight.md"], { gateInitialPull: true });
5957
+ await settle();
5958
+ const relativePath = "companies/indigo/knowledge/in-flight.md";
5959
+ const absolutePath = path.join(h.companyRoot, "knowledge/in-flight.md");
5960
+ fs.unlinkSync(absolutePath);
5961
+ const snapshots = h.capture()(relativePath, "unlink");
5962
+ expect(snapshots).toHaveLength(1);
5963
+ expect(readJournal("indigo").files["knowledge/in-flight.md"]
5964
+ ?.localDeleteIntent).toBeDefined();
5965
+ h.watcher.emit(relativePath, {
5966
+ paths: new Map([[absolutePath, relativePath]]),
5967
+ changes: new Map([
5968
+ [absolutePath, { kind: "unlink", deleteSnapshots: snapshots }],
5969
+ ]),
5970
+ });
5971
+ h.releaseInitialPull();
5972
+ await settle();
5973
+ expect(h.downloads()).toBe(0);
5974
+ expect(fs.existsSync(absolutePath)).toBe(false);
5975
+ h.tick();
5976
+ await settle();
5977
+ expect(h.remote.has("knowledge/in-flight.md")).toBe(false);
5978
+ expect(h.downloads()).toBe(0);
5979
+ await stopHarness(h);
5980
+ });
5981
+ it("uses direct journal-key lookup for file unlink snapshots", () => {
5982
+ const exactEntry = {
5983
+ hash: "hash",
5984
+ size: 4,
5985
+ syncedAt: new Date().toISOString(),
5986
+ direction: "down",
5987
+ remoteEtag: "etag",
5988
+ kind: "file",
5989
+ };
5990
+ const files = new Proxy({ "knowledge/exact.md": exactEntry }, {
5991
+ ownKeys: () => {
5992
+ throw new Error("file unlink must not enumerate the journal");
5993
+ },
5994
+ });
5995
+ expect(journalEntriesForWatcherDelete(files, "knowledge/exact.md", "unlink")).toEqual([["knowledge/exact.md", exactEntry]]);
5996
+ });
5997
+ it("keeps a deleted top-level personal file in the scoped push delete roots", async () => {
5998
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-personal-delete-"));
5999
+ const key = "personal-note.md";
6000
+ const absolutePath = path.join(hqRoot, key);
6001
+ fs.writeFileSync(absolutePath, "personal content");
6002
+ const remote = new Map([[key, "personal content"]]);
6003
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
6004
+ version: "2",
6005
+ lastSync: new Date().toISOString(),
6006
+ pulls: [],
6007
+ files: {
6008
+ [key]: {
6009
+ hash: "personal-hash",
6010
+ size: 16,
6011
+ syncedAt: new Date().toISOString(),
6012
+ direction: "down",
6013
+ remoteEtag: "personal-etag",
6014
+ kind: "file",
6015
+ },
6016
+ },
6017
+ });
6018
+ const shareSpy = vi.fn(async (options) => {
6019
+ const journal = readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
6020
+ const entry = journal.files[key];
6021
+ const intent = entry?.localDeleteIntent;
6022
+ const currentIntent = intent?.remoteEtag === entry?.remoteEtag &&
6023
+ intent?.localHash === entry?.hash &&
6024
+ intent?.localKind === entry?.kind;
6025
+ if (!fs.existsSync(absolutePath) &&
6026
+ options.deleteScopeRoots?.includes(key) &&
6027
+ currentIntent) {
6028
+ remote.delete(key);
6029
+ delete journal.files[key];
6030
+ writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, journal);
6031
+ return defaultShareResult({
6032
+ filesDeleted: 1,
6033
+ pathResults: [
6034
+ { path: key, status: "accepted", operation: "delete" },
6035
+ ],
6036
+ });
6037
+ }
6038
+ return defaultShareResult();
6039
+ });
6040
+ let downloads = 0;
6041
+ const syncSpy = vi.fn(async () => {
6042
+ if (remote.has(key) && !fs.existsSync(absolutePath)) {
6043
+ const entry = readJournal(PERSONAL_VAULT_JOURNAL_SLUG).files[key];
6044
+ const intent = entry?.localDeleteIntent;
6045
+ const currentIntent = intent?.remoteEtag === entry?.remoteEtag &&
6046
+ intent?.localHash === entry?.hash &&
6047
+ intent?.localKind === entry?.kind;
6048
+ if (!currentIntent) {
6049
+ fs.writeFileSync(absolutePath, remote.get(key));
6050
+ downloads++;
6051
+ }
6052
+ }
6053
+ return defaultSyncResult();
6054
+ });
6055
+ const runnerDeps = makeDeps({
6056
+ share: shareSpy,
6057
+ sync: syncSpy,
6058
+ createVaultClient: () => makeVaultStub({
6059
+ listPersons: () => Promise.resolve([
6060
+ {
6061
+ uid: "prs_personal",
6062
+ type: "person",
6063
+ bucketName: "hq-vault-personal",
6064
+ status: "active",
6065
+ },
6066
+ ]),
6067
+ }),
6068
+ operationLockAlreadyHeld: true,
6069
+ });
6070
+ const watcher = makeBatchWatcherStub();
6071
+ const sleep = makeSteppableSleep();
6072
+ let capture;
6073
+ let triggerShutdown = () => { };
6074
+ const loop = runRunnerWithLoop([
6075
+ "--personal",
6076
+ "--watch",
6077
+ "--event-push",
6078
+ "--direction",
6079
+ "both",
6080
+ "--hq-root",
6081
+ hqRoot,
6082
+ "--poll-remote-ms",
6083
+ "60000",
6084
+ ], {
6085
+ runPass: (passArgv) => runRunner(passArgv, runnerDeps),
6086
+ createWatcher: (options) => {
6087
+ capture = options.captureLocalDeleteSnapshots;
6088
+ return watcher;
6089
+ },
6090
+ sleep: sleep.sleep,
6091
+ onShutdownSignal: (handler) => {
6092
+ triggerShutdown = handler;
6093
+ return () => { };
6094
+ },
6095
+ });
6096
+ await vi.waitFor(() => expect(syncSpy).toHaveBeenCalled());
6097
+ await settle();
6098
+ shareSpy.mockClear();
6099
+ fs.unlinkSync(absolutePath);
6100
+ const snapshots = capture(key, "unlink");
6101
+ expect(snapshots).toHaveLength(1);
6102
+ expect(snapshots[0].deleteScopeRoot).toBe(key);
6103
+ expect(readJournal(PERSONAL_VAULT_JOURNAL_SLUG).files[key]
6104
+ ?.localDeleteIntent).toBeDefined();
6105
+ watcher.emit(key, {
6106
+ paths: new Map([[absolutePath, key]]),
6107
+ changes: new Map([
6108
+ [absolutePath, { kind: "unlink", deleteSnapshots: snapshots }],
6109
+ ]),
6110
+ });
6111
+ sleep.tick();
6112
+ await settle();
6113
+ expect(remote.has(key)).toBe(false);
6114
+ expect(downloads).toBe(0);
6115
+ expect(fs.existsSync(absolutePath)).toBe(false);
6116
+ const scopedShareOptions = shareSpy.mock.calls.at(-1)?.[0];
6117
+ expect(scopedShareOptions?.paths).not.toContain(absolutePath);
6118
+ expect(scopedShareOptions?.deleteScopeRoots).toEqual([key]);
6119
+ triggerShutdown();
6120
+ await loop;
6121
+ fs.rmSync(hqRoot, { recursive: true, force: true });
6122
+ });
6123
+ it("rename uploads the new path, deletes the old path, and neither resurrects", async () => {
6124
+ const h = startDeleteHarness(["knowledge/old.md"]);
6125
+ await settle();
6126
+ const oldPath = path.join(h.companyRoot, "knowledge/old.md");
6127
+ const newPath = path.join(h.companyRoot, "knowledge/new.md");
6128
+ const snapshots = h.capture()("companies/indigo/knowledge/old.md", "unlink");
6129
+ fs.renameSync(oldPath, newPath);
6130
+ h.watcher.emit("companies/indigo/knowledge/old.md", {
6131
+ paths: new Map([
6132
+ [oldPath, "companies/indigo/knowledge/old.md"],
6133
+ [newPath, "companies/indigo/knowledge/new.md"],
6134
+ ]),
6135
+ changes: new Map([
6136
+ [oldPath, { kind: "unlink", deleteSnapshots: snapshots }],
6137
+ [newPath, { kind: "add" }],
6138
+ ]),
6139
+ });
6140
+ h.tick();
6141
+ await settle();
6142
+ expect(h.remote.has("knowledge/old.md")).toBe(false);
6143
+ expect(h.remote.get("knowledge/new.md")).toBe("synced:knowledge/old.md");
6144
+ expect(fs.existsSync(oldPath)).toBe(false);
6145
+ expect(fs.existsSync(newPath)).toBe(true);
6146
+ await stopHarness(h);
6147
+ });
6148
+ it("unlinkDir records every tracked descendant across a 12-file breaker-sized subtree", async () => {
6149
+ const keys = Array.from({ length: 12 }, (_, i) => `knowledge/moved/f-${i}.md`);
6150
+ const h = startDeleteHarness(keys);
6151
+ await settle();
6152
+ const subtree = path.join(h.companyRoot, "knowledge/moved");
6153
+ const snapshots = h.capture()("companies/indigo/knowledge/moved", "unlinkDir");
6154
+ expect(snapshots).toHaveLength(12);
6155
+ fs.rmSync(subtree, { recursive: true, force: true });
6156
+ h.watcher.emit("companies/indigo/knowledge/moved", {
6157
+ paths: new Map([[subtree, "companies/indigo/knowledge/moved"]]),
6158
+ changes: new Map([
6159
+ [subtree, { kind: "unlinkDir", deleteSnapshots: snapshots }],
6160
+ ]),
6161
+ });
6162
+ h.tick();
6163
+ await settle();
6164
+ expect(h.remote.size).toBe(0);
6165
+ expect(Object.keys(readJournal("indigo").files)).toHaveLength(0);
6166
+ await stopHarness(h);
6167
+ });
6168
+ it("concurrent recreation before delete execution invalidates the observation and preserves remote", async () => {
6169
+ const h = startDeleteHarness(["knowledge/race.md"]);
6170
+ await settle();
6171
+ const absolutePath = path.join(h.companyRoot, "knowledge/race.md");
6172
+ fs.unlinkSync(absolutePath);
6173
+ const snapshots = h.capture()("companies/indigo/knowledge/race.md", "unlink");
6174
+ h.watcher.emit("companies/indigo/knowledge/race.md", {
6175
+ paths: new Map([[absolutePath, "companies/indigo/knowledge/race.md"]]),
6176
+ changes: new Map([
6177
+ [absolutePath, { kind: "unlink", deleteSnapshots: snapshots }],
6178
+ ]),
6179
+ });
6180
+ fs.writeFileSync(absolutePath, "concurrent recreation");
6181
+ h.tick();
6182
+ await settle();
6183
+ expect(h.remote.has("knowledge/race.md")).toBe(true);
6184
+ expect(readJournal("indigo").files["knowledge/race.md"]?.localDeleteIntent)
6185
+ .toBeUndefined();
6186
+ await stopHarness(h);
6187
+ });
6188
+ });
5697
6189
  //# sourceMappingURL=sync-runner.test.js.map