@indigoai-us/hq-cloud 6.0.1 → 6.0.3

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.
@@ -928,14 +928,12 @@ describe("share", () => {
928
928
  },
929
929
  },
930
930
  }));
931
- // Currency-gated HEADs the candidate; return an etag matching the
932
- // journal so the entry resolves as "remote still current → safe to
933
- // tombstone."
934
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
935
- lastModified: new Date(),
936
- etag: '"personal-vault-drift-etag"',
937
- size: 1054,
938
- });
931
+ // (Pre-6.0.2: currency-gated would have HEADed the candidate; the
932
+ // mockResolvedValueOnce that lived here is now redundant because the
933
+ // vault-litter drain bypasses HEAD entirely for `.drift-` markers.
934
+ // Leaving the mock here would queue an unconsumed return value and
935
+ // poison the next test's HEAD call — removed so the assertion above
936
+ // verifies the litter path, not the etag path.)
939
937
  const personalCtx = makeEntityContext({
940
938
  uid: "prs_01HASSAANTESTUSER",
941
939
  slug: "__hq_personal_vault__",
@@ -1615,14 +1613,22 @@ describe("share", () => {
1615
1613
  // tooling that hands a conflict mirror to share() directly.
1616
1614
  expect(uploadFile).not.toHaveBeenCalled();
1617
1615
  });
1618
- it("conflict-mirror exclusion: journaled mirror with local-missing is NOT swept by delete plan", async () => {
1616
+ it("vault-litter drain (6.0.2): journaled `.conflict-*` mirror with local-missing IS swept by the delete plan", async () => {
1617
+ // Updated semantics (6.0.2): the existing-litter state — a conflict
1618
+ // mirror that leaked into the journal from a prior buggy upload and was
1619
+ // subsequently deleted locally — used to survive every sync because the
1620
+ // `isEphemeralPath` skip in computeDeletePlan was wired to drop it
1621
+ // before tombstoning. The deferred "dedicated reconcile command" the
1622
+ // doc-comment promised never materialized, and the litter accumulated
1623
+ // until users noticed it (e.g. the May-27 `personal/.obsidian/*.drift-*`
1624
+ // pair on the EC2 outpost). The fix: drain unconditionally via the
1625
+ // litter bucket — bypass shouldSync, ephemeral-skip, policy, and the
1626
+ // bulk-asymmetry breaker. By construction litter is not user content
1627
+ // (the EPHEMERAL_PATH_PATTERN / DRIFT_PATH_PATTERN regexes are precise
1628
+ // enough that a false-positive on a real user filename is vanishingly
1629
+ // unlikely), so the absence of those gates is the correct behavior.
1619
1630
  const companyRoot = path.join(tmpDir, "companies", "acme");
1620
1631
  fs.mkdirSync(companyRoot, { recursive: true });
1621
- // Simulate the existing-litter state: a conflict mirror that leaked
1622
- // into the journal in a prior buggy version. Locally missing (user
1623
- // already deleted it). The regular delete plan must NOT issue a
1624
- // DeleteObject — that's the dedicated reconcile command's job, and
1625
- // a sync should not accidentally race a user reviewing the mirror.
1626
1632
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1627
1633
  fs.writeFileSync(journalPath, JSON.stringify({
1628
1634
  version: "1",
@@ -1640,7 +1646,10 @@ describe("share", () => {
1640
1646
  },
1641
1647
  },
1642
1648
  }));
1643
- // HEAD needed for the non-mirror entry under currency-gated.
1649
+ // HEAD needed for the non-mirror entry under currency-gated. The litter
1650
+ // drain bypasses HEAD by design — the etag check encodes a "remote
1651
+ // hasn't drifted since I last synced" invariant that doesn't apply to
1652
+ // never-should-have-been-there litter.
1644
1653
  vi.mocked(headRemoteFile).mockResolvedValue({
1645
1654
  lastModified: new Date(),
1646
1655
  etag: '"regular-etag"',
@@ -1654,14 +1663,60 @@ describe("share", () => {
1654
1663
  skipUnchanged: true,
1655
1664
  propagateDeletes: true,
1656
1665
  });
1657
- expect(result.filesDeleted).toBe(1);
1658
- expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
1666
+ // BOTH the regular file AND the conflict mirror tombstone in one pass.
1667
+ expect(result.filesDeleted).toBe(2);
1668
+ expect(deleteRemoteFile).toHaveBeenCalledTimes(2);
1659
1669
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
1660
- expect(deleteRemoteFile).not.toHaveBeenCalledWith(expect.anything(), expect.stringContaining("conflict-"));
1661
- // Mirror's journal entry survives reconcile command (separate skill)
1662
- // sweeps it once the user explicitly opts in.
1670
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
1671
+ // Both journal entries are removed by the delete loop's removeEntry call.
1663
1672
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1664
- expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeDefined();
1673
+ expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
1674
+ expect(journal.files["regular.md"]).toBeUndefined();
1675
+ });
1676
+ it("vault-litter drain (6.0.2): `.drift-<unixts>-<pid>` rescue markers drain regardless of personal-vault exclusion path (the live obsidian case)", async () => {
1677
+ // The exact failure mode that motivated the fix: a rescue-overlay
1678
+ // `.drift-` marker sitting under a personal-vault default exclusion
1679
+ // (`.obsidian/**`) survives every sync because `shouldSync` rejects
1680
+ // the parent path before the delete plan ever considers the entry.
1681
+ // The litter bypass routes such keys around shouldSync directly into
1682
+ // `litterToDelete`.
1683
+ const stateDirPersonal = fs.mkdtempSync(path.join(os.tmpdir(), "hq-state-prs-drift-"));
1684
+ process.env.HQ_STATE_DIR = stateDirPersonal;
1685
+ try {
1686
+ const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
1687
+ fs.writeFileSync(journalPath, JSON.stringify({
1688
+ version: "1",
1689
+ lastSync: new Date().toISOString(),
1690
+ files: {
1691
+ "personal/.obsidian/graph.json.drift-1779863862-42519": {
1692
+ hash: "h", size: 1054, syncedAt: new Date().toISOString(),
1693
+ direction: "down",
1694
+ remoteEtag: "obsidian-drift-etag",
1695
+ },
1696
+ },
1697
+ }));
1698
+ const personalCtx = makeEntityContext({
1699
+ uid: "prs_01HASSAANTESTUSER",
1700
+ slug: "__hq_personal_vault__",
1701
+ bucketName: "hq-vault-prs-01HASSAANTESTUSER",
1702
+ });
1703
+ const result = await share({
1704
+ paths: [tmpDir],
1705
+ entityContext: personalCtx,
1706
+ hqRoot: tmpDir,
1707
+ personalMode: true,
1708
+ skipUnchanged: true,
1709
+ propagateDeletes: true,
1710
+ });
1711
+ expect(result.filesDeleted).toBe(1);
1712
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
1713
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1714
+ expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
1715
+ }
1716
+ finally {
1717
+ fs.rmSync(stateDirPersonal, { recursive: true, force: true });
1718
+ delete process.env.HQ_STATE_DIR;
1719
+ }
1665
1720
  });
1666
1721
  // ── personalMode ───────────────────────────────────────────────────────────
1667
1722
  //