@indigoai-us/hq-cloud 6.18.15 → 6.18.16-alpha.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.
@@ -4698,6 +4698,150 @@ describe("sync", () => {
4698
4698
  expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
4699
4699
  expect(journal.files["docs/kept.md"]).toBeDefined();
4700
4700
  });
4701
+ it("does NOT delete a live file when a case-only folder rename leaves the old spelling in the journal (feedback_c8ed69d1)", async () => {
4702
+ // Solace incident 2026-09-27. A teammate renamed `TikTok-Shop/` to
4703
+ // `tiktok-shop/`. Another Mac still had the folder spelled `TikTok-Shop/`
4704
+ // on disk and journal rows for the old keys. On its next pull the old keys
4705
+ // were absent from the remote LIST, so the sweep lstat'ed
4706
+ // `TikTok-Shop/plan.md`, found the file (the only copy, which is also the
4707
+ // local file of the live key `tiktok-shop/plan.md` on a case-insensitive
4708
+ // disk), saw the journal hash match, and unlinked it. The push leg then
4709
+ // deleted 255 live keys from the vault.
4710
+ //
4711
+ // On a case-folding volume, a journal key whose case-folded spelling is
4712
+ // still listed remotely is an alias of a live file: retire its journal row
4713
+ // and leave the disk alone. `caseFoldingVolume` forces that volume
4714
+ // behaviour so the test also runs on case-sensitive CI filesystems.
4715
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4716
+ fs.mkdirSync(path.join(companyRoot, "TikTok-Shop"), { recursive: true });
4717
+ fs.writeFileSync(path.join(companyRoot, "TikTok-Shop", "plan.md"), "live content");
4718
+ const crypto = await import("node:crypto");
4719
+ const liveHash = crypto.createHash("sha256").update("live content").digest("hex");
4720
+ fs.writeFileSync(journalPath, JSON.stringify({
4721
+ version: "1",
4722
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4723
+ files: {
4724
+ "TikTok-Shop/plan.md": {
4725
+ hash: liveHash,
4726
+ size: 12,
4727
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4728
+ direction: "down",
4729
+ remoteEtag: "remote-old-spelling",
4730
+ },
4731
+ // An earlier pass already recorded the renamed key against the same
4732
+ // bytes (on a case-insensitive disk it resolves to this file), so
4733
+ // nothing is downloaded that could mask the sweep's unlink.
4734
+ "tiktok-shop/plan.md": {
4735
+ hash: liveHash,
4736
+ size: 12,
4737
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4738
+ direction: "down",
4739
+ remoteEtag: "remote-new-spelling",
4740
+ },
4741
+ },
4742
+ }));
4743
+ // After the rename only the lower-case key is live in the vault.
4744
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
4745
+ {
4746
+ key: "tiktok-shop/plan.md",
4747
+ size: 12,
4748
+ lastModified: new Date(),
4749
+ etag: '"remote-new-spelling"',
4750
+ },
4751
+ ]);
4752
+ const result = await sync({
4753
+ company: "acme",
4754
+ vaultConfig: mockConfig,
4755
+ hqRoot: tmpDir,
4756
+ caseFoldingVolume: true,
4757
+ });
4758
+ // The live file survives under the spelling that is on disk.
4759
+ expect(fs.readFileSync(path.join(companyRoot, "TikTok-Shop", "plan.md"), "utf8")).toBe("live content");
4760
+ expect(result.filesTombstoned).toBe(0);
4761
+ // The stale old-spelling row is retired so the sweep does not revisit it.
4762
+ const journal = journalAt(journalPath);
4763
+ expect(journal.files["TikTok-Shop/plan.md"]).toBeUndefined();
4764
+ });
4765
+ it("does not let an invalid remote key protect a case-only twin from deletion", async () => {
4766
+ // A malformed legacy object (`foo\\bar.md`) is skipped by remote-key
4767
+ // validation. It never materializes locally, so it must not stop the sweep
4768
+ // from deleting the remotely-deleted `Foo/bar.md`; otherwise the stale
4769
+ // journal row is retired, the local file stays, and the next push
4770
+ // resurrects it.
4771
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4772
+ fs.mkdirSync(path.join(companyRoot, "Foo"), { recursive: true });
4773
+ fs.writeFileSync(path.join(companyRoot, "Foo", "bar.md"), "old content");
4774
+ const crypto = await import("node:crypto");
4775
+ const oldHash = crypto.createHash("sha256").update("old content").digest("hex");
4776
+ fs.writeFileSync(journalPath, JSON.stringify({
4777
+ version: "1",
4778
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4779
+ files: {
4780
+ "Foo/bar.md": {
4781
+ hash: oldHash,
4782
+ size: 11,
4783
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4784
+ direction: "down",
4785
+ remoteEtag: "remote-old",
4786
+ },
4787
+ },
4788
+ }));
4789
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
4790
+ {
4791
+ key: "foo\\bar.md",
4792
+ size: 11,
4793
+ lastModified: new Date(),
4794
+ etag: '"remote-invalid"',
4795
+ },
4796
+ ]);
4797
+ const result = await sync({
4798
+ company: "acme",
4799
+ vaultConfig: mockConfig,
4800
+ hqRoot: tmpDir,
4801
+ caseFoldingVolume: true,
4802
+ });
4803
+ expect(fs.existsSync(path.join(companyRoot, "Foo", "bar.md"))).toBe(false);
4804
+ expect(result.filesTombstoned).toBe(1);
4805
+ expect(journalAt(journalPath).files["Foo/bar.md"]).toBeUndefined();
4806
+ });
4807
+ it("still deletes a journal key on a case-folding volume when no live key shares its folded spelling", async () => {
4808
+ // The case-alias guard must not turn off ordinary cross-machine deletes.
4809
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4810
+ fs.mkdirSync(path.join(companyRoot, "TikTok-Shop"), { recursive: true });
4811
+ fs.writeFileSync(path.join(companyRoot, "TikTok-Shop", "gone.md"), "old content");
4812
+ const crypto = await import("node:crypto");
4813
+ const oldHash = crypto.createHash("sha256").update("old content").digest("hex");
4814
+ fs.writeFileSync(journalPath, JSON.stringify({
4815
+ version: "1",
4816
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4817
+ files: {
4818
+ "TikTok-Shop/gone.md": {
4819
+ hash: oldHash,
4820
+ size: 11,
4821
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4822
+ direction: "down",
4823
+ remoteEtag: "remote-old",
4824
+ },
4825
+ },
4826
+ }));
4827
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
4828
+ {
4829
+ key: "tiktok-shop/other.md",
4830
+ size: 5,
4831
+ lastModified: new Date(),
4832
+ etag: '"remote-other"',
4833
+ },
4834
+ ]);
4835
+ const result = await sync({
4836
+ company: "acme",
4837
+ vaultConfig: mockConfig,
4838
+ hqRoot: tmpDir,
4839
+ caseFoldingVolume: true,
4840
+ });
4841
+ expect(fs.existsSync(path.join(companyRoot, "TikTok-Shop", "gone.md"))).toBe(false);
4842
+ expect(result.filesTombstoned).toBe(1);
4843
+ expect(journalAt(journalPath).files["TikTok-Shop/gone.md"]).toBeUndefined();
4844
+ });
4701
4845
  it("does NOT tombstone a live file recorded under a Windows backslash journal key whose POSIX twin still exists remotely (ridge data-loss regression — feedback_b8d09d0f)", async () => {
4702
4846
  // Root-cause regression. A pre-5.47.2 Windows client stamped the journal
4703
4847
  // key with path.sep ("\\"): `projects\forecast-development\...\sean-frank.csv`.