@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.
- package/dist/bin/sync-runner-watch-loop.d.ts +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +25 -10
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.test.js +199 -2
- package/dist/bin/sync-runner-watch-loop.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +12 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +32 -14
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +4 -4
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/case-alias.d.ts +37 -0
- package/dist/cli/case-alias.d.ts.map +1 -0
- package/dist/cli/case-alias.js +140 -0
- package/dist/cli/case-alias.js.map +1 -0
- package/dist/cli/case-alias.test.d.ts +2 -0
- package/dist/cli/case-alias.test.d.ts.map +1 -0
- package/dist/cli/case-alias.test.js +96 -0
- package/dist/cli/case-alias.test.js.map +1 -0
- package/dist/cli/sync.d.ts +17 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +56 -4
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +144 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/qmd-reindex.d.ts +30 -10
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +148 -41
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.js +191 -78
- package/dist/qmd-reindex.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -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`.
|