@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.
- package/dist/bin/sync-runner.test.js +44 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +1 -9
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/share.d.ts +13 -14
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +61 -16
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +141 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -5
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +68 -10
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +154 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/lib/readlink-safe.d.ts +11 -0
- package/dist/lib/readlink-safe.d.ts.map +1 -0
- package/dist/lib/readlink-safe.js +27 -0
- package/dist/lib/readlink-safe.js.map +1 -0
- package/dist/lib/readlink-safe.test.d.ts +2 -0
- package/dist/lib/readlink-safe.test.d.ts.map +1 -0
- package/dist/lib/readlink-safe.test.js +34 -0
- package/dist/lib/readlink-safe.test.js.map +1 -0
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +52 -0
- package/src/cli/reindex.ts +1 -9
- package/src/cli/share.test.ts +180 -0
- package/src/cli/share.ts +75 -30
- package/src/cli/sync.test.ts +167 -0
- package/src/cli/sync.ts +78 -15
- package/src/lib/readlink-safe.test.ts +43 -0
- package/src/lib/readlink-safe.ts +29 -0
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +191 -0
package/dist/cli/sync.test.js
CHANGED
|
@@ -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).
|