@indigoai-us/hq-cloud 6.14.0 → 6.14.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-runner-company.d.ts +4 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +50 -1
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +3 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +21 -7
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +6 -2
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +162 -11
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +155 -17
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/accept.d.ts.map +1 -1
- package/dist/cli/accept.js +29 -0
- package/dist/cli/accept.js.map +1 -1
- package/dist/cli/invite.d.ts.map +1 -1
- package/dist/cli/invite.js +33 -0
- package/dist/cli/invite.js.map +1 -1
- package/dist/cli/promote.d.ts.map +1 -1
- package/dist/cli/promote.js +33 -0
- package/dist/cli/promote.js.map +1 -1
- package/dist/cli/reindex.d.ts +23 -11
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +39 -10
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +40 -13
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/cli/share.js +47 -4
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +123 -2
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +84 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +44 -3
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +156 -7
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +133 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/company-resolver.d.ts +21 -0
- package/dist/company-resolver.d.ts.map +1 -1
- package/dist/company-resolver.js +31 -6
- package/dist/company-resolver.js.map +1 -1
- package/dist/company-resolver.test.js +44 -1
- package/dist/company-resolver.test.js.map +1 -1
- package/dist/ignore.d.ts +10 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +23 -2
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +13 -1
- package/dist/ignore.test.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts +12 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +12 -0
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +25 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +25 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +45 -14
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +61 -1
- package/dist/object-io.test.js.map +1 -1
- package/dist/operation-lock.d.ts +36 -6
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +34 -11
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +30 -0
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/s3.d.ts +1 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +75 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +56 -1
- package/dist/s3.test.js.map +1 -1
- package/dist/scope-shrink.d.ts +12 -0
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +12 -0
- package/dist/scope-shrink.js.map +1 -1
- package/dist/scope-shrink.test.js +31 -0
- package/dist/scope-shrink.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +41 -0
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +111 -7
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +187 -5
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +3 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +23 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/pull-scope.d.ts +21 -0
- package/dist/sync/pull-scope.d.ts.map +1 -1
- package/dist/sync/pull-scope.js +46 -13
- package/dist/sync/pull-scope.js.map +1 -1
- package/dist/telemetry-events.d.ts +31 -0
- package/dist/telemetry-events.d.ts.map +1 -0
- package/dist/telemetry-events.js +140 -0
- package/dist/telemetry-events.js.map +1 -0
- package/dist/telemetry-events.test.d.ts +2 -0
- package/dist/telemetry-events.test.d.ts.map +1 -0
- package/dist/telemetry-events.test.js +73 -0
- package/dist/telemetry-events.test.js.map +1 -0
- package/dist/telemetry.js +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/vault-client.d.ts +38 -5
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +69 -6
- package/dist/vault-client.js.map +1 -1
- package/dist/vault-client.test.js +31 -1
- package/dist/vault-client.test.js.map +1 -1
- package/dist/watcher.d.ts +7 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +53 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +57 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +2 -0
- package/scripts/presign-transport-e2e.mjs +2 -2
- package/src/bin/sync-runner-company.ts +65 -1
- package/src/bin/sync-runner-watch-loop.ts +24 -8
- package/src/bin/sync-runner.test.ts +194 -16
- package/src/bin/sync-runner.ts +209 -11
- package/src/cli/accept.ts +27 -0
- package/src/cli/invite.ts +31 -0
- package/src/cli/promote.ts +31 -0
- package/src/cli/reindex.test.ts +44 -13
- package/src/cli/reindex.ts +74 -21
- package/src/cli/share.test.ts +139 -2
- package/src/cli/share.ts +48 -2
- package/src/cli/sync-scope.test.ts +90 -0
- package/src/cli/sync.test.ts +154 -0
- package/src/cli/sync.ts +224 -5
- package/src/company-resolver.test.ts +51 -0
- package/src/company-resolver.ts +40 -5
- package/src/ignore.test.ts +18 -1
- package/src/ignore.ts +24 -2
- package/src/index.ts +22 -1
- package/src/journal.ts +12 -0
- package/src/lib/exit-codes.ts +24 -0
- package/src/object-io.test.ts +66 -1
- package/src/object-io.ts +51 -14
- package/src/operation-lock.test.ts +33 -0
- package/src/operation-lock.ts +46 -11
- package/src/s3.test.ts +87 -0
- package/src/s3.ts +93 -2
- package/src/scope-shrink.test.ts +32 -0
- package/src/scope-shrink.ts +12 -0
- package/src/skill-telemetry.test.ts +247 -4
- package/src/skill-telemetry.ts +134 -6
- package/src/sync/event-sync.ts +37 -0
- package/src/sync/pull-scope.ts +62 -13
- package/src/telemetry-events.test.ts +88 -0
- package/src/telemetry-events.ts +205 -0
- package/src/telemetry.ts +1 -1
- package/src/vault-client.test.ts +39 -1
- package/src/vault-client.ts +115 -14
- package/src/watcher.test.ts +82 -0
- package/src/watcher.ts +69 -0
|
@@ -10,13 +10,14 @@ 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 { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, defaultCollectTelemetry, } from "./sync-runner.js";
|
|
13
|
+
import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, } from "./sync-runner.js";
|
|
14
14
|
import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
15
15
|
import { FakeClock } from "../watcher.js";
|
|
16
16
|
import { PERSONAL_VAULT_JOURNAL_SLUG } from "../journal.js";
|
|
17
17
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
18
18
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
19
19
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
20
|
+
import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
20
21
|
import { isCoveredByAny } from "../prefix-coalesce.js";
|
|
21
22
|
import { VaultAuthError } from "../vault-client.js";
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
@@ -1335,6 +1336,52 @@ describe("all-complete aggregate", () => {
|
|
|
1335
1336
|
expect(deps.stdout.events().some((e) => e.type === "all-complete")).toBe(true);
|
|
1336
1337
|
expect(deps.stderr.raw()).toContain("runner.telemetry.timeout");
|
|
1337
1338
|
});
|
|
1339
|
+
it("emits action telemetry for completion and telemetry transport failure does not block completion", async () => {
|
|
1340
|
+
const posts = [];
|
|
1341
|
+
const client = {
|
|
1342
|
+
...makeVaultStub({
|
|
1343
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1344
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1345
|
+
}),
|
|
1346
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1347
|
+
posts.push(batch);
|
|
1348
|
+
throw new Error("telemetry transport down");
|
|
1349
|
+
}),
|
|
1350
|
+
};
|
|
1351
|
+
const deps = makeDeps({
|
|
1352
|
+
createVaultClient: () => client,
|
|
1353
|
+
collectTelemetry: vi.fn().mockResolvedValue(undefined),
|
|
1354
|
+
sync: vi
|
|
1355
|
+
.fn()
|
|
1356
|
+
.mockResolvedValue(defaultSyncResult({
|
|
1357
|
+
filesDownloaded: 2,
|
|
1358
|
+
bytesDownloaded: 300,
|
|
1359
|
+
})),
|
|
1360
|
+
});
|
|
1361
|
+
const code = await runRunner(["--companies"], deps);
|
|
1362
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1363
|
+
expect(code).toBe(0);
|
|
1364
|
+
expect(deps.stdout.events().some((e) => e.type === "all-complete")).toBe(true);
|
|
1365
|
+
const completed = posts
|
|
1366
|
+
.flatMap((post) => post.events)
|
|
1367
|
+
.find((event) => event.eventName === "sync_runner_completed");
|
|
1368
|
+
expect(completed).toMatchObject({
|
|
1369
|
+
app: "hq-cloud",
|
|
1370
|
+
source: "sync-runner",
|
|
1371
|
+
schemaVersion: 1,
|
|
1372
|
+
properties: {
|
|
1373
|
+
result: "completed",
|
|
1374
|
+
attemptedCount: 1,
|
|
1375
|
+
companyCount: 1,
|
|
1376
|
+
errorCount: 0,
|
|
1377
|
+
filesDownloaded: 2,
|
|
1378
|
+
bytesDownloaded: 300,
|
|
1379
|
+
filesUploaded: 0,
|
|
1380
|
+
bytesUploaded: 0,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
expect(deps.stderr.raw()).toContain("runner.telemetry_events.nonfatal");
|
|
1384
|
+
});
|
|
1338
1385
|
it("sums filesDownloaded and bytesDownloaded across all companies", async () => {
|
|
1339
1386
|
const slugs = { cmp_a: "acme", cmp_b: "beta" };
|
|
1340
1387
|
const deps = makeDeps({
|
|
@@ -1974,6 +2021,13 @@ describe("personal slot fanout", () => {
|
|
|
1974
2021
|
const keys = Object.keys(args);
|
|
1975
2022
|
expect(keys).not.toContain("personalMode");
|
|
1976
2023
|
expect(keys).not.toContain("journalSlug");
|
|
2024
|
+
// Positive assertion on the runner→sync() glue (sync-runner-company.ts:278):
|
|
2025
|
+
// the push-only sessions exclusion IS threaded into the company pull leg.
|
|
2026
|
+
// The ABSENT-key checks above would still pass if a regression dropped the
|
|
2027
|
+
// `...(excludePrefixes.length > 0 ? { excludePrefixes } : {})` spread or
|
|
2028
|
+
// flipped its guard — silently re-enabling full session pulls. Pin the
|
|
2029
|
+
// exact leak against the exported constant, not a string literal.
|
|
2030
|
+
expect(args.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
|
|
1977
2031
|
}
|
|
1978
2032
|
});
|
|
1979
2033
|
// ── Push-side personal-vault contract (Slice 2 / personal-vault scope) ────
|
|
@@ -2822,9 +2876,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2822
2876
|
// No pending FakeClock timers left behind by the driver's debounce.
|
|
2823
2877
|
expect(clock.pendingTimerCount()).toBe(0);
|
|
2824
2878
|
});
|
|
2825
|
-
it("a
|
|
2879
|
+
it("a hard-error (exit 1) poll pass returns immediately", async () => {
|
|
2826
2880
|
const watcher = makeWatcherStub();
|
|
2827
|
-
const runPass = vi.fn().mockResolvedValue(
|
|
2881
|
+
const runPass = vi.fn().mockResolvedValue(1);
|
|
2828
2882
|
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
2829
2883
|
runPass,
|
|
2830
2884
|
clock: new FakeClock(),
|
|
@@ -2832,7 +2886,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2832
2886
|
sleep: () => Promise.resolve(),
|
|
2833
2887
|
onShutdownSignal: () => () => { },
|
|
2834
2888
|
});
|
|
2835
|
-
expect(code).toBe(
|
|
2889
|
+
expect(code).toBe(1);
|
|
2836
2890
|
expect(watcher.disposed).toBe(true);
|
|
2837
2891
|
});
|
|
2838
2892
|
it("a TRANSIENT_NETWORK_EXIT poll pass does NOT surface as a crash — the loop retries (HQ-SYNC-1W)", async () => {
|
|
@@ -2846,7 +2900,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2846
2900
|
n++;
|
|
2847
2901
|
// First pass: machine is offline (transient). Second pass: a real hard
|
|
2848
2902
|
// error, used here only to end the otherwise-infinite loop deterministically.
|
|
2849
|
-
return n === 1 ? TRANSIENT_NETWORK_EXIT :
|
|
2903
|
+
return n === 1 ? TRANSIENT_NETWORK_EXIT : 1;
|
|
2850
2904
|
});
|
|
2851
2905
|
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
2852
2906
|
runPass,
|
|
@@ -2858,10 +2912,40 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
2858
2912
|
// The transient pass did NOT end the loop — it ran again…
|
|
2859
2913
|
expect(runPass).toHaveBeenCalledTimes(2);
|
|
2860
2914
|
// …and the transient code was never surfaced as the watcher's exit code.
|
|
2861
|
-
expect(code).toBe(
|
|
2915
|
+
expect(code).toBe(1);
|
|
2862
2916
|
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
2863
2917
|
expect(watcher.disposed).toBe(true);
|
|
2864
2918
|
});
|
|
2919
|
+
it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
|
|
2920
|
+
// A partial pass (some per-file transfers threw: oversized object on an
|
|
2921
|
+
// old runner, presign denial, expired token, corrupt vault key) must keep
|
|
2922
|
+
// the watcher alive. Exiting on it got the watcher hot-respawned by the
|
|
2923
|
+
// menubar supervisor and Sentry-captured on EVERY failing pass — the
|
|
2924
|
+
// July 2026 "watcher exited unexpectedly (code=Some(2))" alert flood. The
|
|
2925
|
+
// per-file errors still surface via ndjson `error` events; the loop logs
|
|
2926
|
+
// and retries next tick. A one-shot `hq sync` still exits 2.
|
|
2927
|
+
const watcher = makeWatcherStub();
|
|
2928
|
+
let n = 0;
|
|
2929
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
2930
|
+
n++;
|
|
2931
|
+
// Two partial passes prove a DETERMINISTIC per-file failure doesn't kill
|
|
2932
|
+
// the loop either. A hard error then ends the otherwise-infinite loop.
|
|
2933
|
+
return n <= 2 ? PARTIAL_SYNC_EXIT : 1;
|
|
2934
|
+
});
|
|
2935
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
2936
|
+
runPass,
|
|
2937
|
+
clock: new FakeClock(),
|
|
2938
|
+
createWatcher: () => watcher,
|
|
2939
|
+
sleep: () => Promise.resolve(),
|
|
2940
|
+
onShutdownSignal: () => () => { },
|
|
2941
|
+
});
|
|
2942
|
+
// Both partial passes stayed alive — the loop ran a third time…
|
|
2943
|
+
expect(runPass).toHaveBeenCalledTimes(3);
|
|
2944
|
+
// …and the partial code was never surfaced as the watcher's exit code.
|
|
2945
|
+
expect(code).toBe(1);
|
|
2946
|
+
expect(code).not.toBe(PARTIAL_SYNC_EXIT);
|
|
2947
|
+
expect(watcher.disposed).toBe(true);
|
|
2948
|
+
});
|
|
2865
2949
|
it("without --event-push, no watcher is created (poll-only safety net)", async () => {
|
|
2866
2950
|
let triggerShutdown = () => { };
|
|
2867
2951
|
const createWatcher = vi.fn(() => makeWatcherStub());
|
|
@@ -3409,7 +3493,61 @@ describe("resolvePullScope", () => {
|
|
|
3409
3493
|
});
|
|
3410
3494
|
it("returns all when the client has no getMembershipSyncConfig", async () => {
|
|
3411
3495
|
const scope = await resolvePullScope(stubClient({}), "cmp_a", "acme");
|
|
3412
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3496
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3497
|
+
});
|
|
3498
|
+
// US-006 AC4: the sessions prefix is push-only, so `resolvePullScope` stamps
|
|
3499
|
+
// `excludePrefixes: ["sessions/"]` on EVERY return — including `all`
|
|
3500
|
+
// (full-bucket), owner→all, agent→all, shared, and custom. A single missed
|
|
3501
|
+
// return site would silently regress to a full session pull; this test pins
|
|
3502
|
+
// the invariant across every mode the resolver can return.
|
|
3503
|
+
it("stamps the push-only sessions exclusion on EVERY resolved mode (AC4)", async () => {
|
|
3504
|
+
expect(SESSIONS_SCOPE_PREFIX).toBe("sessions/");
|
|
3505
|
+
const modes = [
|
|
3506
|
+
// no-client early return → all
|
|
3507
|
+
await resolvePullScope(stubClient({}), "cmp_a", "acme"),
|
|
3508
|
+
// owner role-bypass → all
|
|
3509
|
+
await resolvePullScope(stubClient({
|
|
3510
|
+
listMyMemberships: async () => [ownerMembership("cmp_a", "mk_owner")],
|
|
3511
|
+
}), "cmp_a", "acme"),
|
|
3512
|
+
// agent membership → all (short-circuit before the always-403 endpoint)
|
|
3513
|
+
await resolvePullScope(stubClient({
|
|
3514
|
+
listMyMemberships: async () => [membership("cmp_a", "agt_9#cmp_a")],
|
|
3515
|
+
}), "cmp_a", "acme"),
|
|
3516
|
+
// config-all → all
|
|
3517
|
+
await resolvePullScope(stubClient({
|
|
3518
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
3519
|
+
getMembershipSyncConfig: async () => ({
|
|
3520
|
+
membershipId: "mk_a",
|
|
3521
|
+
syncMode: "all",
|
|
3522
|
+
isDefault: true,
|
|
3523
|
+
}),
|
|
3524
|
+
}), "cmp_a", "acme"),
|
|
3525
|
+
// shared
|
|
3526
|
+
await resolvePullScope(stubClient({
|
|
3527
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
3528
|
+
getMembershipSyncConfig: async () => ({
|
|
3529
|
+
membershipId: "mk_a",
|
|
3530
|
+
syncMode: "shared",
|
|
3531
|
+
isDefault: false,
|
|
3532
|
+
}),
|
|
3533
|
+
listMyExplicitGrants: async () => [
|
|
3534
|
+
{ companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
|
|
3535
|
+
],
|
|
3536
|
+
}), "cmp_a", "acme"),
|
|
3537
|
+
// custom
|
|
3538
|
+
await resolvePullScope(stubClient({
|
|
3539
|
+
listMyMemberships: async () => [membership("cmp_a", "mk_a")],
|
|
3540
|
+
getMembershipSyncConfig: async () => ({
|
|
3541
|
+
membershipId: "mk_a",
|
|
3542
|
+
syncMode: "custom",
|
|
3543
|
+
customPaths: ["projects/x/"],
|
|
3544
|
+
isDefault: false,
|
|
3545
|
+
}),
|
|
3546
|
+
}), "cmp_a", "acme"),
|
|
3547
|
+
];
|
|
3548
|
+
for (const scope of modes) {
|
|
3549
|
+
expect(scope.excludePrefixes).toEqual([SESSIONS_SCOPE_PREFIX]);
|
|
3550
|
+
}
|
|
3413
3551
|
});
|
|
3414
3552
|
// OWNER role-bypass regression (feedback_67bdb8a4 / feedback_a46c3b37).
|
|
3415
3553
|
// An owner owns the whole vault by ROLE; the grants endpoint deliberately
|
|
@@ -3434,7 +3572,7 @@ describe("resolvePullScope", () => {
|
|
|
3434
3572
|
return [];
|
|
3435
3573
|
},
|
|
3436
3574
|
}), "cmp_a", "acme");
|
|
3437
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3575
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3438
3576
|
// The short-circuit must fire BEFORE any grant resolution.
|
|
3439
3577
|
expect(grantsCalled).toBe(false);
|
|
3440
3578
|
});
|
|
@@ -3455,7 +3593,7 @@ describe("resolvePullScope", () => {
|
|
|
3455
3593
|
// Even a `custom` row cannot scope an owner below their role on the sync
|
|
3456
3594
|
// path (the server would 403 their own writes), so `all` wins and the
|
|
3457
3595
|
// config fetch is never needed.
|
|
3458
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3596
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3459
3597
|
expect(configCalled).toBe(false);
|
|
3460
3598
|
});
|
|
3461
3599
|
it("still narrows a NON-owner (member) to explicit grants — the fix is owner-scoped", async () => {
|
|
@@ -3470,7 +3608,7 @@ describe("resolvePullScope", () => {
|
|
|
3470
3608
|
{ companyUid: "cmp_a", path: "knowledge/", permission: "read", source: "person" },
|
|
3471
3609
|
],
|
|
3472
3610
|
}), "cmp_a", "acme");
|
|
3473
|
-
expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"] });
|
|
3611
|
+
expect(scope).toEqual({ syncMode: "shared", prefixSet: ["knowledge/"], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3474
3612
|
});
|
|
3475
3613
|
it("returns all for an all-mode membership (no prefixSet)", async () => {
|
|
3476
3614
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3481,7 +3619,7 @@ describe("resolvePullScope", () => {
|
|
|
3481
3619
|
isDefault: true,
|
|
3482
3620
|
}),
|
|
3483
3621
|
}), "cmp_a", "acme");
|
|
3484
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3622
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3485
3623
|
});
|
|
3486
3624
|
it("returns all for an AGENT membership WITHOUT calling sync-config (HQ-1R: agents own no person entity → server 403s the person-gate)", async () => {
|
|
3487
3625
|
let called = false;
|
|
@@ -3500,7 +3638,7 @@ describe("resolvePullScope", () => {
|
|
|
3500
3638
|
},
|
|
3501
3639
|
}), "cmp_a", "acme");
|
|
3502
3640
|
expect(called).toBe(false); // never hit the always-403 endpoint for an agent
|
|
3503
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3641
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3504
3642
|
});
|
|
3505
3643
|
it("coalesces explicit grants for a shared-mode membership", async () => {
|
|
3506
3644
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3597,7 +3735,7 @@ describe("resolvePullScope", () => {
|
|
|
3597
3735
|
// A `*` grant = everything. Because coalescePrefixes drops the empty
|
|
3598
3736
|
// prefix (which would otherwise collapse to "nothing" and prune the
|
|
3599
3737
|
// tree), an everything-scope resolves to full-access `all`.
|
|
3600
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3738
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3601
3739
|
});
|
|
3602
3740
|
it("uses customPaths for a custom-mode membership", async () => {
|
|
3603
3741
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3622,7 +3760,7 @@ describe("resolvePullScope", () => {
|
|
|
3622
3760
|
}),
|
|
3623
3761
|
// listMyExplicitGrants intentionally absent.
|
|
3624
3762
|
}), "cmp_a", "acme");
|
|
3625
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3763
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3626
3764
|
});
|
|
3627
3765
|
it("allows a genuinely-empty shared scope (grants method present, returns [])", async () => {
|
|
3628
3766
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3634,7 +3772,7 @@ describe("resolvePullScope", () => {
|
|
|
3634
3772
|
}),
|
|
3635
3773
|
listMyExplicitGrants: async () => [],
|
|
3636
3774
|
}), "cmp_a", "acme");
|
|
3637
|
-
expect(scope).toEqual({ syncMode: "shared", prefixSet: [] });
|
|
3775
|
+
expect(scope).toEqual({ syncMode: "shared", prefixSet: [], excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3638
3776
|
});
|
|
3639
3777
|
it("degrades to all when the membership is not found", async () => {
|
|
3640
3778
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3643,7 +3781,7 @@ describe("resolvePullScope", () => {
|
|
|
3643
3781
|
throw new Error("should not be called");
|
|
3644
3782
|
},
|
|
3645
3783
|
}), "cmp_a", "acme");
|
|
3646
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3784
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3647
3785
|
});
|
|
3648
3786
|
it("degrades to all when sync-config resolution throws (never prune on error)", async () => {
|
|
3649
3787
|
const scope = await resolvePullScope(stubClient({
|
|
@@ -3652,7 +3790,7 @@ describe("resolvePullScope", () => {
|
|
|
3652
3790
|
throw new Error("network blip");
|
|
3653
3791
|
},
|
|
3654
3792
|
}), "cmp_a", "acme");
|
|
3655
|
-
expect(scope).toEqual({ syncMode: "all" });
|
|
3793
|
+
expect(scope).toEqual({ syncMode: "all", excludePrefixes: [SESSIONS_SCOPE_PREFIX] });
|
|
3656
3794
|
});
|
|
3657
3795
|
// ── pin union (Phase C: hq files get) ──────────────────────────────────────
|
|
3658
3796
|
it("unions pinned prefixes into a shared-mode scope", async () => {
|