@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.
- package/dist/cli/rescue-drift-reconcile.test.js +1 -1
- package/dist/cli/rescue-drift-reconcile.test.js.map +1 -1
- package/dist/cli/rescue-mtime-preserve.test.d.ts +2 -0
- package/dist/cli/rescue-mtime-preserve.test.d.ts.map +1 -0
- package/dist/cli/rescue-mtime-preserve.test.js +162 -0
- package/dist/cli/rescue-mtime-preserve.test.js.map +1 -0
- package/dist/cli/share.d.ts +54 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +96 -2
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +76 -21
- package/dist/cli/share.test.js.map +1 -1
- package/package.json +1 -1
- package/scripts/replace-rescue.sh +90 -11
- package/src/cli/rescue-drift-reconcile.test.ts +1 -1
- package/src/cli/rescue-mtime-preserve.test.ts +182 -0
- package/src/cli/share.test.ts +94 -22
- package/src/cli/share.ts +101 -2
package/dist/cli/share.test.js
CHANGED
|
@@ -928,14 +928,12 @@ describe("share", () => {
|
|
|
928
928
|
},
|
|
929
929
|
},
|
|
930
930
|
}));
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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("
|
|
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
|
-
|
|
1658
|
-
expect(
|
|
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).
|
|
1661
|
-
//
|
|
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"]).
|
|
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
|
//
|