@indigoai-us/hq-cloud 6.14.37 → 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.
@@ -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
@@ -3467,6 +3582,45 @@ describe("sync", () => {
3467
3582
  expect(result.filesSkipped).toBe(1);
3468
3583
  expect(result.conflicts).toBe(0);
3469
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
+ });
3470
3624
  it("classifies a dangling-symlink + remote-change as a conflict without crashing on statSync", async () => {
3471
3625
  // Codex round-11 P2 follow-up: pre-fix, the conflict executor
3472
3626
  // built the resolveConflict prompt with `fs.statSync(localPath).