@indigoai-us/hq-cloud 6.16.58-alpha.3 → 6.16.59
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 +0 -14
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +89 -57
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +313 -56
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +3 -12
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +3 -65
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +1 -172
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -51
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +165 -409
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +2 -19
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +5 -40
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +0 -47
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +13 -59
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +4 -48
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +229 -1499
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +0 -19
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +0 -55
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +1 -87
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +0 -8
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -12
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +152 -302
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +126 -256
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +34 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +92 -21
- package/dist/cognito-auth.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +92 -12
- package/dist/context.js.map +1 -1
- package/dist/context.test.js +77 -2
- package/dist/context.test.js.map +1 -1
- package/dist/ignore.d.ts +10 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +47 -5
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +30 -3
- package/dist/ignore.test.js.map +1 -1
- package/dist/journal-row-store.d.ts +5 -40
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +21 -240
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +0 -54
- package/dist/journal-row-store.test.js.map +1 -1
- package/dist/journal-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +6 -11
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +7 -324
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +16 -51
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +79 -303
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +0 -15
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +0 -15
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/lib/net-errors.d.ts +12 -0
- package/dist/lib/net-errors.d.ts.map +1 -1
- package/dist/lib/net-errors.js +35 -0
- package/dist/lib/net-errors.js.map +1 -1
- package/dist/lib/net-errors.test.js +19 -1
- package/dist/lib/net-errors.test.js.map +1 -1
- package/dist/machine-auth.test.js +6 -4
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/machine-token-cache-isolation.test.d.ts +24 -0
- package/dist/machine-token-cache-isolation.test.d.ts.map +1 -0
- package/dist/machine-token-cache-isolation.test.js +241 -0
- package/dist/machine-token-cache-isolation.test.js.map +1 -0
- package/dist/manifest/build-manifest-resident-parity.test.js +3 -3
- package/dist/manifest/build-manifest-resident-parity.test.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +3 -10
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +0 -35
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +2 -9
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +2 -59
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +0 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +2 -20
- package/dist/s3.js.map +1 -1
- package/dist/sync/area-ledger.d.ts +3 -27
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +39 -149
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +5 -192
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +0 -29
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/realtime-telemetry.d.ts +1 -1
- package/dist/sync/realtime-telemetry.d.ts.map +1 -1
- package/dist/sync/realtime-telemetry.js +6 -14
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +16 -136
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/state-store.d.ts +0 -6
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +2 -14
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watcher.d.ts +0 -12
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +0 -131
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +0 -63
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner.test.js +0 -266
- package/dist/cli/onepass-pull-planner.test.js.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
- package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
- package/dist/s3.list-cost.test.d.ts +0 -2
- package/dist/s3.list-cost.test.d.ts.map +0 -1
- package/dist/s3.list-cost.test.js +0 -51
- package/dist/s3.list-cost.test.js.map +0 -1
- package/dist/sync/remote-file-spool.d.ts +0 -16
- package/dist/sync/remote-file-spool.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.js +0 -91
- package/dist/sync/remote-file-spool.js.map +0 -1
- package/dist/sync/remote-file-spool.test.d.ts +0 -2
- package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.test.js +0 -77
- package/dist/sync/remote-file-spool.test.js.map +0 -1
|
@@ -10,9 +10,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as os from "os";
|
|
12
12
|
import * as path from "path";
|
|
13
|
-
import * as v8 from "node:v8";
|
|
14
|
-
import * as vm from "node:vm";
|
|
15
|
-
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
16
13
|
import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
|
|
17
14
|
import { SESSION_LOGS_PUSH_ONLY_PREFIX } from "../sync/pull-scope.js";
|
|
18
15
|
import { AUTH_WAIT_INITIAL_RECHECK_MS, AUTH_WAIT_RESUME_MESSAGE, AUTH_WAIT_START_MESSAGE, AUTH_WAIT_STEADY_RECHECK_MS, } from "./sync-runner-auth-wait.js";
|
|
@@ -20,20 +17,17 @@ import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
|
20
17
|
import { setBandwidthGovernorForTesting } from "../bandwidth.js";
|
|
21
18
|
import { POST_SYNC_FLAG_KEYS, } from "./sync-runner-post-sync-flags.js";
|
|
22
19
|
import { FakeClock, TreeWatcher, } from "../watcher.js";
|
|
23
|
-
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, resolveWatchPollMs,
|
|
24
|
-
import { AreaLedgerMigration, JournalStore, openJournalStateStoreForMigration, PERSONAL_VAULT_JOURNAL_SLUG, readJournal,
|
|
25
|
-
import { AreaLedger } from "../sync/area-ledger.js";
|
|
20
|
+
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, resolveWatchPollMs, normalizeCoordinatorPaths, shouldQuarantineWatcherDeleteIntents, shouldUseAreaStorage, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
|
|
21
|
+
import { AreaLedgerMigration, JournalStore, openJournalStateStoreForMigration, PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
|
|
26
22
|
import { AreaResolver } from "../sync/area-resolver.js";
|
|
27
|
-
import { AREA_LEGACY_MIRROR_LINGER_MS } from "../sync/area-ledger-migration.js";
|
|
28
23
|
import { AreaMaintenanceCursorStore } from "../sync/per-area-maintenance.js";
|
|
29
24
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
30
25
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
31
26
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
32
27
|
import { getStateStoreSnapshotRecoveryCountForTest, resetStateStoreSnapshotRecoveryCountForTest, StateStoreLockError, } from "../sync/state-store.js";
|
|
33
|
-
import { PARTIAL_SYNC_EXIT
|
|
28
|
+
import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
34
29
|
import { isCoveredByAny } from "../prefix-coalesce.js";
|
|
35
30
|
import { VaultAuthError, VaultNotFoundError } from "../vault-client.js";
|
|
36
|
-
import { buildRoutePullArgv } from "./sync-runner-watch-routes.js";
|
|
37
31
|
import { CognitoAuthError, CognitoRefreshError } from "../cognito-auth.js";
|
|
38
32
|
import { reconcileCompanyManifest, } from "../manifest-reconcile.js";
|
|
39
33
|
import yaml from "js-yaml";
|
|
@@ -84,7 +78,11 @@ describe("area storage rollout gate", () => {
|
|
|
84
78
|
}),
|
|
85
79
|
legacyStore: openJournalStateStoreForMigration(slug),
|
|
86
80
|
});
|
|
87
|
-
|
|
81
|
+
// Production cutOver budgets 1s. Under `pnpm run coverage` the copy+parity
|
|
82
|
+
// work can exceed that and leave the phase at catching-up, which is what
|
|
83
|
+
// failed CI on this PR. The gate under test is the storage flag, not the
|
|
84
|
+
// wall-clock budget, so give the helper enough time.
|
|
85
|
+
expect(migration.cutOver(60_000).phase).toBe("area-authoritative");
|
|
88
86
|
expect(shouldUseAreaStorage(false, __runnerTestStateDir, slug)).toBe(false);
|
|
89
87
|
expect(shouldUseAreaStorage(true, __runnerTestStateDir, slug)).toBe(true);
|
|
90
88
|
expect(shouldUseAreaStorage("auto", __runnerTestStateDir, slug)).toBe(true);
|
|
@@ -168,7 +166,6 @@ function makeVaultStub(opts = {}) {
|
|
|
168
166
|
bucketName: `bucket-${uid}`,
|
|
169
167
|
status: "active",
|
|
170
168
|
})),
|
|
171
|
-
findInMyNamespace: opts.findInMyNamespace ?? (() => Promise.resolve(null)),
|
|
172
169
|
listByType: opts.listPersons ??
|
|
173
170
|
(() => Promise.resolve([])),
|
|
174
171
|
},
|
|
@@ -961,164 +958,6 @@ describe("target resolution", () => {
|
|
|
961
958
|
const call = deps.sync.mock.calls[0][0];
|
|
962
959
|
expect(call.company).toBe("cmp_abc");
|
|
963
960
|
});
|
|
964
|
-
it("returns the unresolved-company exit for an empty single-company plan instead of emitting a clean all-complete", async () => {
|
|
965
|
-
const deps = makeDeps({
|
|
966
|
-
createVaultClient: () => makeVaultStub({
|
|
967
|
-
entityGet: () => Promise.resolve(undefined),
|
|
968
|
-
}),
|
|
969
|
-
});
|
|
970
|
-
expect(await runRunner(["--company", "cmp_missing"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
|
|
971
|
-
expect(deps.stderr.events()).toContainEqual({
|
|
972
|
-
type: "error",
|
|
973
|
-
company: "cmp_missing",
|
|
974
|
-
path: "(company-resolution)",
|
|
975
|
-
message: "company entity lookup returned no entity",
|
|
976
|
-
});
|
|
977
|
-
expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
|
|
978
|
-
expect(deps.sync).not.toHaveBeenCalled();
|
|
979
|
-
});
|
|
980
|
-
it("returns the unresolved-company exit when a cmp-prefixed route gets a 404", async () => {
|
|
981
|
-
const client = makeVaultStub({
|
|
982
|
-
entityGet: () => Promise.reject(new VaultNotFoundError("company deleted")),
|
|
983
|
-
});
|
|
984
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(null);
|
|
985
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
986
|
-
const deps = makeDeps({
|
|
987
|
-
createVaultClient: () => client,
|
|
988
|
-
});
|
|
989
|
-
expect(await runRunner(["--company", "cmp_ghost"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
|
|
990
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_ghost");
|
|
991
|
-
expect(deps.stderr.events()).toContainEqual({
|
|
992
|
-
type: "error",
|
|
993
|
-
company: "cmp_ghost",
|
|
994
|
-
path: "(company-resolution)",
|
|
995
|
-
message: "company entity lookup returned 404",
|
|
996
|
-
});
|
|
997
|
-
expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
|
|
998
|
-
});
|
|
999
|
-
it("reports an intentional skipCompanies skip without treating it as a failed pass", async () => {
|
|
1000
|
-
const previous = process.env.HQ_SYNC_SKIP_COMPANIES;
|
|
1001
|
-
process.env.HQ_SYNC_SKIP_COMPANIES = "paused";
|
|
1002
|
-
try {
|
|
1003
|
-
const deps = makeDeps({
|
|
1004
|
-
createVaultClient: () => makeVaultStub({
|
|
1005
|
-
entityGet: (uid) => Promise.resolve({
|
|
1006
|
-
uid,
|
|
1007
|
-
slug: "paused",
|
|
1008
|
-
type: "company",
|
|
1009
|
-
status: "active",
|
|
1010
|
-
}),
|
|
1011
|
-
}),
|
|
1012
|
-
});
|
|
1013
|
-
expect(await runRunner(["--company", "cmp_paused"], deps)).toBe(0);
|
|
1014
|
-
expect(deps.stdout.events()).toContainEqual({
|
|
1015
|
-
type: "company-skipped",
|
|
1016
|
-
company: "paused",
|
|
1017
|
-
reason: "skip-companies",
|
|
1018
|
-
});
|
|
1019
|
-
expect(deps.stderr.events().some((event) => event.type === "error")).toBe(false);
|
|
1020
|
-
expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
|
|
1021
|
-
expect(deps.sync).not.toHaveBeenCalled();
|
|
1022
|
-
}
|
|
1023
|
-
finally {
|
|
1024
|
-
if (previous === undefined)
|
|
1025
|
-
delete process.env.HQ_SYNC_SKIP_COMPANIES;
|
|
1026
|
-
else
|
|
1027
|
-
process.env.HQ_SYNC_SKIP_COMPANIES = previous;
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
it("resolves a slug emitted by the company watch route to its namespace UID", async () => {
|
|
1031
|
-
const company = {
|
|
1032
|
-
uid: "cmp_indigo",
|
|
1033
|
-
slug: "indigo",
|
|
1034
|
-
type: "company",
|
|
1035
|
-
status: "active",
|
|
1036
|
-
bucketName: "bucket-cmp_indigo",
|
|
1037
|
-
};
|
|
1038
|
-
const client = makeVaultStub({
|
|
1039
|
-
entityGet: (uid) => Promise.resolve(uid === company.uid ? company : undefined),
|
|
1040
|
-
});
|
|
1041
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(company);
|
|
1042
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
1043
|
-
const deps = makeDeps({ createVaultClient: () => client });
|
|
1044
|
-
const argv = buildRoutePullArgv({ kind: "company", slug: "indigo" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
|
|
1045
|
-
expect(argv).toEqual([
|
|
1046
|
-
"--company",
|
|
1047
|
-
"indigo",
|
|
1048
|
-
"--direction",
|
|
1049
|
-
"pull",
|
|
1050
|
-
"--hq-root",
|
|
1051
|
-
"/tmp/hq",
|
|
1052
|
-
]);
|
|
1053
|
-
expect(await runRunner(argv, deps)).toBe(0);
|
|
1054
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", "indigo");
|
|
1055
|
-
expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: "cmp_indigo" }));
|
|
1056
|
-
expect(deps.stdout.events()).toContainEqual({
|
|
1057
|
-
type: "fanout-plan",
|
|
1058
|
-
companies: [
|
|
1059
|
-
expect.objectContaining({ uid: "cmp_indigo", slug: "indigo" }),
|
|
1060
|
-
],
|
|
1061
|
-
});
|
|
1062
|
-
});
|
|
1063
|
-
it("resolves a cmp-prefixed directory slug in the caller namespace before treating it as a UID", async () => {
|
|
1064
|
-
const company = {
|
|
1065
|
-
uid: "cmp_01JTEAM000000000000000000",
|
|
1066
|
-
slug: "cmp_team",
|
|
1067
|
-
type: "company",
|
|
1068
|
-
status: "active",
|
|
1069
|
-
bucketName: "bucket-cmp-team",
|
|
1070
|
-
};
|
|
1071
|
-
const get = vi.fn().mockImplementation((uid) => Promise.resolve(uid === company.uid ? company : undefined));
|
|
1072
|
-
const client = makeVaultStub({ entityGet: get });
|
|
1073
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(company);
|
|
1074
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
1075
|
-
const deps = makeDeps({ createVaultClient: () => client });
|
|
1076
|
-
const argv = buildRoutePullArgv({ kind: "company", slug: "cmp_team" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
|
|
1077
|
-
expect(await runRunner(argv, deps)).toBe(0);
|
|
1078
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_team");
|
|
1079
|
-
expect(get).toHaveBeenCalledWith(company.uid);
|
|
1080
|
-
expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: company.uid }));
|
|
1081
|
-
expect(deps.stdout.events()).toContainEqual({
|
|
1082
|
-
type: "fanout-plan",
|
|
1083
|
-
companies: [expect.objectContaining({ uid: company.uid, slug: "cmp_team" })],
|
|
1084
|
-
});
|
|
1085
|
-
});
|
|
1086
|
-
it("falls back to a literal company UID after caller-namespace slug resolution misses", async () => {
|
|
1087
|
-
const uid = "cmp_01JUID0000000000000000000";
|
|
1088
|
-
const company = {
|
|
1089
|
-
uid,
|
|
1090
|
-
slug: "operator-company",
|
|
1091
|
-
type: "company",
|
|
1092
|
-
status: "active",
|
|
1093
|
-
bucketName: "bucket-operator-company",
|
|
1094
|
-
};
|
|
1095
|
-
const get = vi.fn().mockResolvedValue(company);
|
|
1096
|
-
const client = makeVaultStub({ entityGet: get });
|
|
1097
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(null);
|
|
1098
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
1099
|
-
const deps = makeDeps({ createVaultClient: () => client });
|
|
1100
|
-
expect(await runRunner(["--company", uid], deps)).toBe(0);
|
|
1101
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", uid);
|
|
1102
|
-
expect(get).toHaveBeenCalledWith(uid);
|
|
1103
|
-
expect(deps.sync).toHaveBeenCalledWith(expect.objectContaining({ company: uid }));
|
|
1104
|
-
});
|
|
1105
|
-
it("reports a foreign company slug when the literal UID fallback finds nothing", async () => {
|
|
1106
|
-
const get = vi.fn().mockRejectedValue(new VaultNotFoundError("company not found"));
|
|
1107
|
-
const client = makeVaultStub({ entityGet: get });
|
|
1108
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(null);
|
|
1109
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
1110
|
-
const deps = makeDeps({ createVaultClient: () => client });
|
|
1111
|
-
expect(await runRunner(["--company", "indigo"], deps)).toBe(UNRESOLVED_COMPANY_EXIT);
|
|
1112
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", "indigo");
|
|
1113
|
-
expect(get).toHaveBeenCalledWith("indigo");
|
|
1114
|
-
expect(deps.sync).not.toHaveBeenCalled();
|
|
1115
|
-
expect(deps.stderr.events()).toContainEqual({
|
|
1116
|
-
type: "error",
|
|
1117
|
-
company: "indigo",
|
|
1118
|
-
path: "(company-resolution)",
|
|
1119
|
-
message: "company entity lookup returned 404",
|
|
1120
|
-
});
|
|
1121
|
-
});
|
|
1122
961
|
});
|
|
1123
962
|
// ---------------------------------------------------------------------------
|
|
1124
963
|
// --personal mode (personal-vault-only, skip company fanout)
|
|
@@ -1716,43 +1555,6 @@ describe("automatic journal maintenance", () => {
|
|
|
1716
1555
|
},
|
|
1717
1556
|
]);
|
|
1718
1557
|
});
|
|
1719
|
-
it("signals watch readiness after journal preparation and before fanout", async () => {
|
|
1720
|
-
const order = [];
|
|
1721
|
-
let releaseFanout = () => { };
|
|
1722
|
-
const fanout = new Promise((resolve) => { releaseFanout = resolve; });
|
|
1723
|
-
const deps = makeDeps({
|
|
1724
|
-
createVaultClient: () => makeVaultStub({
|
|
1725
|
-
memberships: [{ companyUid: "cmp_indigo" }],
|
|
1726
|
-
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
1727
|
-
}),
|
|
1728
|
-
repairJournalStateIfNeeded: vi.fn((slug) => {
|
|
1729
|
-
order.push(`repair:${slug}`);
|
|
1730
|
-
return { status: "not-needed", slug };
|
|
1731
|
-
}),
|
|
1732
|
-
migratePersonalVaultJournal: vi.fn(() => {
|
|
1733
|
-
order.push("seed:personal");
|
|
1734
|
-
}),
|
|
1735
|
-
onJournalsReady: () => {
|
|
1736
|
-
order.push("journals-ready");
|
|
1737
|
-
},
|
|
1738
|
-
sync: vi.fn(async () => {
|
|
1739
|
-
order.push("fanout");
|
|
1740
|
-
await fanout;
|
|
1741
|
-
return defaultSyncResult();
|
|
1742
|
-
}),
|
|
1743
|
-
});
|
|
1744
|
-
const running = runRunner(["--companies", "--skip-personal"], deps);
|
|
1745
|
-
await vi.waitFor(() => expect(order).toContain("fanout"));
|
|
1746
|
-
expect(order).toEqual([
|
|
1747
|
-
"repair:indigo",
|
|
1748
|
-
"repair:personal",
|
|
1749
|
-
"seed:personal",
|
|
1750
|
-
"journals-ready",
|
|
1751
|
-
"fanout",
|
|
1752
|
-
]);
|
|
1753
|
-
releaseFanout();
|
|
1754
|
-
await expect(running).resolves.toBe(0);
|
|
1755
|
-
});
|
|
1756
1558
|
it("continues the pass when one journal repair is transiently lock-contended", async () => {
|
|
1757
1559
|
const repaired = [];
|
|
1758
1560
|
const sync = vi.fn().mockResolvedValue(defaultSyncResult());
|
|
@@ -2361,6 +2163,93 @@ describe("per-company fanout", () => {
|
|
|
2361
2163
|
expect.objectContaining({ company: "acme", status: "transient-network" }),
|
|
2362
2164
|
]);
|
|
2363
2165
|
});
|
|
2166
|
+
it("a diagnostic tombstone HEAD deferral does not error the company or exit 2", async () => {
|
|
2167
|
+
const deps = makeDeps({
|
|
2168
|
+
createVaultClient: () => makeVaultStub({
|
|
2169
|
+
memberships: [{ companyUid: "cmp_ridge" }],
|
|
2170
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "ridge" }),
|
|
2171
|
+
}),
|
|
2172
|
+
sync: vi.fn(async (opts) => {
|
|
2173
|
+
opts.onEvent?.({
|
|
2174
|
+
type: "error",
|
|
2175
|
+
diagnostic: true,
|
|
2176
|
+
path: "skills/wbr-finalize/SKILL.md",
|
|
2177
|
+
message: "tombstone HEAD verify failed (deferring): RateLimited rate limited (100/hr) — get skills/wbr-finalize/SKILL.md deferred to next sync",
|
|
2178
|
+
});
|
|
2179
|
+
return defaultSyncResult();
|
|
2180
|
+
}),
|
|
2181
|
+
});
|
|
2182
|
+
expect(await runRunner(["--companies"], deps)).toBe(0);
|
|
2183
|
+
expect(deps.stderr.events().some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
|
|
2184
|
+
expect(deps.stderr.events()).toContainEqual(expect.objectContaining({
|
|
2185
|
+
type: "error",
|
|
2186
|
+
diagnostic: true,
|
|
2187
|
+
company: "ridge",
|
|
2188
|
+
path: "skills/wbr-finalize/SKILL.md",
|
|
2189
|
+
}));
|
|
2190
|
+
const all = deps.stdout.events().find((e) => e.type === "all-complete");
|
|
2191
|
+
expect(all.errors).toEqual([]);
|
|
2192
|
+
expect(all.transient).toEqual([]);
|
|
2193
|
+
expect(all.companies).toContainEqual(expect.objectContaining({ company: "ridge", status: "complete" }));
|
|
2194
|
+
});
|
|
2195
|
+
it("429 on every entity-fetch attempt is reported as transient, not errored", async () => {
|
|
2196
|
+
const throttle = Object.assign(new Error('Failed to fetch entity cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG: 429 {"message":"Too Many Requests"}'), { statusCode: 429 });
|
|
2197
|
+
const deps = makeDeps({
|
|
2198
|
+
createVaultClient: () => makeVaultStub({
|
|
2199
|
+
memberships: [{ companyUid: "cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG" }],
|
|
2200
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
2201
|
+
}),
|
|
2202
|
+
sync: vi.fn().mockRejectedValue(throttle),
|
|
2203
|
+
});
|
|
2204
|
+
expect(await runRunner(["--companies"], deps)).toBe(TRANSIENT_NETWORK_EXIT);
|
|
2205
|
+
expect(deps.stderr
|
|
2206
|
+
.events()
|
|
2207
|
+
.some((e) => e.type === "error" && e.company === "indigo")).toBe(false);
|
|
2208
|
+
expect(deps.stdout.events().find((e) => e.type === "transient-network")).toMatchObject({
|
|
2209
|
+
company: "indigo",
|
|
2210
|
+
path: "(company)",
|
|
2211
|
+
message: expect.stringContaining("Failed to fetch entity cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG: 429"),
|
|
2212
|
+
});
|
|
2213
|
+
const all = deps.stdout
|
|
2214
|
+
.events()
|
|
2215
|
+
.find((e) => e.type === "all-complete");
|
|
2216
|
+
expect(all.partial).toBe(true);
|
|
2217
|
+
expect(all.errors).toEqual([]);
|
|
2218
|
+
expect(all.transient).toEqual([
|
|
2219
|
+
{
|
|
2220
|
+
company: "indigo",
|
|
2221
|
+
message: expect.stringContaining('Failed to fetch entity cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG: 429 {"message":"Too Many Requests"}'),
|
|
2222
|
+
},
|
|
2223
|
+
]);
|
|
2224
|
+
expect(all.companies).toEqual([
|
|
2225
|
+
expect.objectContaining({ company: "indigo", status: "transient-network" }),
|
|
2226
|
+
]);
|
|
2227
|
+
});
|
|
2228
|
+
it("a 403 entity fetch is still errored immediately with no retry", async () => {
|
|
2229
|
+
const forbidden = Object.assign(new Error('Failed to fetch entity cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG: 403 {"message":"Forbidden"}'), { statusCode: 403 });
|
|
2230
|
+
const deps = makeDeps({
|
|
2231
|
+
createVaultClient: () => makeVaultStub({
|
|
2232
|
+
memberships: [{ companyUid: "cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG" }],
|
|
2233
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "indigo" }),
|
|
2234
|
+
}),
|
|
2235
|
+
sync: vi.fn().mockRejectedValue(forbidden),
|
|
2236
|
+
});
|
|
2237
|
+
expect(await runRunner(["--companies"], deps)).toBe(PARTIAL_SYNC_EXIT);
|
|
2238
|
+
expect(deps.stderr.events().find((e) => e.type === "error" && e.company === "indigo" && e.path === "(company)")).toMatchObject({
|
|
2239
|
+
message: expect.stringContaining("Failed to fetch entity cmp_01KQ2RYAHXHDPCTY9GPQPTH3DG: 403"),
|
|
2240
|
+
});
|
|
2241
|
+
expect(deps.stdout
|
|
2242
|
+
.events()
|
|
2243
|
+
.some((e) => e.type === "transient-network")).toBe(false);
|
|
2244
|
+
const all = deps.stdout
|
|
2245
|
+
.events()
|
|
2246
|
+
.find((e) => e.type === "all-complete");
|
|
2247
|
+
expect(all.errors).toEqual([expect.objectContaining({ company: "indigo" })]);
|
|
2248
|
+
expect(all.transient).toEqual([]);
|
|
2249
|
+
expect(all.companies).toEqual([
|
|
2250
|
+
expect.objectContaining({ company: "indigo", status: "errored" }),
|
|
2251
|
+
]);
|
|
2252
|
+
});
|
|
2364
2253
|
it("keeps deterministic company failures as exit 2 errors", async () => {
|
|
2365
2254
|
const deps = makeDeps({
|
|
2366
2255
|
createVaultClient: () => makeVaultStub({
|
|
@@ -4260,17 +4149,6 @@ describe("routeChangeToTarget", () => {
|
|
|
4260
4149
|
});
|
|
4261
4150
|
expect(routeChangeToTarget("companies/acme/projects/x/prd.json")).toEqual({ kind: "company", slug: "acme" });
|
|
4262
4151
|
});
|
|
4263
|
-
it("keeps a company-root directory delete attributable without routing company-root files", () => {
|
|
4264
|
-
// A whole-company removal has no child path, but it still owns every
|
|
4265
|
-
// journal row below the slug and must reach the delete-intent guard.
|
|
4266
|
-
expect(routeChangeToTarget("companies/indigo", "unlinkDir")).toEqual({
|
|
4267
|
-
kind: "company",
|
|
4268
|
-
slug: "indigo",
|
|
4269
|
-
});
|
|
4270
|
-
// A file directly under companies/ is shared metadata, never a tenant.
|
|
4271
|
-
expect(routeChangeToTarget("companies/manifest.yaml", "change")).toBeNull();
|
|
4272
|
-
expect(routeChangeToTarget("companies/.DS_Store", "add")).toBeNull();
|
|
4273
|
-
});
|
|
4274
4152
|
it("routes non-company top-levels to the personal target", () => {
|
|
4275
4153
|
expect(routeChangeToTarget("personal/notes.md")).toEqual({
|
|
4276
4154
|
kind: "personal",
|
|
@@ -4295,12 +4173,6 @@ describe("routeChangeToTarget", () => {
|
|
|
4295
4173
|
// edit always lands at companies/<slug>/..., never at bare "companies".
|
|
4296
4174
|
expect(routeChangeToTarget("companies")).toBeNull();
|
|
4297
4175
|
expect(routeChangeToTarget("companies/")).toBeNull();
|
|
4298
|
-
// The companies directory itself contains shared manifest and scaffold
|
|
4299
|
-
// files. They do not belong to a company journal, so a direct child must
|
|
4300
|
-
// never become a bogus `--company <filename>` pass.
|
|
4301
|
-
expect(routeChangeToTarget("companies/manifest.yaml")).toBeNull();
|
|
4302
|
-
expect(routeChangeToTarget("companies/.DS_Store")).toBeNull();
|
|
4303
|
-
expect(routeChangeToTarget("companies/../personal/notes.md")).toBeNull();
|
|
4304
4176
|
});
|
|
4305
4177
|
it("does not route the company scaffold to cloud sync", () => {
|
|
4306
4178
|
expect(routeChangeToTarget("companies/_template/knowledge/README.md")).toBeNull();
|
|
@@ -4382,26 +4254,6 @@ async function flushLoopMicrotasks(n = 8) {
|
|
|
4382
4254
|
for (let i = 0; i < n; i++)
|
|
4383
4255
|
await Promise.resolve();
|
|
4384
4256
|
}
|
|
4385
|
-
/** Retained journal rows live in typed-array backing stores as well as heap. */
|
|
4386
|
-
function forceGarbageCollectionForTest() {
|
|
4387
|
-
const existing = globalThis.gc;
|
|
4388
|
-
if (typeof existing === "function") {
|
|
4389
|
-
existing();
|
|
4390
|
-
existing();
|
|
4391
|
-
return;
|
|
4392
|
-
}
|
|
4393
|
-
v8.setFlagsFromString("--expose-gc");
|
|
4394
|
-
const collect = vm.runInNewContext("gc");
|
|
4395
|
-
v8.setFlagsFromString("--no-expose-gc");
|
|
4396
|
-
if (typeof collect !== "function")
|
|
4397
|
-
throw new Error("test could not expose GC");
|
|
4398
|
-
collect();
|
|
4399
|
-
collect();
|
|
4400
|
-
}
|
|
4401
|
-
function retainedProcessBytes() {
|
|
4402
|
-
const usage = process.memoryUsage();
|
|
4403
|
-
return usage.heapUsed + usage.external + usage.arrayBuffers;
|
|
4404
|
-
}
|
|
4405
4257
|
function makeSteppableSleep() {
|
|
4406
4258
|
const pending = [];
|
|
4407
4259
|
return {
|
|
@@ -4414,19 +4266,8 @@ function makeSteppableSleep() {
|
|
|
4414
4266
|
throw new Error("poll sleep was not pending");
|
|
4415
4267
|
resolve();
|
|
4416
4268
|
},
|
|
4417
|
-
pendingCount() {
|
|
4418
|
-
return pending.length;
|
|
4419
|
-
},
|
|
4420
4269
|
};
|
|
4421
4270
|
}
|
|
4422
|
-
async function waitForPollSleep(sleep) {
|
|
4423
|
-
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
4424
|
-
if (sleep.pendingCount() > 0)
|
|
4425
|
-
return;
|
|
4426
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
4427
|
-
}
|
|
4428
|
-
throw new Error("timed out waiting for a poll sleep");
|
|
4429
|
-
}
|
|
4430
4271
|
// ---------------------------------------------------------------------------
|
|
4431
4272
|
// Adaptive remote-poll backoff — whole-box CPU aware (no --poll-remote-ms)
|
|
4432
4273
|
// ---------------------------------------------------------------------------
|
|
@@ -5518,83 +5359,6 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
5518
5359
|
triggerShutdown();
|
|
5519
5360
|
await loop;
|
|
5520
5361
|
});
|
|
5521
|
-
it.each([
|
|
5522
|
-
["serialized", "0"],
|
|
5523
|
-
["parallel coordinator", "1"],
|
|
5524
|
-
])("starts event ingress once after journals are ready while the first %s full pass is unfinished", async (_mode, coordinator) => {
|
|
5525
|
-
const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
5526
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = coordinator;
|
|
5527
|
-
const watcher = makeWatcherStub();
|
|
5528
|
-
const createWatcher = vi.fn(() => watcher);
|
|
5529
|
-
let releaseFullPass = () => { };
|
|
5530
|
-
const fullPass = new Promise((resolve) => {
|
|
5531
|
-
releaseFullPass = resolve;
|
|
5532
|
-
});
|
|
5533
|
-
let activeDurableWriters = 0;
|
|
5534
|
-
let maxDurableWriters = 0;
|
|
5535
|
-
const runPass = vi.fn(async (...args) => {
|
|
5536
|
-
const journalsReady = args[2];
|
|
5537
|
-
activeDurableWriters += 1;
|
|
5538
|
-
maxDurableWriters = Math.max(maxDurableWriters, activeDurableWriters);
|
|
5539
|
-
journalsReady?.();
|
|
5540
|
-
await fullPass;
|
|
5541
|
-
activeDurableWriters -= 1;
|
|
5542
|
-
return completedPassOutcome();
|
|
5543
|
-
});
|
|
5544
|
-
let shutdown = () => { };
|
|
5545
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
5546
|
-
runPass: runPass,
|
|
5547
|
-
runPassSupportsCooperativeCheckpoint: true,
|
|
5548
|
-
createWatcher,
|
|
5549
|
-
sleep: () => new Promise(() => { }),
|
|
5550
|
-
onShutdownSignal: (handler) => {
|
|
5551
|
-
shutdown = handler;
|
|
5552
|
-
return () => { };
|
|
5553
|
-
},
|
|
5554
|
-
});
|
|
5555
|
-
try {
|
|
5556
|
-
await flushLoopMicrotasks(30);
|
|
5557
|
-
expect(runPass).toHaveBeenCalledOnce();
|
|
5558
|
-
expect(createWatcher).toHaveBeenCalledOnce();
|
|
5559
|
-
expect(watcher.started).toBe(true);
|
|
5560
|
-
expect(maxDurableWriters).toBe(1);
|
|
5561
|
-
await flushLoopMicrotasks();
|
|
5562
|
-
expect(runPass).toHaveBeenCalledOnce();
|
|
5563
|
-
}
|
|
5564
|
-
finally {
|
|
5565
|
-
releaseFullPass();
|
|
5566
|
-
shutdown();
|
|
5567
|
-
await loop;
|
|
5568
|
-
if (previousCoordinator === undefined)
|
|
5569
|
-
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
5570
|
-
else
|
|
5571
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
5572
|
-
}
|
|
5573
|
-
});
|
|
5574
|
-
it.each([
|
|
5575
|
-
["auth-required", AUTH_REQUIRED_PASS_EXIT],
|
|
5576
|
-
["journal initialization failure", 1],
|
|
5577
|
-
])("does not arm event ingress when the first pass exits %s before journal readiness", async (_reason, exitCode) => {
|
|
5578
|
-
const watcher = makeWatcherStub();
|
|
5579
|
-
let shutdown = () => { };
|
|
5580
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
5581
|
-
runPass: vi.fn().mockResolvedValue(exitCode),
|
|
5582
|
-
createWatcher: () => watcher,
|
|
5583
|
-
sleep: () => new Promise(() => { }),
|
|
5584
|
-
onShutdownSignal: (handler) => {
|
|
5585
|
-
shutdown = handler;
|
|
5586
|
-
return () => { };
|
|
5587
|
-
},
|
|
5588
|
-
});
|
|
5589
|
-
try {
|
|
5590
|
-
await flushLoopMicrotasks(30);
|
|
5591
|
-
expect(watcher.started).toBe(false);
|
|
5592
|
-
}
|
|
5593
|
-
finally {
|
|
5594
|
-
shutdown();
|
|
5595
|
-
await expect(loop).resolves.toBe(0);
|
|
5596
|
-
}
|
|
5597
|
-
});
|
|
5598
5362
|
it("pushes a file created before watcher ready through the warm-up catch-up", async () => {
|
|
5599
5363
|
const watcher = makeBatchWatcherStub();
|
|
5600
5364
|
let markReady = () => { };
|
|
@@ -5913,7 +5677,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
5913
5677
|
});
|
|
5914
5678
|
await expect(superseded).resolves.toBeUndefined();
|
|
5915
5679
|
expect(runPass.mock.calls.filter(([argv]) => argv.includes("superseded.md"))).toHaveLength(0);
|
|
5916
|
-
expect(stderr.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain('"event":"sync.realtime.timing","schemaVersion":
|
|
5680
|
+
expect(stderr.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain('"event":"sync.realtime.timing","schemaVersion":1,"source":"receiver","direction":"pull","outcome":"refused"');
|
|
5917
5681
|
releaseHeld();
|
|
5918
5682
|
await heldReceiverWork;
|
|
5919
5683
|
await replacement;
|
|
@@ -6226,7 +5990,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6226
5990
|
triggerShutdown();
|
|
6227
5991
|
await loop;
|
|
6228
5992
|
});
|
|
6229
|
-
it("fast lane: a small watcher batch
|
|
5993
|
+
it("fast lane: a small watcher batch dispatches during a slow bulk pass", async () => {
|
|
6230
5994
|
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-watch-"));
|
|
6231
5995
|
const relativePath = "companies/indigo/realtime.md";
|
|
6232
5996
|
const absolutePath = path.join(hqRoot, relativePath);
|
|
@@ -6266,17 +6030,15 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6266
6030
|
expect(order).toEqual(["bulk"]);
|
|
6267
6031
|
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
6268
6032
|
await flushLoopMicrotasks(30);
|
|
6269
|
-
//
|
|
6270
|
-
// owner. Realtime is promoted at the checkpoint, never run concurrently.
|
|
6271
|
-
expect(order).toEqual(["bulk"]);
|
|
6272
|
-
releaseBulk();
|
|
6273
|
-
await flushLoopMicrotasks(30);
|
|
6033
|
+
// Regression: origin/main leaves this as ["bulk"] until releaseBulk().
|
|
6274
6034
|
expect(order).toEqual(["bulk", "fast", "fast"]);
|
|
6035
|
+
releaseBulk();
|
|
6036
|
+
await flushLoopMicrotasks();
|
|
6275
6037
|
triggerShutdown();
|
|
6276
6038
|
await loop;
|
|
6277
6039
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6278
6040
|
});
|
|
6279
|
-
it("parallel coordinator: a watcher push
|
|
6041
|
+
it("parallel coordinator: a watcher push starts while a full chore is blocked", async () => {
|
|
6280
6042
|
const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
6281
6043
|
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
6282
6044
|
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-parallel-watch-"));
|
|
@@ -6319,10 +6081,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6319
6081
|
expect(order).toEqual(["chore"]);
|
|
6320
6082
|
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
6321
6083
|
await flushLoopMicrotasks(30);
|
|
6322
|
-
expect(order).toEqual(["chore"]);
|
|
6323
|
-
releaseBulk();
|
|
6324
|
-
await flushLoopMicrotasks(30);
|
|
6325
6084
|
expect(order).toEqual(["chore", "realtime"]);
|
|
6085
|
+
releaseBulk();
|
|
6086
|
+
await flushLoopMicrotasks();
|
|
6326
6087
|
triggerShutdown();
|
|
6327
6088
|
await loop;
|
|
6328
6089
|
}
|
|
@@ -6407,7 +6168,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6407
6168
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6408
6169
|
}
|
|
6409
6170
|
});
|
|
6410
|
-
it("parallel coordinator partitions receiver events into fast and slow realtime lanes
|
|
6171
|
+
it("parallel coordinator partitions receiver events into fast and slow realtime lanes", async () => {
|
|
6411
6172
|
const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
6412
6173
|
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
6413
6174
|
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-parallel-receiver-lanes-"));
|
|
@@ -6478,7 +6239,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6478
6239
|
signal: new AbortController().signal,
|
|
6479
6240
|
});
|
|
6480
6241
|
await flushLoopMicrotasks(20);
|
|
6481
|
-
expect(order).toEqual(["slow-held"]);
|
|
6242
|
+
expect(order).toEqual(["slow-held", "fast-small"]);
|
|
6482
6243
|
releaseSlow();
|
|
6483
6244
|
await held;
|
|
6484
6245
|
await partitioned;
|
|
@@ -6644,521 +6405,23 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
6644
6405
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6645
6406
|
}
|
|
6646
6407
|
});
|
|
6647
|
-
it("
|
|
6648
|
-
const
|
|
6408
|
+
it("fast lane: prefers declared receiver sizes and falls back to the journal", async () => {
|
|
6409
|
+
const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
6649
6410
|
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
6650
|
-
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-
|
|
6651
|
-
const watchedRoot = path.join(hqRoot, "personal", "watch-load");
|
|
6652
|
-
const targetPaths = Array.from({ length: 32 }, (_, index) => `personal/watch-load/${String(index).padStart(5, "0")}/target.md`);
|
|
6653
|
-
const files = {};
|
|
6654
|
-
for (let index = 0; index < 700_000; index += 1) {
|
|
6655
|
-
files[`personal/history/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
|
|
6656
|
-
hash: `sha256:history-${index}`,
|
|
6657
|
-
size: index % 4096,
|
|
6658
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
6659
|
-
direction: "up",
|
|
6660
|
-
};
|
|
6661
|
-
}
|
|
6662
|
-
for (const target of targetPaths) {
|
|
6663
|
-
files[target] = {
|
|
6664
|
-
hash: `sha256:${target}`,
|
|
6665
|
-
size: 1,
|
|
6666
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
6667
|
-
direction: "up",
|
|
6668
|
-
};
|
|
6669
|
-
}
|
|
6670
|
-
writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
6671
|
-
version: "2",
|
|
6672
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
6673
|
-
pulls: [],
|
|
6674
|
-
files,
|
|
6675
|
-
});
|
|
6676
|
-
for (let index = 0; index < 50_000; index += 1) {
|
|
6677
|
-
const directory = path.join(watchedRoot, String(index).padStart(5, "0"));
|
|
6678
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
6679
|
-
fs.writeFileSync(path.join(directory, "seed.md"), "seed");
|
|
6680
|
-
}
|
|
6411
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-declared-size-"));
|
|
6681
6412
|
const watcher = makeBatchWatcherStub();
|
|
6682
|
-
let
|
|
6683
|
-
let
|
|
6684
|
-
const
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
scoped.files[target] = {
|
|
6695
|
-
...scoped.files[target],
|
|
6696
|
-
syncedAt: "2026-09-16T21:38:18.000Z",
|
|
6697
|
-
};
|
|
6698
|
-
writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, scoped);
|
|
6699
|
-
committedAt = performance.now();
|
|
6700
|
-
}
|
|
6701
|
-
return completedPassOutcome();
|
|
6702
|
-
});
|
|
6703
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
|
|
6704
|
-
runPass,
|
|
6705
|
-
createWatcher: () => watcher,
|
|
6706
|
-
sleep: () => new Promise(() => { }),
|
|
6707
|
-
onShutdownSignal: (handler) => {
|
|
6708
|
-
shutdown = handler;
|
|
6709
|
-
return () => { };
|
|
6710
|
-
},
|
|
6711
|
-
});
|
|
6712
|
-
try {
|
|
6713
|
-
await flushLoopMicrotasks(30);
|
|
6714
|
-
runPass.mockClear();
|
|
6715
|
-
resetStateStoreSnapshotRecoveryCountForTest();
|
|
6716
|
-
const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
|
|
6717
|
-
eventLoopDelay.enable();
|
|
6718
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
6719
|
-
const submittedAt = performance.now();
|
|
6720
|
-
let timerDelayMs;
|
|
6721
|
-
const eventLoopTurn = new Promise((resolve) => {
|
|
6722
|
-
setTimeout(() => {
|
|
6723
|
-
timerDelayMs = performance.now() - submittedAt;
|
|
6724
|
-
resolve();
|
|
6725
|
-
}, 0);
|
|
6726
|
-
});
|
|
6727
|
-
const batch = {
|
|
6728
|
-
paths: new Map(targetPaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
|
|
6729
|
-
changes: new Map(targetPaths.map((relativePath) => [
|
|
6730
|
-
path.join(hqRoot, relativePath),
|
|
6731
|
-
{ kind: "change" },
|
|
6732
|
-
])),
|
|
6733
|
-
};
|
|
6734
|
-
watcher.emit(targetPaths[0], batch);
|
|
6735
|
-
await vi.waitFor(() => expect(committedAt).toBeDefined(), { timeout: 30_000, interval: 10 });
|
|
6736
|
-
await eventLoopTurn;
|
|
6737
|
-
eventLoopDelay.disable();
|
|
6738
|
-
expect(committedAt - submittedAt).toBeLessThan(1_000);
|
|
6739
|
-
expect(Number(eventLoopDelay.max) / 1_000_000).toBeLessThan(100);
|
|
6740
|
-
expect(timerDelayMs).toBeLessThan(100);
|
|
6741
|
-
expect(getStateStoreSnapshotRecoveryCountForTest()).toBe(0);
|
|
6742
|
-
expect(runPass.mock.calls.filter(([argv]) => argv.includes("--scope-path"))).toHaveLength(1);
|
|
6743
|
-
}
|
|
6744
|
-
finally {
|
|
6745
|
-
shutdown();
|
|
6746
|
-
await loop;
|
|
6747
|
-
if (previousCoordinator === undefined)
|
|
6748
|
-
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
6749
|
-
else
|
|
6750
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
6751
|
-
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6752
|
-
}
|
|
6753
|
-
}, 180_000);
|
|
6754
|
-
it("keeps an area-authoritative 700k-row personal aggregate hot across alternating realtime and company batches", async () => {
|
|
6755
|
-
const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
6756
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
6757
|
-
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-multi-journal-watch-"));
|
|
6758
|
-
const watchedRoot = path.join(hqRoot, "personal", "watch-load");
|
|
6759
|
-
const personalPath = "personal/watch-load/00000/target.md";
|
|
6760
|
-
const companyPath = "companies/indigo/watch-load/00000/target.md";
|
|
6761
|
-
const companyKey = "watch-load/00000/target.md";
|
|
6762
|
-
const personalFiles = {};
|
|
6763
|
-
for (let index = 0; index < 700_000; index += 1) {
|
|
6764
|
-
personalFiles[`personal/history-${index % 5}/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
|
|
6765
|
-
hash: `sha256:history-${index}`,
|
|
6766
|
-
size: index % 4096,
|
|
6767
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
6768
|
-
direction: "up",
|
|
6769
|
-
};
|
|
6770
|
-
}
|
|
6771
|
-
personalFiles[personalPath] = {
|
|
6772
|
-
hash: "sha256:personal-target",
|
|
6773
|
-
size: 1,
|
|
6774
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
6775
|
-
direction: "up",
|
|
6776
|
-
};
|
|
6777
|
-
const resolver = new AreaResolver({
|
|
6778
|
-
format: 1,
|
|
6779
|
-
routerVersion: 1,
|
|
6780
|
-
areas: [
|
|
6781
|
-
...Array.from({ length: 5 }, (_, index) => ({
|
|
6782
|
-
id: `root:history-${index}`,
|
|
6783
|
-
prefixes: [`personal/history-${index}`],
|
|
6784
|
-
paths: [],
|
|
6785
|
-
})),
|
|
6786
|
-
{ id: "root:watch", prefixes: ["personal/watch-load"], paths: [] },
|
|
6787
|
-
{ id: "root:@files", prefixes: [], paths: [], rootFiles: true },
|
|
6788
|
-
],
|
|
6789
|
-
});
|
|
6790
|
-
writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
6791
|
-
version: "2",
|
|
6792
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
6793
|
-
pulls: [],
|
|
6794
|
-
files: {},
|
|
6795
|
-
});
|
|
6796
|
-
writeJournal("indigo", {
|
|
6797
|
-
version: "2",
|
|
6798
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
6799
|
-
pulls: [],
|
|
6800
|
-
files: {
|
|
6801
|
-
[companyKey]: {
|
|
6802
|
-
hash: "sha256:company-target",
|
|
6803
|
-
size: 1,
|
|
6804
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
6805
|
-
direction: "up",
|
|
6806
|
-
},
|
|
6807
|
-
},
|
|
6808
|
-
});
|
|
6809
|
-
const migration = new AreaLedgerMigration({
|
|
6810
|
-
rootDir: __runnerTestStateDir,
|
|
6811
|
-
journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6812
|
-
resolver,
|
|
6813
|
-
legacyStore: openJournalStateStoreForMigration(PERSONAL_VAULT_JOURNAL_SLUG),
|
|
6814
|
-
});
|
|
6815
|
-
expect(migration.cutOver(120_000).phase).toBe("area-authoritative");
|
|
6816
|
-
migration.dispose();
|
|
6817
|
-
// The migration state itself is covered by the area-ledger migration
|
|
6818
|
-
// suite. Seed the already-authoritative production shape in one durable
|
|
6819
|
-
// transaction so this watch-loop test measures repeated realtime reads,
|
|
6820
|
-
// rather than spending most of its worker budget replaying a one-time
|
|
6821
|
-
// legacy-to-area copy.
|
|
6822
|
-
openJournalStateStoreForMigration(PERSONAL_VAULT_JOURNAL_SLUG).replaceState({
|
|
6823
|
-
journal: {
|
|
6824
|
-
version: "2",
|
|
6825
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
6826
|
-
pulls: [],
|
|
6827
|
-
files: personalFiles,
|
|
6828
|
-
},
|
|
6829
|
-
});
|
|
6830
|
-
AreaLedger.open({
|
|
6831
|
-
rootDir: __runnerTestStateDir,
|
|
6832
|
-
journalSlug: PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6833
|
-
layoutEpoch: 1,
|
|
6834
|
-
areaIds: resolver.layout.areas.map((area) => area.id),
|
|
6835
|
-
resolveArea: (key) => resolver.resolve(key),
|
|
6836
|
-
}).append({
|
|
6837
|
-
upserts: personalFiles,
|
|
6838
|
-
metadata: { lastSync: "2026-09-16T21:30:00.000Z", pulls: [] },
|
|
6839
|
-
});
|
|
6840
|
-
fs.mkdirSync(watchedRoot, { recursive: true });
|
|
6841
|
-
for (let index = 0; index < 50_000; index += 1) {
|
|
6842
|
-
fs.writeFileSync(path.join(watchedRoot, `seed-${String(index).padStart(5, "0")}.md`), "seed");
|
|
6843
|
-
}
|
|
6844
|
-
fs.mkdirSync(path.dirname(path.join(hqRoot, personalPath)), { recursive: true });
|
|
6845
|
-
fs.writeFileSync(path.join(hqRoot, personalPath), "personal");
|
|
6846
|
-
fs.mkdirSync(path.dirname(path.join(hqRoot, companyPath)), { recursive: true });
|
|
6847
|
-
fs.writeFileSync(path.join(hqRoot, companyPath), "company");
|
|
6848
|
-
const watcher = makeBatchWatcherStub();
|
|
6849
|
-
const sleep = makeSteppableSleep();
|
|
6850
|
-
let shutdown = () => { };
|
|
6851
|
-
let isUntrackedDelete = (_relativePath) => false;
|
|
6852
|
-
const commits = [];
|
|
6853
|
-
let submittedAt = 0;
|
|
6854
|
-
const aggregatePrime = vi.spyOn(AreaLedger.prototype, "primeAggregateCache");
|
|
6855
|
-
const runPass = vi.fn(async (argv) => {
|
|
6856
|
-
if (!argv.includes("--scope-path")) {
|
|
6857
|
-
readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
6858
|
-
readJournal("indigo");
|
|
6859
|
-
}
|
|
6860
|
-
if (argv.includes("--scope-path") && argv.includes("push")) {
|
|
6861
|
-
const slug = argv.includes("--company") ? "indigo" : PERSONAL_VAULT_JOURNAL_SLUG;
|
|
6862
|
-
const key = slug === "indigo" ? companyKey : personalPath;
|
|
6863
|
-
const scoped = readJournalScoped(slug, [key]);
|
|
6864
|
-
scoped.files[key] = {
|
|
6865
|
-
...scoped.files[key],
|
|
6866
|
-
syncedAt: "2026-09-16T21:38:18.000Z",
|
|
6867
|
-
};
|
|
6868
|
-
writeJournal(slug, scoped);
|
|
6869
|
-
commits.push({ slug, elapsedMs: performance.now() - submittedAt });
|
|
6870
|
-
}
|
|
6871
|
-
return completedPassOutcome();
|
|
6872
|
-
});
|
|
6873
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
|
|
6874
|
-
runPass,
|
|
6875
|
-
createWatcher: (options) => {
|
|
6876
|
-
isUntrackedDelete = options.isUntrackedDelete;
|
|
6877
|
-
return watcher;
|
|
6878
|
-
},
|
|
6879
|
-
startEventSync: vi.fn().mockResolvedValue(null),
|
|
6880
|
-
sleep: sleep.sleep,
|
|
6881
|
-
onShutdownSignal: (handler) => {
|
|
6882
|
-
shutdown = handler;
|
|
6883
|
-
return () => { };
|
|
6884
|
-
},
|
|
6885
|
-
});
|
|
6886
|
-
const emitAndWait = async (relativePaths) => {
|
|
6887
|
-
const before = commits.length;
|
|
6888
|
-
if (relativePaths.includes(personalPath)) {
|
|
6889
|
-
// This is the same non-blocking watcher callback which starts an
|
|
6890
|
-
// openJournalStoreSessionAsync before a delete episode is drained.
|
|
6891
|
-
// It is intentionally issued before each realtime batch: a completed
|
|
6892
|
-
// watch pass must not have thrown away the warmed area aggregate.
|
|
6893
|
-
expect(isUntrackedDelete(personalPath)).toBe(false);
|
|
6894
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
6895
|
-
}
|
|
6896
|
-
submittedAt = performance.now();
|
|
6897
|
-
const batch = {
|
|
6898
|
-
paths: new Map(relativePaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
|
|
6899
|
-
changes: new Map(relativePaths.map((relativePath) => [
|
|
6900
|
-
path.join(hqRoot, relativePath),
|
|
6901
|
-
{ kind: "change" },
|
|
6902
|
-
])),
|
|
6903
|
-
};
|
|
6904
|
-
watcher.emit(relativePaths[0], batch);
|
|
6905
|
-
await vi.waitFor(() => expect(commits.length).toBe(before + relativePaths.length), {
|
|
6906
|
-
timeout: 30_000,
|
|
6907
|
-
interval: 10,
|
|
6908
|
-
});
|
|
6909
|
-
// `commits` is observed inside the injected pass, before the runner has
|
|
6910
|
-
// completed its pass-final cleanup. Let that cleanup run before the next
|
|
6911
|
-
// watcher episode so the assertion observes the cache lifetime at the
|
|
6912
|
-
// same boundary as a real completed watch pass.
|
|
6913
|
-
await flushLoopMicrotasks(30);
|
|
6914
|
-
};
|
|
6915
|
-
try {
|
|
6916
|
-
await flushLoopMicrotasks(30);
|
|
6917
|
-
runPass.mockClear();
|
|
6918
|
-
// The initial runner pass is the one permitted cold open. Every later
|
|
6919
|
-
// scoped batch must reuse that authoritative aggregate. Warm the same
|
|
6920
|
-
// yielding watcher-session path that production uses before its first
|
|
6921
|
-
// delete episode, then measure only post-warmup primes.
|
|
6922
|
-
aggregatePrime.mockClear();
|
|
6923
|
-
expect(isUntrackedDelete(personalPath)).toBe(false);
|
|
6924
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
6925
|
-
// The initial full pass may already have opened this bridge (zero new
|
|
6926
|
-
// primes), or this watcher callback may perform the one permitted cold
|
|
6927
|
-
// warm-up. The post-poll assertion below is the regression boundary.
|
|
6928
|
-
expect(aggregatePrime.mock.calls.length).toBeLessThanOrEqual(1);
|
|
6929
|
-
aggregatePrime.mockClear();
|
|
6930
|
-
// Reproduce the normal poll boundary observed on the outposts. On the
|
|
6931
|
-
// current implementation it drops every cached area bridge even though
|
|
6932
|
-
// the watcher and runner remain alive.
|
|
6933
|
-
sleep.tick();
|
|
6934
|
-
await flushLoopMicrotasks(30);
|
|
6935
|
-
forceGarbageCollectionForTest();
|
|
6936
|
-
const retainedAfterWarmupBytes = retainedProcessBytes();
|
|
6937
|
-
const postWarmCpuStart = process.cpuUsage();
|
|
6938
|
-
const postWarmStartedAt = performance.now();
|
|
6939
|
-
resetStateStoreSnapshotRecoveryCountForTest();
|
|
6940
|
-
const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
|
|
6941
|
-
eventLoopDelay.enable();
|
|
6942
|
-
await emitAndWait([personalPath, companyPath]);
|
|
6943
|
-
for (let round = 0; round < 3; round += 1) {
|
|
6944
|
-
await emitAndWait([personalPath]);
|
|
6945
|
-
await emitAndWait([companyPath]);
|
|
6946
|
-
}
|
|
6947
|
-
eventLoopDelay.disable();
|
|
6948
|
-
const recoveryCount = getStateStoreSnapshotRecoveryCountForTest();
|
|
6949
|
-
const maxDelayMs = Number(eventLoopDelay.max) / 1_000_000;
|
|
6950
|
-
const maxCommitMs = Math.max(...commits.map((commit) => commit.elapsedMs));
|
|
6951
|
-
// A first area delta briefly retains the reverse-shadow mirror to
|
|
6952
|
-
// amortize a burst. Retained memory means after that documented linger
|
|
6953
|
-
// has released its second full-vault projection, not during it.
|
|
6954
|
-
await new Promise((resolve) => setTimeout(resolve, AREA_LEGACY_MIRROR_LINGER_MS + 25));
|
|
6955
|
-
forceGarbageCollectionForTest();
|
|
6956
|
-
const retainedAfterChurnBytes = retainedProcessBytes();
|
|
6957
|
-
const retainedGrowthBytes = retainedAfterChurnBytes - retainedAfterWarmupBytes;
|
|
6958
|
-
const postWarmCpu = process.cpuUsage(postWarmCpuStart);
|
|
6959
|
-
const postWarmCpuPercent = ((postWarmCpu.user + postWarmCpu.system) /
|
|
6960
|
-
((performance.now() - postWarmStartedAt) * 1_000)) * 100;
|
|
6961
|
-
console.info({
|
|
6962
|
-
aggregatePrimeCount: aggregatePrime.mock.calls.length,
|
|
6963
|
-
recoveryCount,
|
|
6964
|
-
maxDelayMs,
|
|
6965
|
-
maxCommitMs,
|
|
6966
|
-
postWarmCpuPercent,
|
|
6967
|
-
retainedAfterWarmupBytes,
|
|
6968
|
-
retainedAfterChurnBytes,
|
|
6969
|
-
retainedGrowthBytes,
|
|
6970
|
-
commits: commits.length,
|
|
6971
|
-
});
|
|
6972
|
-
expect(aggregatePrime).not.toHaveBeenCalled();
|
|
6973
|
-
// The first area delta opens its reverse-shadow v3 mirror once; its
|
|
6974
|
-
// linger keeps all later scoped commits off snapshot recovery. This is
|
|
6975
|
-
// deliberately exact so a second cold bridge cannot hide as a cache hit.
|
|
6976
|
-
expect.soft(recoveryCount).toBe(1);
|
|
6977
|
-
// The production-shape run remains bounded under the coverage job's
|
|
6978
|
-
// concurrent workers: CI measured 1,090.52 ms of timer delay and
|
|
6979
|
-
// 1,087.79 ms to commit. 1.5 s retains a concrete latency ceiling while
|
|
6980
|
-
// leaving headroom for that instrumentation, rather than making the
|
|
6981
|
-
// assertion depend on a single host's scheduling jitter.
|
|
6982
|
-
expect.soft(maxDelayMs).toBeLessThan(1_500);
|
|
6983
|
-
expect.soft(maxCommitMs).toBeLessThan(1_500);
|
|
6984
|
-
expect.soft(retainedGrowthBytes).toBeLessThan(32 * 1024 * 1024);
|
|
6985
|
-
expect(commits.map((commit) => commit.slug)).toEqual([
|
|
6986
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6987
|
-
"indigo",
|
|
6988
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6989
|
-
"indigo",
|
|
6990
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6991
|
-
"indigo",
|
|
6992
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
6993
|
-
"indigo",
|
|
6994
|
-
]);
|
|
6995
|
-
}
|
|
6996
|
-
finally {
|
|
6997
|
-
aggregatePrime.mockRestore();
|
|
6998
|
-
shutdown();
|
|
6999
|
-
await loop;
|
|
7000
|
-
if (previousCoordinator === undefined)
|
|
7001
|
-
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
7002
|
-
else
|
|
7003
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
7004
|
-
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
7005
|
-
}
|
|
7006
|
-
}, 180_000);
|
|
7007
|
-
it("keeps warmed personal and company journals hot across alternating realtime batches", async () => {
|
|
7008
|
-
const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
7009
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
7010
|
-
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-multi-journal-legacy-watch-"));
|
|
7011
|
-
const watchedRoot = path.join(hqRoot, "personal", "watch-load");
|
|
7012
|
-
const personalPath = "personal/watch-load/00000/target.md";
|
|
7013
|
-
const companyPath = "companies/indigo/watch-load/00000/target.md";
|
|
7014
|
-
const companyKey = "watch-load/00000/target.md";
|
|
7015
|
-
const personalFiles = {};
|
|
7016
|
-
for (let index = 0; index < 700_000; index += 1) {
|
|
7017
|
-
personalFiles[`personal/history/${String(index % 50_000).padStart(5, "0")}/entry-${index}.md`] = {
|
|
7018
|
-
hash: `sha256:history-${index}`,
|
|
7019
|
-
size: index % 4096,
|
|
7020
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
7021
|
-
direction: "up",
|
|
7022
|
-
};
|
|
7023
|
-
}
|
|
7024
|
-
personalFiles[personalPath] = {
|
|
7025
|
-
hash: "sha256:personal-target",
|
|
7026
|
-
size: 1,
|
|
7027
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
7028
|
-
direction: "up",
|
|
7029
|
-
};
|
|
7030
|
-
writeJournal(PERSONAL_VAULT_JOURNAL_SLUG, {
|
|
7031
|
-
version: "2",
|
|
7032
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
7033
|
-
pulls: [],
|
|
7034
|
-
files: personalFiles,
|
|
7035
|
-
});
|
|
7036
|
-
writeJournal("indigo", {
|
|
7037
|
-
version: "2",
|
|
7038
|
-
lastSync: "2026-09-16T21:30:00.000Z",
|
|
7039
|
-
pulls: [],
|
|
7040
|
-
files: {
|
|
7041
|
-
[companyKey]: {
|
|
7042
|
-
hash: "sha256:company-target",
|
|
7043
|
-
size: 1,
|
|
7044
|
-
syncedAt: "2026-09-16T21:30:00.000Z",
|
|
7045
|
-
direction: "up",
|
|
7046
|
-
},
|
|
7047
|
-
},
|
|
7048
|
-
});
|
|
7049
|
-
for (let index = 0; index < 50_000; index += 1) {
|
|
7050
|
-
const directory = path.join(watchedRoot, String(index).padStart(5, "0"));
|
|
7051
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
7052
|
-
fs.writeFileSync(path.join(directory, "seed.md"), "seed");
|
|
7053
|
-
}
|
|
7054
|
-
fs.mkdirSync(path.dirname(path.join(hqRoot, personalPath)), { recursive: true });
|
|
7055
|
-
fs.writeFileSync(path.join(hqRoot, personalPath), "personal");
|
|
7056
|
-
fs.mkdirSync(path.dirname(path.join(hqRoot, companyPath)), { recursive: true });
|
|
7057
|
-
fs.writeFileSync(path.join(hqRoot, companyPath), "company");
|
|
7058
|
-
const watcher = makeBatchWatcherStub();
|
|
7059
|
-
let shutdown = () => { };
|
|
7060
|
-
const commits = [];
|
|
7061
|
-
let submittedAt = 0;
|
|
7062
|
-
const runPass = vi.fn(async (argv) => {
|
|
7063
|
-
if (!argv.includes("--scope-path")) {
|
|
7064
|
-
readJournal(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
7065
|
-
readJournal("indigo");
|
|
7066
|
-
}
|
|
7067
|
-
if (argv.includes("--scope-path") && argv.includes("push")) {
|
|
7068
|
-
const slug = argv.includes("--company") ? "indigo" : PERSONAL_VAULT_JOURNAL_SLUG;
|
|
7069
|
-
const key = slug === "indigo" ? companyKey : personalPath;
|
|
7070
|
-
const scoped = readJournalScoped(slug, [key]);
|
|
7071
|
-
scoped.files[key] = {
|
|
7072
|
-
...scoped.files[key],
|
|
7073
|
-
syncedAt: "2026-09-16T21:38:18.000Z",
|
|
7074
|
-
};
|
|
7075
|
-
writeJournal(slug, scoped);
|
|
7076
|
-
commits.push({ slug, elapsedMs: performance.now() - submittedAt });
|
|
7077
|
-
}
|
|
7078
|
-
return completedPassOutcome();
|
|
7079
|
-
});
|
|
7080
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
|
|
7081
|
-
runPass,
|
|
7082
|
-
createWatcher: () => watcher,
|
|
7083
|
-
sleep: () => new Promise(() => { }),
|
|
7084
|
-
onShutdownSignal: (handler) => {
|
|
7085
|
-
shutdown = handler;
|
|
7086
|
-
return () => { };
|
|
7087
|
-
},
|
|
7088
|
-
});
|
|
7089
|
-
const emitAndWait = async (relativePaths) => {
|
|
7090
|
-
const before = commits.length;
|
|
7091
|
-
submittedAt = performance.now();
|
|
7092
|
-
const batch = {
|
|
7093
|
-
paths: new Map(relativePaths.map((relativePath) => [path.join(hqRoot, relativePath), relativePath])),
|
|
7094
|
-
changes: new Map(relativePaths.map((relativePath) => [
|
|
7095
|
-
path.join(hqRoot, relativePath),
|
|
7096
|
-
{ kind: "change" },
|
|
7097
|
-
])),
|
|
7098
|
-
};
|
|
7099
|
-
watcher.emit(relativePaths[0], batch);
|
|
7100
|
-
await vi.waitFor(() => expect(commits.length).toBe(before + relativePaths.length), {
|
|
7101
|
-
timeout: 30_000,
|
|
7102
|
-
interval: 10,
|
|
7103
|
-
});
|
|
7104
|
-
};
|
|
7105
|
-
try {
|
|
7106
|
-
await flushLoopMicrotasks(30);
|
|
7107
|
-
runPass.mockClear();
|
|
7108
|
-
resetStateStoreSnapshotRecoveryCountForTest();
|
|
7109
|
-
const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
|
|
7110
|
-
eventLoopDelay.enable();
|
|
7111
|
-
await emitAndWait([personalPath, companyPath]);
|
|
7112
|
-
for (let round = 0; round < 3; round += 1) {
|
|
7113
|
-
await emitAndWait([personalPath]);
|
|
7114
|
-
await emitAndWait([companyPath]);
|
|
7115
|
-
}
|
|
7116
|
-
eventLoopDelay.disable();
|
|
7117
|
-
const recoveryCount = getStateStoreSnapshotRecoveryCountForTest();
|
|
7118
|
-
const maxDelayMs = Number(eventLoopDelay.max) / 1_000_000;
|
|
7119
|
-
const maxCommitMs = Math.max(...commits.map((commit) => commit.elapsedMs));
|
|
7120
|
-
console.info({ recoveryCount, maxDelayMs, maxCommitMs, commits: commits.length });
|
|
7121
|
-
expect.soft(recoveryCount).toBe(0);
|
|
7122
|
-
expect.soft(maxDelayMs).toBeLessThan(100);
|
|
7123
|
-
expect.soft(maxCommitMs).toBeLessThan(1_000);
|
|
7124
|
-
expect(commits.map((commit) => commit.slug)).toEqual([
|
|
7125
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
7126
|
-
"indigo",
|
|
7127
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
7128
|
-
"indigo",
|
|
7129
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
7130
|
-
"indigo",
|
|
7131
|
-
PERSONAL_VAULT_JOURNAL_SLUG,
|
|
7132
|
-
"indigo",
|
|
7133
|
-
]);
|
|
7134
|
-
}
|
|
7135
|
-
finally {
|
|
7136
|
-
shutdown();
|
|
7137
|
-
await loop;
|
|
7138
|
-
if (previousCoordinator === undefined)
|
|
7139
|
-
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
7140
|
-
else
|
|
7141
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
7142
|
-
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
7143
|
-
}
|
|
7144
|
-
}, 180_000);
|
|
7145
|
-
it("fast lane: prefers declared receiver sizes and falls back to the journal", async () => {
|
|
7146
|
-
const previous = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
7147
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
7148
|
-
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-receiver-declared-size-"));
|
|
7149
|
-
const watcher = makeBatchWatcherStub();
|
|
7150
|
-
let receiverSync;
|
|
7151
|
-
let triggerShutdown = () => { };
|
|
7152
|
-
const event = (relativePath, sizeBytes) => ({
|
|
7153
|
-
kind: "upsert",
|
|
7154
|
-
relativePath,
|
|
7155
|
-
contentHash: "sha256:remote",
|
|
7156
|
-
mtime: "2026-08-28T00:00:00.000Z",
|
|
7157
|
-
...(sizeBytes === undefined ? {} : { sizeBytes }),
|
|
7158
|
-
originDeviceId: "peer-device",
|
|
7159
|
-
originTenantId: "tenant-indigo",
|
|
7160
|
-
sequenceNumber: 1,
|
|
7161
|
-
eventTimestamp: "2026-08-28T00:00:00.000Z",
|
|
6413
|
+
let receiverSync;
|
|
6414
|
+
let triggerShutdown = () => { };
|
|
6415
|
+
const event = (relativePath, sizeBytes) => ({
|
|
6416
|
+
kind: "upsert",
|
|
6417
|
+
relativePath,
|
|
6418
|
+
contentHash: "sha256:remote",
|
|
6419
|
+
mtime: "2026-08-28T00:00:00.000Z",
|
|
6420
|
+
...(sizeBytes === undefined ? {} : { sizeBytes }),
|
|
6421
|
+
originDeviceId: "peer-device",
|
|
6422
|
+
originTenantId: "tenant-indigo",
|
|
6423
|
+
sequenceNumber: 1,
|
|
6424
|
+
eventTimestamp: "2026-08-28T00:00:00.000Z",
|
|
7162
6425
|
});
|
|
7163
6426
|
const journal = readJournal("indigo");
|
|
7164
6427
|
journal.files["journal-small.md"] = {
|
|
@@ -7317,19 +6580,19 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7317
6580
|
]),
|
|
7318
6581
|
});
|
|
7319
6582
|
await flushLoopMicrotasks(30);
|
|
7320
|
-
//
|
|
7321
|
-
//
|
|
7322
|
-
expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(
|
|
6583
|
+
// origin/main failure: expected a scoped tiny path while the bulk pass is
|
|
6584
|
+
// held, but #408 kept all four paths in the slow lane.
|
|
6585
|
+
expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(1);
|
|
6586
|
+
expect(calls.flat().join(" ")).toContain("realtime.md");
|
|
7323
6587
|
releaseBulk();
|
|
7324
6588
|
await flushLoopMicrotasks(30);
|
|
7325
6589
|
expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(2);
|
|
7326
|
-
expect(calls.flat().join(" ")).toContain("realtime.md");
|
|
7327
6590
|
expect(calls.flat().join(" ")).toContain("session-0.log");
|
|
7328
6591
|
triggerShutdown();
|
|
7329
6592
|
await loop;
|
|
7330
6593
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
7331
6594
|
});
|
|
7332
|
-
it("fast lane: a small receiver pull
|
|
6595
|
+
it("fast lane: a small receiver pull dispatches during a slow bulk pass", async () => {
|
|
7333
6596
|
const watcher = makeWatcherStub();
|
|
7334
6597
|
let triggerShutdown = () => { };
|
|
7335
6598
|
let receiverSync;
|
|
@@ -7374,7 +6637,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7374
6637
|
watcher.emit();
|
|
7375
6638
|
await flushLoopMicrotasks();
|
|
7376
6639
|
expect(order).toEqual(["bulk"]);
|
|
7377
|
-
|
|
6640
|
+
await receiverSync({
|
|
7378
6641
|
events: [{
|
|
7379
6642
|
kind: "upsert", relativePath: "companies/acme/realtime.md", contentHash: "sha256:x",
|
|
7380
6643
|
mtime: "2026-08-26T00:00:00.000Z", originDeviceId: "peer", originTenantId: "acme",
|
|
@@ -7382,11 +6645,8 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7382
6645
|
}],
|
|
7383
6646
|
signal: new AbortController().signal,
|
|
7384
6647
|
});
|
|
7385
|
-
await flushLoopMicrotasks(20);
|
|
7386
|
-
expect(order).toEqual(["bulk"]);
|
|
7387
|
-
releaseBulk();
|
|
7388
|
-
await receiverPull;
|
|
7389
6648
|
expect(order).toEqual(["bulk", "fast-pull"]);
|
|
6649
|
+
releaseBulk();
|
|
7390
6650
|
await flushLoopMicrotasks();
|
|
7391
6651
|
triggerShutdown();
|
|
7392
6652
|
await loop;
|
|
@@ -7430,14 +6690,12 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7430
6690
|
await flushLoopMicrotasks();
|
|
7431
6691
|
watcher.emit();
|
|
7432
6692
|
await flushLoopMicrotasks();
|
|
7433
|
-
releaseBulk();
|
|
7434
|
-
// The full chore has no explicit safe checkpoint in this stub, so the
|
|
7435
|
-
// realtime pass begins only after the chore has released ownership.
|
|
7436
6693
|
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
7437
6694
|
await fastStarted;
|
|
6695
|
+
releaseBulk();
|
|
7438
6696
|
await flushLoopMicrotasks(20);
|
|
7439
|
-
// The
|
|
7440
|
-
//
|
|
6697
|
+
// The slow pass has finished, but its fast bypass still mutates. A second
|
|
6698
|
+
// sync/rescue must not acquire the root operation lock in this interval.
|
|
7441
6699
|
expect(fs.existsSync(lockPathFor(hqRoot))).toBe(true);
|
|
7442
6700
|
releaseFast();
|
|
7443
6701
|
await flushLoopMicrotasks(20);
|
|
@@ -7612,68 +6870,59 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7612
6870
|
return () => { };
|
|
7613
6871
|
},
|
|
7614
6872
|
});
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
};
|
|
7651
|
-
}
|
|
7652
|
-
writeJournal(slug, journal);
|
|
6873
|
+
await flushLoopMicrotasks();
|
|
6874
|
+
watcher.emit();
|
|
6875
|
+
await flushLoopMicrotasks();
|
|
6876
|
+
runPass.mockClear();
|
|
6877
|
+
const events = [
|
|
6878
|
+
...Array.from({ length: 17 }, (_, index) => ({
|
|
6879
|
+
kind: "upsert",
|
|
6880
|
+
relativePath: `companies/acme/a-${index}.md`,
|
|
6881
|
+
contentHash: "sha256:x",
|
|
6882
|
+
mtime: "2026-08-26T00:00:00.000Z",
|
|
6883
|
+
originDeviceId: "peer",
|
|
6884
|
+
originTenantId: "acme",
|
|
6885
|
+
sequenceNumber: index,
|
|
6886
|
+
eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
6887
|
+
})),
|
|
6888
|
+
...Array.from({ length: 16 }, (_, index) => ({
|
|
6889
|
+
kind: "upsert",
|
|
6890
|
+
relativePath: `companies/beta/b-${index}.md`,
|
|
6891
|
+
contentHash: "sha256:x",
|
|
6892
|
+
mtime: "2026-08-26T00:00:00.000Z",
|
|
6893
|
+
originDeviceId: "peer",
|
|
6894
|
+
originTenantId: "beta",
|
|
6895
|
+
sequenceNumber: index + 17,
|
|
6896
|
+
eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
6897
|
+
})),
|
|
6898
|
+
];
|
|
6899
|
+
for (const [slug, prefix, count] of [["acme", "a", 17], ["beta", "b", 16]]) {
|
|
6900
|
+
const journal = readJournal(slug);
|
|
6901
|
+
for (let index = 0; index < count; index++) {
|
|
6902
|
+
journal.files[`${prefix}-${index}.md`] = {
|
|
6903
|
+
hash: "sha256:x",
|
|
6904
|
+
size: 16,
|
|
6905
|
+
syncedAt: "2026-08-26T00:00:00.000Z",
|
|
6906
|
+
direction: "down",
|
|
6907
|
+
};
|
|
7653
6908
|
}
|
|
7654
|
-
|
|
7655
|
-
events,
|
|
7656
|
-
signal: new AbortController().signal,
|
|
7657
|
-
});
|
|
7658
|
-
await flushLoopMicrotasks(20);
|
|
7659
|
-
// The running full chore has not yet yielded a durable hand-off, so no
|
|
7660
|
-
// receiver work may overlap it. Once it releases ownership, the
|
|
7661
|
-
// scheduler admits each route's bounded work before the residual slow
|
|
7662
|
-
// beta batch (acme fast, beta fast, then beta slow).
|
|
7663
|
-
expect(runPass).not.toHaveBeenCalled();
|
|
7664
|
-
releaseBulk();
|
|
7665
|
-
await receiverDrain;
|
|
7666
|
-
const scopedCalls = runPass.mock.calls.filter(([argv]) => argv.includes("--scope-path"));
|
|
7667
|
-
expect(scopedCalls).toHaveLength(3);
|
|
7668
|
-
expect(scopedCalls.flat().join(" ")).toContain("a-16.md");
|
|
7669
|
-
expect(scopedCalls.flat().join(" ")).toContain("b-14.md");
|
|
7670
|
-
}
|
|
7671
|
-
finally {
|
|
7672
|
-
releaseBulk();
|
|
7673
|
-
triggerShutdown();
|
|
7674
|
-
await loop;
|
|
7675
|
-
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
6909
|
+
writeJournal(slug, journal);
|
|
7676
6910
|
}
|
|
6911
|
+
const receiverDrain = receiverSync({
|
|
6912
|
+
events,
|
|
6913
|
+
signal: new AbortController().signal,
|
|
6914
|
+
});
|
|
6915
|
+
await flushLoopMicrotasks(20);
|
|
6916
|
+
// The 33-event mixed delivery peels its first 32 bounded paths into the
|
|
6917
|
+
// fast lane (17 acme + 15 beta); the final beta event remains slow.
|
|
6918
|
+
expect(runPass).toHaveBeenCalledTimes(2);
|
|
6919
|
+
expect(runPass.mock.calls.flat().join(" ")).toContain("a-16.md");
|
|
6920
|
+
expect(runPass.mock.calls.flat().join(" ")).toContain("b-14.md");
|
|
6921
|
+
releaseBulk();
|
|
6922
|
+
await receiverDrain;
|
|
6923
|
+
triggerShutdown();
|
|
6924
|
+
await loop;
|
|
6925
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
7677
6926
|
});
|
|
7678
6927
|
it("fast lane: queues slow work behind a receiver pass that owns the root lock", async () => {
|
|
7679
6928
|
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-slow-queue-"));
|
|
@@ -7845,12 +7094,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7845
7094
|
await flushLoopMicrotasks();
|
|
7846
7095
|
watcher.emit(paths[0][1], { paths: new Map(paths) });
|
|
7847
7096
|
await flushLoopMicrotasks(30);
|
|
7848
|
-
|
|
7849
|
-
// only journal writer. The scoped realtime pass is admitted immediately
|
|
7850
|
-
// after this safe hand-off, preserving both sets of rows.
|
|
7851
|
-
expect(Object.keys(readJournal("indigo").files)).toHaveLength(0);
|
|
7097
|
+
expect(Object.keys(readJournal("indigo").files)).toHaveLength(paths.length);
|
|
7852
7098
|
releaseBulk();
|
|
7853
|
-
await flushLoopMicrotasks(
|
|
7099
|
+
await flushLoopMicrotasks();
|
|
7854
7100
|
const journal = readJournal("indigo").files;
|
|
7855
7101
|
expect(Object.keys(journal)).toHaveLength(paths.length * 2);
|
|
7856
7102
|
for (let index = 0; index < paths.length; index++) {
|
|
@@ -7902,12 +7148,10 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
7902
7148
|
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
7903
7149
|
}
|
|
7904
7150
|
await flushLoopMicrotasks(60);
|
|
7905
|
-
|
|
7906
|
-
// The queued realtime batches run immediately after it completes.
|
|
7907
|
-
expect(completed).toEqual([]);
|
|
7151
|
+
expect(completed).toEqual(["fast", "fast", "fast", "fast", "fast"]);
|
|
7908
7152
|
releaseBulk();
|
|
7909
|
-
await flushLoopMicrotasks(
|
|
7910
|
-
expect(completed).
|
|
7153
|
+
await flushLoopMicrotasks();
|
|
7154
|
+
expect(completed).toContain("bulk");
|
|
7911
7155
|
triggerShutdown();
|
|
7912
7156
|
await loop;
|
|
7913
7157
|
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
@@ -8141,33 +7385,18 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
8141
7385
|
// No pending FakeClock timers left behind by the driver's debounce.
|
|
8142
7386
|
expect(clock.pendingTimerCount()).toBe(0);
|
|
8143
7387
|
});
|
|
8144
|
-
it("a
|
|
7388
|
+
it("a hard-error (exit 1) poll pass returns immediately", async () => {
|
|
8145
7389
|
const watcher = makeWatcherStub();
|
|
8146
7390
|
const runPass = vi.fn().mockResolvedValue(1);
|
|
8147
|
-
const
|
|
8148
|
-
let triggerShutdown = () => { };
|
|
8149
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
7391
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8150
7392
|
runPass,
|
|
8151
7393
|
clock: new FakeClock(),
|
|
8152
7394
|
createWatcher: () => watcher,
|
|
8153
|
-
sleep:
|
|
8154
|
-
onShutdownSignal: (
|
|
8155
|
-
triggerShutdown = handler;
|
|
8156
|
-
return () => { };
|
|
8157
|
-
},
|
|
7395
|
+
sleep: () => Promise.resolve(),
|
|
7396
|
+
onShutdownSignal: () => () => { },
|
|
8158
7397
|
});
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
expect(runPass).toHaveBeenCalledTimes(1);
|
|
8162
|
-
sleep.tick();
|
|
8163
|
-
await flushLoopMicrotasks();
|
|
8164
|
-
expect(runPass).toHaveBeenCalledTimes(2);
|
|
8165
|
-
}
|
|
8166
|
-
finally {
|
|
8167
|
-
triggerShutdown();
|
|
8168
|
-
await expect(loop).resolves.toBe(0);
|
|
8169
|
-
expect(watcher.started).toBe(false);
|
|
8170
|
-
}
|
|
7398
|
+
expect(code).toBe(1);
|
|
7399
|
+
expect(watcher.disposed).toBe(false);
|
|
8171
7400
|
});
|
|
8172
7401
|
it("an auth-required watch pass parks in-process instead of exiting", async () => {
|
|
8173
7402
|
const watcher = makeWatcherStub();
|
|
@@ -8216,92 +7445,29 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
8216
7445
|
it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
|
|
8217
7446
|
// A transient offline blip must keep the watcher alive: the loop logs and
|
|
8218
7447
|
// polls again instead of returning the code and letting the process exit
|
|
8219
|
-
// (which the menubar reports as "watcher exited unexpectedly"). A
|
|
8220
|
-
//
|
|
7448
|
+
// (which the menubar reports as "watcher exited unexpectedly"). A genuine
|
|
7449
|
+
// hard error on a later pass still terminates the loop.
|
|
8221
7450
|
const watcher = makeWatcherStub();
|
|
8222
7451
|
let n = 0;
|
|
8223
7452
|
const runPass = vi.fn().mockImplementation(async () => {
|
|
8224
7453
|
n++;
|
|
8225
|
-
// First pass: machine is offline (transient). Second pass: a
|
|
8226
|
-
//
|
|
7454
|
+
// First pass: machine is offline (transient). Second pass: a real hard
|
|
7455
|
+
// error, used here only to end the otherwise-infinite loop deterministically.
|
|
8227
7456
|
return n === 1 ? TRANSIENT_NETWORK_EXIT : 1;
|
|
8228
7457
|
});
|
|
8229
|
-
const
|
|
8230
|
-
let triggerShutdown = () => { };
|
|
8231
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
7458
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8232
7459
|
runPass,
|
|
8233
7460
|
clock: new FakeClock(),
|
|
8234
7461
|
createWatcher: () => watcher,
|
|
8235
|
-
sleep:
|
|
8236
|
-
onShutdownSignal: (
|
|
8237
|
-
triggerShutdown = handler;
|
|
8238
|
-
return () => { };
|
|
8239
|
-
},
|
|
8240
|
-
});
|
|
8241
|
-
await flushLoopMicrotasks();
|
|
8242
|
-
sleep.tick();
|
|
8243
|
-
await flushLoopMicrotasks();
|
|
8244
|
-
// The transient pass did NOT end the loop — it ran again, and neither
|
|
8245
|
-
// error causes the daemon itself to exit.
|
|
8246
|
-
expect(runPass).toHaveBeenCalledTimes(2);
|
|
8247
|
-
triggerShutdown();
|
|
8248
|
-
await expect(loop).resolves.toBe(0);
|
|
8249
|
-
expect(watcher.disposed).toBe(false);
|
|
8250
|
-
});
|
|
8251
|
-
it("keeps the watcher alive for an unresolved company watch route (HQ-SYNC-COMPANY-ROUTE-UID)", async () => {
|
|
8252
|
-
const watcher = makeWatcherStub();
|
|
8253
|
-
const client = makeVaultStub({
|
|
8254
|
-
entityGet: () => Promise.reject(new VaultNotFoundError("company deleted")),
|
|
8255
|
-
});
|
|
8256
|
-
const findInMyNamespace = vi.fn().mockResolvedValue(null);
|
|
8257
|
-
client.entity.findInMyNamespace = findInMyNamespace;
|
|
8258
|
-
const routedPass = makeDeps({ createVaultClient: () => client });
|
|
8259
|
-
const routedArgv = buildRoutePullArgv({ kind: "company", slug: "cmp_ghost" }, ["--companies", "--watch", "--hq-root", "/tmp/hq"]);
|
|
8260
|
-
let attempts = 0;
|
|
8261
|
-
let routedExit;
|
|
8262
|
-
const runPass = vi.fn().mockImplementation(async () => {
|
|
8263
|
-
attempts += 1;
|
|
8264
|
-
if (attempts === 1) {
|
|
8265
|
-
routedExit = await runRunner(routedArgv, routedPass);
|
|
8266
|
-
return routedExit;
|
|
8267
|
-
}
|
|
8268
|
-
return UNRESOLVED_COMPANY_EXIT;
|
|
8269
|
-
});
|
|
8270
|
-
const sleep = makeSteppableSleep();
|
|
8271
|
-
let triggerShutdown = () => { };
|
|
8272
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8273
|
-
runPass,
|
|
8274
|
-
clock: new FakeClock(),
|
|
8275
|
-
createWatcher: () => watcher,
|
|
8276
|
-
sleep: sleep.sleep,
|
|
8277
|
-
onShutdownSignal: (handler) => {
|
|
8278
|
-
triggerShutdown = handler;
|
|
8279
|
-
return () => { };
|
|
8280
|
-
},
|
|
7462
|
+
sleep: () => Promise.resolve(),
|
|
7463
|
+
onShutdownSignal: () => () => { },
|
|
8281
7464
|
});
|
|
8282
|
-
|
|
8283
|
-
sleep.tick();
|
|
8284
|
-
await flushLoopMicrotasks();
|
|
8285
|
-
expect(routedArgv).toEqual([
|
|
8286
|
-
"--company",
|
|
8287
|
-
"cmp_ghost",
|
|
8288
|
-
"--direction",
|
|
8289
|
-
"pull",
|
|
8290
|
-
"--hq-root",
|
|
8291
|
-
"/tmp/hq",
|
|
8292
|
-
]);
|
|
8293
|
-
expect(routedExit).toBe(UNRESOLVED_COMPANY_EXIT);
|
|
7465
|
+
// The transient pass did NOT end the loop — it ran again…
|
|
8294
7466
|
expect(runPass).toHaveBeenCalledTimes(2);
|
|
8295
|
-
|
|
8296
|
-
|
|
7467
|
+
// …and the transient code was never surfaced as the watcher's exit code.
|
|
7468
|
+
expect(code).toBe(1);
|
|
7469
|
+
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
8297
7470
|
expect(watcher.disposed).toBe(false);
|
|
8298
|
-
expect(findInMyNamespace).toHaveBeenCalledWith("company", "cmp_ghost");
|
|
8299
|
-
expect(routedPass.stderr.events()).toContainEqual({
|
|
8300
|
-
type: "error",
|
|
8301
|
-
company: "cmp_ghost",
|
|
8302
|
-
path: "(company-resolution)",
|
|
8303
|
-
message: "company entity lookup returned 404",
|
|
8304
|
-
});
|
|
8305
7471
|
});
|
|
8306
7472
|
it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
|
|
8307
7473
|
const watcher = makeWatcherStub();
|
|
@@ -8318,36 +7484,24 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
8318
7484
|
let passes = 0;
|
|
8319
7485
|
const runPass = vi.fn().mockImplementation(async () => {
|
|
8320
7486
|
passes += 1;
|
|
8321
|
-
//
|
|
8322
|
-
//
|
|
7487
|
+
// End the otherwise-open watch loop with a later hard failure. The first
|
|
7488
|
+
// pass uses the real company-leg fanout path, not a synthetic exit code.
|
|
8323
7489
|
return passes === 1 ? runRunner(["--companies"], transientPass) : 1;
|
|
8324
7490
|
});
|
|
8325
|
-
const
|
|
8326
|
-
let triggerShutdown = () => { };
|
|
8327
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
7491
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8328
7492
|
runPass,
|
|
8329
7493
|
clock: new FakeClock(),
|
|
8330
7494
|
createWatcher: () => watcher,
|
|
8331
|
-
sleep:
|
|
8332
|
-
onShutdownSignal: (
|
|
8333
|
-
triggerShutdown = handler;
|
|
8334
|
-
return () => { };
|
|
8335
|
-
},
|
|
7495
|
+
sleep: () => Promise.resolve(),
|
|
7496
|
+
onShutdownSignal: () => () => { },
|
|
8336
7497
|
});
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
.some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
|
|
8345
|
-
expect(watcher.disposed).toBe(false);
|
|
8346
|
-
}
|
|
8347
|
-
finally {
|
|
8348
|
-
triggerShutdown();
|
|
8349
|
-
await expect(loop).resolves.toBe(0);
|
|
8350
|
-
}
|
|
7498
|
+
expect(runPass).toHaveBeenCalledTimes(2);
|
|
7499
|
+
expect(code).toBe(1);
|
|
7500
|
+
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
7501
|
+
expect(transientPass.stderr
|
|
7502
|
+
.events()
|
|
7503
|
+
.some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
|
|
7504
|
+
expect(watcher.disposed).toBe(false);
|
|
8351
7505
|
});
|
|
8352
7506
|
it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
|
|
8353
7507
|
// A partial pass (some per-file transfers threw: oversized object on an
|
|
@@ -8361,32 +7515,22 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
8361
7515
|
let n = 0;
|
|
8362
7516
|
const runPass = vi.fn().mockImplementation(async () => {
|
|
8363
7517
|
n++;
|
|
8364
|
-
// Two partial passes prove a
|
|
8365
|
-
// the loop either. A
|
|
7518
|
+
// Two partial passes prove a DETERMINISTIC per-file failure doesn't kill
|
|
7519
|
+
// the loop either. A hard error then ends the otherwise-infinite loop.
|
|
8366
7520
|
return n <= 2 ? PARTIAL_SYNC_EXIT : 1;
|
|
8367
7521
|
});
|
|
8368
|
-
const
|
|
8369
|
-
let triggerShutdown = () => { };
|
|
8370
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
7522
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8371
7523
|
runPass,
|
|
8372
7524
|
clock: new FakeClock(),
|
|
8373
7525
|
createWatcher: () => watcher,
|
|
8374
|
-
sleep:
|
|
8375
|
-
onShutdownSignal: (
|
|
8376
|
-
triggerShutdown = handler;
|
|
8377
|
-
return () => { };
|
|
8378
|
-
},
|
|
7526
|
+
sleep: () => Promise.resolve(),
|
|
7527
|
+
onShutdownSignal: () => () => { },
|
|
8379
7528
|
});
|
|
8380
|
-
await flushLoopMicrotasks();
|
|
8381
|
-
sleep.tick();
|
|
8382
|
-
await flushLoopMicrotasks();
|
|
8383
|
-
sleep.tick();
|
|
8384
|
-
await flushLoopMicrotasks();
|
|
8385
7529
|
// Both partial passes stayed alive — the loop ran a third time…
|
|
8386
7530
|
expect(runPass).toHaveBeenCalledTimes(3);
|
|
8387
|
-
// …and
|
|
8388
|
-
|
|
8389
|
-
|
|
7531
|
+
// …and the partial code was never surfaced as the watcher's exit code.
|
|
7532
|
+
expect(code).toBe(1);
|
|
7533
|
+
expect(code).not.toBe(PARTIAL_SYNC_EXIT);
|
|
8390
7534
|
expect(watcher.disposed).toBe(false);
|
|
8391
7535
|
});
|
|
8392
7536
|
it("alerts when a partial pass failed before a company's download leg", async () => {
|
|
@@ -8414,33 +7558,24 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
8414
7558
|
return 1;
|
|
8415
7559
|
});
|
|
8416
7560
|
// Every clock read advances a full cadence interval, so the tick after
|
|
8417
|
-
// the partial pass is a scheduled full reconcile
|
|
7561
|
+
// the partial pass is a scheduled full reconcile that returns the exit
|
|
7562
|
+
// code this test asserts on.
|
|
8418
7563
|
let monotonic = 0;
|
|
8419
|
-
const sleep = makeSteppableSleep();
|
|
8420
|
-
let triggerShutdown = () => { };
|
|
8421
7564
|
try {
|
|
8422
|
-
const
|
|
7565
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8423
7566
|
runPass,
|
|
8424
7567
|
clock: new FakeClock(),
|
|
8425
7568
|
createWatcher: () => watcher,
|
|
8426
7569
|
monotonicNow: () => (monotonic += DEFAULT_FULL_RECONCILE_MS),
|
|
8427
|
-
sleep:
|
|
8428
|
-
onShutdownSignal: (
|
|
8429
|
-
triggerShutdown = handler;
|
|
8430
|
-
return () => { };
|
|
8431
|
-
},
|
|
7570
|
+
sleep: () => Promise.resolve(),
|
|
7571
|
+
onShutdownSignal: () => () => { },
|
|
8432
7572
|
});
|
|
8433
|
-
|
|
8434
|
-
sleep.tick();
|
|
8435
|
-
await flushLoopMicrotasks();
|
|
8436
|
-
expect(runPass).toHaveBeenCalledTimes(2);
|
|
7573
|
+
expect(code).toBe(1);
|
|
8437
7574
|
const stderr = stderrWrite.mock.calls
|
|
8438
7575
|
.map(([chunk]) => String(chunk))
|
|
8439
7576
|
.join("");
|
|
8440
7577
|
expect(stderr).toContain("hq-sync-runner: COMPANY PASS FAILED company=personal message=RangeError Maximum call stack size exceeded (downloads for this company did not run)");
|
|
8441
7578
|
expect(stderr).toContain("hq-sync-runner: watch pass completed with per-file errors; failed-companies=personal; staying alive, retrying next poll");
|
|
8442
|
-
triggerShutdown();
|
|
8443
|
-
await expect(loop).resolves.toBe(0);
|
|
8444
7579
|
}
|
|
8445
7580
|
finally {
|
|
8446
7581
|
stderrWrite.mockRestore();
|
|
@@ -8627,85 +7762,6 @@ describe("runRunnerWithLoop — watch-mode auth-wait", () => {
|
|
|
8627
7762
|
});
|
|
8628
7763
|
});
|
|
8629
7764
|
describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
8630
|
-
it("hands a wake realtime drain to the guard before a long-running full chore completes", async () => {
|
|
8631
|
-
const previousCoordinator = process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
8632
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = "1";
|
|
8633
|
-
const watcher = makeWatcherStub();
|
|
8634
|
-
let triggerShutdown = () => { };
|
|
8635
|
-
let releaseCheckpoint = () => { };
|
|
8636
|
-
const checkpointGate = new Promise((resolve) => { releaseCheckpoint = resolve; });
|
|
8637
|
-
let releaseRealtime = () => { };
|
|
8638
|
-
const realtimeGate = new Promise((resolve) => { releaseRealtime = resolve; });
|
|
8639
|
-
const order = [];
|
|
8640
|
-
let passes = 0;
|
|
8641
|
-
let runRealtime = null;
|
|
8642
|
-
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
8643
|
-
runPass: vi.fn().mockImplementation(async (_argv, checkpoint) => {
|
|
8644
|
-
passes += 1;
|
|
8645
|
-
if (passes === 2) {
|
|
8646
|
-
order.push("chore");
|
|
8647
|
-
// The fake pass has completed its current durable unit and is now
|
|
8648
|
-
// at the same explicit hand-off seam production uses between
|
|
8649
|
-
// fully-settled fanout targets.
|
|
8650
|
-
await checkpointGate;
|
|
8651
|
-
await checkpoint?.();
|
|
8652
|
-
order.push("chore-complete");
|
|
8653
|
-
}
|
|
8654
|
-
return completedPassOutcome();
|
|
8655
|
-
}),
|
|
8656
|
-
runPassSupportsCooperativeCheckpoint: true,
|
|
8657
|
-
clock: new FakeClock(),
|
|
8658
|
-
createWatcher: () => watcher,
|
|
8659
|
-
sleep: makeSteppableSleep().sleep,
|
|
8660
|
-
onShutdownSignal: (handler) => {
|
|
8661
|
-
triggerShutdown = handler;
|
|
8662
|
-
return () => { };
|
|
8663
|
-
},
|
|
8664
|
-
startRealtimeScheduler: async (opts) => {
|
|
8665
|
-
// This is the same wake path used by the realtime scheduler: it
|
|
8666
|
-
// enters runGuardedTask behind the coordinator worker.
|
|
8667
|
-
runRealtime = () => opts.runGuarded(async () => {
|
|
8668
|
-
order.push("realtime");
|
|
8669
|
-
await realtimeGate;
|
|
8670
|
-
order.push("realtime-complete");
|
|
8671
|
-
});
|
|
8672
|
-
return {
|
|
8673
|
-
signal: () => undefined,
|
|
8674
|
-
checkHighWater: async () => undefined,
|
|
8675
|
-
dispose: async () => undefined,
|
|
8676
|
-
};
|
|
8677
|
-
},
|
|
8678
|
-
});
|
|
8679
|
-
try {
|
|
8680
|
-
await flushLoopMicrotasks(30);
|
|
8681
|
-
watcher.emit();
|
|
8682
|
-
await flushLoopMicrotasks(20);
|
|
8683
|
-
expect(order).toEqual(["chore"]);
|
|
8684
|
-
const realtime = runRealtime();
|
|
8685
|
-
releaseCheckpoint();
|
|
8686
|
-
await flushLoopMicrotasks(30);
|
|
8687
|
-
expect(order).toEqual(["chore", "realtime"]);
|
|
8688
|
-
releaseRealtime();
|
|
8689
|
-
await realtime;
|
|
8690
|
-
await flushLoopMicrotasks(20);
|
|
8691
|
-
expect(order).toEqual([
|
|
8692
|
-
"chore",
|
|
8693
|
-
"realtime",
|
|
8694
|
-
"realtime-complete",
|
|
8695
|
-
"chore-complete",
|
|
8696
|
-
]);
|
|
8697
|
-
}
|
|
8698
|
-
finally {
|
|
8699
|
-
releaseCheckpoint();
|
|
8700
|
-
releaseRealtime();
|
|
8701
|
-
triggerShutdown();
|
|
8702
|
-
await loop;
|
|
8703
|
-
if (previousCoordinator === undefined)
|
|
8704
|
-
delete process.env.HQ_SYNC_PARALLEL_COORDINATOR;
|
|
8705
|
-
else
|
|
8706
|
-
process.env.HQ_SYNC_PARALLEL_COORDINATOR = previousCoordinator;
|
|
8707
|
-
}
|
|
8708
|
-
});
|
|
8709
7765
|
it("feeds a watcher-originated V2 drain through the same single-flight guard as the poll", async () => {
|
|
8710
7766
|
const watcher = makeWatcherStub();
|
|
8711
7767
|
let triggerShutdown = () => { };
|
|
@@ -8818,173 +7874,6 @@ describe("runRunnerWithLoop — U58 realtime scheduler guard", () => {
|
|
|
8818
7874
|
await loop;
|
|
8819
7875
|
});
|
|
8820
7876
|
});
|
|
8821
|
-
describe("CooperativePassScheduler", () => {
|
|
8822
|
-
it("admits an explicitly safe fast realtime pass while a slow scoped realtime pass remains in flight", async () => {
|
|
8823
|
-
const scheduler = new CooperativePassScheduler(() => 0);
|
|
8824
|
-
const order = [];
|
|
8825
|
-
let releaseSlow = () => { };
|
|
8826
|
-
const slowGate = new Promise((resolve) => { releaseSlow = resolve; });
|
|
8827
|
-
let markSlowStarted = () => { };
|
|
8828
|
-
const slowStarted = new Promise((resolve) => { markSlowStarted = resolve; });
|
|
8829
|
-
const slow = scheduler.submit(async () => {
|
|
8830
|
-
order.push("slow-start");
|
|
8831
|
-
markSlowStarted();
|
|
8832
|
-
await slowGate;
|
|
8833
|
-
order.push("slow-complete");
|
|
8834
|
-
return 0;
|
|
8835
|
-
}, "realtime", "slow");
|
|
8836
|
-
await slowStarted;
|
|
8837
|
-
const fast = scheduler.submit(async () => {
|
|
8838
|
-
order.push("fast");
|
|
8839
|
-
return 0;
|
|
8840
|
-
}, "realtime", "fast", true);
|
|
8841
|
-
try {
|
|
8842
|
-
await Promise.resolve();
|
|
8843
|
-
await Promise.resolve();
|
|
8844
|
-
expect([...order]).toEqual(["slow-start", "fast"]);
|
|
8845
|
-
}
|
|
8846
|
-
finally {
|
|
8847
|
-
releaseSlow();
|
|
8848
|
-
await Promise.all([slow, fast]);
|
|
8849
|
-
}
|
|
8850
|
-
});
|
|
8851
|
-
it("preserves journal order and resumes full work across repeated randomized hand-offs", async () => {
|
|
8852
|
-
// These deterministic seeds deliberately vary the number and enqueue
|
|
8853
|
-
// ordering of realtime tasks. Every append asserts one durable-state owner
|
|
8854
|
-
// at a time; the full continuation must resume after every realtime burst.
|
|
8855
|
-
for (let seed = 1; seed <= 32; seed += 1) {
|
|
8856
|
-
let now = 0;
|
|
8857
|
-
const scheduler = new CooperativePassScheduler(() => now);
|
|
8858
|
-
const journal = [];
|
|
8859
|
-
const expected = new Set();
|
|
8860
|
-
let writer = null;
|
|
8861
|
-
let maximumWriters = 0;
|
|
8862
|
-
const append = async (entry) => {
|
|
8863
|
-
expect(writer).toBeNull();
|
|
8864
|
-
writer = entry;
|
|
8865
|
-
maximumWriters = Math.max(maximumWriters, 1);
|
|
8866
|
-
// A journal append crosses an async durable-write boundary. If the
|
|
8867
|
-
// scheduler admitted another pass here, its ownership assertion would
|
|
8868
|
-
// fail rather than merely observing JavaScript's synchronous turn.
|
|
8869
|
-
await Promise.resolve();
|
|
8870
|
-
expect(writer).toBe(entry);
|
|
8871
|
-
journal.push(entry);
|
|
8872
|
-
writer = null;
|
|
8873
|
-
};
|
|
8874
|
-
let releaseCheckpoint = () => { };
|
|
8875
|
-
const checkpointReady = new Promise((resolve) => { releaseCheckpoint = resolve; });
|
|
8876
|
-
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
8877
|
-
expected.add(`chore-${seed}-before`);
|
|
8878
|
-
await append(`chore-${seed}-before`);
|
|
8879
|
-
await checkpointReady;
|
|
8880
|
-
await checkpoint();
|
|
8881
|
-
expected.add(`chore-${seed}-after`);
|
|
8882
|
-
await append(`chore-${seed}-after`);
|
|
8883
|
-
return 0;
|
|
8884
|
-
}, "chore");
|
|
8885
|
-
await Promise.resolve();
|
|
8886
|
-
const realtimeCount = (seed % 7) + 1;
|
|
8887
|
-
const shuffledIndexes = Array.from({ length: realtimeCount }, (_, index) => index);
|
|
8888
|
-
let randomState = seed;
|
|
8889
|
-
for (let index = shuffledIndexes.length - 1; index > 0; index -= 1) {
|
|
8890
|
-
randomState = (randomState * 1_664_525 + 1_013_904_223) >>> 0;
|
|
8891
|
-
const swapIndex = randomState % (index + 1);
|
|
8892
|
-
[shuffledIndexes[index], shuffledIndexes[swapIndex]] = [
|
|
8893
|
-
shuffledIndexes[swapIndex],
|
|
8894
|
-
shuffledIndexes[index],
|
|
8895
|
-
];
|
|
8896
|
-
}
|
|
8897
|
-
const realtime = shuffledIndexes.map((index) => {
|
|
8898
|
-
const entry = `realtime-${seed}-${(index * 17 + seed) % realtimeCount}`;
|
|
8899
|
-
expected.add(entry);
|
|
8900
|
-
return scheduler.submit(async () => {
|
|
8901
|
-
// Shuffle the microtask timing without ever allowing a second writer.
|
|
8902
|
-
if ((seed + index) % 3 === 0)
|
|
8903
|
-
await Promise.resolve();
|
|
8904
|
-
await append(entry);
|
|
8905
|
-
now += 1;
|
|
8906
|
-
return 0;
|
|
8907
|
-
}, "realtime");
|
|
8908
|
-
});
|
|
8909
|
-
releaseCheckpoint();
|
|
8910
|
-
await Promise.all([chore, ...realtime]);
|
|
8911
|
-
expect(maximumWriters).toBe(1);
|
|
8912
|
-
expect(new Set(journal)).toEqual(expected);
|
|
8913
|
-
expect(journal).toHaveLength(expected.size);
|
|
8914
|
-
expect(journal[0]).toBe(`chore-${seed}-before`);
|
|
8915
|
-
expect(journal.at(-1)).toBe(`chore-${seed}-after`);
|
|
8916
|
-
}
|
|
8917
|
-
});
|
|
8918
|
-
it("lets realtime win at 15 minutes minus one millisecond, then gives the chore the exact-boundary admission", async () => {
|
|
8919
|
-
const runCase = async (elapsedMs) => {
|
|
8920
|
-
let now = 0;
|
|
8921
|
-
const scheduler = new CooperativePassScheduler(() => now);
|
|
8922
|
-
const order = [];
|
|
8923
|
-
let releaseActive = () => { };
|
|
8924
|
-
const activeGate = new Promise((resolve) => { releaseActive = resolve; });
|
|
8925
|
-
const activeRealtime = scheduler.submit(async () => {
|
|
8926
|
-
order.push("active-realtime");
|
|
8927
|
-
await activeGate;
|
|
8928
|
-
return 0;
|
|
8929
|
-
}, "realtime");
|
|
8930
|
-
await Promise.resolve();
|
|
8931
|
-
const chore = scheduler.submit(async () => {
|
|
8932
|
-
order.push("chore");
|
|
8933
|
-
return 0;
|
|
8934
|
-
}, "chore");
|
|
8935
|
-
const queuedRealtime = scheduler.submit(async () => {
|
|
8936
|
-
order.push("queued-realtime");
|
|
8937
|
-
return 0;
|
|
8938
|
-
}, "realtime");
|
|
8939
|
-
now = elapsedMs;
|
|
8940
|
-
releaseActive();
|
|
8941
|
-
await Promise.all([activeRealtime, chore, queuedRealtime]);
|
|
8942
|
-
return order;
|
|
8943
|
-
};
|
|
8944
|
-
await expect(runCase(CHORE_STARVATION_CAP_MS - 1)).resolves.toEqual([
|
|
8945
|
-
"active-realtime",
|
|
8946
|
-
"queued-realtime",
|
|
8947
|
-
"chore",
|
|
8948
|
-
]);
|
|
8949
|
-
await expect(runCase(CHORE_STARVATION_CAP_MS)).resolves.toEqual([
|
|
8950
|
-
"active-realtime",
|
|
8951
|
-
"chore",
|
|
8952
|
-
"queued-realtime",
|
|
8953
|
-
]);
|
|
8954
|
-
});
|
|
8955
|
-
it("resumes a handed-off chore when a continuous realtime burst reaches the 15-minute cap", async () => {
|
|
8956
|
-
let now = 0;
|
|
8957
|
-
const scheduler = new CooperativePassScheduler(() => now);
|
|
8958
|
-
const order = [];
|
|
8959
|
-
let releaseCheckpoint = () => { };
|
|
8960
|
-
const checkpointReady = new Promise((resolve) => { releaseCheckpoint = resolve; });
|
|
8961
|
-
const chore = scheduler.submit(async ({ checkpoint }) => {
|
|
8962
|
-
order.push("chore-before");
|
|
8963
|
-
await checkpointReady;
|
|
8964
|
-
await checkpoint();
|
|
8965
|
-
order.push("chore-after");
|
|
8966
|
-
return 0;
|
|
8967
|
-
}, "chore");
|
|
8968
|
-
await Promise.resolve();
|
|
8969
|
-
const firstRealtime = scheduler.submit(async () => {
|
|
8970
|
-
order.push("realtime-before-cap");
|
|
8971
|
-
now = CHORE_STARVATION_CAP_MS;
|
|
8972
|
-
return 0;
|
|
8973
|
-
}, "realtime");
|
|
8974
|
-
const laterRealtime = scheduler.submit(async () => {
|
|
8975
|
-
order.push("realtime-after-cap");
|
|
8976
|
-
return 0;
|
|
8977
|
-
}, "realtime");
|
|
8978
|
-
releaseCheckpoint();
|
|
8979
|
-
await Promise.all([chore, firstRealtime, laterRealtime]);
|
|
8980
|
-
expect(order).toEqual([
|
|
8981
|
-
"chore-before",
|
|
8982
|
-
"realtime-before-cap",
|
|
8983
|
-
"chore-after",
|
|
8984
|
-
"realtime-after-cap",
|
|
8985
|
-
]);
|
|
8986
|
-
});
|
|
8987
|
-
});
|
|
8988
7877
|
describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
8989
7878
|
const watchArgv = [
|
|
8990
7879
|
"--companies",
|
|
@@ -11148,9 +10037,6 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
11148
10037
|
"/tmp/hq",
|
|
11149
10038
|
], {
|
|
11150
10039
|
runPass: runPass,
|
|
11151
|
-
...(opts.supportsCooperativeCheckpoint
|
|
11152
|
-
? { runPassSupportsCooperativeCheckpoint: true }
|
|
11153
|
-
: {}),
|
|
11154
10040
|
clock,
|
|
11155
10041
|
createWatcher: (options) => {
|
|
11156
10042
|
opts.onCreateWatcher?.({
|
|
@@ -11263,123 +10149,6 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
|
|
|
11263
10149
|
shutdown();
|
|
11264
10150
|
await loop;
|
|
11265
10151
|
});
|
|
11266
|
-
it("records initial-pull writes before its cooperative checkpoint can admit watcher work", async () => {
|
|
11267
|
-
const markContentPublished = vi.fn();
|
|
11268
|
-
const startEventSync = vi.fn().mockResolvedValue({
|
|
11269
|
-
publishBatch: vi.fn(),
|
|
11270
|
-
markContentPublished,
|
|
11271
|
-
receiver: { start: async () => { }, dispose: async () => { }, connected: true },
|
|
11272
|
-
ownDeviceId: "dev-test",
|
|
11273
|
-
dispose: vi.fn().mockResolvedValue(undefined),
|
|
11274
|
-
});
|
|
11275
|
-
const watcher = makeBatchWatcherStub();
|
|
11276
|
-
let suppressionWasInstalledWhenRealtimeStarted;
|
|
11277
|
-
const runPass = vi.fn(async (argv, checkpoint, onJournalsReady, onAppliedUpserts) => {
|
|
11278
|
-
if (argv.includes("--scope-path")) {
|
|
11279
|
-
suppressionWasInstalledWhenRealtimeStarted = markContentPublished
|
|
11280
|
-
.mock.calls
|
|
11281
|
-
.some(([relativePath]) => relativePath === "companies/indigo/initial-pull.md");
|
|
11282
|
-
return completedPassOutcome();
|
|
11283
|
-
}
|
|
11284
|
-
onJournalsReady?.();
|
|
11285
|
-
await microtasks();
|
|
11286
|
-
watcher.emit(undefined, {
|
|
11287
|
-
paths: new Map([[
|
|
11288
|
-
"/tmp/hq/companies/indigo/watcher-arrived-during-pull.md",
|
|
11289
|
-
"companies/indigo/watcher-arrived-during-pull.md",
|
|
11290
|
-
]]),
|
|
11291
|
-
changes: new Map([[
|
|
11292
|
-
"/tmp/hq/companies/indigo/watcher-arrived-during-pull.md",
|
|
11293
|
-
{ kind: "change" },
|
|
11294
|
-
]]),
|
|
11295
|
-
});
|
|
11296
|
-
// Let the watcher submit its realtime work before the full pass reaches
|
|
11297
|
-
// its explicit hand-off. No timer is involved: the submission is a
|
|
11298
|
-
// promise-only scheduling boundary.
|
|
11299
|
-
await microtasks();
|
|
11300
|
-
onAppliedUpserts?.([{
|
|
11301
|
-
relativePath: "companies/indigo/initial-pull.md",
|
|
11302
|
-
contentHash: "sha256:initial-pull",
|
|
11303
|
-
}]);
|
|
11304
|
-
await checkpoint?.();
|
|
11305
|
-
return {
|
|
11306
|
-
exitCode: 0,
|
|
11307
|
-
result: {
|
|
11308
|
-
pushPathResults: [],
|
|
11309
|
-
appliedUpserts: [{
|
|
11310
|
-
relativePath: "companies/indigo/initial-pull.md",
|
|
11311
|
-
contentHash: "sha256:initial-pull",
|
|
11312
|
-
}],
|
|
11313
|
-
},
|
|
11314
|
-
};
|
|
11315
|
-
});
|
|
11316
|
-
const { loop, shutdown } = runLoop({
|
|
11317
|
-
claims: EMAIL_CLAIMS,
|
|
11318
|
-
startEventSync,
|
|
11319
|
-
runPass,
|
|
11320
|
-
watcher,
|
|
11321
|
-
supportsCooperativeCheckpoint: true,
|
|
11322
|
-
});
|
|
11323
|
-
await microtasks(16);
|
|
11324
|
-
expect(suppressionWasInstalledWhenRealtimeStarted).toBe(true);
|
|
11325
|
-
shutdown();
|
|
11326
|
-
await loop;
|
|
11327
|
-
});
|
|
11328
|
-
it("does not let a yielding warm-up catch-up overwrite a newer watcher unlink", async () => {
|
|
11329
|
-
const watcher = makeBatchWatcherStub();
|
|
11330
|
-
let resolveCatchup;
|
|
11331
|
-
watcher.collectWarmupCatchupAsync = vi.fn(() => new Promise((resolve) => {
|
|
11332
|
-
resolveCatchup = resolve;
|
|
11333
|
-
}));
|
|
11334
|
-
const runPass = vi.fn(async (_argv, _checkpoint, onJournalsReady) => {
|
|
11335
|
-
onJournalsReady?.();
|
|
11336
|
-
return completedPassOutcome();
|
|
11337
|
-
});
|
|
11338
|
-
const { loop, tick, shutdown } = runLoop({
|
|
11339
|
-
claims: EMAIL_CLAIMS,
|
|
11340
|
-
watcher,
|
|
11341
|
-
runPass,
|
|
11342
|
-
supportsCooperativeCheckpoint: true,
|
|
11343
|
-
});
|
|
11344
|
-
await microtasks(12);
|
|
11345
|
-
const absolutePath = "/tmp/hq/companies/indigo/recreated.md";
|
|
11346
|
-
const relativePath = "companies/indigo/recreated.md";
|
|
11347
|
-
watcher.emit(undefined, {
|
|
11348
|
-
paths: new Map([[absolutePath, relativePath]]),
|
|
11349
|
-
changes: new Map([[absolutePath, {
|
|
11350
|
-
kind: "unlink",
|
|
11351
|
-
deleteSnapshots: [{
|
|
11352
|
-
journalSlug: "indigo",
|
|
11353
|
-
journalPath: "recreated.md",
|
|
11354
|
-
deleteScopeRoot: "recreated.md",
|
|
11355
|
-
absolutePath,
|
|
11356
|
-
remoteEtag: "etag-before-recreate",
|
|
11357
|
-
localHash: "hash-before-recreate",
|
|
11358
|
-
localKind: "file",
|
|
11359
|
-
}],
|
|
11360
|
-
}]]),
|
|
11361
|
-
});
|
|
11362
|
-
// This is the older filesystem state collected before the live unlink.
|
|
11363
|
-
resolveCatchup?.({
|
|
11364
|
-
batch: {
|
|
11365
|
-
paths: new Map([[absolutePath, relativePath]]),
|
|
11366
|
-
changes: new Map([[absolutePath, { kind: "change" }]]),
|
|
11367
|
-
},
|
|
11368
|
-
scannedPaths: 1,
|
|
11369
|
-
});
|
|
11370
|
-
await microtasks(16);
|
|
11371
|
-
tick();
|
|
11372
|
-
await microtasks(16);
|
|
11373
|
-
const scopedPush = runPass.mock.calls.find((call) => call[0].includes("--scope-path"))?.[0];
|
|
11374
|
-
expect(scopedPush).toEqual(expect.arrayContaining([
|
|
11375
|
-
"--scope-path",
|
|
11376
|
-
"recreated.md",
|
|
11377
|
-
"--delete-scope-path",
|
|
11378
|
-
"recreated.md",
|
|
11379
|
-
]));
|
|
11380
|
-
shutdown();
|
|
11381
|
-
await loop;
|
|
11382
|
-
});
|
|
11383
10152
|
it("replays pull upsert marks after event-sync startup becomes live", async () => {
|
|
11384
10153
|
const markContentPublished = vi.fn();
|
|
11385
10154
|
let resolveStart;
|
|
@@ -13770,45 +12539,6 @@ describe("watcher delete intents without pre-seeded authorization", () => {
|
|
|
13770
12539
|
expect(Object.keys(readJournal("indigo").files)).toHaveLength(12);
|
|
13771
12540
|
await stopHarness(h);
|
|
13772
12541
|
});
|
|
13773
|
-
it("attributes a company-root unlinkDir to its journal while rejecting company-root metadata files", async () => {
|
|
13774
|
-
const keys = Array.from({ length: 12 }, (_, i) => `knowledge/company-root/f-${i}.md`);
|
|
13775
|
-
const pushPasses = [];
|
|
13776
|
-
const h = startDeleteHarness(keys, {
|
|
13777
|
-
direction: "push",
|
|
13778
|
-
onPass: (argv) => {
|
|
13779
|
-
if (argv[argv.indexOf("--direction") + 1] === "push") {
|
|
13780
|
-
pushPasses.push(argv);
|
|
13781
|
-
}
|
|
13782
|
-
},
|
|
13783
|
-
});
|
|
13784
|
-
await settle();
|
|
13785
|
-
pushPasses.length = 0;
|
|
13786
|
-
const childKey = keys[0];
|
|
13787
|
-
const childPath = path.join(h.companyRoot, childKey);
|
|
13788
|
-
fs.unlinkSync(childPath);
|
|
13789
|
-
expect(h.capture()(`companies/indigo/${childKey}`, "unlink")).toHaveLength(1);
|
|
13790
|
-
expect(readJournal("indigo").files[childKey]?.localDeleteIntent).toBeDefined();
|
|
13791
|
-
// The child intent is safe only until the enclosing whole-company removal
|
|
13792
|
-
// is classified. At that scope the exact denominator is the whole shard,
|
|
13793
|
-
// so the existing bulk guard must revoke it rather than lose attribution.
|
|
13794
|
-
fs.rmSync(h.companyRoot, { recursive: true, force: true });
|
|
13795
|
-
const rootSnapshots = h.capture()("companies/indigo", "unlinkDir");
|
|
13796
|
-
expect(rootSnapshots).toHaveLength(0);
|
|
13797
|
-
expect(Object.values(readJournal("indigo").files).filter((entry) => entry.localDeleteIntent)).toHaveLength(0);
|
|
13798
|
-
h.watcher.emit("companies/indigo", {
|
|
13799
|
-
paths: new Map([[h.companyRoot, "companies/indigo"]]),
|
|
13800
|
-
changes: new Map([[h.companyRoot, {
|
|
13801
|
-
kind: "unlinkDir",
|
|
13802
|
-
deleteSnapshots: rootSnapshots,
|
|
13803
|
-
}]]),
|
|
13804
|
-
});
|
|
13805
|
-
h.tick();
|
|
13806
|
-
await settle();
|
|
13807
|
-
expect(pushPasses).toEqual([
|
|
13808
|
-
expect.arrayContaining(["--company", "indigo", "--direction", "push"]),
|
|
13809
|
-
]);
|
|
13810
|
-
await stopHarness(h);
|
|
13811
|
-
});
|
|
13812
12542
|
// The native macOS/Windows backends make no child-before-parent guarantee,
|
|
13813
12543
|
// so the refusal has to hold for descendants that arrive after it too.
|
|
13814
12544
|
it("a descendant unlink arriving after a refused unlinkDir records no intent", async () => {
|