@indigoai-us/hq-cloud 6.15.0 → 6.15.2
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-mutation.d.ts +16 -0
- package/dist/bin/sync-mutation.d.ts.map +1 -0
- package/dist/bin/sync-mutation.js +60 -0
- package/dist/bin/sync-mutation.js.map +1 -0
- package/dist/bin/sync-mutation.test.d.ts +2 -0
- package/dist/bin/sync-mutation.test.d.ts.map +1 -0
- package/dist/bin/sync-mutation.test.js +165 -0
- package/dist/bin/sync-mutation.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts +8 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +16 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.d.ts +2 -0
- package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-company.test.js +36 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +98 -8
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +109 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict-recovery.test.d.ts +2 -0
- package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
- package/dist/cli/conflict-recovery.test.js +201 -0
- package/dist/cli/conflict-recovery.test.js.map +1 -0
- package/dist/cli/conflict.d.ts +60 -0
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +333 -0
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/sync.d.ts +35 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +100 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +85 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -2
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +79 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/candidate-uploader.d.ts +88 -0
- package/dist/sync/candidate-uploader.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.js +212 -0
- package/dist/sync/candidate-uploader.js.map +1 -0
- package/dist/sync/candidate-uploader.test.d.ts +2 -0
- package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
- package/dist/sync/candidate-uploader.test.js +132 -0
- package/dist/sync/candidate-uploader.test.js.map +1 -0
- package/dist/sync/delta-client.d.ts +73 -0
- package/dist/sync/delta-client.d.ts.map +1 -0
- package/dist/sync/delta-client.js +201 -0
- package/dist/sync/delta-client.js.map +1 -0
- package/dist/sync/delta-client.test.d.ts +2 -0
- package/dist/sync/delta-client.test.d.ts.map +1 -0
- package/dist/sync/delta-client.test.js +97 -0
- package/dist/sync/delta-client.test.js.map +1 -0
- package/dist/sync/durable-apply.d.ts +76 -0
- package/dist/sync/durable-apply.d.ts.map +1 -0
- package/dist/sync/durable-apply.js +530 -0
- package/dist/sync/durable-apply.js.map +1 -0
- package/dist/sync/durable-apply.test.d.ts +2 -0
- package/dist/sync/durable-apply.test.d.ts.map +1 -0
- package/dist/sync/durable-apply.test.js +180 -0
- package/dist/sync/durable-apply.test.js.map +1 -0
- package/dist/sync/event-sync.d.ts +33 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +149 -1
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +142 -1
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/multipart-uploader.d.ts +99 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.js +447 -0
- package/dist/sync/multipart-uploader.js.map +1 -0
- package/dist/sync/multipart-uploader.test.d.ts +2 -0
- package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
- package/dist/sync/multipart-uploader.test.js +119 -0
- package/dist/sync/multipart-uploader.test.js.map +1 -0
- package/dist/sync/mutation-client.d.ts +85 -0
- package/dist/sync/mutation-client.d.ts.map +1 -0
- package/dist/sync/mutation-client.js +245 -0
- package/dist/sync/mutation-client.js.map +1 -0
- package/dist/sync/mutation-client.test.d.ts +2 -0
- package/dist/sync/mutation-client.test.d.ts.map +1 -0
- package/dist/sync/mutation-client.test.js +51 -0
- package/dist/sync/mutation-client.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +45 -0
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +101 -0
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +54 -2
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts +69 -0
- package/dist/sync/scope-inventory-client.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.js +210 -0
- package/dist/sync/scope-inventory-client.js.map +1 -0
- package/dist/sync/scope-inventory-client.test.d.ts +2 -0
- package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
- package/dist/sync/scope-inventory-client.test.js +94 -0
- package/dist/sync/scope-inventory-client.test.js.map +1 -0
- package/dist/sync/snapshot-client.d.ts +98 -0
- package/dist/sync/snapshot-client.d.ts.map +1 -0
- package/dist/sync/snapshot-client.js +402 -0
- package/dist/sync/snapshot-client.js.map +1 -0
- package/dist/sync/snapshot-client.test.d.ts +2 -0
- package/dist/sync/snapshot-client.test.d.ts.map +1 -0
- package/dist/sync/snapshot-client.test.js +169 -0
- package/dist/sync/snapshot-client.test.js.map +1 -0
- package/dist/sync/uploader-finalization.d.ts +97 -0
- package/dist/sync/uploader-finalization.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.js +273 -0
- package/dist/sync/uploader-finalization.js.map +1 -0
- package/dist/sync/uploader-finalization.test.d.ts +2 -0
- package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
- package/dist/sync/uploader-finalization.test.js +92 -0
- package/dist/sync/uploader-finalization.test.js.map +1 -0
- package/dist/telemetry.d.ts +11 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +21 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +80 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +6 -1
- package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
- package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
- package/.github/workflows/ci.yml +0 -84
- package/.github/workflows/publish.yml +0 -56
- package/.github/workflows/unreleased-commits-nag.yml +0 -256
- package/eslint.config.js +0 -67
- package/pnpm-workspace.yaml +0 -2
- package/scripts/presign-transport-e2e.mjs +0 -250
- package/scripts/vault-rebaseline.sh +0 -323
- package/scripts/vault-rescue.sh +0 -332
- package/src/active-company.test.ts +0 -188
- package/src/active-company.ts +0 -168
- package/src/agent-codex-instructions.test.ts +0 -332
- package/src/agent-codex-instructions.ts +0 -309
- package/src/auth.ts +0 -146
- package/src/backup-prune.test.ts +0 -98
- package/src/backup-prune.ts +0 -182
- package/src/bin/backup-prune-runner.ts +0 -33
- package/src/bin/rescue-runner.ts +0 -25
- package/src/bin/sync-runner-company.ts +0 -695
- package/src/bin/sync-runner-events.test.ts +0 -143
- package/src/bin/sync-runner-events.ts +0 -55
- package/src/bin/sync-runner-planning.test.ts +0 -311
- package/src/bin/sync-runner-planning.ts +0 -258
- package/src/bin/sync-runner-rollup.test.ts +0 -37
- package/src/bin/sync-runner-rollup.ts +0 -97
- package/src/bin/sync-runner-telemetry.ts +0 -15
- package/src/bin/sync-runner-watch-loop.ts +0 -1235
- package/src/bin/sync-runner-watch-routes.test.ts +0 -71
- package/src/bin/sync-runner-watch-routes.ts +0 -184
- package/src/bin/sync-runner.test.ts +0 -8767
- package/src/bin/sync-runner.ts +0 -2190
- package/src/cli/accept.ts +0 -124
- package/src/cli/conflict.ts +0 -119
- package/src/cli/doctor.test.ts +0 -581
- package/src/cli/doctor.ts +0 -642
- package/src/cli/index.ts +0 -49
- package/src/cli/invite.test.ts +0 -250
- package/src/cli/invite.ts +0 -214
- package/src/cli/promote.ts +0 -157
- package/src/cli/reindex-knowledge.test.ts +0 -307
- package/src/cli/reindex-knowledge.ts +0 -450
- package/src/cli/reindex.test.ts +0 -957
- package/src/cli/reindex.ts +0 -979
- package/src/cli/rescue-classify-ordering.test.ts +0 -548
- package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
- package/src/cli/rescue-core.ts +0 -3011
- package/src/cli/rescue-drift-reconcile.test.ts +0 -179
- package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
- package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
- package/src/cli/rescue-hq-root-guard.test.ts +0 -232
- package/src/cli/rescue-journal-reconcile.test.ts +0 -215
- package/src/cli/rescue-mtime-preserve.test.ts +0 -203
- package/src/cli/rescue-settings-reconcile.test.ts +0 -637
- package/src/cli/rescue-snapshot.test.ts +0 -57
- package/src/cli/rescue-snapshot.ts +0 -51
- package/src/cli/rescue.reindex.test.ts +0 -63
- package/src/cli/rescue.test.ts +0 -131
- package/src/cli/rescue.ts +0 -182
- package/src/cli/share.test.ts +0 -7843
- package/src/cli/share.ts +0 -3663
- package/src/cli/sync-scope.test.ts +0 -652
- package/src/cli/sync.test.ts +0 -5207
- package/src/cli/sync.ts +0 -3470
- package/src/cli/tombstones.ts +0 -106
- package/src/cli/watch-event-push-conflict.test.ts +0 -234
- package/src/client-info.test.ts +0 -214
- package/src/client-info.ts +0 -121
- package/src/cognito-auth.test.ts +0 -712
- package/src/cognito-auth.ts +0 -1422
- package/src/company-resolver.test.ts +0 -618
- package/src/company-resolver.ts +0 -521
- package/src/context.test.ts +0 -583
- package/src/context.ts +0 -378
- package/src/daemon-worker.ts +0 -26
- package/src/daemon.ts +0 -99
- package/src/entity-resolver.test.ts +0 -315
- package/src/entity-resolver.ts +0 -180
- package/src/ignore.test.ts +0 -466
- package/src/ignore.ts +0 -469
- package/src/index.ts +0 -439
- package/src/journal.test.ts +0 -968
- package/src/journal.ts +0 -765
- package/src/lib/cloud-authoritative.test.ts +0 -45
- package/src/lib/cloud-authoritative.ts +0 -59
- package/src/lib/conflict-file.ts +0 -86
- package/src/lib/conflict-index.ts +0 -289
- package/src/lib/conflict.test.ts +0 -348
- package/src/lib/describe-error.test.ts +0 -100
- package/src/lib/describe-error.ts +0 -58
- package/src/lib/exit-codes.ts +0 -24
- package/src/lib/machine-id.test.ts +0 -231
- package/src/lib/machine-id.ts +0 -175
- package/src/lib/net-errors.test.ts +0 -65
- package/src/lib/net-errors.ts +0 -86
- package/src/lib/readlink-safe.test.ts +0 -43
- package/src/lib/readlink-safe.ts +0 -29
- package/src/local-path-codec.test.ts +0 -138
- package/src/local-path-codec.ts +0 -161
- package/src/machine-auth.test.ts +0 -1323
- package/src/manifest-reconcile.test.ts +0 -1123
- package/src/manifest-reconcile.ts +0 -518
- package/src/object-io.test.ts +0 -1221
- package/src/object-io.ts +0 -1306
- package/src/operation-lock.test.ts +0 -484
- package/src/operation-lock.ts +0 -680
- package/src/outcome-telemetry.test.ts +0 -498
- package/src/outcome-telemetry.ts +0 -639
- package/src/personal-vault-exclusions.test.ts +0 -308
- package/src/personal-vault-exclusions.ts +0 -354
- package/src/personal-vault.test.ts +0 -756
- package/src/personal-vault.ts +0 -496
- package/src/prefix-coalesce.test.ts +0 -240
- package/src/prefix-coalesce.ts +0 -273
- package/src/public-surface.test.ts +0 -117
- package/src/qmd-reindex.test.ts +0 -877
- package/src/qmd-reindex.ts +0 -842
- package/src/read-only-state-dir.test.ts +0 -188
- package/src/remote-pull.test.ts +0 -1130
- package/src/remote-pull.ts +0 -618
- package/src/s3.symlink-materialize.test.ts +0 -492
- package/src/s3.test.ts +0 -1789
- package/src/s3.ts +0 -1532
- package/src/schemas/signal-types.test.ts +0 -82
- package/src/schemas/signal-types.ts +0 -38
- package/src/schemas/source-channels.test.ts +0 -82
- package/src/schemas/source-channels.ts +0 -53
- package/src/scope-shrink.test.ts +0 -633
- package/src/scope-shrink.ts +0 -481
- package/src/signals/get.test.ts +0 -310
- package/src/signals/get.ts +0 -75
- package/src/signals/internals.ts +0 -195
- package/src/signals/list.test.ts +0 -420
- package/src/signals/list.ts +0 -79
- package/src/signals/parse.ts +0 -8
- package/src/signals/types.ts +0 -91
- package/src/skill-telemetry.test.ts +0 -1825
- package/src/skill-telemetry.ts +0 -1439
- package/src/sources/get.test.ts +0 -293
- package/src/sources/get.ts +0 -66
- package/src/sources/internals.ts +0 -198
- package/src/sources/list.test.ts +0 -402
- package/src/sources/list.ts +0 -84
- package/src/sources/parse.ts +0 -43
- package/src/sources/types.ts +0 -84
- package/src/sync/event-sync.test.ts +0 -594
- package/src/sync/event-sync.ts +0 -545
- package/src/sync/feature-flags.test.ts +0 -378
- package/src/sync/feature-flags.ts +0 -62
- package/src/sync/index.ts +0 -76
- package/src/sync/lease-client.test.ts +0 -128
- package/src/sync/lease-client.ts +0 -207
- package/src/sync/logger.test.ts +0 -242
- package/src/sync/logger.ts +0 -79
- package/src/sync/metrics.test.ts +0 -462
- package/src/sync/metrics.ts +0 -213
- package/src/sync/pull-scope.ts +0 -265
- package/src/sync/push-event.test.ts +0 -266
- package/src/sync/push-event.ts +0 -224
- package/src/sync/push-receiver.test.ts +0 -566
- package/src/sync/push-receiver.ts +0 -1048
- package/src/sync/push-transport.ts +0 -231
- package/src/sync/realtime-rollout.test.ts +0 -86
- package/src/sync/realtime-rollout.ts +0 -262
- package/src/sync/state-store.test.ts +0 -194
- package/src/sync/state-store.ts +0 -727
- package/src/sync-core.ts +0 -58
- package/src/sync-progress.test.ts +0 -94
- package/src/sync-progress.ts +0 -140
- package/src/telemetry-events.test.ts +0 -88
- package/src/telemetry-events.ts +0 -205
- package/src/telemetry.test.ts +0 -1280
- package/src/telemetry.ts +0 -1109
- package/src/types.ts +0 -314
- package/src/vault-client.test.ts +0 -1380
- package/src/vault-client.ts +0 -1694
- package/src/version.ts +0 -24
- package/src/watch-roots.test.ts +0 -278
- package/src/watch-roots.ts +0 -162
- package/src/watcher-event-gate.test.ts +0 -212
- package/src/watcher.test.ts +0 -1079
- package/src/watcher.ts +0 -1741
- package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
- package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
- package/test/e2e/sync/transient-company-leg.test.ts +0 -384
- package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
- package/test/e2e/watcher-real-chokidar.test.ts +0 -165
- package/test/e2e/watcher-recursive-backend.test.ts +0 -181
- package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
- package/test/invite-flow.integration.test.ts +0 -244
- package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
- package/test/share-sync.integration.test.ts +0 -213
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -22
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* US-008 — client push transport unit tests.
|
|
3
|
-
*
|
|
4
|
-
* The PRD names this file as the home for US-008's coverage; it spans the
|
|
5
|
-
* four required assertions across the three US-008 modules:
|
|
6
|
-
*
|
|
7
|
-
* 1. flag gating — feature-flags.ts (env list, legacy global,
|
|
8
|
-
* static provider, malformed-entry tolerance)
|
|
9
|
-
* 2. POST — push-transport.ts HttpPushTransport posts an
|
|
10
|
-
* encoded PushEvent to `<apiUrl>/sync/push` with
|
|
11
|
-
* a Bearer token (mocked fetch — no real server)
|
|
12
|
-
* 3. emit — watcher.ts PushEventEmitter builds a valid
|
|
13
|
-
* PushEvent (sha256 hash, monotonic seq, tenant)
|
|
14
|
-
* per debounced change and hands it to the
|
|
15
|
-
* transport; dormant when the flag is OFF
|
|
16
|
-
* 4. transport failure — a publish rejection (or hash error) is caught,
|
|
17
|
-
* surfaced via onError, and never crashes / never
|
|
18
|
-
* propagates (cadence poll is the safety net)
|
|
19
|
-
*
|
|
20
|
-
* The live `/sync/push` endpoint is NOT deployed yet — every POST is asserted
|
|
21
|
-
* against a mocked fetch / mocked transport.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
-
import { tmpdir } from "node:os";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
EVENT_SYNC_LOCAL_DISABLE_ENV_VAR,
|
|
32
|
-
isEventSyncLocallyDisabled,
|
|
33
|
-
} from "./feature-flags.js";
|
|
34
|
-
import {
|
|
35
|
-
HttpPushTransport,
|
|
36
|
-
NoopPushTransport,
|
|
37
|
-
type FetchLike,
|
|
38
|
-
type PushTransport,
|
|
39
|
-
} from "./push-transport.js";
|
|
40
|
-
import { decodePushEvent, type PushEvent } from "./push-event.js";
|
|
41
|
-
import { PushEventEmitter } from "../watcher.js";
|
|
42
|
-
import type { TreeChangeBatch } from "../watcher.js";
|
|
43
|
-
|
|
44
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
function fakePushEvent(overrides: Partial<PushEvent> = {}): PushEvent {
|
|
47
|
-
return {
|
|
48
|
-
relativePath: "personal/notes/a.md",
|
|
49
|
-
kind: "upsert",
|
|
50
|
-
contentHash: `sha256:${"a".repeat(64)}`,
|
|
51
|
-
mtime: "2026-05-21T12:00:00.000Z",
|
|
52
|
-
originDeviceId: "device-1",
|
|
53
|
-
originTenantId: "indigo",
|
|
54
|
-
sequenceNumber: 0,
|
|
55
|
-
eventTimestamp: "2026-05-21T12:00:01.000Z",
|
|
56
|
-
...overrides,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** A fetch double that records calls and returns a configurable response. */
|
|
61
|
-
function makeFetch(
|
|
62
|
-
res: { ok: boolean; status: number; body?: string } = {
|
|
63
|
-
ok: true,
|
|
64
|
-
status: 200,
|
|
65
|
-
},
|
|
66
|
-
): { fetch: FetchLike; calls: Array<{ url: string; init: unknown }> } {
|
|
67
|
-
const calls: Array<{ url: string; init: unknown }> = [];
|
|
68
|
-
const fetch: FetchLike = async (url, init) => {
|
|
69
|
-
calls.push({ url, init });
|
|
70
|
-
return {
|
|
71
|
-
ok: res.ok,
|
|
72
|
-
status: res.status,
|
|
73
|
-
text: async () => res.body ?? "",
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
return { fetch, calls };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ─── 1. Local disable (feature-flags.ts) ─────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
const serverGranted = { isEnabled: () => true };
|
|
82
|
-
const serverDenied = { isEnabled: () => false };
|
|
83
|
-
|
|
84
|
-
describe("Event sync local control", () => {
|
|
85
|
-
it("exports only the local disable control", () => {
|
|
86
|
-
expect(EVENT_SYNC_LOCAL_DISABLE_ENV_VAR).toBe("HQ_SYNC_EVENT_SYNC");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it.each(["0", "false", "no", "off", " OFF "])(
|
|
90
|
-
"recognizes %j as a local disable",
|
|
91
|
-
(value) => expect(isEventSyncLocallyDisabled(value)).toBe(true),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
it.each([undefined, "", "1", "true", "yes", "on", "indigo,acme"])(
|
|
95
|
-
"%j does not grant authority",
|
|
96
|
-
(value) => expect(isEventSyncLocallyDisabled(value)).toBe(false),
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ─── 2. POST (HttpPushTransport) ──────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
describe("HttpPushTransport — POST /sync/push", () => {
|
|
103
|
-
it("POSTs an encoded PushEvent to <apiUrl>/sync/push with a Bearer token", async () => {
|
|
104
|
-
const { fetch, calls } = makeFetch({ ok: true, status: 200 });
|
|
105
|
-
const transport = new HttpPushTransport({
|
|
106
|
-
apiUrl: "https://vault-api.example.com/",
|
|
107
|
-
authToken: "tok-123",
|
|
108
|
-
fetchImpl: fetch,
|
|
109
|
-
});
|
|
110
|
-
await transport.start();
|
|
111
|
-
expect(transport.connected).toBe(true);
|
|
112
|
-
|
|
113
|
-
const event = fakePushEvent();
|
|
114
|
-
await transport.publish(event);
|
|
115
|
-
|
|
116
|
-
expect(calls).toHaveLength(1);
|
|
117
|
-
const call = calls[0];
|
|
118
|
-
// Trailing slash on apiUrl is normalized; default path is /sync/push.
|
|
119
|
-
expect(call.url).toBe("https://vault-api.example.com/sync/push");
|
|
120
|
-
const init = call.init as {
|
|
121
|
-
method: string;
|
|
122
|
-
headers: Record<string, string>;
|
|
123
|
-
body: string;
|
|
124
|
-
};
|
|
125
|
-
expect(init.method).toBe("POST");
|
|
126
|
-
expect(init.headers.Authorization).toBe("Bearer tok-123");
|
|
127
|
-
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
128
|
-
// Body is a valid encoded PushEvent that round-trips.
|
|
129
|
-
expect(decodePushEvent(init.body)).toEqual(event);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("POSTs a delete tombstone without contentHash or mtime", async () => {
|
|
133
|
-
const { fetch, calls } = makeFetch({ ok: true, status: 200 });
|
|
134
|
-
const transport = new HttpPushTransport({
|
|
135
|
-
apiUrl: "https://vault-api.example.com/",
|
|
136
|
-
authToken: "tok-123",
|
|
137
|
-
fetchImpl: fetch,
|
|
138
|
-
});
|
|
139
|
-
const event = fakePushEvent({
|
|
140
|
-
kind: "delete",
|
|
141
|
-
contentHash: undefined,
|
|
142
|
-
mtime: undefined,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
await transport.publish(event);
|
|
146
|
-
|
|
147
|
-
expect(calls).toHaveLength(1);
|
|
148
|
-
const body = (calls[0].init as { body: string }).body;
|
|
149
|
-
expect(JSON.parse(body)).toEqual({
|
|
150
|
-
relativePath: "personal/notes/a.md",
|
|
151
|
-
kind: "delete",
|
|
152
|
-
originDeviceId: "device-1",
|
|
153
|
-
originTenantId: "indigo",
|
|
154
|
-
sequenceNumber: 0,
|
|
155
|
-
eventTimestamp: "2026-05-21T12:00:01.000Z",
|
|
156
|
-
});
|
|
157
|
-
expect(decodePushEvent(body)).toEqual(JSON.parse(body));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("resolves the auth token per-request via an async getter (self-heals across refresh)", async () => {
|
|
161
|
-
const { fetch, calls } = makeFetch();
|
|
162
|
-
let n = 0;
|
|
163
|
-
const transport = new HttpPushTransport({
|
|
164
|
-
apiUrl: "https://api.example.com",
|
|
165
|
-
authToken: () => `tok-${n++}`,
|
|
166
|
-
fetchImpl: fetch,
|
|
167
|
-
});
|
|
168
|
-
await transport.publish(fakePushEvent());
|
|
169
|
-
await transport.publish(fakePushEvent());
|
|
170
|
-
expect((calls[0].init as any).headers.Authorization).toBe("Bearer tok-0");
|
|
171
|
-
expect((calls[1].init as any).headers.Authorization).toBe("Bearer tok-1");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("honors a custom pushPath + extra headers", async () => {
|
|
175
|
-
const { fetch, calls } = makeFetch();
|
|
176
|
-
const transport = new HttpPushTransport({
|
|
177
|
-
apiUrl: "https://api.example.com",
|
|
178
|
-
pushPath: "v1/sync/push",
|
|
179
|
-
authToken: "t",
|
|
180
|
-
headers: { "x-hq-client-name": "hq-sync-runner" },
|
|
181
|
-
fetchImpl: fetch,
|
|
182
|
-
});
|
|
183
|
-
await transport.publish(fakePushEvent());
|
|
184
|
-
expect(calls[0].url).toBe("https://api.example.com/v1/sync/push");
|
|
185
|
-
expect((calls[0].init as any).headers["x-hq-client-name"]).toBe(
|
|
186
|
-
"hq-sync-runner",
|
|
187
|
-
);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("rejects when apiUrl is missing", () => {
|
|
191
|
-
expect(
|
|
192
|
-
() => new HttpPushTransport({ apiUrl: "", authToken: "t" }),
|
|
193
|
-
).toThrow(/apiUrl is required/);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("rejects (does not swallow) on a non-2xx response", async () => {
|
|
197
|
-
const { fetch } = makeFetch({ ok: false, status: 403, body: "forbidden" });
|
|
198
|
-
const transport = new HttpPushTransport({
|
|
199
|
-
apiUrl: "https://api.example.com",
|
|
200
|
-
authToken: "t",
|
|
201
|
-
fetchImpl: fetch,
|
|
202
|
-
});
|
|
203
|
-
await expect(transport.publish(fakePushEvent())).rejects.toThrow(/403/);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("rejects on a malformed event before hitting the network", async () => {
|
|
207
|
-
const { fetch, calls } = makeFetch();
|
|
208
|
-
const transport = new HttpPushTransport({
|
|
209
|
-
apiUrl: "https://api.example.com",
|
|
210
|
-
authToken: "t",
|
|
211
|
-
fetchImpl: fetch,
|
|
212
|
-
});
|
|
213
|
-
await expect(
|
|
214
|
-
transport.publish(fakePushEvent({ contentHash: "not-a-hash" })),
|
|
215
|
-
).rejects.toBeTruthy();
|
|
216
|
-
expect(calls).toHaveLength(0);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// ─── 3 + 4. Emit + gating + failure handling (PushEventEmitter) ───────────────
|
|
221
|
-
|
|
222
|
-
describe("PushEventEmitter — emit, gating, failure handling", () => {
|
|
223
|
-
let dir: string;
|
|
224
|
-
let filePath: string;
|
|
225
|
-
const REL = "a.md";
|
|
226
|
-
|
|
227
|
-
beforeEach(() => {
|
|
228
|
-
dir = mkdtempSync(path.join(tmpdir(), "us008-"));
|
|
229
|
-
filePath = path.join(dir, REL);
|
|
230
|
-
writeFileSync(filePath, "hello world");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
afterEach(() => {
|
|
234
|
-
rmSync(dir, { recursive: true, force: true });
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
function batchFor(...rels: string[]): TreeChangeBatch {
|
|
238
|
-
const paths = new Map<string, string>();
|
|
239
|
-
for (const rel of rels) paths.set(path.join(dir, rel), rel);
|
|
240
|
-
return { paths };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
it("emits a valid PushEvent (sha256 hash, tenant) per changed path and ships it", async () => {
|
|
244
|
-
const transport = new NoopPushTransport();
|
|
245
|
-
const published: PushEvent[] = [];
|
|
246
|
-
const spyTransport: PushTransport = {
|
|
247
|
-
start: () => transport.start(),
|
|
248
|
-
dispose: () => transport.dispose(),
|
|
249
|
-
get connected() {
|
|
250
|
-
return transport.connected;
|
|
251
|
-
},
|
|
252
|
-
publish: async (e) => {
|
|
253
|
-
published.push(e);
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
const emitter = new PushEventEmitter({
|
|
257
|
-
originTenantId: "indigo",
|
|
258
|
-
originDeviceId: "dev-1",
|
|
259
|
-
transport: spyTransport,
|
|
260
|
-
flagProvider: serverGranted,
|
|
261
|
-
now: () => new Date("2026-05-21T00:00:00.000Z"),
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
await emitter.emitForBatch(batchFor(REL));
|
|
265
|
-
|
|
266
|
-
expect(published).toHaveLength(1);
|
|
267
|
-
const e = published[0];
|
|
268
|
-
expect(e.relativePath).toBe(REL);
|
|
269
|
-
expect(e.kind).toBe("upsert");
|
|
270
|
-
expect(e.originTenantId).toBe("indigo");
|
|
271
|
-
expect(e.originDeviceId).toBe("dev-1");
|
|
272
|
-
const expectedHex = createHash("sha256").update("hello world").digest("hex");
|
|
273
|
-
expect(e.contentHash).toBe(`sha256:${expectedHex}`);
|
|
274
|
-
// Round-trips through the wire contract.
|
|
275
|
-
expect(() => decodePushEvent(e)).not.toThrow();
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("assigns monotonic per-device sequence numbers across emits", async () => {
|
|
279
|
-
const published: PushEvent[] = [];
|
|
280
|
-
const emitter = new PushEventEmitter({
|
|
281
|
-
originTenantId: "indigo",
|
|
282
|
-
originDeviceId: "dev-1",
|
|
283
|
-
transport: { start: async () => {}, dispose: async () => {}, connected: true, publish: async (e) => { published.push(e); } },
|
|
284
|
-
flagProvider: serverGranted,
|
|
285
|
-
});
|
|
286
|
-
writeFileSync(path.join(dir, "b.md"), "second");
|
|
287
|
-
await emitter.emitForBatch(batchFor(REL));
|
|
288
|
-
await emitter.emitForBatch(batchFor("b.md"));
|
|
289
|
-
expect(published.map((e) => e.sequenceNumber)).toEqual([0, 1]);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("is DORMANT when the flag is OFF: ships nothing, attach is a no-op", async () => {
|
|
293
|
-
const publish = vi.fn(async () => {});
|
|
294
|
-
const emitter = new PushEventEmitter({
|
|
295
|
-
originTenantId: "indigo",
|
|
296
|
-
originDeviceId: "dev-1",
|
|
297
|
-
transport: { start: async () => {}, dispose: async () => {}, connected: false, publish },
|
|
298
|
-
flagProvider: serverDenied, // indigo NOT enabled
|
|
299
|
-
});
|
|
300
|
-
expect(emitter.enabled).toBe(false);
|
|
301
|
-
await emitter.emitForBatch(batchFor(REL));
|
|
302
|
-
expect(publish).not.toHaveBeenCalled();
|
|
303
|
-
// attach returns a callable no-op when dormant.
|
|
304
|
-
const fakeWatcher = { onChange: vi.fn() } as any;
|
|
305
|
-
const off = emitter.attach(fakeWatcher);
|
|
306
|
-
expect(fakeWatcher.onChange).not.toHaveBeenCalled();
|
|
307
|
-
expect(() => off()).not.toThrow();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("transport publish failure is caught + surfaced, never throws (cadence safety net)", async () => {
|
|
311
|
-
const onError = vi.fn();
|
|
312
|
-
const emitter = new PushEventEmitter({
|
|
313
|
-
originTenantId: "indigo",
|
|
314
|
-
originDeviceId: "dev-1",
|
|
315
|
-
transport: {
|
|
316
|
-
start: async () => {},
|
|
317
|
-
dispose: async () => {},
|
|
318
|
-
connected: true,
|
|
319
|
-
publish: async () => {
|
|
320
|
-
throw new Error("network down");
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
flagProvider: serverGranted,
|
|
324
|
-
onError,
|
|
325
|
-
});
|
|
326
|
-
// Must not reject — the daemon stays alive.
|
|
327
|
-
await expect(emitter.emitForBatch(batchFor(REL))).resolves.toBeUndefined();
|
|
328
|
-
expect(onError).toHaveBeenCalledTimes(1);
|
|
329
|
-
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
330
|
-
expect(onError.mock.calls[0][1]).toMatchObject({ relativePath: REL });
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("a missing file emits a delete tombstone, and other paths still ship", async () => {
|
|
334
|
-
const published: PushEvent[] = [];
|
|
335
|
-
const onError = vi.fn();
|
|
336
|
-
const emitter = new PushEventEmitter({
|
|
337
|
-
originTenantId: "indigo",
|
|
338
|
-
originDeviceId: "dev-1",
|
|
339
|
-
transport: { start: async () => {}, dispose: async () => {}, connected: true, publish: async (e) => { published.push(e); } },
|
|
340
|
-
flagProvider: serverGranted,
|
|
341
|
-
onError,
|
|
342
|
-
});
|
|
343
|
-
// REL exists; "ghost.md" does not.
|
|
344
|
-
await emitter.emitForBatch(batchFor(REL, "ghost.md"));
|
|
345
|
-
expect(published.map((e) => e.relativePath).sort()).toEqual([
|
|
346
|
-
REL,
|
|
347
|
-
"ghost.md",
|
|
348
|
-
].sort());
|
|
349
|
-
const tombstone = published.find((e) => e.relativePath === "ghost.md");
|
|
350
|
-
expect(tombstone).toMatchObject({ kind: "delete" });
|
|
351
|
-
expect(tombstone).not.toHaveProperty("contentHash");
|
|
352
|
-
expect(tombstone).not.toHaveProperty("mtime");
|
|
353
|
-
expect(onError).not.toHaveBeenCalled();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("end-to-end: emit → HttpPushTransport POST with a valid schema (mocked fetch)", async () => {
|
|
357
|
-
const { fetch, calls } = makeFetch({ ok: true, status: 200 });
|
|
358
|
-
const transport = new HttpPushTransport({
|
|
359
|
-
apiUrl: "https://api.example.com",
|
|
360
|
-
authToken: "tok",
|
|
361
|
-
fetchImpl: fetch,
|
|
362
|
-
});
|
|
363
|
-
const emitter = new PushEventEmitter({
|
|
364
|
-
originTenantId: "indigo",
|
|
365
|
-
originDeviceId: "dev-1",
|
|
366
|
-
transport,
|
|
367
|
-
flagProvider: serverGranted,
|
|
368
|
-
});
|
|
369
|
-
await transport.start();
|
|
370
|
-
await emitter.emitForBatch(batchFor(REL));
|
|
371
|
-
|
|
372
|
-
expect(calls).toHaveLength(1);
|
|
373
|
-
expect(calls[0].url).toBe("https://api.example.com/sync/push");
|
|
374
|
-
const posted = decodePushEvent((calls[0].init as any).body);
|
|
375
|
-
expect(posted.relativePath).toBe(REL);
|
|
376
|
-
expect(posted.originTenantId).toBe("indigo");
|
|
377
|
-
});
|
|
378
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local control for the dormant event-sync wiring.
|
|
3
|
-
*
|
|
4
|
-
* U15 removes client-side positive selectors. Environment values, tenant IDs,
|
|
5
|
-
* and cached identity cannot grant authority; the authenticated server
|
|
6
|
-
* inventory is the sole positive authority. This module preserves only the
|
|
7
|
-
* operator's local off-switch.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** A local-only kill switch. Any other value is deliberately non-authorizing. */
|
|
11
|
-
export const EVENT_SYNC_LOCAL_DISABLE_ENV_VAR = "HQ_SYNC_EVENT_SYNC";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Compatibility seam consumed by the existing emitter. Production event-sync
|
|
15
|
-
* supplies it only after authenticated server inventory has granted access.
|
|
16
|
-
*/
|
|
17
|
-
export interface EventDrivenPushFlagProvider {
|
|
18
|
-
isEnabled(scopeId: string): boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Deprecated inert compatibility shape; no environment is ever read. */
|
|
22
|
-
export type FlagProviderWarnFn = (
|
|
23
|
-
message: string,
|
|
24
|
-
context: Record<string, unknown>,
|
|
25
|
-
) => void;
|
|
26
|
-
|
|
27
|
-
/** Deprecated inert compatibility shape; no environment is ever read. */
|
|
28
|
-
export interface EnvTenantListFlagProviderOptions {
|
|
29
|
-
env?: Record<string, string | undefined>;
|
|
30
|
-
warn?: FlagProviderWarnFn;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Compatibility fixture for older direct emitter tests. It represents a
|
|
35
|
-
* previously authenticated server inventory result; the supplied legacy value
|
|
36
|
-
* is deliberately ignored, so it cannot select a tenant or grant by email.
|
|
37
|
-
*/
|
|
38
|
-
export class StaticFlagProvider implements EventDrivenPushFlagProvider {
|
|
39
|
-
constructor(_authenticatedLegacyInventory: unknown) {}
|
|
40
|
-
|
|
41
|
-
isEnabled(_scopeId: string): boolean {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Returns true only for an explicit local disable value. Values such as
|
|
48
|
-
* `true`, `1`, a tenant list, or an unrecognized string never enable sync.
|
|
49
|
-
*/
|
|
50
|
-
export function isEventSyncLocallyDisabled(
|
|
51
|
-
value: string | undefined,
|
|
52
|
-
): boolean {
|
|
53
|
-
switch ((value ?? "").trim().toLowerCase()) {
|
|
54
|
-
case "0":
|
|
55
|
-
case "false":
|
|
56
|
-
case "no":
|
|
57
|
-
case "off":
|
|
58
|
-
return true;
|
|
59
|
-
default:
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/sync/index.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @indigoai-us/hq-cloud — event-driven sync (`src/sync`) barrel.
|
|
3
|
-
*
|
|
4
|
-
* Surfaces the PushEvent wire contract + the PushTransport shipping seam
|
|
5
|
-
* ported from hq-pro PR #112 (project event-driven-sync-menubar US-007).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
CONTENT_HASH_PATTERN,
|
|
10
|
-
ISO8601_DATETIME_PATTERN,
|
|
11
|
-
PushEventSchema,
|
|
12
|
-
PushEventDecodeError,
|
|
13
|
-
encodePushEvent,
|
|
14
|
-
decodePushEvent,
|
|
15
|
-
} from "./push-event.js";
|
|
16
|
-
export type {
|
|
17
|
-
PushEvent,
|
|
18
|
-
PushEventInput,
|
|
19
|
-
PushEventDecodeIssue,
|
|
20
|
-
} from "./push-event.js";
|
|
21
|
-
|
|
22
|
-
export { NoopPushTransport, HttpPushTransport } from "./push-transport.js";
|
|
23
|
-
export type {
|
|
24
|
-
PushTransport,
|
|
25
|
-
HttpPushTransportOptions,
|
|
26
|
-
AuthTokenSource,
|
|
27
|
-
FetchLike,
|
|
28
|
-
} from "./push-transport.js";
|
|
29
|
-
|
|
30
|
-
export {
|
|
31
|
-
EVENT_SYNC_LOCAL_DISABLE_ENV_VAR,
|
|
32
|
-
isEventSyncLocallyDisabled,
|
|
33
|
-
StaticFlagProvider,
|
|
34
|
-
} from "./feature-flags.js";
|
|
35
|
-
export type {
|
|
36
|
-
EventDrivenPushFlagProvider,
|
|
37
|
-
FlagProviderWarnFn,
|
|
38
|
-
EnvTenantListFlagProviderOptions,
|
|
39
|
-
} from "./feature-flags.js";
|
|
40
|
-
|
|
41
|
-
export {
|
|
42
|
-
NoopPushReceiver,
|
|
43
|
-
SqsPushReceiver,
|
|
44
|
-
InMemoryFanout,
|
|
45
|
-
InMemoryPushReceiver,
|
|
46
|
-
createPushReceiver,
|
|
47
|
-
DEFAULT_RECEIVER_DISPOSE_DRAIN_MS,
|
|
48
|
-
DEFAULT_WAIT_TIME_SECONDS,
|
|
49
|
-
DEFAULT_MAX_MESSAGES,
|
|
50
|
-
} from "./push-receiver.js";
|
|
51
|
-
export type {
|
|
52
|
-
PushReceiver,
|
|
53
|
-
PushReceiverContext,
|
|
54
|
-
SyncEngineFn,
|
|
55
|
-
PublishMetricFn,
|
|
56
|
-
ReceiverLogger,
|
|
57
|
-
SqsClientLike,
|
|
58
|
-
SqsMessageLike,
|
|
59
|
-
SqsPushReceiverOptions,
|
|
60
|
-
InMemoryPushReceiverOptions,
|
|
61
|
-
CreatePushReceiverOptions,
|
|
62
|
-
} from "./push-receiver.js";
|
|
63
|
-
|
|
64
|
-
export {
|
|
65
|
-
SYNC_METRIC_NAMESPACE,
|
|
66
|
-
SYNC_LATENCY_METRIC_NAME,
|
|
67
|
-
_setSyncCloudWatchClient,
|
|
68
|
-
publishSyncLatencyMetric,
|
|
69
|
-
} from "./metrics.js";
|
|
70
|
-
export type {
|
|
71
|
-
SyncLatencyMetric,
|
|
72
|
-
PublishSyncLatencyMetricOptions,
|
|
73
|
-
} from "./metrics.js";
|
|
74
|
-
|
|
75
|
-
export { createLogger } from "./logger.js";
|
|
76
|
-
export type { CreateLoggerOptions, Logger } from "./logger.js";
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { RealtimeLeaseClient, RealtimePassAuthorizer } from "./lease-client.js";
|
|
4
|
-
import { RealtimeRolloutClient, type RealtimeCapabilityVector } from "./realtime-rollout.js";
|
|
5
|
-
|
|
6
|
-
const capability: RealtimeCapabilityVector = {
|
|
7
|
-
hqCloud: "6.14.49",
|
|
8
|
-
hqDesktop: "none",
|
|
9
|
-
protocol: 2,
|
|
10
|
-
journal: 3,
|
|
11
|
-
durableApply: 1,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function jsonResponse(body: unknown, status = 200) {
|
|
15
|
-
return { ok: status >= 200 && status < 300, status, text: async () => JSON.stringify(body) };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("RealtimeLeaseClient", () => {
|
|
19
|
-
it("uses the pass bearer and rejects a lease longer than sixty seconds", async () => {
|
|
20
|
-
let received: { method?: string; headers?: Record<string, string> } | undefined;
|
|
21
|
-
const fetchImpl = async (_url: string, init?: { method?: string; headers?: Record<string, string> }) => {
|
|
22
|
-
received = init;
|
|
23
|
-
return jsonResponse({
|
|
24
|
-
lease: "lease_1",
|
|
25
|
-
issuedAt: "2026-08-12T00:00:00.000Z",
|
|
26
|
-
expiresAt: "2026-08-12T00:01:01.000Z",
|
|
27
|
-
authorizationEpoch: "epoch_1",
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
|
-
const client = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => Date.parse("2026-08-12T00:00:00.000Z") });
|
|
31
|
-
|
|
32
|
-
await expect(client.issue({ bearer: "token-a", deviceId: "machine_a", capabilityVector: capability, scopeHandle: "handle_1", scopeRevision: "rev_1" })).rejects.toThrow("invalid lease lifetime");
|
|
33
|
-
expect(received).toMatchObject({
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: expect.objectContaining({ Authorization: "Bearer token-a", "X-HQ-Sync-Contract": "2" }),
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects a lease issued too far into the future", async () => {
|
|
40
|
-
const fetchImpl = async () => jsonResponse({
|
|
41
|
-
lease: "lease_1",
|
|
42
|
-
issuedAt: "2026-08-12T00:00:06.000Z",
|
|
43
|
-
expiresAt: "2026-08-12T00:01:00.000Z",
|
|
44
|
-
authorizationEpoch: "epoch_1",
|
|
45
|
-
});
|
|
46
|
-
const client = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => Date.parse("2026-08-12T00:00:00.000Z") });
|
|
47
|
-
await expect(client.issue({ bearer: "token-a", deviceId: "machine_a", capabilityVector: capability, scopeHandle: "handle_1", scopeRevision: "rev_1" })).rejects.toThrow("invalid lease lifetime");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("resolves one bearer and freshly leases every selected scope before I/O", async () => {
|
|
51
|
-
const now = Date.parse("2026-08-12T00:00:00.000Z");
|
|
52
|
-
const requests: Array<{ headers?: Record<string, string> }> = [];
|
|
53
|
-
const fetchImpl = async (url: string, init?: { headers?: Record<string, string> }) => {
|
|
54
|
-
requests.push({ headers: init?.headers });
|
|
55
|
-
if (url.endsWith("/scopes")) {
|
|
56
|
-
return jsonResponse({ scopes: [
|
|
57
|
-
{ scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "v2-ready", cursor: "cursor_1" },
|
|
58
|
-
{ scopeUid: "cmp_1", scopeHandle: "handle_2", scopeRevision: "rev_2", state: "v2-ready", cursor: "cursor_2" },
|
|
59
|
-
] });
|
|
60
|
-
}
|
|
61
|
-
return jsonResponse({
|
|
62
|
-
lease: "lease_issued",
|
|
63
|
-
issuedAt: "2026-08-12T00:00:00.000Z",
|
|
64
|
-
expiresAt: "2026-08-12T00:01:00.000Z",
|
|
65
|
-
authorizationEpoch: "epoch_1",
|
|
66
|
-
}, 201);
|
|
67
|
-
};
|
|
68
|
-
const token = vi.fn(async () => "token-a");
|
|
69
|
-
const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl, now: () => now });
|
|
70
|
-
const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now: () => now });
|
|
71
|
-
const authorizer = new RealtimePassAuthorizer(token, inventory, leases);
|
|
72
|
-
const operation = vi.fn(async () => "done");
|
|
73
|
-
|
|
74
|
-
await expect(authorizer.runBeforeIo({
|
|
75
|
-
deviceId: "machine_a",
|
|
76
|
-
capabilityVector: capability,
|
|
77
|
-
configurationFingerprint: "config-a",
|
|
78
|
-
scopeFingerprint: "prs_1,cmp_1",
|
|
79
|
-
scopeUids: ["prs_1", "cmp_1"],
|
|
80
|
-
}, operation)).resolves.toBe("done");
|
|
81
|
-
|
|
82
|
-
expect(token).toHaveBeenCalledTimes(1);
|
|
83
|
-
expect(requests).toHaveLength(3);
|
|
84
|
-
for (const call of requests) expect(call.headers?.Authorization).toBe("Bearer token-a");
|
|
85
|
-
expect(operation).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("does not invoke I/O when inventory loses a requested scope", async () => {
|
|
89
|
-
const fetchImpl = vi.fn(async () => jsonResponse({ scopes: [
|
|
90
|
-
{ scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "paused", reasonCode: "maintenance" },
|
|
91
|
-
] }));
|
|
92
|
-
const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl });
|
|
93
|
-
const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl });
|
|
94
|
-
const authorizer = new RealtimePassAuthorizer("token-a", inventory, leases);
|
|
95
|
-
const operation = vi.fn(async () => undefined);
|
|
96
|
-
|
|
97
|
-
await expect(authorizer.runBeforeIo({
|
|
98
|
-
deviceId: "machine_a", capabilityVector: capability, configurationFingerprint: "config-a", scopeFingerprint: "prs_1", scopeUids: ["prs_1"],
|
|
99
|
-
}, operation)).rejects.toThrow("does not authorize every requested scope");
|
|
100
|
-
expect(operation).not.toHaveBeenCalled();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("does not invoke I/O when parallel lease issues disagree on epoch", async () => {
|
|
104
|
-
let leaseCalls = 0;
|
|
105
|
-
const fetchImpl = async (url: string) => {
|
|
106
|
-
if (url.endsWith("/scopes")) return jsonResponse({ scopes: [
|
|
107
|
-
{ scopeUid: "prs_1", scopeHandle: "handle_1", scopeRevision: "rev_1", state: "v2-ready", cursor: "cursor_1" },
|
|
108
|
-
{ scopeUid: "cmp_1", scopeHandle: "handle_2", scopeRevision: "rev_2", state: "v2-ready", cursor: "cursor_2" },
|
|
109
|
-
] });
|
|
110
|
-
leaseCalls += 1;
|
|
111
|
-
return jsonResponse({
|
|
112
|
-
lease: `lease_${leaseCalls}`,
|
|
113
|
-
issuedAt: "2026-08-12T00:00:00.000Z",
|
|
114
|
-
expiresAt: "2026-08-12T00:00:30.000Z",
|
|
115
|
-
authorizationEpoch: `epoch_${leaseCalls}`,
|
|
116
|
-
}, 201);
|
|
117
|
-
};
|
|
118
|
-
const now = () => Date.parse("2026-08-12T00:00:00.000Z");
|
|
119
|
-
const inventory = new RealtimeRolloutClient({ apiUrl: "https://api.example", fetchImpl, now });
|
|
120
|
-
const leases = new RealtimeLeaseClient({ apiUrl: "https://api.example", fetchImpl, now });
|
|
121
|
-
const authorizer = new RealtimePassAuthorizer("token-a", inventory, leases);
|
|
122
|
-
const operation = vi.fn(async () => undefined);
|
|
123
|
-
await expect(authorizer.runBeforeIo({
|
|
124
|
-
deviceId: "machine_a", capabilityVector: capability, configurationFingerprint: "config-a", scopeFingerprint: "all", scopeUids: ["prs_1", "cmp_1"],
|
|
125
|
-
}, operation)).rejects.toThrow("leases disagree on authorization epoch");
|
|
126
|
-
expect(operation).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
});
|