@indigoai-us/hq-cloud 5.11.3 → 5.13.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/src/s3.test.ts CHANGED
@@ -15,6 +15,11 @@ import * as path from "path";
15
15
  // in beforeEach so per-test assertions don't leak from neighbours.
16
16
  const sentCommands: Array<{ name: string; input: Record<string, unknown> }> = [];
17
17
 
18
+ // Per-test override for the ListObjectsV2Command response. Tests of
19
+ // `listRemoteFiles` push the Contents shape they want returned; the default
20
+ // is an empty bucket. Reset in beforeEach so cross-test leakage is impossible.
21
+ let nextListObjectsResponse: Record<string, unknown> = { Contents: [] };
22
+
18
23
  vi.mock("@aws-sdk/client-s3", () => {
19
24
  class FakeS3Client {
20
25
  async send(command: { constructor: { name: string }; input: Record<string, unknown> }): Promise<Record<string, unknown>> {
@@ -27,6 +32,9 @@ vi.mock("@aws-sdk/client-s3", () => {
27
32
  if (command.constructor.name === "PutObjectCommand") {
28
33
  return { ETag: '"fake-etag"' };
29
34
  }
35
+ if (command.constructor.name === "ListObjectsV2Command") {
36
+ return nextListObjectsResponse;
37
+ }
30
38
  return {};
31
39
  }
32
40
  }
@@ -58,7 +66,7 @@ vi.mock("@aws-sdk/client-s3", () => {
58
66
  };
59
67
  });
60
68
 
61
- import { uploadFile } from "./s3.js";
69
+ import { uploadFile, listRemoteFiles } from "./s3.js";
62
70
  import type { EntityContext } from "./types.js";
63
71
 
64
72
  function makeCtx(): EntityContext {
@@ -164,3 +172,47 @@ describe("uploadFile", () => {
164
172
  expect(meta["created-by-sub"]).toBeUndefined();
165
173
  });
166
174
  });
175
+
176
+ describe("listRemoteFiles", () => {
177
+ beforeEach(() => {
178
+ sentCommands.length = 0;
179
+ nextListObjectsResponse = { Contents: [] };
180
+ });
181
+
182
+ // Regression: pre-fix, `if (!obj.Key || !obj.Size) continue;` skipped every
183
+ // 0-byte object on the listing. The user's personal vault stored
184
+ // `projects/.gitkeep` (a 0-byte placeholder) and the sync pull plan never
185
+ // saw it — the file existed on S3 but was invisible to the runner, so the
186
+ // box never created `projects/` on first pull. The fix narrows the guard
187
+ // to `if (!obj.Key) continue;` so Size === 0 passes through with its real
188
+ // value (handled `?? 0` to keep TS Size?: number happy).
189
+ it("includes 0-byte objects in the result (no implicit skip-on-empty)", async () => {
190
+ nextListObjectsResponse = {
191
+ Contents: [
192
+ { Key: "projects/.gitkeep", Size: 0, LastModified: new Date(), ETag: '"d41d8cd98f00b204e9800998ecf8427e"' },
193
+ { Key: "AGENTS.md", Size: 100, LastModified: new Date(), ETag: '"e1"' },
194
+ ],
195
+ };
196
+
197
+ const files = await listRemoteFiles(makeCtx());
198
+
199
+ expect(files).toHaveLength(2);
200
+ const gitkeep = files.find((f) => f.key === "projects/.gitkeep");
201
+ expect(gitkeep).toBeDefined();
202
+ expect(gitkeep!.size).toBe(0);
203
+ });
204
+
205
+ it("still skips Contents entries with no Key (defensive against malformed responses)", async () => {
206
+ nextListObjectsResponse = {
207
+ Contents: [
208
+ { Size: 100, LastModified: new Date(), ETag: '"e1"' }, // no Key
209
+ { Key: "real.md", Size: 100, LastModified: new Date(), ETag: '"e2"' },
210
+ ],
211
+ };
212
+
213
+ const files = await listRemoteFiles(makeCtx());
214
+
215
+ expect(files).toHaveLength(1);
216
+ expect(files[0].key).toBe("real.md");
217
+ });
218
+ });
package/src/s3.ts CHANGED
@@ -173,11 +173,16 @@ export async function listRemoteFiles(
173
173
  );
174
174
 
175
175
  for (const obj of response.Contents || []) {
176
- if (!obj.Key || !obj.Size) continue;
176
+ // Pre-fix this guard was `!obj.Key || !obj.Size`. The `!obj.Size` test
177
+ // is truthy when Size === 0 (a real 0-byte object like `.gitkeep`),
178
+ // silently filtering legitimate placeholder files out of every pull
179
+ // plan. Narrow the guard to "no key" only; surface real 0-byte
180
+ // objects to the planner.
181
+ if (!obj.Key) continue;
177
182
 
178
183
  files.push({
179
184
  key: obj.Key,
180
- size: obj.Size,
185
+ size: obj.Size ?? 0,
181
186
  lastModified: obj.LastModified || new Date(),
182
187
  etag: obj.ETag || "",
183
188
  });