@indigoai-us/hq-cloud 6.15.18 → 6.15.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.
@@ -5598,45 +5598,41 @@ describe("currency-gated: journal version 2 fixtures", () => {
5598
5598
  expect(result.filesDeleted).toBe(1);
5599
5599
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
5600
5600
  });
5601
- // ── Fix #1 (5.36.0): lstat fast-path ─────────────────────────────────────
5602
- //
5603
- // When skipUnchanged is on AND the journal carries an mtimeMs for the
5604
- // file AND (lstat.size, lstat.mtimeMs) match the recorded values, the
5605
- // push planner classifies the file `unchanged` WITHOUT reading its
5606
- // bytes. Same trade-off rsync/gitignore use; turns no-op syncs from
5607
- // O(file bytes) into O(file count).
5608
- //
5609
- // We prove "the SHA256 path didn't run" by seeding the journal with an
5610
- // intentionally WRONG hash but matching mtimeMs+size. If the fast-path
5611
- // fires → file is classified `unchanged` purely on lstat (no hash
5612
- // compare). If the fast-path is skipped → hashFile runs, hashes the
5613
- // real file, fails the !== journal-hash check, and uploads. The
5614
- // upload-vs-skip outcome is the diagnostic signal.
5615
- describe("lstat fast-path (5.36.0)", () => {
5616
- it("skips upload when mtimeMs + size match journal (even with wrong stored hash)", async () => {
5601
+ // ── Regular-file content identity ─────────────────────────────────────────
5602
+ describe("regular-file content identity", () => {
5603
+ it("uploads a same-size edit whose mtime matches the journal", async () => {
5617
5604
  const companyRoot = path.join(tmpDir, "companies", "acme");
5618
5605
  fs.mkdirSync(companyRoot, { recursive: true });
5619
5606
  const testFile = path.join(companyRoot, "stable.md");
5620
5607
  fs.writeFileSync(testFile, "stable bytes");
5621
- const lstat = fs.lstatSync(testFile);
5622
- // Intentionally-wrong hash; matching mtimeMs+size. If the fast-path
5623
- // doesn't fire, hashFile runs, the real hash != "sentinel-wrong-hash",
5624
- // and the file gets uploaded — which would fail this test.
5608
+ const preservedMtime = new Date("2026-01-02T03:04:05.000Z");
5609
+ fs.utimesSync(testFile, preservedMtime, preservedMtime);
5610
+ const baselineLstat = fs.lstatSync(testFile);
5611
+ const { hashFile } = await import("../journal.js");
5612
+ const baselineHash = hashFile(testFile);
5625
5613
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5626
5614
  fs.writeFileSync(journalPath, JSON.stringify({
5627
5615
  version: "2",
5628
5616
  lastSync: new Date().toISOString(),
5629
5617
  files: {
5630
5618
  "stable.md": {
5631
- hash: "sentinel-wrong-hash-fast-path-should-skip-without-checking",
5632
- size: lstat.size,
5633
- mtimeMs: lstat.mtimeMs,
5619
+ hash: baselineHash,
5620
+ size: baselineLstat.size,
5621
+ mtimeMs: baselineLstat.mtimeMs,
5634
5622
  syncedAt: new Date().toISOString(),
5635
5623
  direction: "up",
5636
5624
  },
5637
5625
  },
5638
5626
  pulls: [],
5639
5627
  }));
5628
+ // Rewrite the bytes without changing either cheap stat signal. A planner
5629
+ // that treats (size, mtimeMs) as content identity will skip this forever.
5630
+ fs.writeFileSync(testFile, "edited bytes");
5631
+ fs.utimesSync(testFile, preservedMtime, preservedMtime);
5632
+ const editedLstat = fs.lstatSync(testFile);
5633
+ expect(editedLstat.size).toBe(baselineLstat.size);
5634
+ expect(editedLstat.mtimeMs).toBe(baselineLstat.mtimeMs);
5635
+ expect(hashFile(testFile)).not.toBe(baselineHash);
5640
5636
  const result = await share({
5641
5637
  paths: [testFile],
5642
5638
  company: "acme",
@@ -5644,21 +5640,17 @@ describe("currency-gated: journal version 2 fixtures", () => {
5644
5640
  hqRoot: tmpDir,
5645
5641
  skipUnchanged: true,
5646
5642
  });
5647
- // Fast-path fired → no hash read → no upload despite wrong stored hash.
5648
- expect(result.filesUploaded).toBe(0);
5649
- expect(result.filesSkipped).toBe(1);
5650
- expect(uploadFile).not.toHaveBeenCalled();
5643
+ expect(result.filesUploaded).toBe(1);
5644
+ expect(result.filesSkipped).toBe(0);
5645
+ expect(uploadFile).toHaveBeenCalled();
5651
5646
  });
5652
- it("uploads when mtime changes (fast-path misses → hash runs → mismatch)", async () => {
5647
+ it("uploads when the content hash differs after an mtime change", async () => {
5653
5648
  const companyRoot = path.join(tmpDir, "companies", "acme");
5654
5649
  fs.mkdirSync(companyRoot, { recursive: true });
5655
5650
  const testFile = path.join(companyRoot, "touched.md");
5656
5651
  fs.writeFileSync(testFile, "original bytes!");
5657
5652
  const originalLstat = fs.lstatSync(testFile);
5658
- // Wrong hash + matching mtimeMs+size — would skip via fast-path if
5659
- // mtime stayed the same. We're about to bump mtime, so fast-path
5660
- // MUST miss, hashFile MUST run, hash MUST mismatch the wrong stored
5661
- // value, and the file MUST upload.
5653
+ // The recorded hash intentionally differs from the current bytes.
5662
5654
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5663
5655
  fs.writeFileSync(journalPath, JSON.stringify({
5664
5656
  version: "2",
@@ -5674,7 +5666,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5674
5666
  },
5675
5667
  pulls: [],
5676
5668
  }));
5677
- // Bump mtime forward — fast-path comparison mtime fails.
5669
+ // Bump mtime forward; content hashing remains the source of truth.
5678
5670
  const futureTime = new Date(Date.now() + 60_000);
5679
5671
  fs.utimesSync(testFile, futureTime, futureTime);
5680
5672
  const result = await share({
@@ -5684,8 +5676,6 @@ describe("currency-gated: journal version 2 fixtures", () => {
5684
5676
  hqRoot: tmpDir,
5685
5677
  skipUnchanged: true,
5686
5678
  });
5687
- // Fast-path missed → second-stage hash compare ran → wrong stored hash
5688
- // forced the upload.
5689
5679
  expect(result.filesUploaded).toBe(1);
5690
5680
  expect(uploadFile).toHaveBeenCalled();
5691
5681
  });
@@ -5693,13 +5683,11 @@ describe("currency-gated: journal version 2 fixtures", () => {
5693
5683
  // create a conflict/drift file." This is the push-side complement of the
5694
5684
  // pull-side reconcile test (sync.test.ts "byte-identical ... no litter").
5695
5685
  //
5696
- // A pure `touch` (bytes identical, mtime bumped) MUST miss the lstat
5697
- // fast-path (mtime != stored mtimeMs), fall through to hashFile, hit the
5698
- // second-stage hash compare (real hash === CORRECT stored hash), and
5686
+ // A pure `touch` (bytes identical, mtime bumped) MUST hash the bytes,
5687
+ // compare the real hash with the correct stored hash, and
5699
5688
  // classify the file `unchanged`. No upload, no conflict event, and no
5700
5689
  // `.conflict-*`/`.drift-*` mirror on disk — mtime alone never manufactures
5701
- // divergence. (3722 above proves the fast-path MISSES on an mtime bump;
5702
- // this proves a CORRECT stored hash then reconciles instead of uploading.)
5690
+ // divergence.
5703
5691
  it("mtime bumped but bytes identical → re-hash reconciles → no upload, no conflict, no mirror", async () => {
5704
5692
  const companyRoot = path.join(tmpDir, "companies", "acme");
5705
5693
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -5707,8 +5695,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5707
5695
  fs.writeFileSync(testFile, "identical bytes, only mtime moves");
5708
5696
  const originalLstat = fs.lstatSync(testFile);
5709
5697
  // CORRECT stored hash (unlike 3722's sentinel) + matching size. Once the
5710
- // mtime bump forces hashFile to run, the real hash MUST equal this value
5711
- // and the second-stage compare MUST classify the file unchanged.
5698
+ // The real hash MUST equal this value and classify the file unchanged.
5712
5699
  const { hashFile } = await import("../journal.js");
5713
5700
  const realHash = hashFile(testFile);
5714
5701
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
@@ -5726,7 +5713,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5726
5713
  },
5727
5714
  pulls: [],
5728
5715
  }));
5729
- // Bump mtime forward only — bytes untouched. Fast-path mtime compare fails.
5716
+ // Bump mtime forward only — bytes untouched.
5730
5717
  const futureTime = new Date(Date.now() + 60_000);
5731
5718
  fs.utimesSync(testFile, futureTime, futureTime);
5732
5719
  const events = [];
@@ -5738,7 +5725,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5738
5725
  skipUnchanged: true,
5739
5726
  onEvent: (e) => events.push(e),
5740
5727
  });
5741
- // Fast-path missed (mtime moved), hash ran, hash matched → reconciled.
5728
+ // Hash matched → reconciled.
5742
5729
  expect(result.filesUploaded).toBe(0);
5743
5730
  expect(result.filesSkipped).toBe(1);
5744
5731
  expect(uploadFile).not.toHaveBeenCalled();
@@ -5751,12 +5738,9 @@ describe("currency-gated: journal version 2 fixtures", () => {
5751
5738
  .filter((f) => f.includes(".conflict-") || f.includes(".drift-"));
5752
5739
  expect(litter).toEqual([]);
5753
5740
  });
5754
- // Efficiency follow-up to the directive lock above: a touched-but-identical
5755
- // file currently re-hashes on EVERY sync because the no-op skip never
5756
- // refreshes the journal's (mtimeMs, size). Re-stamp them on the skip so the
5757
- // NEXT sync short-circuits on lstat alone. Pure performance — still no
5758
- // upload, no conflict, no mirror.
5759
- it("re-stamps journal mtimeMs after a no-op skip so the next sync hits the fast-path", async () => {
5741
+ // Keep stat metadata current after a touched-but-identical no-op. Content
5742
+ // identity still comes exclusively from the hash.
5743
+ it("re-stamps journal mtimeMs after a no-op hash match", async () => {
5760
5744
  const companyRoot = path.join(tmpDir, "companies", "acme");
5761
5745
  fs.mkdirSync(companyRoot, { recursive: true });
5762
5746
  const testFile = path.join(companyRoot, "touched-then-fast.md");
@@ -5779,7 +5763,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5779
5763
  },
5780
5764
  pulls: [],
5781
5765
  }));
5782
- // Bump mtime — fast-path misses, hash runs, hash matches → no-op skip.
5766
+ // Bump mtime — hash still matches, so this is a no-op skip.
5783
5767
  const futureTime = new Date(Date.now() + 120_000);
5784
5768
  fs.utimesSync(testFile, futureTime, futureTime);
5785
5769
  const bumpedLstat = fs.lstatSync(testFile);
@@ -5795,8 +5779,8 @@ describe("currency-gated: journal version 2 fixtures", () => {
5795
5779
  expect(result.filesUploaded).toBe(0);
5796
5780
  expect(result.filesSkipped).toBe(1);
5797
5781
  expect(uploadFile).not.toHaveBeenCalled();
5798
- // Journal mtimeMs is now the BUMPED value (not the stale original), so the
5799
- // next sync's lstat fast-path matches and skips without re-hashing.
5782
+ // Journal mtimeMs is now the bumped value, while content hashing remains
5783
+ // authoritative on the next sync.
5800
5784
  const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5801
5785
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
5802
5786
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
@@ -5822,11 +5806,9 @@ describe("currency-gated: journal version 2 fixtures", () => {
5822
5806
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
5823
5807
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
5824
5808
  });
5825
- it("falls through to hashFile when journal has no mtimeMs (pre-5.36 entry)", async () => {
5826
- // Back-compat: pre-5.36 journals don't carry mtimeMs. The first sync
5827
- // after upgrade must fall through to the hash path so legacy entries
5828
- // still work; the post-upload stamp adds the field so subsequent
5829
- // syncs use the fast-path.
5809
+ it("hashes content when the journal has no mtimeMs (pre-5.36 entry)", async () => {
5810
+ // Back-compat: pre-5.36 journals don't carry mtimeMs, but the content
5811
+ // hash still decides whether the file is unchanged.
5830
5812
  const companyRoot = path.join(tmpDir, "companies", "acme");
5831
5813
  fs.mkdirSync(companyRoot, { recursive: true });
5832
5814
  const testFile = path.join(companyRoot, "legacy.md");
@@ -5834,8 +5816,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5834
5816
  const { hashFile: realHashFile } = await import("../journal.js");
5835
5817
  const realHash = realHashFile(testFile);
5836
5818
  const realSize = fs.statSync(testFile).size;
5837
- // Real hash but NO mtimeMs — must take hash path. Hash matches →
5838
- // second-stage compare skips upload.
5819
+ // Real hash but NO mtimeMs. Hash matches → skip upload.
5839
5820
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5840
5821
  fs.writeFileSync(journalPath, JSON.stringify({
5841
5822
  version: "1",
@@ -5857,13 +5838,11 @@ describe("currency-gated: journal version 2 fixtures", () => {
5857
5838
  hqRoot: tmpDir,
5858
5839
  skipUnchanged: true,
5859
5840
  });
5860
- // Pre-5.36 entry → fast-path skipped → hash path ran → real hash
5861
- // matched → upload skipped at second stage. Same outcome as 5.35.0.
5841
+ // Real hash matched → upload skipped.
5862
5842
  expect(result.filesSkipped).toBe(1);
5863
5843
  expect(uploadFile).not.toHaveBeenCalled();
5864
- // And the upgrade path: a follow-up upload (forced by changing the
5865
- // file) must stamp mtimeMs so the NEXT sync gets the fast-path.
5866
- // Verified separately by the "stamps mtimeMs" test above.
5844
+ // A follow-up upload still stamps mtimeMs for journal compatibility,
5845
+ // verified separately by the "stamps mtimeMs" test above.
5867
5846
  });
5868
5847
  });
5869
5848
  // ── Fix #2 (5.36.0): bounded-parallel transfer pool ──────────────────────
@@ -6395,7 +6374,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6395
6374
  // entries for the 9 successful uploads. Pre-fix the journal was
6396
6375
  // never written (the throw escaped before writeJournal()), so
6397
6376
  // the next sync re-uploaded all 9 successful files unnecessarily
6398
- // AND lost the etag/mtime stamps needed by the lstat fast-path.
6377
+ // AND lost their etag/content-hash/stat journal metadata.
6399
6378
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6400
6379
  expect(fs.existsSync(journalPath)).toBe(true);
6401
6380
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));