@indigoai-us/hq-cloud 6.11.10 → 6.11.11

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.
@@ -1794,6 +1794,71 @@ describe("personal slot fanout", () => {
1794
1794
  fs.rmSync(tmpHqRoot, { recursive: true, force: true });
1795
1795
  }
1796
1796
  });
1797
+ it("E2: personal slot pierces the workspace/ exclusion for the session-continuity pointer + active thread file, while the rest of workspace/ stays local (DEV-1778)", async () => {
1798
+ // Regression guard for the cross-machine session-handoff carve-out. Test
1799
+ // "E" proves `workspace/` is excluded wholesale; this proves the ONE
1800
+ // exception travels through the REAL runner push composition (not just the
1801
+ // pure computeContinuityPointerPaths helper): `/handoff` writes
1802
+ // workspace/threads/handoff.json + the thread file it points to, and both
1803
+ // must reach share() so a session handed off on machine A resumes on
1804
+ // machine B. Everything else under workspace/ (locks, other threads, the
1805
+ // INDEX) must remain machine-local.
1806
+ const shareSpy = vi.fn().mockResolvedValue(defaultShareResult());
1807
+ const tmpHqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-runner-test-"));
1808
+ try {
1809
+ // A normal included top-level dir, so the personal push is non-empty
1810
+ // regardless of the carve-out.
1811
+ fs.mkdirSync(path.join(tmpHqRoot, "knowledge"));
1812
+ // The session-continuity carve-out: handoff.json + the active thread it
1813
+ // references via `thread_path` (hq-root-relative, as handoff-finalize.sh
1814
+ // writes it).
1815
+ const threadsDir = path.join(tmpHqRoot, "workspace", "threads");
1816
+ fs.mkdirSync(threadsDir, { recursive: true });
1817
+ const activeThreadRel = "workspace/threads/T-20260619-1200-resume-me.json";
1818
+ fs.writeFileSync(path.join(tmpHqRoot, activeThreadRel), JSON.stringify({ conversation_summary: "pick up here" }));
1819
+ fs.writeFileSync(path.join(threadsDir, "handoff.json"), JSON.stringify({ thread_path: activeThreadRel }));
1820
+ // The rest of workspace/ that must NOT travel: a lock, an inactive
1821
+ // thread, and the regenerated index.
1822
+ fs.mkdirSync(path.join(tmpHqRoot, "workspace", "locks"), {
1823
+ recursive: true,
1824
+ });
1825
+ fs.writeFileSync(path.join(tmpHqRoot, "workspace", "locks", "sync.lock"), "1");
1826
+ fs.writeFileSync(path.join(threadsDir, "T-20260101-0000-stale.json"), "{}");
1827
+ fs.writeFileSync(path.join(threadsDir, "INDEX.md"), "# threads");
1828
+ const deps = makeDeps({
1829
+ createVaultClient: () => makeVaultStub({
1830
+ memberships: [{ companyUid: "cmp_a" }],
1831
+ entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
1832
+ listPersons: () => Promise.resolve([olderPerson]),
1833
+ }),
1834
+ share: shareSpy,
1835
+ });
1836
+ const code = await runRunner(["--companies", "--direction", "push", "--hq-root", tmpHqRoot], deps);
1837
+ expect(code).toBe(0);
1838
+ const personalCall = shareSpy.mock.calls.find((c) => c[0].company?.startsWith("prs_"));
1839
+ expect(personalCall).toBeDefined();
1840
+ const personalArgs = personalCall[0];
1841
+ // Compare as hq-root-relative, forward-slash paths (the namespace the
1842
+ // continuity carve-out is specified in).
1843
+ const relPaths = personalArgs.paths.map((p) => path.relative(tmpHqRoot, p).split(path.sep).join("/"));
1844
+ // The two continuity files MUST be in the push set.
1845
+ expect(relPaths).toContain("workspace/threads/handoff.json");
1846
+ expect(relPaths).toContain(activeThreadRel);
1847
+ // The rest of workspace/ must NOT leak in — neither as the parent dir
1848
+ // nor as the individual machine-local files.
1849
+ expect(relPaths).not.toContain("workspace");
1850
+ expect(relPaths).not.toContain("workspace/locks");
1851
+ expect(relPaths.some((p) => p === "workspace/locks" || p.startsWith("workspace/locks/"))).toBe(false);
1852
+ expect(relPaths).not.toContain("workspace/threads/T-20260101-0000-stale.json");
1853
+ expect(relPaths).not.toContain("workspace/threads/INDEX.md");
1854
+ // The bare threads dir is never handed in wholesale — only the two
1855
+ // explicit files (otherwise share()'s walk would sweep stale threads).
1856
+ expect(relPaths).not.toContain("workspace/threads");
1857
+ }
1858
+ finally {
1859
+ fs.rmSync(tmpHqRoot, { recursive: true, force: true });
1860
+ }
1861
+ });
1797
1862
  it("G: personal slot includes companies/{slug}/ subdirs when cloud:false marker is set AND HQ_SYNC_LOCAL_COMPANIES_TO_PERSONAL=1, excluding _template + team-synced slugs + missing/cloud:true markers", async () => {
1798
1863
  // Gate the new behavior: without this env var the runner falls back to
1799
1864
  // the legacy "companies/ never enumerated" personal vault. Test H below