@indigoai-us/hq-cloud 6.18.17 → 6.18.19

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.
@@ -174,7 +174,7 @@ vi.mock("../sync/mass-delete-guards.js", async (importOriginal) => {
174
174
  return { ...actual, resolveMassDeleteGuards: vi.fn().mockResolvedValue(false) };
175
175
  });
176
176
  import { resolveMassDeleteGuards } from "../sync/mass-delete-guards.js";
177
- import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, FULL_PULL_COOPERATIVE_CHECKPOINT_BATCH_SIZE, FULL_PULL_LIST_PAGE_CHECKPOINT_SIZE, setListCooperativeYieldMsForTesting, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, _testing as syncTesting, } from "./sync.js";
177
+ import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, FULL_PULL_COOPERATIVE_CHECKPOINT_BATCH_SIZE, FULL_PULL_LIST_PAGE_CHECKPOINT_SIZE, setListCooperativeYieldMsForTesting, setNewFilesEnrichmentBulkCapForTesting, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, _testing as syncTesting, } from "./sync.js";
178
178
  import { share } from "./share.js";
179
179
  import * as s3Module from "../s3.js";
180
180
  import { resolvePersonalConnectorSettingsSync } from "../lib/personal-connector-settings-sync.js";
@@ -2314,6 +2314,50 @@ describe("sync", () => {
2314
2314
  expect(result.filesDownloaded).toBe(0);
2315
2315
  expect(getEntry(readJournal("acme"), vanishedKey)).toBeUndefined();
2316
2316
  });
2317
+ /**
2318
+ * REGRESSION: New Flow Test 1 (agt_529GZC4SD0874S43QR29FWCG1M) 2026-09-30.
2319
+ * On the indigo first pull, 18 planned `chat/attachments/chan/...` keys
2320
+ * failed presign with FILES_PRESIGN_FORBIDDEN — the caller is a hosted
2321
+ * bot with no read grant on those channels. object-io throws the
2322
+ * accessDeniedError shape (`name: "Forbidden"`), sync.ts's pull loop
2323
+ * previously incremented `filesSkipped` silently, and the progress
2324
+ * recorder never advanced `filesDone` — leaving 63787/63805 forever
2325
+ * and paging the wizard's inactivity watch. Emit a proper
2326
+ * `skip-access-denied` event so both the recorder and any downstream
2327
+ * summary can count the key.
2328
+ */
2329
+ it("REGRESSION: a presign-denied GET emits skip-access-denied (never type:error) so filesDone can advance", async () => {
2330
+ const deniedKey = "chat/attachments/chan/chn_01M2G9Y0SW73N7T6S5V6YW0XZ9/fe890ae2-5e95-4694-af5c-f5334422b8e2-pasted-2026-09-15T14-22-42-1.png";
2331
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
2332
+ { key: deniedKey, size: 4096, lastModified: new Date(), etag: '"denied"' },
2333
+ ]);
2334
+ // object-io's accessDeniedError shape: name: "Forbidden" with the
2335
+ // presign code embedded in the message. Mirrors the exact failure
2336
+ // sync.ts's isAccessDenied catches (New Flow Test 1 evidence).
2337
+ vi.mocked(s3Module.downloadFile).mockRejectedValueOnce(Object.assign(new Error(`Access denied for ${deniedKey}: no get access to '${deniedKey}' (server get denied: FILES_PRESIGN_FORBIDDEN)`), { name: "Forbidden" }));
2338
+ const events = [];
2339
+ const result = await sync({
2340
+ company: "acme",
2341
+ vaultConfig: mockConfig,
2342
+ hqRoot: tmpDir,
2343
+ skipReindex: true,
2344
+ onEvent: (e) => events.push(e),
2345
+ });
2346
+ const skip = events.find((e) => e.type === "skip-access-denied");
2347
+ expect(skip).toMatchObject({
2348
+ type: "skip-access-denied",
2349
+ path: deniedKey,
2350
+ reason: "FILES_PRESIGN_FORBIDDEN",
2351
+ });
2352
+ // Load-bearing: no non-diagnostic type:"error" — the runner must keep
2353
+ // exit 0 and the company must stay `complete` on ACL denials.
2354
+ expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
2355
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
2356
+ expect(result.filesDownloaded).toBe(0);
2357
+ // Deliberate skip contract: no journal entry, so a later pass that
2358
+ // gains access materializes the file with no manual reset.
2359
+ expect(getEntry(readJournal("acme"), deniedKey)).toBeUndefined();
2360
+ });
2317
2361
  it("a 403 GET error still surfaces as type:error — only a vanished remote is downgraded", async () => {
2318
2362
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
2319
2363
  { key: "docs/secret.md", size: 10, lastModified: new Date(), etag: '"x"' },
@@ -7101,6 +7145,94 @@ describe("sync", () => {
7101
7145
  const newFilesIdx = eventTypes.indexOf("new-files");
7102
7146
  expect(newFilesIdx).toBeGreaterThan(lastProgressIdx);
7103
7147
  });
7148
+ // ── Bulk-sync post-pull tail bypass ─────────────────────────────────
7149
+ // Regression for the 2026-09-30 first-sync case
7150
+ // (i-0b8c4677512e81136 · agt_3FZG3TC9GEEMRVWSRF9NJ7Q98C, hq-cloud
7151
+ // 6.18.18): 63,836 new files kept the runner ~20 minutes past
7152
+ // filesDone == filesTotal doing per-file HEAD enrichment
7153
+ // + /v1/notify/file-added POST, while the wizard already showed
7154
+ // "Sync did not finish". Above the bulk cap, the enrichment and the
7155
+ // notify POST are skipped so the pull tail after the last download is
7156
+ // O(1); new-files still emits (with addedBy: null) so downstream
7157
+ // consumers see the full list.
7158
+ describe("bulk-sync post-pull tail bypass (new-files > cap)", () => {
7159
+ const bulkFiles = Array.from({ length: 3 }, (_, i) => ({
7160
+ key: `bulk/file-${i}.md`,
7161
+ size: 10,
7162
+ lastModified: new Date(),
7163
+ etag: `"bulk-${i}"`,
7164
+ }));
7165
+ beforeEach(() => {
7166
+ // Cap at 2 so a 3-file remote trips the bypass in this test suite.
7167
+ setNewFilesEnrichmentBulkCapForTesting(2);
7168
+ });
7169
+ afterEach(() => {
7170
+ setNewFilesEnrichmentBulkCapForTesting(undefined);
7171
+ });
7172
+ it("skips per-file HeadObject enrichment when new files exceed the cap", async () => {
7173
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(bulkFiles);
7174
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue({
7175
+ lastModified: new Date(),
7176
+ etag: '"any"',
7177
+ size: 10,
7178
+ metadata: { "created-by": "should-not-be-read@example.com" },
7179
+ });
7180
+ const newFilesEvents = [];
7181
+ await sync({
7182
+ company: "acme",
7183
+ vaultConfig: mockConfig,
7184
+ hqRoot: tmpDir,
7185
+ onEvent: (e) => {
7186
+ if (e.type === "new-files")
7187
+ newFilesEvents.push(e);
7188
+ },
7189
+ });
7190
+ // Bypass path: enrichment loop does not run for any of the 3 new
7191
+ // files, and the entire pull tail (HEAD + notify POST) is O(1).
7192
+ expect(vi.mocked(s3Module.headRemoteFile)).not.toHaveBeenCalled();
7193
+ expect(newFilesEvents).toHaveLength(1);
7194
+ expect(newFilesEvents[0].files).toHaveLength(3);
7195
+ for (const f of newFilesEvents[0].files) {
7196
+ expect(f.addedBy).toBeNull();
7197
+ }
7198
+ });
7199
+ it("skips the /v1/notify/file-added POST when new files exceed the cap", async () => {
7200
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce(bulkFiles);
7201
+ await sync({
7202
+ company: "acme",
7203
+ vaultConfig: mockConfig,
7204
+ hqRoot: tmpDir,
7205
+ onEvent: () => { },
7206
+ });
7207
+ const calls = vi.mocked(globalThis.fetch).mock.calls;
7208
+ const notifyCalls = calls.filter(([u]) => String(u).includes("/v1/notify/file-added"));
7209
+ expect(notifyCalls).toHaveLength(0);
7210
+ });
7211
+ it("still enriches when new files fit under the cap", async () => {
7212
+ // Two default remote files, cap 2 → NOT over cap → enrichment runs.
7213
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue({
7214
+ lastModified: new Date(),
7215
+ etag: '"any"',
7216
+ size: 10,
7217
+ metadata: { "created-by": "author@example.com" },
7218
+ });
7219
+ const newFilesEvents = [];
7220
+ await sync({
7221
+ company: "acme",
7222
+ vaultConfig: mockConfig,
7223
+ hqRoot: tmpDir,
7224
+ onEvent: (e) => {
7225
+ if (e.type === "new-files")
7226
+ newFilesEvents.push(e);
7227
+ },
7228
+ });
7229
+ expect(vi.mocked(s3Module.headRemoteFile)).toHaveBeenCalled();
7230
+ expect(newFilesEvents).toHaveLength(1);
7231
+ for (const f of newFilesEvents[0].files) {
7232
+ expect(f.addedBy).toBe("author@example.com");
7233
+ }
7234
+ });
7235
+ });
7104
7236
  it("stamps progress.author from the downloaded object's created-by metadata", async () => {
7105
7237
  vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([
7106
7238
  { key: "docs/authored.md", size: 10, lastModified: new Date(), etag: '"e1"' },