@indigoai-us/hq-cloud 6.14.36 → 6.14.39

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.
Files changed (43) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +20 -3
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner.d.ts +5 -0
  5. package/dist/bin/sync-runner.d.ts.map +1 -1
  6. package/dist/bin/sync-runner.js.map +1 -1
  7. package/dist/bin/sync-runner.test.js +46 -0
  8. package/dist/bin/sync-runner.test.js.map +1 -1
  9. package/dist/cli/reindex.d.ts.map +1 -1
  10. package/dist/cli/reindex.js +1 -9
  11. package/dist/cli/reindex.js.map +1 -1
  12. package/dist/cli/share.d.ts +178 -1
  13. package/dist/cli/share.d.ts.map +1 -1
  14. package/dist/cli/share.js +555 -32
  15. package/dist/cli/share.js.map +1 -1
  16. package/dist/cli/share.test.js +780 -2
  17. package/dist/cli/share.test.js.map +1 -1
  18. package/dist/cli/sync.d.ts +27 -0
  19. package/dist/cli/sync.d.ts.map +1 -1
  20. package/dist/cli/sync.js +113 -13
  21. package/dist/cli/sync.js.map +1 -1
  22. package/dist/cli/sync.test.js +188 -0
  23. package/dist/cli/sync.test.js.map +1 -1
  24. package/dist/lib/readlink-safe.d.ts +11 -0
  25. package/dist/lib/readlink-safe.d.ts.map +1 -0
  26. package/dist/lib/readlink-safe.js +27 -0
  27. package/dist/lib/readlink-safe.js.map +1 -0
  28. package/dist/lib/readlink-safe.test.d.ts +2 -0
  29. package/dist/lib/readlink-safe.test.d.ts.map +1 -0
  30. package/dist/lib/readlink-safe.test.js +34 -0
  31. package/dist/lib/readlink-safe.test.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/bin/sync-runner-company.ts +19 -3
  34. package/src/bin/sync-runner.test.ts +54 -0
  35. package/src/bin/sync-runner.ts +4 -0
  36. package/src/cli/reindex.ts +1 -9
  37. package/src/cli/share.test.ts +974 -2
  38. package/src/cli/share.ts +675 -32
  39. package/src/cli/sync.test.ts +209 -0
  40. package/src/cli/sync.ts +151 -13
  41. package/src/lib/readlink-safe.test.ts +43 -0
  42. package/src/lib/readlink-safe.ts +29 -0
  43. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +191 -0
@@ -7,6 +7,12 @@ import * as path from "path";
7
7
  import * as os from "os";
8
8
  import { clearContextCache } from "../context.js";
9
9
  import { lockPathFor } from "../operation-lock.js";
10
+ // Re-export node:fs as a mutable module so unreadable-link regressions can
11
+ // inject a win32 EINVAL without needing a real Windows reparse point.
12
+ vi.mock("fs", async (importOriginal) => {
13
+ const actual = await importOriginal();
14
+ return { ...actual };
15
+ });
10
16
  // Mock s3 module at the top level
11
17
  vi.mock("../s3.js", async (importOriginal) => {
12
18
  const actual = await importOriginal();
@@ -53,6 +59,11 @@ const mockConfig = {
53
59
  authToken: "test-jwt-token",
54
60
  region: "us-east-1",
55
61
  };
62
+ function errnoError(code) {
63
+ const err = new Error(`${code}: invalid argument, readlink`);
64
+ err.code = code;
65
+ return err;
66
+ }
56
67
  const mockEntity = {
57
68
  uid: "cmp_01ABCDEF",
58
69
  slug: "acme",
@@ -312,6 +323,49 @@ describe("sync", () => {
312
323
  const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
313
324
  expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
314
325
  });
326
+ it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
327
+ const linkKey = "policies/downloaded-unreadable-link";
328
+ const linkPath = path.join(tmpDir, "companies", "acme", linkKey);
329
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
330
+ { key: linkKey, size: 0, lastModified: new Date(), etag: '"link-etag"' },
331
+ ]);
332
+ vi.mocked(s3Module.downloadFile).mockImplementationOnce(async (_ctx, _key, localPath) => {
333
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
334
+ fs.symlinkSync("missing-target.md", localPath);
335
+ return { metadata: {} };
336
+ });
337
+ const realReadlinkSync = fs.readlinkSync;
338
+ const readlinkSpy = vi
339
+ .spyOn(fs, "readlinkSync")
340
+ .mockImplementation(((candidate) => {
341
+ if (candidate === linkPath)
342
+ throw errnoError("EINVAL");
343
+ return realReadlinkSync(candidate);
344
+ }));
345
+ const events = [];
346
+ try {
347
+ const result = await sync({
348
+ company: "acme",
349
+ vaultConfig: mockConfig,
350
+ hqRoot: tmpDir,
351
+ onEvent: (event) => events.push(event),
352
+ });
353
+ expect(result.filesDownloaded).toBe(0);
354
+ expect(result.filesSkipped).toBe(1);
355
+ expect(events).toContainEqual({
356
+ type: "not-shipped",
357
+ reason: "unreadable-link",
358
+ count: 1,
359
+ samplePaths: [linkKey],
360
+ });
361
+ expect(events.some((event) => event.type === "error")).toBe(false);
362
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
363
+ expect(journal.files[linkKey]).toBeUndefined();
364
+ }
365
+ finally {
366
+ readlinkSpy.mockRestore();
367
+ }
368
+ });
315
369
  it("emits a conflict event with path + resolution on hash mismatch", async () => {
316
370
  const companyDocs = path.join(tmpDir, "companies", "acme", "docs");
317
371
  fs.mkdirSync(companyDocs, { recursive: true });
@@ -407,6 +461,67 @@ describe("sync", () => {
407
461
  expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
408
462
  expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
409
463
  });
464
+ it("defers a conflict whose downloaded link target cannot be read", async () => {
465
+ const linkKey = "docs/handoff.md";
466
+ const localPath = path.join(tmpDir, "companies", "acme", linkKey);
467
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
468
+ fs.writeFileSync(localPath, "local version");
469
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
470
+ { key: linkKey, size: 0, lastModified: new Date(), etag: '"remote-link"' },
471
+ ]);
472
+ let mirrorPath = "";
473
+ vi.mocked(s3Module.downloadFile).mockImplementationOnce(async (_ctx, _key, destination) => {
474
+ mirrorPath = destination;
475
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
476
+ fs.symlinkSync("missing-target.md", destination);
477
+ return { metadata: {} };
478
+ });
479
+ fs.writeFileSync(journalPath, JSON.stringify({
480
+ version: "1",
481
+ lastSync: new Date().toISOString(),
482
+ files: {
483
+ [linkKey]: {
484
+ hash: "stale-hash",
485
+ size: 20,
486
+ syncedAt: new Date(Date.now() - 3600000).toISOString(),
487
+ direction: "down",
488
+ },
489
+ },
490
+ }));
491
+ const realReadlinkSync = fs.readlinkSync;
492
+ const readlinkSpy = vi
493
+ .spyOn(fs, "readlinkSync")
494
+ .mockImplementation(((candidate) => {
495
+ if (candidate === mirrorPath)
496
+ throw errnoError("EINVAL");
497
+ return realReadlinkSync(candidate);
498
+ }));
499
+ const events = [];
500
+ try {
501
+ const result = await sync({
502
+ company: "acme",
503
+ onConflict: "keep",
504
+ vaultConfig: mockConfig,
505
+ hqRoot: tmpDir,
506
+ onEvent: (event) => events.push(event),
507
+ });
508
+ expect(result.conflicts).toBe(0);
509
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
510
+ expect(events).toContainEqual({
511
+ type: "not-shipped",
512
+ reason: "unreadable-link",
513
+ count: 1,
514
+ samplePaths: [linkKey],
515
+ });
516
+ expect(events.some((event) => event.type === "error")).toBe(false);
517
+ expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
518
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
519
+ expect(journal.files[linkKey].hash).toBe("stale-hash");
520
+ }
521
+ finally {
522
+ readlinkSpy.mockRestore();
523
+ }
524
+ });
410
525
  it("still writes a `.conflict-*` mirror when remote genuinely diverges from local", async () => {
411
526
  // Guard the other side of the convergence branch: when the probe bytes
412
527
  // differ from local, it remains a real conflict — counted, kept, and the
@@ -2319,6 +2434,40 @@ describe("sync", () => {
2319
2434
  expect(result.filesDownloaded).toBe(1);
2320
2435
  expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
2321
2436
  });
2437
+ it("skips remote keys under companies/<slug>/ in a company vault (doubly-scoped corrupt object — frogbear exit-2 regression)", async () => {
2438
+ // A company vault is already anchored at its company root, so its keys are
2439
+ // bucket-relative. A remote key literally beginning with `companies/...` is
2440
+ // a doubly-scoped corrupt object: the vault-service refuses to presign it
2441
+ // (INVALID_KEY_COMPANIES_SCOPED), so the per-file GET fails and the whole
2442
+ // company wedges at "errored" (runner exit 2) on every run. Verified live
2443
+ // 2026-06-16 against frogbear. The pull planner must refuse it at planning
2444
+ // time — same policy bucket as the malformed-key / ephemeral filters.
2445
+ const corruptKey = "companies/acme/drafts/reports/signals-2026-06-15.html";
2446
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
2447
+ // Doubly-scoped corrupt key — must be filtered, never downloaded.
2448
+ { key: corruptKey, size: 2152, lastModified: new Date(), etag: '"corrupt"' },
2449
+ // A legitimate bucket-relative key — must still download.
2450
+ { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"ok"' },
2451
+ ]);
2452
+ const result = await sync({
2453
+ company: "acme",
2454
+ vaultConfig: mockConfig,
2455
+ hqRoot: tmpDir,
2456
+ });
2457
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2458
+ // The corrupt key MUST NOT be materialized at its doubled local path.
2459
+ expect(fs.existsSync(path.join(companyRoot, "companies", "acme", "drafts", "reports", "signals-2026-06-15.html"))).toBe(false);
2460
+ // The vault-service GET must never be attempted for the corrupt key — the
2461
+ // failing presign is the exact symptom this guard removes.
2462
+ for (const call of vi.mocked(s3Module.downloadFile).mock.calls) {
2463
+ expect(call[1]).not.toBe(corruptKey);
2464
+ }
2465
+ // The legitimate bucket-relative key MUST still download.
2466
+ expect(fs.existsSync(path.join(companyRoot, "docs", "handoff.md"))).toBe(true);
2467
+ expect(result.filesDownloaded).toBe(1);
2468
+ expect(result.filesExcludedByPolicy).toBeGreaterThanOrEqual(1);
2469
+ expect(result.aborted).toBe(false);
2470
+ });
2322
2471
  it("F02: rejects traversal remote keys before they can escape the company root", async () => {
2323
2472
  const escapeName = `${path.basename(tmpDir)}-escaped.md`;
2324
2473
  const traversalKey = `../../../${escapeName}`;
@@ -3433,6 +3582,45 @@ describe("sync", () => {
3433
3582
  expect(result.filesSkipped).toBe(1);
3434
3583
  expect(result.conflicts).toBe(0);
3435
3584
  });
3585
+ it("defers an unreadable local symlink instead of aborting the pull plan", async () => {
3586
+ const linkKey = "policies/unreadable-link";
3587
+ const companyRoot = path.join(tmpDir, "companies", "acme");
3588
+ const linkPath = path.join(companyRoot, linkKey);
3589
+ fs.mkdirSync(path.dirname(linkPath), { recursive: true });
3590
+ fs.symlinkSync("target.md", linkPath);
3591
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
3592
+ { key: linkKey, size: 0, lastModified: new Date(), etag: '"link-etag"' },
3593
+ ]);
3594
+ const realReadlinkSync = fs.readlinkSync;
3595
+ const readlinkSpy = vi
3596
+ .spyOn(fs, "readlinkSync")
3597
+ .mockImplementation(((candidate) => {
3598
+ if (candidate === linkPath)
3599
+ throw errnoError("EINVAL");
3600
+ return realReadlinkSync(candidate);
3601
+ }));
3602
+ try {
3603
+ const events = [];
3604
+ const result = await sync({
3605
+ company: "acme",
3606
+ vaultConfig: mockConfig,
3607
+ hqRoot: tmpDir,
3608
+ onEvent: (event) => events.push(event),
3609
+ });
3610
+ expect(result.filesDownloaded).toBe(0);
3611
+ expect(result.filesSkipped).toBe(1);
3612
+ expect(s3Module.downloadFile).not.toHaveBeenCalled();
3613
+ expect(events).toContainEqual({
3614
+ type: "not-shipped",
3615
+ reason: "unreadable-link",
3616
+ count: 1,
3617
+ samplePaths: [linkKey],
3618
+ });
3619
+ }
3620
+ finally {
3621
+ readlinkSpy.mockRestore();
3622
+ }
3623
+ });
3436
3624
  it("classifies a dangling-symlink + remote-change as a conflict without crashing on statSync", async () => {
3437
3625
  // Codex round-11 P2 follow-up: pre-fix, the conflict executor
3438
3626
  // built the resolveConflict prompt with `fs.statSync(localPath).