@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +17 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/sync.d.ts +26 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +90 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +133 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/sync-progress.d.ts.map +1 -1
- package/dist/sync-progress.js +29 -0
- package/dist/sync-progress.js.map +1 -1
- package/dist/sync-progress.test.js +86 -0
- package/dist/sync-progress.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -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"' },
|