@indigoai-us/hq-cloud 6.15.12 → 6.15.13

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.
@@ -52,7 +52,7 @@ vi.mock("readline", () => ({
52
52
  })),
53
53
  }));
54
54
  import * as readline from "readline";
55
- import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, _testing as shareTesting } from "./share.js";
55
+ import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
56
  import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError } from "../s3.js";
57
57
  import { VaultAuthError } from "../vault-client.js";
58
58
  import { hashFile } from "../journal.js";
@@ -5873,6 +5873,50 @@ describe("currency-gated: journal version 2 fixtures", () => {
5873
5873
  // still fire (now at file-settle time, not plan-walk time) so the menubar
5874
5874
  // stream parser sees the same shape. Aborts mid-batch let in-flight PUTs
5875
5875
  // finish but stop queueing new ones.
5876
+ describe("currentUploadBytes (byte-budget admission size)", () => {
5877
+ it("returns the CURRENT on-disk size, not a stale planned size", () => {
5878
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cub-grow-"));
5879
+ try {
5880
+ const abs = path.join(dir, "grew.log");
5881
+ fs.writeFileSync(abs, Buffer.alloc(5000)); // on disk NOW: 5000 bytes
5882
+ const item = {
5883
+ action: "upload",
5884
+ kind: "file",
5885
+ absolutePath: abs,
5886
+ relativePath: "grew.log",
5887
+ localHash: "h",
5888
+ size: 10, // stale planned size
5889
+ };
5890
+ expect(currentUploadBytes(item)).toBe(5000);
5891
+ }
5892
+ finally {
5893
+ fs.rmSync(dir, { recursive: true, force: true });
5894
+ }
5895
+ });
5896
+ it("returns 0 for a symlink (empty wire body)", () => {
5897
+ const item = {
5898
+ action: "upload",
5899
+ kind: "symlink",
5900
+ absolutePath: "/nonexistent/link",
5901
+ relativePath: "link",
5902
+ target: "target",
5903
+ localHash: "h",
5904
+ size: 0,
5905
+ };
5906
+ expect(currentUploadBytes(item)).toBe(0);
5907
+ });
5908
+ it("falls back to the planned size when the file cannot be stat'd", () => {
5909
+ const item = {
5910
+ action: "upload",
5911
+ kind: "file",
5912
+ absolutePath: "/nonexistent/x.bin",
5913
+ relativePath: "x.bin",
5914
+ localHash: "h",
5915
+ size: 42,
5916
+ };
5917
+ expect(currentUploadBytes(item)).toBe(42);
5918
+ });
5919
+ });
5876
5920
  describe("parallel upload pool (5.36.0)", () => {
5877
5921
  it("runs uploads with measurable parallelism (8+ in flight before first settles)", async () => {
5878
5922
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -5999,6 +6043,91 @@ describe("currency-gated: journal version 2 fixtures", () => {
5999
6043
  delete process.env.HQ_SYNC_TRANSFER_CONCURRENCY;
6000
6044
  }
6001
6045
  });
6046
+ it("byte budget caps in-flight uploads below the worker count", async () => {
6047
+ const companyRoot = path.join(tmpDir, "companies", "acme");
6048
+ fs.mkdirSync(companyRoot, { recursive: true });
6049
+ // 10 files of exactly 2048 bytes each.
6050
+ for (let i = 0; i < 10; i++) {
6051
+ fs.writeFileSync(path.join(companyRoot, `f-${i}.bin`), Buffer.alloc(2048));
6052
+ }
6053
+ // Worker count is high (16) so it is NOT the limiter; the byte budget is.
6054
+ // 0.0048 MiB = 5033 bytes: two 2048-byte bodies fit (4096), three do not
6055
+ // (6144), so at most 2 uploads may be in flight regardless of the 16 slots.
6056
+ process.env.HQ_SYNC_TRANSFER_CONCURRENCY = "16";
6057
+ process.env.HQ_SYNC_UPLOAD_BYTE_BUDGET_MB = "0.0048";
6058
+ const startTimes = [];
6059
+ const endTimes = [];
6060
+ vi.mocked(uploadFile).mockImplementation(async () => {
6061
+ startTimes.push(Date.now());
6062
+ await new Promise((r) => setTimeout(r, 30));
6063
+ endTimes.push(Date.now());
6064
+ return { etag: '"upload-etag"' };
6065
+ });
6066
+ try {
6067
+ const result = await share({
6068
+ paths: [companyRoot],
6069
+ company: "acme",
6070
+ vaultConfig: mockConfig,
6071
+ hqRoot: tmpDir,
6072
+ onConflict: "keep",
6073
+ });
6074
+ expect(result.filesUploaded).toBe(10); // all made progress, no deadlock
6075
+ const sortedEvents = [];
6076
+ for (const t of startTimes)
6077
+ sortedEvents.push({ t, kind: "start" });
6078
+ for (const t of endTimes)
6079
+ sortedEvents.push({ t, kind: "end" });
6080
+ sortedEvents.sort((a, b) => a.t - b.t || (a.kind === "end" ? -1 : 1));
6081
+ let cur = 0;
6082
+ let maxConcurrent = 0;
6083
+ for (const ev of sortedEvents) {
6084
+ if (ev.kind === "start") {
6085
+ cur++;
6086
+ if (cur > maxConcurrent)
6087
+ maxConcurrent = cur;
6088
+ }
6089
+ else {
6090
+ cur--;
6091
+ }
6092
+ }
6093
+ // Byte-limited to 2, far below the 16-slot worker cap.
6094
+ expect(maxConcurrent).toBeLessThanOrEqual(2);
6095
+ }
6096
+ finally {
6097
+ delete process.env.HQ_SYNC_TRANSFER_CONCURRENCY;
6098
+ delete process.env.HQ_SYNC_UPLOAD_BYTE_BUDGET_MB;
6099
+ }
6100
+ });
6101
+ it("a file larger than the whole byte budget still uploads (no deadlock)", async () => {
6102
+ const companyRoot = path.join(tmpDir, "companies", "acme");
6103
+ fs.mkdirSync(companyRoot, { recursive: true });
6104
+ // Three 4096-byte files against a ~1 KiB budget: each exceeds the budget
6105
+ // alone, so the "admit one when nothing is in flight" escape hatch must
6106
+ // let every file through one at a time instead of wedging the pool.
6107
+ for (let i = 0; i < 3; i++) {
6108
+ fs.writeFileSync(path.join(companyRoot, `big-${i}.bin`), Buffer.alloc(4096));
6109
+ }
6110
+ process.env.HQ_SYNC_TRANSFER_CONCURRENCY = "16";
6111
+ process.env.HQ_SYNC_UPLOAD_BYTE_BUDGET_MB = "0.001"; // 1048 bytes < 4096
6112
+ vi.mocked(uploadFile).mockImplementation(async () => {
6113
+ await new Promise((r) => setTimeout(r, 10));
6114
+ return { etag: '"upload-etag"' };
6115
+ });
6116
+ try {
6117
+ const result = await share({
6118
+ paths: [companyRoot],
6119
+ company: "acme",
6120
+ vaultConfig: mockConfig,
6121
+ hqRoot: tmpDir,
6122
+ onConflict: "keep",
6123
+ });
6124
+ expect(result.filesUploaded).toBe(3);
6125
+ }
6126
+ finally {
6127
+ delete process.env.HQ_SYNC_TRANSFER_CONCURRENCY;
6128
+ delete process.env.HQ_SYNC_UPLOAD_BYTE_BUDGET_MB;
6129
+ }
6130
+ });
6002
6131
  it("one failed upload does not abort sibling uploads in the pool", async () => {
6003
6132
  const companyRoot = path.join(tmpDir, "companies", "acme");
6004
6133
  fs.mkdirSync(companyRoot, { recursive: true });