@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,618 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the cwd → owning-company resolver (surface-hq-console-telemetry
|
|
3
|
-
* US-002). Covers manifest parsing, cmp_* extraction, longest-prefix matching,
|
|
4
|
-
* the trailing-slash sibling boundary, and the unattributed (no-match) path.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
8
|
-
import * as fs from "fs";
|
|
9
|
-
import * as os from "os";
|
|
10
|
-
import * as path from "path";
|
|
11
|
-
import {
|
|
12
|
-
buildRepoCompanyMap,
|
|
13
|
-
resolveCompanyForCwd,
|
|
14
|
-
resolveCompanyForSkill,
|
|
15
|
-
} from "./company-resolver.js";
|
|
16
|
-
|
|
17
|
-
const INDIGO = "cmp_01INDIGO";
|
|
18
|
-
const HPO = "cmp_01HPO";
|
|
19
|
-
|
|
20
|
-
const MANIFEST = `companies:
|
|
21
|
-
personal:
|
|
22
|
-
name: Personal
|
|
23
|
-
repos: []
|
|
24
|
-
indigo:
|
|
25
|
-
name: Indigo
|
|
26
|
-
repos:
|
|
27
|
-
- repos/private/hq-cloud
|
|
28
|
-
- repos/public/hq-core
|
|
29
|
-
cloud_uid: ${INDIGO}
|
|
30
|
-
hpo:
|
|
31
|
-
name: HPO
|
|
32
|
-
repos:
|
|
33
|
-
- repos/private/hpo-jobs
|
|
34
|
-
cloud_uid: ${HPO}
|
|
35
|
-
liverecover:
|
|
36
|
-
name: LiveRecover
|
|
37
|
-
repos:
|
|
38
|
-
- repos/private/liverecover-site
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
function setupHqRoot(manifest: string = MANIFEST): string {
|
|
42
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-resolver-test-"));
|
|
43
|
-
fs.mkdirSync(path.join(root, "companies"), { recursive: true });
|
|
44
|
-
fs.writeFileSync(path.join(root, "companies", "manifest.yaml"), manifest);
|
|
45
|
-
return root;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe("buildRepoCompanyMap + resolveCompanyForCwd", () => {
|
|
49
|
-
let hqRoot: string;
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("resolves a cwd inside a company repo to that company's cmp_ uid", async () => {
|
|
55
|
-
hqRoot = setupHqRoot();
|
|
56
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
57
|
-
const cwd = path.join(hqRoot, "repos/private/hq-cloud/src");
|
|
58
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(INDIGO);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("resolves the repo root itself (exact match, no trailing slash)", async () => {
|
|
62
|
-
hqRoot = setupHqRoot();
|
|
63
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
64
|
-
const cwd = path.join(hqRoot, "repos/public/hq-core");
|
|
65
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(INDIGO);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("maps each repo to its own company", async () => {
|
|
69
|
-
hqRoot = setupHqRoot();
|
|
70
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
71
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/private/hpo-jobs/x"), map)).toBe(HPO);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns undefined for a cwd outside any company repo (unattributed)", async () => {
|
|
75
|
-
hqRoot = setupHqRoot();
|
|
76
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
77
|
-
expect(resolveCompanyForCwd("/Users/x/random-repo", map)).toBeUndefined();
|
|
78
|
-
// The HQ root itself is company-agnostic — it belongs to no single tenant.
|
|
79
|
-
expect(resolveCompanyForCwd(hqRoot, map)).toBeUndefined();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Behaviour change (was: `companies/<slug>` resolved to undefined).
|
|
84
|
-
*
|
|
85
|
-
* A company's own HQ directory holds its skills, knowledge, and project
|
|
86
|
-
* files, so work done there is unambiguously that company's work. The prior
|
|
87
|
-
* repos-only scope dropped it, and in production a large share of sessions
|
|
88
|
-
* run from inside the HQ root rather than a repo checkout — leaving that work
|
|
89
|
-
* unattributed and the owner's Projects view empty. Attribution is scoped to
|
|
90
|
-
* the single tenant that owns the directory, so this widens coverage without
|
|
91
|
-
* crossing a tenant boundary.
|
|
92
|
-
*/
|
|
93
|
-
it("attributes a cwd inside the company's own HQ directory", async () => {
|
|
94
|
-
hqRoot = setupHqRoot();
|
|
95
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
96
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "companies/indigo"), map)).toBe(INDIGO);
|
|
97
|
-
expect(
|
|
98
|
-
resolveCompanyForCwd(path.join(hqRoot, "companies/hpo/knowledge"), map),
|
|
99
|
-
).toBe(HPO);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("does NOT attribute a company that lacks a cloud_uid (not cloud-backed)", async () => {
|
|
103
|
-
hqRoot = setupHqRoot();
|
|
104
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
105
|
-
// liverecover-site has no cloud_uid → unattributed.
|
|
106
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/private/liverecover-site"), map)).toBeUndefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("does not match a sibling that merely shares a path prefix", async () => {
|
|
110
|
-
hqRoot = setupHqRoot();
|
|
111
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
112
|
-
// `<repo>-other` shares the string prefix but must NOT match (slash boundary).
|
|
113
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud-other"), map)).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("handles a trailing slash on the cwd", async () => {
|
|
117
|
-
hqRoot = setupHqRoot();
|
|
118
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
119
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud") + "/", map)).toBe(INDIGO);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("returns undefined for an undefined/empty cwd", async () => {
|
|
123
|
-
hqRoot = setupHqRoot();
|
|
124
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
125
|
-
expect(resolveCompanyForCwd(undefined, map)).toBeUndefined();
|
|
126
|
-
expect(resolveCompanyForCwd("", map)).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("never returns the reserved 'unattributed' sentinel", async () => {
|
|
130
|
-
hqRoot = setupHqRoot(`companies:
|
|
131
|
-
bad:
|
|
132
|
-
repos:
|
|
133
|
-
- repos/x
|
|
134
|
-
cloud_uid: unattributed
|
|
135
|
-
`);
|
|
136
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
137
|
-
// `unattributed` is not a cmp_* uid → not added to the map → no match.
|
|
138
|
-
expect(map.entries).toHaveLength(0);
|
|
139
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/x"), map)).toBeUndefined();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("yields an empty map for a missing manifest", async () => {
|
|
143
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-resolver-empty-"));
|
|
144
|
-
try {
|
|
145
|
-
const map = await buildRepoCompanyMap(root);
|
|
146
|
-
expect(map.entries).toHaveLength(0);
|
|
147
|
-
expect(resolveCompanyForCwd(path.join(root, "repos/private/hq-cloud"), map)).toBeUndefined();
|
|
148
|
-
} finally {
|
|
149
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("yields an empty map for an unparseable manifest", async () => {
|
|
154
|
-
hqRoot = setupHqRoot(":\n not: [valid: yaml");
|
|
155
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
156
|
-
expect(map.entries).toHaveLength(0);
|
|
157
|
-
expect(map.bySlug.size).toBe(0);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe("resolveCompanyForSkill", () => {
|
|
162
|
-
let hqRoot: string;
|
|
163
|
-
afterEach(() => {
|
|
164
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("attributes a company-namespaced skill to its company, regardless of cwd", async () => {
|
|
168
|
-
hqRoot = setupHqRoot();
|
|
169
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
170
|
-
// The slug map is populated from the manifest keys with a cloud_uid.
|
|
171
|
-
expect(resolveCompanyForSkill("indigo:signals", map)).toBe(INDIGO);
|
|
172
|
-
expect(resolveCompanyForSkill("hpo:some-skill", map)).toBe(HPO);
|
|
173
|
-
// Multi-segment skill name: only the leading slug is the company.
|
|
174
|
-
expect(resolveCompanyForSkill("indigo:hq-dev-thread", map)).toBe(INDIGO);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("returns undefined for a core (un-namespaced) skill", async () => {
|
|
178
|
-
hqRoot = setupHqRoot();
|
|
179
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
180
|
-
expect(resolveCompanyForSkill("startwork", map)).toBeUndefined();
|
|
181
|
-
expect(resolveCompanyForSkill("review", map)).toBeUndefined();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("returns undefined for a non-company namespace (personal/pack) and a leading colon", async () => {
|
|
185
|
-
hqRoot = setupHqRoot();
|
|
186
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
187
|
-
// `personal` has no cloud_uid → not in the slug map.
|
|
188
|
-
expect(resolveCompanyForSkill("personal:startwork", map)).toBeUndefined();
|
|
189
|
-
expect(
|
|
190
|
-
resolveCompanyForSkill("hq-pack-engineering:review", map),
|
|
191
|
-
).toBeUndefined();
|
|
192
|
-
expect(resolveCompanyForSkill(":oops", map)).toBeUndefined();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("returns undefined for a slug whose company is not cloud-backed", async () => {
|
|
196
|
-
hqRoot = setupHqRoot();
|
|
197
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
198
|
-
// `liverecover` is in the manifest but carries no cloud_uid.
|
|
199
|
-
expect(resolveCompanyForSkill("liverecover:foo", map)).toBeUndefined();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("handles a missing/undefined skill safely", async () => {
|
|
203
|
-
hqRoot = setupHqRoot();
|
|
204
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
205
|
-
expect(resolveCompanyForSkill(undefined, map)).toBeUndefined();
|
|
206
|
-
expect(resolveCompanyForSkill("", map)).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Regression: the real `companies/manifest.yaml` records `repos` as BARE NAMES
|
|
212
|
-
* (`hq-cloud`), not HQ-root-relative paths (`repos/private/hq-cloud`). The
|
|
213
|
-
* original resolver did `path.resolve(hqRoot, repo)` and produced
|
|
214
|
-
* `<hqRoot>/hq-cloud` — a path no checkout lives at — so every entry matched
|
|
215
|
-
* nothing and NO telemetry event was ever attributed. Verified in prod at the
|
|
216
|
-
* time of the fix: 0 of 3200 sampled usage events across 9 people carried a
|
|
217
|
-
* `companyUid`, which left the Team Activity "Projects" tab permanently empty.
|
|
218
|
-
*/
|
|
219
|
-
describe("real-manifest shape: bare repo names + company directories", () => {
|
|
220
|
-
const ACME = "cmp_01ACME";
|
|
221
|
-
const BETA = "cmp_01BETA";
|
|
222
|
-
|
|
223
|
-
// Mirrors the live manifest: bare repo names, an explicit `path`, and a
|
|
224
|
-
// company with no cloud_uid (not cloud-backed).
|
|
225
|
-
const REAL_SHAPE = `companies:
|
|
226
|
-
acme:
|
|
227
|
-
name: Acme
|
|
228
|
-
path: companies/acme
|
|
229
|
-
repos:
|
|
230
|
-
- hq-cloud
|
|
231
|
-
- hq-console
|
|
232
|
-
cloud_uid: ${ACME}
|
|
233
|
-
beta:
|
|
234
|
-
name: Beta
|
|
235
|
-
path: companies/beta
|
|
236
|
-
repos:
|
|
237
|
-
- beta-site
|
|
238
|
-
cloud_uid: ${BETA}
|
|
239
|
-
nocloud:
|
|
240
|
-
name: NoCloud
|
|
241
|
-
path: companies/nocloud
|
|
242
|
-
repos:
|
|
243
|
-
- orphan-repo
|
|
244
|
-
`;
|
|
245
|
-
|
|
246
|
-
let hqRoot: string;
|
|
247
|
-
afterEach(() => {
|
|
248
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("resolves a bare repo name to its repos/private checkout", async () => {
|
|
252
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
253
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
254
|
-
const cwd = path.join(hqRoot, "repos/private/hq-console/src/app");
|
|
255
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(ACME);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("resolves a bare repo name to its repos/public checkout", async () => {
|
|
259
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
260
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
261
|
-
const cwd = path.join(hqRoot, "repos/public/hq-cloud");
|
|
262
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(ACME);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("attributes work inside the company's own HQ directory", async () => {
|
|
266
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
267
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
268
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "companies/acme"), map)).toBe(ACME);
|
|
269
|
-
expect(
|
|
270
|
-
resolveCompanyForCwd(path.join(hqRoot, "companies/beta/projects/x"), map),
|
|
271
|
-
).toBe(BETA);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("still honours an explicit HQ-root-relative repo path (back-compat)", async () => {
|
|
275
|
-
hqRoot = setupHqRoot();
|
|
276
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
277
|
-
expect(
|
|
278
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud/src"), map),
|
|
279
|
-
).toBe("cmp_01INDIGO");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("leaves a non-cloud-backed company's repos and directory unattributed", async () => {
|
|
283
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
284
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
285
|
-
expect(
|
|
286
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/orphan-repo"), map),
|
|
287
|
-
).toBeUndefined();
|
|
288
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "companies/nocloud"), map)).toBeUndefined();
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Case folding follows the FILESYSTEM, not the OS.
|
|
293
|
-
*
|
|
294
|
-
* Where the filesystem folds case, `.../HQ` and `.../hq` are literally the
|
|
295
|
-
* same directory and a differently-cased cwd must still attribute. Where it
|
|
296
|
-
* does not, they are different directories and an unattributed event is the
|
|
297
|
-
* correct (safe) outcome.
|
|
298
|
-
*
|
|
299
|
-
* The expectation is derived from the resolver's own probe (`map.foldsCase`),
|
|
300
|
-
* NOT from `process.platform` — case-sensitive APFS and case-insensitive
|
|
301
|
-
* volumes mounted on Linux both make the platform a liar, and this test would
|
|
302
|
-
* fail on them despite the resolver behaving correctly.
|
|
303
|
-
*/
|
|
304
|
-
it("follows the probed filesystem for a differently-cased path", async () => {
|
|
305
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
306
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
307
|
-
const cwd = path.join(hqRoot, "repos/private/hq-console").toUpperCase();
|
|
308
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(map.foldsCase ? ACME : undefined);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("always matches an exactly-cased path", async () => {
|
|
312
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
313
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
314
|
-
expect(
|
|
315
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-console/src"), map),
|
|
316
|
-
).toBe(ACME);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("treats backslash-separated paths as equivalent to forward slashes", async () => {
|
|
320
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
321
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
322
|
-
const cwd = path.join(hqRoot, "repos/private/hq-console/src").replace(/\//g, "\\");
|
|
323
|
-
expect(resolveCompanyForCwd(cwd, map)).toBe(ACME);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("does not let a sibling sharing a string prefix match", async () => {
|
|
327
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
328
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
329
|
-
expect(
|
|
330
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-console-other"), map),
|
|
331
|
-
).toBeUndefined();
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("emits no duplicate match keys", async () => {
|
|
335
|
-
hqRoot = setupHqRoot(REAL_SHAPE);
|
|
336
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
337
|
-
const keys = map.entries.map((e) => e.matchKey);
|
|
338
|
-
expect(new Set(keys).size).toBe(keys.length);
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Codex-review follow-ups. Each of these is a real HQ layout that pure lexical
|
|
344
|
-
* matching against the primary checkout silently misses, leaving the work
|
|
345
|
-
* unattributed — the same class of gap that left the Projects tab empty.
|
|
346
|
-
*/
|
|
347
|
-
describe("real HQ layouts: worktrees, symlinks, and ambiguity", () => {
|
|
348
|
-
const ACME = "cmp_01ACME";
|
|
349
|
-
const BETA = "cmp_01BETA";
|
|
350
|
-
|
|
351
|
-
const SHAPE = `companies:
|
|
352
|
-
acme:
|
|
353
|
-
name: Acme
|
|
354
|
-
path: companies/acme
|
|
355
|
-
repos:
|
|
356
|
-
- hq-cloud
|
|
357
|
-
cloud_uid: ${ACME}
|
|
358
|
-
beta:
|
|
359
|
-
name: Beta
|
|
360
|
-
path: companies/beta
|
|
361
|
-
repos:
|
|
362
|
-
- beta-site
|
|
363
|
-
cloud_uid: ${BETA}
|
|
364
|
-
`;
|
|
365
|
-
|
|
366
|
-
let hqRoot: string;
|
|
367
|
-
afterEach(() => {
|
|
368
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
function mkdirp(...segments: string[]): string {
|
|
372
|
-
const p = path.join(hqRoot, ...segments);
|
|
373
|
-
fs.mkdirSync(p, { recursive: true });
|
|
374
|
-
return p;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* HQ does most feature work in a sibling git worktree named
|
|
379
|
-
* `<repo>-wt-<branch>`. The resolver's slash boundary rejects that as a mere
|
|
380
|
-
* string-prefix sibling of `<repo>`, so without explicit expansion every
|
|
381
|
-
* worktree session is unattributed.
|
|
382
|
-
*/
|
|
383
|
-
it("attributes work inside a `<repo>-wt-*` git worktree", async () => {
|
|
384
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
385
|
-
mkdirp("repos/private/hq-cloud");
|
|
386
|
-
const wt = mkdirp("repos/private/hq-cloud-wt-feature-x");
|
|
387
|
-
|
|
388
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
389
|
-
expect(resolveCompanyForCwd(wt, map)).toBe(ACME);
|
|
390
|
-
expect(resolveCompanyForCwd(path.join(wt, "src/lib"), map)).toBe(ACME);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("still rejects an unrelated sibling that merely shares the prefix", async () => {
|
|
394
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
395
|
-
mkdirp("repos/private/hq-cloud");
|
|
396
|
-
const other = mkdirp("repos/private/hq-cloud-other");
|
|
397
|
-
|
|
398
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
399
|
-
expect(resolveCompanyForCwd(other, map)).toBeUndefined();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* A symlinked checkout reports its PHYSICAL path as cwd, which lives outside
|
|
404
|
-
* the HQ tree entirely and can never match the lexical root.
|
|
405
|
-
*/
|
|
406
|
-
it("attributes a symlinked checkout reached by its physical path", async () => {
|
|
407
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
408
|
-
const physical = fs.mkdtempSync(path.join(os.tmpdir(), "hq-physical-repo-"));
|
|
409
|
-
fs.mkdirSync(path.join(hqRoot, "repos/private"), { recursive: true });
|
|
410
|
-
try {
|
|
411
|
-
fs.symlinkSync(physical, path.join(hqRoot, "repos/private/hq-cloud"), "dir");
|
|
412
|
-
} catch {
|
|
413
|
-
return; // symlinks unavailable (e.g. unprivileged Windows) — skip
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
417
|
-
try {
|
|
418
|
-
expect(resolveCompanyForCwd(fs.realpathSync(physical), map)).toBe(ACME);
|
|
419
|
-
// The lexical path must keep working too.
|
|
420
|
-
expect(
|
|
421
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud"), map),
|
|
422
|
-
).toBe(ACME);
|
|
423
|
-
} finally {
|
|
424
|
-
fs.rmSync(physical, { recursive: true, force: true });
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Fail CLOSED: when two companies claim the same path, attributing to
|
|
430
|
-
* whichever the manifest listed first would be a cross-tenant
|
|
431
|
-
* misattribution. Unattributed is the correct answer.
|
|
432
|
-
*/
|
|
433
|
-
it("leaves a path claimed by two companies unattributed", async () => {
|
|
434
|
-
const AMBIGUOUS = `companies:
|
|
435
|
-
acme:
|
|
436
|
-
name: Acme
|
|
437
|
-
repos:
|
|
438
|
-
- shared-repo
|
|
439
|
-
cloud_uid: ${ACME}
|
|
440
|
-
beta:
|
|
441
|
-
name: Beta
|
|
442
|
-
repos:
|
|
443
|
-
- shared-repo
|
|
444
|
-
cloud_uid: ${BETA}
|
|
445
|
-
`;
|
|
446
|
-
hqRoot = setupHqRoot(AMBIGUOUS);
|
|
447
|
-
mkdirp("repos/private/shared-repo");
|
|
448
|
-
|
|
449
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
450
|
-
expect(
|
|
451
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/shared-repo"), map),
|
|
452
|
-
).toBeUndefined();
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
it("an ambiguous repo does not poison that company's other repos", async () => {
|
|
456
|
-
const AMBIGUOUS = `companies:
|
|
457
|
-
acme:
|
|
458
|
-
name: Acme
|
|
459
|
-
repos:
|
|
460
|
-
- shared-repo
|
|
461
|
-
- acme-only
|
|
462
|
-
cloud_uid: ${ACME}
|
|
463
|
-
beta:
|
|
464
|
-
name: Beta
|
|
465
|
-
repos:
|
|
466
|
-
- shared-repo
|
|
467
|
-
cloud_uid: ${BETA}
|
|
468
|
-
`;
|
|
469
|
-
hqRoot = setupHqRoot(AMBIGUOUS);
|
|
470
|
-
mkdirp("repos/private/shared-repo");
|
|
471
|
-
mkdirp("repos/private/acme-only");
|
|
472
|
-
|
|
473
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
474
|
-
expect(
|
|
475
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/acme-only"), map),
|
|
476
|
-
).toBe(ACME);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it("reports the probed case-folding behaviour of the real filesystem", async () => {
|
|
480
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
481
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
482
|
-
expect(typeof map.foldsCase).toBe("boolean");
|
|
483
|
-
// Whatever the probe found, an exactly-cased path always resolves.
|
|
484
|
-
mkdirp("repos/private/hq-cloud");
|
|
485
|
-
const fresh = await buildRepoCompanyMap(hqRoot);
|
|
486
|
-
expect(
|
|
487
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud"), fresh),
|
|
488
|
-
).toBe(ACME);
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* A worktree that no longer exists on disk.
|
|
494
|
-
*
|
|
495
|
-
* `buildRepoCompanyMap` discovers worktrees by reading the directory, which
|
|
496
|
-
* only sees what exists at build time. Telemetry, though, is read from a
|
|
497
|
-
* cursor — and a branch worktree is routinely deleted before the next pass,
|
|
498
|
-
* while pending JSONL rows still carry its cwd. Since a successful POST
|
|
499
|
-
* advances the cursor permanently, failing to resolve those rows loses the
|
|
500
|
-
* attribution for good.
|
|
501
|
-
*/
|
|
502
|
-
describe("worktree paths that no longer exist on disk", () => {
|
|
503
|
-
const ACME = "cmp_01ACME";
|
|
504
|
-
const SHAPE = `companies:
|
|
505
|
-
acme:
|
|
506
|
-
name: Acme
|
|
507
|
-
path: companies/acme
|
|
508
|
-
repos:
|
|
509
|
-
- hq-cloud
|
|
510
|
-
cloud_uid: ${ACME}
|
|
511
|
-
`;
|
|
512
|
-
|
|
513
|
-
let hqRoot: string;
|
|
514
|
-
afterEach(() => {
|
|
515
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("resolves a deleted worktree's cwd by its documented naming convention", async () => {
|
|
519
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
520
|
-
fs.mkdirSync(path.join(hqRoot, "repos/private/hq-cloud"), { recursive: true });
|
|
521
|
-
// NOTE: the worktree is deliberately NOT created — it was cleaned up before
|
|
522
|
-
// this telemetry pass, exactly as in the real failure.
|
|
523
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
524
|
-
|
|
525
|
-
const gone = path.join(hqRoot, "repos/private/hq-cloud-wt-feature-gone");
|
|
526
|
-
expect(resolveCompanyForCwd(gone, map)).toBe(ACME);
|
|
527
|
-
expect(resolveCompanyForCwd(path.join(gone, "src/lib"), map)).toBe(ACME);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
it("does not let the worktree fallback rescue an unrelated sibling", async () => {
|
|
531
|
-
hqRoot = setupHqRoot(SHAPE);
|
|
532
|
-
fs.mkdirSync(path.join(hqRoot, "repos/private/hq-cloud"), { recursive: true });
|
|
533
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
534
|
-
|
|
535
|
-
// `-other` is not the `-wt-` convention, so it stays a foreign sibling...
|
|
536
|
-
expect(
|
|
537
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/hq-cloud-other"), map),
|
|
538
|
-
).toBeUndefined();
|
|
539
|
-
// ...and a `-wt-` suffix on a repo nobody owns resolves to nothing.
|
|
540
|
-
expect(
|
|
541
|
-
resolveCompanyForCwd(path.join(hqRoot, "repos/private/stranger-wt-x"), map),
|
|
542
|
-
).toBeUndefined();
|
|
543
|
-
});
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Fail-closed must survive the worktree fallback.
|
|
548
|
-
*
|
|
549
|
-
* The fallback collapses `<repo>-wt-<branch>` to `<repo>`. If the worktree-
|
|
550
|
-
* shaped path was itself dropped for ambiguity, collapsing it would hand the
|
|
551
|
-
* event to whoever owns the base repo — quietly undoing the protection.
|
|
552
|
-
*/
|
|
553
|
-
describe("ambiguity is not escapable via the worktree fallback", () => {
|
|
554
|
-
const ACME = "cmp_01ACME";
|
|
555
|
-
const BETA = "cmp_01BETA";
|
|
556
|
-
|
|
557
|
-
let hqRoot: string;
|
|
558
|
-
afterEach(() => {
|
|
559
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it("keeps an ambiguous `<repo>-wt-*` path unattributed instead of collapsing it", async () => {
|
|
563
|
-
// Acme owns `foo`; Beta owns a real repo that happens to be named
|
|
564
|
-
// `foo-wt-team`, which also looks like a worktree of Acme's `foo`.
|
|
565
|
-
hqRoot = setupHqRoot(`companies:
|
|
566
|
-
acme:
|
|
567
|
-
name: Acme
|
|
568
|
-
repos:
|
|
569
|
-
- foo
|
|
570
|
-
cloud_uid: ${ACME}
|
|
571
|
-
beta:
|
|
572
|
-
name: Beta
|
|
573
|
-
repos:
|
|
574
|
-
- foo-wt-team
|
|
575
|
-
cloud_uid: ${BETA}
|
|
576
|
-
`);
|
|
577
|
-
fs.mkdirSync(path.join(hqRoot, "repos/private/foo"), { recursive: true });
|
|
578
|
-
fs.mkdirSync(path.join(hqRoot, "repos/private/foo-wt-team"), { recursive: true });
|
|
579
|
-
|
|
580
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
581
|
-
const contested = path.join(hqRoot, "repos/private/foo-wt-team");
|
|
582
|
-
|
|
583
|
-
expect(resolveCompanyForCwd(contested, map)).toBeUndefined();
|
|
584
|
-
expect(resolveCompanyForCwd(path.join(contested, "src"), map)).toBeUndefined();
|
|
585
|
-
// The unambiguous base repo still resolves.
|
|
586
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "repos/private/foo"), map)).toBe(ACME);
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* `path` is documented as HQ-root-relative, but the manifest is synced and
|
|
592
|
-
* hand-editable. A path that escapes the root would claim a directory spanning
|
|
593
|
-
* other tenants and stamp this company's uid on everything beneath it.
|
|
594
|
-
*/
|
|
595
|
-
describe("company paths that escape the HQ root are rejected", () => {
|
|
596
|
-
const ACME = "cmp_01ACME";
|
|
597
|
-
|
|
598
|
-
let hqRoot: string;
|
|
599
|
-
afterEach(() => {
|
|
600
|
-
if (hqRoot) fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it.each([[" .."], ["."], ["/"]])("ignores path: %s", async (badPath) => {
|
|
604
|
-
hqRoot = setupHqRoot(`companies:
|
|
605
|
-
acme:
|
|
606
|
-
name: Acme
|
|
607
|
-
path: "${badPath.trim()}"
|
|
608
|
-
repos: []
|
|
609
|
-
cloud_uid: ${ACME}
|
|
610
|
-
`);
|
|
611
|
-
const map = await buildRepoCompanyMap(hqRoot);
|
|
612
|
-
|
|
613
|
-
// Neither the HQ root nor its parent may be claimed.
|
|
614
|
-
expect(resolveCompanyForCwd(hqRoot, map)).toBeUndefined();
|
|
615
|
-
expect(resolveCompanyForCwd(path.dirname(hqRoot), map)).toBeUndefined();
|
|
616
|
-
expect(resolveCompanyForCwd(path.join(hqRoot, "companies/other"), map)).toBeUndefined();
|
|
617
|
-
});
|
|
618
|
-
});
|