@indigoai-us/hq-cloud 6.8.0 → 6.9.0
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/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +104 -8
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +190 -20
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +9 -1
- package/dist/cognito-auth.js.map +1 -1
- package/dist/machine-auth.test.js +4 -2
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/object-io.d.ts +28 -2
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +76 -5
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +93 -2
- package/dist/object-io.test.js.map +1 -1
- package/dist/s3.d.ts +3 -2
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +10 -5
- package/dist/s3.js.map +1 -1
- package/dist/vault-client.d.ts +9 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/share.test.ts +245 -9
- package/src/cli/share.ts +116 -8
- package/src/cognito-auth.ts +9 -1
- package/src/machine-auth.test.ts +4 -2
- package/src/object-io.test.ts +105 -2
- package/src/object-io.ts +121 -8
- package/src/s3.ts +11 -4
- package/src/vault-client.ts +9 -0
package/dist/cli/share.test.js
CHANGED
|
@@ -141,7 +141,7 @@ describe("share", () => {
|
|
|
141
141
|
expect(result.filesUploaded).toBe(1);
|
|
142
142
|
expect(result.aborted).toBe(false);
|
|
143
143
|
// Remote key must be company-relative, not hqRoot-relative
|
|
144
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md");
|
|
144
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
|
|
145
145
|
});
|
|
146
146
|
it("respects ignore rules", async () => {
|
|
147
147
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -181,7 +181,7 @@ describe("share", () => {
|
|
|
181
181
|
hqRoot: tmpDir,
|
|
182
182
|
});
|
|
183
183
|
// Key is "knowledge/crawl.json", not "companies/acme/knowledge/crawl.json"
|
|
184
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), nested, "knowledge/crawl.json");
|
|
184
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), nested, "knowledge/crawl.json", undefined, expect.anything());
|
|
185
185
|
});
|
|
186
186
|
it("skips files outside the company folder with a warning", async () => {
|
|
187
187
|
const warnSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
@@ -280,7 +280,7 @@ describe("share", () => {
|
|
|
280
280
|
skipUnchanged: true,
|
|
281
281
|
});
|
|
282
282
|
expect(result.filesUploaded).toBe(1);
|
|
283
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md");
|
|
283
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
|
|
284
284
|
});
|
|
285
285
|
it("populates conflictPaths and emits a conflict event when both local and remote drifted from journal", async () => {
|
|
286
286
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -496,6 +496,142 @@ describe("share", () => {
|
|
|
496
496
|
// Local file untouched under keep.
|
|
497
497
|
expect(fs.readFileSync(testFile, "utf-8")).toBe("my-local-version");
|
|
498
498
|
});
|
|
499
|
+
describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
|
|
500
|
+
const fence412 = () => Object.assign(new Error("Precondition failed"), {
|
|
501
|
+
name: "PreconditionFailed",
|
|
502
|
+
});
|
|
503
|
+
it("fences a PUT over an existing remote with If-Match on the observed etag", async () => {
|
|
504
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
505
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
506
|
+
const testFile = path.join(companyRoot, "fenced.md");
|
|
507
|
+
fs.writeFileSync(testFile, "local edit");
|
|
508
|
+
// Remote exists at the journal baseline; only local changed -> clean
|
|
509
|
+
// push, but the PUT must still assert the observed remote etag so a
|
|
510
|
+
// peer write landing between HEAD and PUT draws a 412 instead of
|
|
511
|
+
// being clobbered (HEAD->PUT TOCTOU).
|
|
512
|
+
const syncedAt = new Date(Date.now() - 60_000).toISOString();
|
|
513
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce({
|
|
514
|
+
lastModified: new Date(Date.parse(syncedAt) - 30_000),
|
|
515
|
+
etag: '"baseline-etag"',
|
|
516
|
+
size: 5,
|
|
517
|
+
});
|
|
518
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
519
|
+
fs.writeFileSync(journalPath, JSON.stringify({
|
|
520
|
+
version: "1",
|
|
521
|
+
lastSync: syncedAt,
|
|
522
|
+
files: {
|
|
523
|
+
"fenced.md": {
|
|
524
|
+
hash: "stale-local-hash",
|
|
525
|
+
size: 5,
|
|
526
|
+
syncedAt,
|
|
527
|
+
direction: "up",
|
|
528
|
+
remoteEtag: "baseline-etag",
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
}));
|
|
532
|
+
const result = await share({
|
|
533
|
+
paths: [testFile],
|
|
534
|
+
company: "acme",
|
|
535
|
+
vaultConfig: mockConfig,
|
|
536
|
+
hqRoot: tmpDir,
|
|
537
|
+
});
|
|
538
|
+
expect(result.filesUploaded).toBe(1);
|
|
539
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "fenced.md", undefined, { ifMatch: '"baseline-etag"' });
|
|
540
|
+
});
|
|
541
|
+
it("fences a brand-new key with If-None-Match:* (create-only)", async () => {
|
|
542
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
543
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
544
|
+
const testFile = path.join(companyRoot, "brand-new.md");
|
|
545
|
+
fs.writeFileSync(testFile, "fresh");
|
|
546
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
|
|
547
|
+
await share({
|
|
548
|
+
paths: [testFile],
|
|
549
|
+
company: "acme",
|
|
550
|
+
vaultConfig: mockConfig,
|
|
551
|
+
hqRoot: tmpDir,
|
|
552
|
+
});
|
|
553
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
|
|
554
|
+
});
|
|
555
|
+
it("412 under keep: conflict event + remote mirrored + ledger entry + NO journal stamp", async () => {
|
|
556
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
557
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
558
|
+
const testFile = path.join(companyRoot, "raced.md");
|
|
559
|
+
fs.writeFileSync(testFile, "my local bytes");
|
|
560
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
|
|
561
|
+
vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
|
|
562
|
+
vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
|
|
563
|
+
fs.writeFileSync(dest, "the racing peer version");
|
|
564
|
+
return undefined;
|
|
565
|
+
});
|
|
566
|
+
const events = [];
|
|
567
|
+
const result = await share({
|
|
568
|
+
paths: [testFile],
|
|
569
|
+
company: "acme",
|
|
570
|
+
vaultConfig: mockConfig,
|
|
571
|
+
hqRoot: tmpDir,
|
|
572
|
+
onConflict: "keep",
|
|
573
|
+
onEvent: (e) => events.push(e),
|
|
574
|
+
});
|
|
575
|
+
// Surfaced as a push conflict, not an error.
|
|
576
|
+
expect(result.conflictPaths).toEqual(["raced.md"]);
|
|
577
|
+
expect(result.filesUploaded).toBe(0);
|
|
578
|
+
expect(result.aborted).toBe(false);
|
|
579
|
+
expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
|
|
580
|
+
// The racing remote bytes were preserved next to the local copy.
|
|
581
|
+
const mirrors = fs
|
|
582
|
+
.readdirSync(companyRoot)
|
|
583
|
+
.filter((f) => f.includes(".conflict-"));
|
|
584
|
+
expect(mirrors).toHaveLength(1);
|
|
585
|
+
// Local copy untouched.
|
|
586
|
+
expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
|
|
587
|
+
// No journal stamp for the failed PUT — next pass re-evaluates fresh.
|
|
588
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
589
|
+
if (fs.existsSync(journalPath)) {
|
|
590
|
+
const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
|
|
591
|
+
expect(journal.files?.["raced.md"]).toBeUndefined();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
|
|
595
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
596
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
597
|
+
const testFile = path.join(companyRoot, "consented.md");
|
|
598
|
+
fs.writeFileSync(testFile, "clobber me in");
|
|
599
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
|
|
600
|
+
vi.mocked(uploadFile)
|
|
601
|
+
.mockRejectedValueOnce(fence412())
|
|
602
|
+
.mockResolvedValueOnce({ etag: '"retried-etag"' });
|
|
603
|
+
const result = await share({
|
|
604
|
+
paths: [testFile],
|
|
605
|
+
company: "acme",
|
|
606
|
+
vaultConfig: mockConfig,
|
|
607
|
+
hqRoot: tmpDir,
|
|
608
|
+
onConflict: "overwrite",
|
|
609
|
+
});
|
|
610
|
+
expect(result.filesUploaded).toBe(1);
|
|
611
|
+
const calls = vi.mocked(uploadFile).mock.calls;
|
|
612
|
+
expect(calls).toHaveLength(2);
|
|
613
|
+
// First attempt fenced; retry deliberately unfenced.
|
|
614
|
+
expect(calls[0][4]).toEqual({ ifNoneMatch: "*" });
|
|
615
|
+
expect(calls[1][4]).toBeUndefined();
|
|
616
|
+
});
|
|
617
|
+
it("412 under abort: halts the pass", async () => {
|
|
618
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
619
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
620
|
+
const testFile = path.join(companyRoot, "halting.md");
|
|
621
|
+
fs.writeFileSync(testFile, "stop here");
|
|
622
|
+
vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
|
|
623
|
+
vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
|
|
624
|
+
const result = await share({
|
|
625
|
+
paths: [testFile],
|
|
626
|
+
company: "acme",
|
|
627
|
+
vaultConfig: mockConfig,
|
|
628
|
+
hqRoot: tmpDir,
|
|
629
|
+
onConflict: "abort",
|
|
630
|
+
});
|
|
631
|
+
expect(result.aborted).toBe(true);
|
|
632
|
+
expect(result.conflictPaths).toEqual(["halting.md"]);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
499
635
|
it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
|
|
500
636
|
// Real case (look-optic): a FILE_ACL grant covered {knowledge,policies,
|
|
501
637
|
// workers}/* + company.yaml, but the upload plan also contained
|
|
@@ -594,6 +730,39 @@ describe("share", () => {
|
|
|
594
730
|
expect(errs[0].path).toBe("out-of-reach.md");
|
|
595
731
|
expect(errs[0].message).toMatch(/outside granted ACL scope/i);
|
|
596
732
|
});
|
|
733
|
+
it("a Forbidden HEAD (presigned-transport denial) skips the key — NEVER an unconditional PUT", async () => {
|
|
734
|
+
// Regression for the 2026-06-10..12 vault regression storm: the presigned
|
|
735
|
+
// transport used to map per-key denials and signed-GET 403s to `null`,
|
|
736
|
+
// which `processUploadItem` read as "no remote object" — skipping every
|
|
737
|
+
// conflict guard and blind-PUTting this machine's (possibly stale) bytes
|
|
738
|
+
// over a newer remote. The transport now throws `name: "Forbidden"`
|
|
739
|
+
// (SDK HeadObject parity); this locks the share side of that contract:
|
|
740
|
+
// a Forbidden HEAD must skip the key, emit a named error, and issue NO PUT.
|
|
741
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
742
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
743
|
+
const staleFile = path.join(companyRoot, "stale-local.md");
|
|
744
|
+
fs.writeFileSync(staleFile, "stale local bytes\n");
|
|
745
|
+
vi.mocked(headRemoteFile).mockImplementation(async () => {
|
|
746
|
+
const err = new Error("Access denied for stale-local.md: presigned HEAD returned 403");
|
|
747
|
+
err.name = "Forbidden";
|
|
748
|
+
throw err;
|
|
749
|
+
});
|
|
750
|
+
const events = [];
|
|
751
|
+
const result = await share({
|
|
752
|
+
paths: [staleFile],
|
|
753
|
+
company: "acme",
|
|
754
|
+
vaultConfig: mockConfig,
|
|
755
|
+
hqRoot: tmpDir,
|
|
756
|
+
onEvent: (e) => events.push(e),
|
|
757
|
+
});
|
|
758
|
+
// The one thing that must never happen: a PUT issued with unknown remote state.
|
|
759
|
+
expect(vi.mocked(uploadFile)).not.toHaveBeenCalled();
|
|
760
|
+
expect(result.filesUploaded).toBe(0);
|
|
761
|
+
expect(result.aborted).toBe(false);
|
|
762
|
+
const errs2 = events.filter((e) => e.type === "error");
|
|
763
|
+
expect(errs2).toHaveLength(1);
|
|
764
|
+
expect(errs2[0].path).toBe("stale-local.md");
|
|
765
|
+
});
|
|
597
766
|
it("uploads (no conflict) when only the local side changed since last sync", async () => {
|
|
598
767
|
// Regression for hq-cloud#<conflict-detection>: a local edit to a file
|
|
599
768
|
// that exists on S3 used to trigger a push conflict because the
|
|
@@ -706,11 +875,12 @@ describe("share", () => {
|
|
|
706
875
|
hqRoot: tmpDir,
|
|
707
876
|
author: { userSub: "abc-123", email: "alice@example.com" },
|
|
708
877
|
});
|
|
709
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "attribution.md", { userSub: "abc-123", email: "alice@example.com" });
|
|
878
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "attribution.md", { userSub: "abc-123", email: "alice@example.com" }, expect.anything());
|
|
710
879
|
});
|
|
711
880
|
it("omits author arg when not provided (back-compat)", async () => {
|
|
712
|
-
//
|
|
713
|
-
//
|
|
881
|
+
// With no author configured the author slot is undefined (the
|
|
882
|
+
// conditional-write fence appended a 5th arg in 6.9.0, so the legacy
|
|
883
|
+
// 3-arg shape grew to 5; author-less calls pass undefined explicitly).
|
|
714
884
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
715
885
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
716
886
|
const testFile = path.join(companyRoot, "no-author.md");
|
|
@@ -721,7 +891,7 @@ describe("share", () => {
|
|
|
721
891
|
vaultConfig: mockConfig,
|
|
722
892
|
hqRoot: tmpDir,
|
|
723
893
|
});
|
|
724
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-author.md");
|
|
894
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-author.md", undefined, expect.anything());
|
|
725
895
|
});
|
|
726
896
|
it("skipUnchanged=false (default) uploads even when hash matches", async () => {
|
|
727
897
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -863,7 +1033,7 @@ describe("share", () => {
|
|
|
863
1033
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
864
1034
|
// The S3 upload sees the pre-vended credentials, not freshly-vended ones.
|
|
865
1035
|
// (uploadFile is mocked, so we just verify it was called with our ctx.)
|
|
866
|
-
expect(uploadFile).toHaveBeenCalledWith(ctx, testFile, "from-appbar.md");
|
|
1036
|
+
expect(uploadFile).toHaveBeenCalledWith(ctx, testFile, "from-appbar.md", undefined, expect.anything());
|
|
867
1037
|
});
|
|
868
1038
|
it("falls back to entityContext.slug when company is not specified", async () => {
|
|
869
1039
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -879,7 +1049,7 @@ describe("share", () => {
|
|
|
879
1049
|
expect(result.filesUploaded).toBe(1);
|
|
880
1050
|
// Confirms the relative-path scoping landed under acme even without an
|
|
881
1051
|
// explicit company arg.
|
|
882
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-company-arg.md");
|
|
1052
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "no-company-arg.md", undefined, expect.anything());
|
|
883
1053
|
});
|
|
884
1054
|
it("does NOT auto-refresh when entityContext is expiring soon (no vending source)", async () => {
|
|
885
1055
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -905,7 +1075,7 @@ describe("share", () => {
|
|
|
905
1075
|
expect(result.filesUploaded).toBe(1);
|
|
906
1076
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
907
1077
|
// The original (still-valid-for-30s) credentials must have been used as-is.
|
|
908
|
-
expect(uploadFile).toHaveBeenCalledWith(expiringCtx, testFile, "race.md");
|
|
1078
|
+
expect(uploadFile).toHaveBeenCalledWith(expiringCtx, testFile, "race.md", undefined, expect.anything());
|
|
909
1079
|
});
|
|
910
1080
|
it("throws when both vaultConfig and entityContext are provided (ambiguous)", async () => {
|
|
911
1081
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -1779,7 +1949,7 @@ describe("share", () => {
|
|
|
1779
1949
|
skipUnchanged: true,
|
|
1780
1950
|
});
|
|
1781
1951
|
expect(uploadFile).toHaveBeenCalledTimes(1);
|
|
1782
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), expect.stringContaining("real.md"), "skills/real.md");
|
|
1952
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), expect.stringContaining("real.md"), "skills/real.md", undefined, expect.anything());
|
|
1783
1953
|
// Spy was called once total — the conflict mirror never reaches uploadFile.
|
|
1784
1954
|
// (Asserted by count above; the explicit non-call below is defensive.)
|
|
1785
1955
|
const calls = vi.mocked(uploadFile).mock.calls;
|
|
@@ -2184,7 +2354,7 @@ describe("share", () => {
|
|
|
2184
2354
|
journalSlug: "personal",
|
|
2185
2355
|
});
|
|
2186
2356
|
expect(result.filesUploaded).toBe(1);
|
|
2187
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), outsideFile, "stray.md");
|
|
2357
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), outsideFile, "stray.md", undefined, expect.anything());
|
|
2188
2358
|
});
|
|
2189
2359
|
it("uploads 0-byte files (e.g. .gitkeep placeholders)", async () => {
|
|
2190
2360
|
// Regression for the bug where `projects/.gitkeep` (0 bytes) was
|
|
@@ -2205,7 +2375,7 @@ describe("share", () => {
|
|
|
2205
2375
|
journalSlug: "personal",
|
|
2206
2376
|
});
|
|
2207
2377
|
expect(result.filesUploaded).toBe(1);
|
|
2208
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), gitkeep, "projects/.gitkeep");
|
|
2378
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), gitkeep, "projects/.gitkeep", undefined, expect.anything());
|
|
2209
2379
|
});
|
|
2210
2380
|
it("personalMode=true + skipUnchanged honors the personal-journal hash", async () => {
|
|
2211
2381
|
fs.mkdirSync(path.join(tmpDir, "knowledge"), { recursive: true });
|
|
@@ -2268,7 +2438,7 @@ describe("share", () => {
|
|
|
2268
2438
|
hqRoot: tmpDir,
|
|
2269
2439
|
});
|
|
2270
2440
|
expect(result.filesUploaded).toBe(1);
|
|
2271
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "target.md", "link.md");
|
|
2441
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "target.md", "link.md", undefined, expect.anything());
|
|
2272
2442
|
// The link itself must NOT be uploaded as a regular file. Pre-fix,
|
|
2273
2443
|
// fs.statSync(link) followed the link and uploadFile got called with
|
|
2274
2444
|
// the link's path → cloud stored a copy of target.md's bytes under
|
|
@@ -2296,8 +2466,8 @@ describe("share", () => {
|
|
|
2296
2466
|
// Pre-fix, walkDir's Dirent.isFile() returned false for the symlink and
|
|
2297
2467
|
// it was silently dropped — only the real file was uploaded.
|
|
2298
2468
|
expect(result.filesUploaded).toBe(2);
|
|
2299
|
-
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), realPolicy, "policies/real.md");
|
|
2300
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "real.md", "policies/link.md");
|
|
2469
|
+
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), realPolicy, "policies/real.md", undefined, expect.anything());
|
|
2470
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "real.md", "policies/link.md", undefined, expect.anything());
|
|
2301
2471
|
});
|
|
2302
2472
|
it("accepts a symlink inside the company folder even when its target lives outside", async () => {
|
|
2303
2473
|
// Codex P2 follow-up: pre-fix, isWithin canonicalized the link
|
|
@@ -2322,7 +2492,7 @@ describe("share", () => {
|
|
|
2322
2492
|
});
|
|
2323
2493
|
warnSpy.mockRestore();
|
|
2324
2494
|
expect(result.filesUploaded).toBe(1);
|
|
2325
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge");
|
|
2495
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge", undefined, expect.anything());
|
|
2326
2496
|
// No "outside company folder" warning for this case.
|
|
2327
2497
|
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringMatching(/outside company folder/i));
|
|
2328
2498
|
});
|
|
@@ -2375,7 +2545,7 @@ describe("share", () => {
|
|
|
2375
2545
|
// symlink's journal hash differs from the regular-file hash.
|
|
2376
2546
|
expect(result.filesUploaded).toBe(1);
|
|
2377
2547
|
expect(result.filesSkipped).toBe(0);
|
|
2378
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), target, "case-key.md");
|
|
2548
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), target, "case-key.md", undefined, expect.anything());
|
|
2379
2549
|
});
|
|
2380
2550
|
it("includes a directory symlink whose only matching allowlist pattern is dir-only", async () => {
|
|
2381
2551
|
// Codex round-6 P1 follow-up: pre-fix, walkDir called the filter
|
|
@@ -2409,7 +2579,7 @@ describe("share", () => {
|
|
|
2409
2579
|
});
|
|
2410
2580
|
// The symlink record uploaded; the sibling regular file did NOT.
|
|
2411
2581
|
expect(result.filesUploaded).toBe(1);
|
|
2412
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge");
|
|
2582
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), externalTarget, "knowledge", undefined, expect.anything());
|
|
2413
2583
|
expect(uploadFile).not.toHaveBeenCalledWith(expect.anything(), expect.stringContaining("stray.md"), expect.anything());
|
|
2414
2584
|
});
|
|
2415
2585
|
it("does not recurse into directory symlinks (record-only)", async () => {
|
|
@@ -2432,7 +2602,7 @@ describe("share", () => {
|
|
|
2432
2602
|
});
|
|
2433
2603
|
// The link record itself uploads; the target's contents do NOT.
|
|
2434
2604
|
expect(result.filesUploaded).toBe(1);
|
|
2435
|
-
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), realDir, "linked-dir");
|
|
2605
|
+
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), realDir, "linked-dir", undefined, expect.anything());
|
|
2436
2606
|
// Crucially, no upload of the dir's contents.
|
|
2437
2607
|
const calls = vi.mocked(uploadFile).mock.calls;
|
|
2438
2608
|
expect(calls.find((c) => c[2].includes("secret.md"))).toBeUndefined();
|