@indigoai-us/hq-cloud 6.15.91 → 6.15.92

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.
@@ -65,8 +65,9 @@ import * as readline from "readline";
65
65
  import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, UnregisteredCompanySkillError, currentUploadBytes, parseRegisteredCompanySkillUid, _testing as shareTesting, } from "./share.js";
66
66
  import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
67
67
  import { VaultAuthError, } from "../vault-client.js";
68
- import { hashFile, readJournal, writeJournal } from "../journal.js";
68
+ import { AreaLedgerMigration, closeCachedAreaJournalMigration, hashFile, openJournalStateStoreForMigration, readJournal, readJournalScoped, setAreaLedgerMigrationTestHooksForTest, writeJournal, } from "../journal.js";
69
69
  import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
70
+ import { AreaResolver } from "../sync/area-resolver.js";
70
71
  /**
71
72
  * Read a journal the way production does.
72
73
  *
@@ -3799,6 +3800,78 @@ describe("share", () => {
3799
3800
  expect(result.filesDeleted).toBe(1);
3800
3801
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "vanished.md");
3801
3802
  });
3803
+ it("defers unscoped delete and exclusion maintenance until the next full personal pass", async () => {
3804
+ const current = path.join(tmpDir, "personal", "current.md");
3805
+ fs.mkdirSync(path.dirname(current), { recursive: true });
3806
+ fs.writeFileSync(current, "current");
3807
+ const journalPath = path.join(stateDir, "sync-journal.personal.json");
3808
+ seedJournalAt(journalPath, {
3809
+ version: "2",
3810
+ lastSync: "",
3811
+ pulls: [],
3812
+ files: {
3813
+ "personal/current.md": {
3814
+ hash: hashFile(current),
3815
+ size: fs.statSync(current).size,
3816
+ syncedAt: new Date().toISOString(),
3817
+ direction: "up",
3818
+ remoteEtag: "current-etag",
3819
+ kind: "file",
3820
+ },
3821
+ ".claude/worktrees/stale.md": {
3822
+ hash: "stale",
3823
+ size: 1,
3824
+ syncedAt: new Date().toISOString(),
3825
+ direction: "up",
3826
+ remoteEtag: "stale-etag",
3827
+ kind: "file",
3828
+ },
3829
+ "personal/vanished.md": {
3830
+ hash: "vanished",
3831
+ size: 1,
3832
+ syncedAt: new Date().toISOString(),
3833
+ direction: "up",
3834
+ remoteEtag: "vanished-etag",
3835
+ kind: "file",
3836
+ localDeleteIntent: deleteIntent("vanished-etag", "vanished"),
3837
+ },
3838
+ },
3839
+ });
3840
+ const context = makeEntityContext({ uid: "prs_personal", slug: "personal" });
3841
+ const scopedEvents = [];
3842
+ await share({
3843
+ paths: [current],
3844
+ hqRoot: tmpDir,
3845
+ entityContext: context,
3846
+ personalMode: true,
3847
+ scopedJournal: true,
3848
+ deleteScopeRoots: ["personal/current.md"],
3849
+ propagateDeletes: true,
3850
+ propagateDeletePolicy: "owned-only",
3851
+ skipUnchanged: true,
3852
+ onEvent: (event) => scopedEvents.push(event),
3853
+ });
3854
+ expect(deleteRemoteFile).not.toHaveBeenCalledWith(expect.anything(), "personal/vanished.md");
3855
+ expect(scopedEvents.some((event) => event.type === "personal-vault-journal-pruned"))
3856
+ .toBe(false);
3857
+ expect(readJournal("personal").files[".claude/worktrees/stale.md"]).toBeDefined();
3858
+ const fullEvents = [];
3859
+ await share({
3860
+ paths: [tmpDir],
3861
+ hqRoot: tmpDir,
3862
+ entityContext: context,
3863
+ personalMode: true,
3864
+ propagateDeletes: true,
3865
+ propagateDeletePolicy: "owned-only",
3866
+ skipUnchanged: true,
3867
+ onEvent: (event) => fullEvents.push(event),
3868
+ });
3869
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/vanished.md");
3870
+ expect(fullEvents.some((event) => event.type === "personal-vault-journal-pruned"))
3871
+ .toBe(true);
3872
+ expect(readJournal("personal").files[".claude/worktrees/stale.md"]).toBeUndefined();
3873
+ expect(readJournal("personal").files["personal/vanished.md"]).toBeUndefined();
3874
+ });
3802
3875
  // ── Delete propagation (propagateDeletes) ──────────────────────────────────
3803
3876
  //
3804
3877
  // The bug: when a user deletes a local file, the next pull re-downloads it
@@ -5697,6 +5770,7 @@ describe("share", () => {
5697
5770
  hqRoot: tmpDir,
5698
5771
  personalMode: true,
5699
5772
  journalSlug,
5773
+ scopedJournal: true,
5700
5774
  skipUnchanged: true,
5701
5775
  propagateDeletes: true,
5702
5776
  propagateDeletePolicy: "owned-only",
@@ -5705,6 +5779,72 @@ describe("share", () => {
5705
5779
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
5706
5780
  expect(journalAt(journalPath).files[key]).toBeUndefined();
5707
5781
  });
5782
+ it("propagates every area-journal descendant of a scoped deleted directory", async () => {
5783
+ const journalSlug = "personal-area-scoped-directory-delete";
5784
+ const deleteRoot = "personal/deleted-directory";
5785
+ const descendants = [
5786
+ `${deleteRoot}/first.md`,
5787
+ `${deleteRoot}/nested/second.md`,
5788
+ ];
5789
+ const deletedEntry = (hash) => ({
5790
+ hash,
5791
+ size: 1,
5792
+ syncedAt: new Date().toISOString(),
5793
+ direction: "up",
5794
+ remoteEtag: `${hash}-etag`,
5795
+ kind: "file",
5796
+ localDeleteIntent: deleteIntent(`${hash}-etag`, hash),
5797
+ });
5798
+ writeJournal(journalSlug, {
5799
+ version: "2",
5800
+ lastSync: "",
5801
+ pulls: [],
5802
+ files: {
5803
+ [descendants[0]]: deletedEntry("first"),
5804
+ [descendants[1]]: deletedEntry("second"),
5805
+ "other/untouched.md": deletedEntry("other"),
5806
+ },
5807
+ });
5808
+ const migration = new AreaLedgerMigration({
5809
+ rootDir: stateDir,
5810
+ journalSlug,
5811
+ resolver: new AreaResolver({
5812
+ format: 1,
5813
+ routerVersion: 1,
5814
+ areas: [
5815
+ { id: "root:personal", prefixes: ["personal"], paths: [] },
5816
+ { id: "root:other", prefixes: ["other"], paths: [] },
5817
+ ],
5818
+ }),
5819
+ legacyStore: openJournalStateStoreForMigration(journalSlug),
5820
+ });
5821
+ expect(migration.cutOver().phase).toBe("area-authoritative");
5822
+ const opened = [];
5823
+ setAreaLedgerMigrationTestHooksForTest({ afterOpen: (bridge) => opened.push(bridge) });
5824
+ expect(Object.keys(readJournalScoped(journalSlug, [], [deleteRoot]).files).sort())
5825
+ .toEqual(descendants.slice().sort());
5826
+ expect(opened).toHaveLength(1);
5827
+ expect(opened[0].openedAreaStoresForTest()).toEqual(["@meta", "root:personal"]);
5828
+ closeCachedAreaJournalMigration(journalSlug);
5829
+ setAreaLedgerMigrationTestHooksForTest(undefined);
5830
+ const result = await share({
5831
+ paths: [],
5832
+ deleteScopeRoots: [deleteRoot],
5833
+ entityContext: makeEntityContext({ uid: "prs_personal", slug: "personal" }),
5834
+ hqRoot: tmpDir,
5835
+ personalMode: true,
5836
+ journalSlug,
5837
+ scopedJournal: true,
5838
+ skipUnchanged: true,
5839
+ propagateDeletes: true,
5840
+ propagateDeletePolicy: "owned-only",
5841
+ });
5842
+ expect(result.filesDeleted).toBe(descendants.length);
5843
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), descendants[0]);
5844
+ expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), descendants[1]);
5845
+ expect(deleteRemoteFile).not.toHaveBeenCalledWith(expect.anything(), "other/untouched.md");
5846
+ closeCachedAreaJournalMigration(journalSlug);
5847
+ });
5708
5848
  // ── Decommission prefixes ──────────────────────────────────────────────────
5709
5849
  //
5710
5850
  // `decommissionPrefixes` is the "company was promoted to its own team