@indigoai-us/hq-cli 5.77.13 → 5.78.0
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/CHANGELOG.md +34 -0
- package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
- package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
- package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
- package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
- package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
- package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
- package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
- package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
- package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
- package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
- package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
- package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
- package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
- package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
- package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
- package/assets/scaffold/core/scripts/worktree.sh +136 -0
- package/dist/commands/agents.js +28 -12
- package/dist/commands/api-keys.js +100 -20
- package/dist/commands/core.d.ts +90 -0
- package/dist/commands/core.js +279 -0
- package/dist/commands/files-browse.d.ts +21 -0
- package/dist/commands/files-browse.js +145 -24
- package/dist/commands/files.js +33 -7
- package/dist/commands/members.js +16 -5
- package/dist/commands/pack-install.d.ts +14 -0
- package/dist/commands/pack-install.js +51 -2
- package/dist/commands/pkg-install.js +43 -0
- package/dist/commands/run.js +8 -0
- package/dist/commands/secrets.js +125 -65
- package/dist/main.js +6 -0
- package/dist/utils/api-key-command-gate.d.ts +45 -0
- package/dist/utils/api-key-command-gate.js +51 -0
- package/dist/utils/hq-roots.d.ts +91 -0
- package/dist/utils/hq-roots.js +212 -0
- package/dist/utils/resolve-vault-credential.d.ts +63 -3
- package/dist/utils/resolve-vault-credential.js +152 -6
- package/dist/utils/run-bundled-script.d.ts +66 -0
- package/dist/utils/run-bundled-script.js +80 -0
- package/dist/utils/vault-api.d.ts +21 -0
- package/dist/utils/vault-api.js +74 -3
- package/package.json +7 -1
- package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
- package/e2e/cli.test.ts +0 -128
- package/eslint.config.js +0 -37
- package/pnpm-workspace.yaml +0 -4
- package/scripts/chmod-bins.mjs +0 -49
- package/scripts/generate-dsn.mjs +0 -26
- package/scripts/generate-scan-packages-table.mjs +0 -113
- package/scripts/smoke-sources-signals.sh +0 -103
- package/src/bin/hq-auth-refresh.ts +0 -43
- package/src/cli-version.ts +0 -13
- package/src/commands/__fixtures__/make-tar.ts +0 -126
- package/src/commands/_patterns.ts +0 -10
- package/src/commands/add.ts +0 -75
- package/src/commands/agents.test.ts +0 -527
- package/src/commands/agents.ts +0 -923
- package/src/commands/api-keys.test.ts +0 -291
- package/src/commands/api-keys.ts +0 -382
- package/src/commands/artifact-verify.test.ts +0 -177
- package/src/commands/auth.test.ts +0 -82
- package/src/commands/auth.ts +0 -213
- package/src/commands/billing.test.ts +0 -158
- package/src/commands/billing.ts +0 -146
- package/src/commands/channels.test.ts +0 -74
- package/src/commands/channels.ts +0 -91
- package/src/commands/cloud-demote.test.ts +0 -401
- package/src/commands/cloud-demote.ts +0 -278
- package/src/commands/cloud-provision.test.ts +0 -1117
- package/src/commands/cloud-provision.ts +0 -942
- package/src/commands/cloud.pull-all.test.ts +0 -711
- package/src/commands/cloud.pull-per-company.test.ts +0 -188
- package/src/commands/cloud.push-all.test.ts +0 -485
- package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
- package/src/commands/cloud.selectors.test.ts +0 -208
- package/src/commands/cloud.status.test.ts +0 -160
- package/src/commands/cloud.test.ts +0 -41
- package/src/commands/cloud.ts +0 -2130
- package/src/commands/company.test.ts +0 -202
- package/src/commands/company.ts +0 -148
- package/src/commands/creators.test.ts +0 -60
- package/src/commands/creators.ts +0 -117
- package/src/commands/crm.test.ts +0 -179
- package/src/commands/crm.ts +0 -236
- package/src/commands/db-migrate.ts +0 -55
- package/src/commands/db-provision.ts +0 -114
- package/src/commands/db-sql.ts +0 -124
- package/src/commands/db-status.ts +0 -108
- package/src/commands/db.ts +0 -26
- package/src/commands/dm.test.ts +0 -748
- package/src/commands/dm.ts +0 -1226
- package/src/commands/feedback.test.ts +0 -413
- package/src/commands/feedback.ts +0 -169
- package/src/commands/files-browse.test.ts +0 -1249
- package/src/commands/files-browse.ts +0 -1506
- package/src/commands/files-delete.test.ts +0 -484
- package/src/commands/files-recovery.test.ts +0 -370
- package/src/commands/files.test.ts +0 -722
- package/src/commands/files.ts +0 -1446
- package/src/commands/group-grants.test.ts +0 -330
- package/src/commands/group-grants.ts +0 -455
- package/src/commands/groups.test.ts +0 -44
- package/src/commands/groups.ts +0 -368
- package/src/commands/integrations.test.ts +0 -633
- package/src/commands/integrations.ts +0 -507
- package/src/commands/list.ts +0 -85
- package/src/commands/login.ts +0 -52
- package/src/commands/logout.ts +0 -30
- package/src/commands/marketplace-install.test.ts +0 -543
- package/src/commands/marketplace-security.test.ts +0 -646
- package/src/commands/mcp-registration.test.ts +0 -2787
- package/src/commands/mcp-registration.ts +0 -2612
- package/src/commands/mcp-status.test.ts +0 -483
- package/src/commands/mcp-status.ts +0 -575
- package/src/commands/mcp-status.us011.test.ts +0 -243
- package/src/commands/meetings.test.ts +0 -522
- package/src/commands/meetings.ts +0 -789
- package/src/commands/members.test.ts +0 -1474
- package/src/commands/members.ts +0 -1181
- package/src/commands/onboard-identity-guard.test.ts +0 -99
- package/src/commands/onboard-identity-guard.ts +0 -103
- package/src/commands/onboard-join.test.ts +0 -96
- package/src/commands/onboard-join.ts +0 -101
- package/src/commands/onboard-warning.test.ts +0 -26
- package/src/commands/onboard-warning.ts +0 -12
- package/src/commands/onboard.ts +0 -267
- package/src/commands/outposts-heartbeat.test.ts +0 -299
- package/src/commands/outposts-heartbeat.ts +0 -310
- package/src/commands/outposts-replica-sync.test.ts +0 -272
- package/src/commands/outposts-self-deploy.test.ts +0 -243
- package/src/commands/outposts.test.ts +0 -1039
- package/src/commands/outposts.ts +0 -1935
- package/src/commands/pack-install-secret-authorization.test.ts +0 -115
- package/src/commands/pack-install.test.ts +0 -1469
- package/src/commands/pack-install.ts +0 -2247
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs.test.ts +0 -88
- package/src/commands/packs.ts +0 -622
- package/src/commands/people.test.ts +0 -807
- package/src/commands/people.ts +0 -422
- package/src/commands/pkg-install.dispatch.test.ts +0 -71
- package/src/commands/pkg-install.ts +0 -215
- package/src/commands/pkg-list.test.ts +0 -79
- package/src/commands/pkg-list.ts +0 -13
- package/src/commands/pkg-remove.ts +0 -71
- package/src/commands/pkg-update.ts +0 -189
- package/src/commands/publish.test.ts +0 -650
- package/src/commands/publish.ts +0 -580
- package/src/commands/reindex.test.ts +0 -259
- package/src/commands/reindex.ts +0 -274
- package/src/commands/rescue.test.ts +0 -39
- package/src/commands/rescue.ts +0 -210
- package/src/commands/run.env-local.test.ts +0 -88
- package/src/commands/run.test.ts +0 -45
- package/src/commands/run.ts +0 -184
- package/src/commands/safe-extract.test.ts +0 -459
- package/src/commands/safe-extract.ts +0 -444
- package/src/commands/secrets-scope.test.ts +0 -56
- package/src/commands/secrets-scope.ts +0 -32
- package/src/commands/secrets.parse-destination.test.ts +0 -38
- package/src/commands/secrets.test.ts +0 -2037
- package/src/commands/secrets.ts +0 -2240
- package/src/commands/signals.ts +0 -351
- package/src/commands/skill.test.ts +0 -770
- package/src/commands/skill.ts +0 -796
- package/src/commands/sources.ts +0 -362
- package/src/commands/sync-mode.test.ts +0 -366
- package/src/commands/sync-mode.ts +0 -387
- package/src/commands/sync-narrow.test.ts +0 -633
- package/src/commands/sync-narrow.ts +0 -555
- package/src/commands/sync.ts +0 -165
- package/src/commands/team-sync.ts +0 -628
- package/src/commands/update.ts +0 -71
- package/src/commands/whoami.ts +0 -83
- package/src/commands/workers.test.ts +0 -158
- package/src/commands/workers.ts +0 -301
- package/src/index.test.ts +0 -32
- package/src/index.ts +0 -20
- package/src/lib/db/control-plane.test.ts +0 -59
- package/src/lib/db/control-plane.ts +0 -113
- package/src/lib/db/local.test.ts +0 -81
- package/src/lib/db/local.ts +0 -148
- package/src/lib/db/migrate.test.ts +0 -133
- package/src/lib/db/migrate.ts +0 -137
- package/src/lib/db/paths.test.ts +0 -112
- package/src/lib/db/paths.ts +0 -128
- package/src/lib/db/remote-engine.test.ts +0 -44
- package/src/lib/db/remote-engine.ts +0 -148
- package/src/lib/db/remote-sql.test.ts +0 -32
- package/src/lib/db/remote-sql.ts +0 -62
- package/src/lib/db/sql.test.ts +0 -106
- package/src/lib/db/sql.ts +0 -192
- package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
- package/src/lib/local-tree-diff.test.ts +0 -301
- package/src/lib/local-tree-diff.ts +0 -362
- package/src/lib/narrow-hint-banner.test.ts +0 -235
- package/src/lib/narrow-hint-banner.ts +0 -212
- package/src/lib/plan-limit-nag.test.ts +0 -317
- package/src/lib/plan-limit-nag.ts +0 -264
- package/src/main.ts +0 -371
- package/src/node-preflight.test.ts +0 -60
- package/src/node-preflight.ts +0 -67
- package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
- package/src/outpost/session-heartbeat-guard.test.ts +0 -105
- package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
- package/src/outpost/session-heartbeat-publisher.ts +0 -186
- package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
- package/src/outpost/session-heartbeat.test.ts +0 -459
- package/src/outpost/session-heartbeat.ts +0 -877
- package/src/packaging.test.ts +0 -109
- package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
- package/src/run/discover-schemas.test.ts +0 -153
- package/src/run/discover-schemas.ts +0 -79
- package/src/run/hq-plugin.test.ts +0 -371
- package/src/run/hq-plugin.ts +0 -277
- package/src/run/varlock-shape.test.ts +0 -57
- package/src/schemas/hq-package.schema.json +0 -155
- package/src/sentry-before-send.test.ts +0 -359
- package/src/sentry-before-send.ts +0 -221
- package/src/sentry-epipe.test.ts +0 -37
- package/src/sentry-release.test.ts +0 -54
- package/src/sentry.test.ts +0 -24
- package/src/sentry.ts +0 -54
- package/src/strategies/link.ts +0 -62
- package/src/strategies/merge.ts +0 -142
- package/src/types.ts +0 -146
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
- package/src/utils/auth-error.test.ts +0 -40
- package/src/utils/auth-error.ts +0 -42
- package/src/utils/billing-gate.test.ts +0 -95
- package/src/utils/billing-gate.ts +0 -228
- package/src/utils/breadcrumb-buffer.ts +0 -18
- package/src/utils/cli-telemetry.test.ts +0 -153
- package/src/utils/cli-telemetry.ts +0 -61
- package/src/utils/cognito-session.machine.test.ts +0 -109
- package/src/utils/cognito-session.refresh.test.ts +0 -84
- package/src/utils/cognito-session.test.ts +0 -328
- package/src/utils/cognito-session.ts +0 -448
- package/src/utils/company-selection-error.test.ts +0 -42
- package/src/utils/company-selection-error.ts +0 -45
- package/src/utils/contribution-table.ts +0 -83
- package/src/utils/environmental-error.test.ts +0 -45
- package/src/utils/environmental-error.ts +0 -39
- package/src/utils/epipe.test.ts +0 -28
- package/src/utils/epipe.ts +0 -29
- package/src/utils/expected-cli-error.test.ts +0 -28
- package/src/utils/expected-cli-error.ts +0 -39
- package/src/utils/feedback-diagnostics.test.ts +0 -183
- package/src/utils/feedback-diagnostics.ts +0 -123
- package/src/utils/feedback-screenshots.test.ts +0 -134
- package/src/utils/feedback-screenshots.ts +0 -124
- package/src/utils/feedback-versions.test.ts +0 -98
- package/src/utils/feedback-versions.ts +0 -68
- package/src/utils/git.ts +0 -74
- package/src/utils/integrity.ts +0 -59
- package/src/utils/intercepted-process-exit.test.ts +0 -37
- package/src/utils/intercepted-process-exit.ts +0 -36
- package/src/utils/login-provider.test.ts +0 -42
- package/src/utils/login-provider.ts +0 -30
- package/src/utils/manifest.test.ts +0 -167
- package/src/utils/manifest.ts +0 -131
- package/src/utils/pack-contributions.test.ts +0 -603
- package/src/utils/pack-contributions.ts +0 -450
- package/src/utils/people.ts +0 -215
- package/src/utils/registry-client.ts +0 -204
- package/src/utils/registry.ts +0 -90
- package/src/utils/resolve-vault-credential.test.ts +0 -69
- package/src/utils/resolve-vault-credential.ts +0 -60
- package/src/utils/sandbox-runner-client.test.ts +0 -279
- package/src/utils/sandbox-runner-client.ts +0 -260
- package/src/utils/secrets-cache.ts +0 -183
- package/src/utils/sentry-identity.ts +0 -45
- package/src/utils/settle-with-timeout.test.ts +0 -21
- package/src/utils/settle-with-timeout.ts +0 -22
- package/src/utils/user-yaml-error.test.ts +0 -24
- package/src/utils/user-yaml-error.ts +0 -30
- package/src/utils/vault-api.test.ts +0 -536
- package/src/utils/vault-api.ts +0 -382
- package/src/utils/version-check.test.ts +0 -176
- package/src/utils/version-check.ts +0 -155
- package/src/utils/version-gate.test.ts +0 -1024
- package/src/utils/version-gate.ts +0 -702
- package/test/commands/db-tenant-isolation.test.ts +0 -94
- package/test/commands/db.test.ts +0 -85
- package/test/commands/signals.test.ts +0 -207
- package/test/commands/sources.test.ts +0 -231
- package/test/e2e/smoke-install-mcp.sh +0 -113
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
- package/test/fixtures/signals/action_item/sample.md +0 -16
- package/test/fixtures/signals/summary/sample.md +0 -12
- package/test/fixtures/sources/meetings/sample.md +0 -25
- package/test/helpers/cli-runner.ts +0 -150
- package/test/helpers/s3-list-mock.ts +0 -79
- package/test/helpers/vault-service-mock.ts +0 -295
- package/test/sources-signals/smoke.test.ts +0 -228
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -11
- package/vitest.e2e.config.ts +0 -7
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helper for `hq sync narrow` (US-007).
|
|
3
|
-
*
|
|
4
|
-
* Walks a local directory tree under `<hqRoot>/companies/{slug}/`, classifies
|
|
5
|
-
* each file against a prospective `shared`-mode prefix set, and groups the
|
|
6
|
-
* results into three buckets:
|
|
7
|
-
*
|
|
8
|
-
* - **staying** — file is covered by the prospective prefix set; survives.
|
|
9
|
-
* - **clean** — file is NOT covered (orphan) AND is provably unchanged
|
|
10
|
-
* since the last journal sync (safe to delete).
|
|
11
|
-
* - **dirty** — file is NOT covered (orphan) AND has been locally
|
|
12
|
-
* modified since the last sync (sacred — abort without
|
|
13
|
-
* `--force`).
|
|
14
|
-
*
|
|
15
|
-
* The clean-vs-dirty rule mirrors hq-cloud's implicit-shrink path
|
|
16
|
-
* (`scope-shrink.ts::classifyOrphan`) verbatim so the two narrowing paths —
|
|
17
|
-
* implicit (next pull after sync-mode flip) and explicit (`hq sync narrow
|
|
18
|
-
* --apply`) — agree on what "dirty" means. See US-000 Task 3 in
|
|
19
|
-
* `companies/indigo/projects/hq-sync-browse-vs-sync/references.md`.
|
|
20
|
-
*
|
|
21
|
-
* Pure: no network. Reads the journal off `SyncJournal` (caller passes it
|
|
22
|
-
* in), stats + hashes files on disk — no journal mutations and no remote
|
|
23
|
-
* calls happen here. The CLI orchestrator (`sync-narrow.ts`) is responsible
|
|
24
|
-
* for the destructive side effects (delete, tombstone, PUT sync-config).
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import * as fs from "node:fs";
|
|
28
|
-
import * as path from "node:path";
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
hashFile,
|
|
32
|
-
isCoveredByAny,
|
|
33
|
-
type JournalEntry,
|
|
34
|
-
type SyncJournal,
|
|
35
|
-
} from "@indigoai-us/hq-cloud";
|
|
36
|
-
|
|
37
|
-
// ── Types ───────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
export type DirtyReason =
|
|
40
|
-
| "modified-after-sync"
|
|
41
|
-
| "hash-mismatch"
|
|
42
|
-
| "not-in-journal"
|
|
43
|
-
| "stat-error";
|
|
44
|
-
|
|
45
|
-
export interface NarrowFile {
|
|
46
|
-
/**
|
|
47
|
-
* COMPANY-RELATIVE path (e.g. `meetings/a.md`) — the canonical namespace
|
|
48
|
-
* shared by the per-company journal keys, the vault S3 keys, and the
|
|
49
|
-
* server's explicit-grant paths. NOT hq-root-relative.
|
|
50
|
-
*/
|
|
51
|
-
relPath: string;
|
|
52
|
-
/** Absolute path on disk (convenience for the CLI delete loop). */
|
|
53
|
-
absPath: string;
|
|
54
|
-
/** Size in bytes from the local stat. 0 for symlinks (lstat size is the link, not target). */
|
|
55
|
-
bytes: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface NarrowDirtyFile extends NarrowFile {
|
|
59
|
-
reason: DirtyReason;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface NarrowPlan {
|
|
63
|
-
/** Files covered by `prospectivePrefixSet` — survive the narrow. */
|
|
64
|
-
staying: NarrowFile[];
|
|
65
|
-
/** Orphan files that are journal-clean — safe to delete on `--apply`. */
|
|
66
|
-
clean: NarrowFile[];
|
|
67
|
-
/** Orphan files that are locally dirty — block `--apply` unless `--force`. */
|
|
68
|
-
dirty: NarrowDirtyFile[];
|
|
69
|
-
|
|
70
|
-
// Convenience aggregates for the CLI summary table.
|
|
71
|
-
totalStayingCount: number;
|
|
72
|
-
totalCleanCount: number;
|
|
73
|
-
totalCleanBytes: number;
|
|
74
|
-
totalDirtyCount: number;
|
|
75
|
-
totalDirtyBytes: number;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface BuildNarrowPlanInput {
|
|
79
|
-
/** Absolute HQ root path (the dir containing `companies/`). */
|
|
80
|
-
hqRoot: string;
|
|
81
|
-
/** Company slug — used to derive the walk root `<hqRoot>/companies/<slug>/`. */
|
|
82
|
-
companySlug: string;
|
|
83
|
-
/**
|
|
84
|
-
* Coalesced prospective `shared`-mode prefix set (the result of running
|
|
85
|
-
* the caller's explicit grants through `coalescePrefixes`). Prefixes are
|
|
86
|
-
* COMPANY-RELATIVE (e.g. `meetings/`) — the namespace the grants endpoint
|
|
87
|
-
* returns and the namespace `relPath` is now computed in.
|
|
88
|
-
*/
|
|
89
|
-
prospectivePrefixSet: readonly string[];
|
|
90
|
-
/**
|
|
91
|
-
* Active per-company journal. Hash + mtime comparisons key off
|
|
92
|
-
* `journal.files[relPath]`.
|
|
93
|
-
*/
|
|
94
|
-
journal: SyncJournal;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ── Pure entry point ────────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build a NarrowPlan by walking `<hqRoot>/companies/<slug>/` and classifying
|
|
101
|
-
* each regular file against `prospectivePrefixSet` + `journal`.
|
|
102
|
-
*
|
|
103
|
-
* Walk semantics:
|
|
104
|
-
* - Recursive `fs.readdirSync(... withFileTypes)`.
|
|
105
|
-
* - Symlinks are RECORDED but never followed (`lstat`, not `stat`), matching
|
|
106
|
-
* the `share()`/sync engine's contract.
|
|
107
|
-
* - Hidden files + `.DS_Store` + `node_modules` are walked as-is — the
|
|
108
|
-
* caller is expected to point this at HQ content, where `.hqignore`
|
|
109
|
-
* filtering happens upstream of the journal. The journal already
|
|
110
|
-
* reflects what's been pushed/pulled, so `not-in-journal` is the
|
|
111
|
-
* classifier signal for "we didn't put this here".
|
|
112
|
-
* - If the walk root is missing entirely, returns an empty plan (rather
|
|
113
|
-
* than throwing) — a freshly-flipped membership may not have synced any
|
|
114
|
-
* files yet.
|
|
115
|
-
*/
|
|
116
|
-
export function buildNarrowPlan(input: BuildNarrowPlanInput): NarrowPlan {
|
|
117
|
-
const { hqRoot, companySlug, prospectivePrefixSet, journal } = input;
|
|
118
|
-
const walkRoot = path.join(hqRoot, "companies", companySlug);
|
|
119
|
-
|
|
120
|
-
const staying: NarrowFile[] = [];
|
|
121
|
-
const clean: NarrowFile[] = [];
|
|
122
|
-
const dirty: NarrowDirtyFile[] = [];
|
|
123
|
-
|
|
124
|
-
if (!fs.existsSync(walkRoot)) {
|
|
125
|
-
return emptyPlan();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Walk with `walkRoot` as the rel-root so each file's `relPath` is
|
|
129
|
-
// COMPANY-RELATIVE (e.g. `meetings/a.md`) — the same namespace as the
|
|
130
|
-
// server's explicit-grant paths, the per-company journal keys, and the
|
|
131
|
-
// hq-cloud sync engine's `RemoteFile.key`. Computing it relative to `hqRoot`
|
|
132
|
-
// (the old behavior) produced `companies/<slug>/meetings/a.md`, which
|
|
133
|
-
// matched neither the company-relative grants nor the journal keys — so
|
|
134
|
-
// every file fell out of `prospectivePrefixSet` AND missed its journal
|
|
135
|
-
// entry, making narrow flag the entire tree as dirty orphans. See the
|
|
136
|
-
// namespace contract in hq-cloud `scope-shrink.ts` / `prefix-coalesce.ts`.
|
|
137
|
-
walkLocal(walkRoot, walkRoot, (file) => {
|
|
138
|
-
if (isCoveredByAny(file.relPath, prospectivePrefixSet)) {
|
|
139
|
-
staying.push(file);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const verdict = classifyAgainstJournal(file, journal);
|
|
143
|
-
if (verdict.clean) {
|
|
144
|
-
clean.push(file);
|
|
145
|
-
} else {
|
|
146
|
-
dirty.push({ ...file, reason: verdict.reason });
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
staying,
|
|
152
|
-
clean,
|
|
153
|
-
dirty,
|
|
154
|
-
totalStayingCount: staying.length,
|
|
155
|
-
totalCleanCount: clean.length,
|
|
156
|
-
totalCleanBytes: clean.reduce((sum, f) => sum + f.bytes, 0),
|
|
157
|
-
totalDirtyCount: dirty.length,
|
|
158
|
-
totalDirtyBytes: dirty.reduce((sum, f) => sum + f.bytes, 0),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ── Internal: walk + classify ───────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
function emptyPlan(): NarrowPlan {
|
|
165
|
-
return {
|
|
166
|
-
staying: [],
|
|
167
|
-
clean: [],
|
|
168
|
-
dirty: [],
|
|
169
|
-
totalStayingCount: 0,
|
|
170
|
-
totalCleanCount: 0,
|
|
171
|
-
totalCleanBytes: 0,
|
|
172
|
-
totalDirtyCount: 0,
|
|
173
|
-
totalDirtyBytes: 0,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Recursive readdir walk yielding regular files (and dangling-target
|
|
179
|
-
* symlinks — those are recorded as files for narrow-plan purposes; the
|
|
180
|
-
* delete path uses `fs.unlinkSync` which handles symlinks correctly).
|
|
181
|
-
*
|
|
182
|
-
* Uses `lstat` rather than `stat` so a symlink's size doesn't follow the
|
|
183
|
-
* target chain (matches the share-engine convention).
|
|
184
|
-
*/
|
|
185
|
-
/**
|
|
186
|
-
* Normalize OS-native path separators to POSIX "/" for vault keys.
|
|
187
|
-
*
|
|
188
|
-
* `path.relative()` yields "\\"-separated paths on Windows, but vault S3 keys
|
|
189
|
-
* are always "/"-separated — the server splits keys on "/" to rebuild the tree
|
|
190
|
-
* and the journal + grants endpoint use that same namespace. Emit POSIX keys
|
|
191
|
-
* regardless of client OS so a Windows narrow-plan lines up with the
|
|
192
|
-
* forward-slash keys everywhere else. Mirrors the same normalization on
|
|
193
|
-
* @indigoai-us/hq-cloud's upload path. Converting "\\" explicitly (rather than
|
|
194
|
-
* only path.sep) keeps the result correct — and the regression test meaningful
|
|
195
|
-
* — on a POSIX CI too.
|
|
196
|
-
*/
|
|
197
|
-
export function toPosixKey(p: string): string {
|
|
198
|
-
return p.split("\\").join("/");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function walkLocal(
|
|
202
|
-
dir: string,
|
|
203
|
-
// Rel-root for `relPath` — the company walk root (`<hqRoot>/companies/<slug>`),
|
|
204
|
-
// so emitted `relPath`s are company-relative. Fixed across recursion.
|
|
205
|
-
relRoot: string,
|
|
206
|
-
emit: (file: NarrowFile) => void,
|
|
207
|
-
): void {
|
|
208
|
-
let entries: fs.Dirent[];
|
|
209
|
-
try {
|
|
210
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
211
|
-
} catch (err) {
|
|
212
|
-
// Permission-denied or ENOENT mid-walk — skip silently; the CLI
|
|
213
|
-
// surfaces the higher-level "company folder missing" message earlier.
|
|
214
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
215
|
-
if (code === "ENOENT" || code === "EACCES") return;
|
|
216
|
-
throw err;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const entry of entries) {
|
|
220
|
-
const absPath = path.join(dir, entry.name);
|
|
221
|
-
const relPath = toPosixKey(path.relative(relRoot, absPath));
|
|
222
|
-
|
|
223
|
-
if (entry.isSymbolicLink()) {
|
|
224
|
-
// Record the link as a file-like entry. Don't descend — narrow is
|
|
225
|
-
// about pruning files that the LOCAL tree has materialized here; a
|
|
226
|
-
// symlink to a knowledge repo is one such "file" from the
|
|
227
|
-
// walk's POV.
|
|
228
|
-
let bytes = 0;
|
|
229
|
-
try {
|
|
230
|
-
bytes = fs.lstatSync(absPath).size;
|
|
231
|
-
} catch {
|
|
232
|
-
// pass — dangling link still gets emitted with size 0
|
|
233
|
-
}
|
|
234
|
-
emit({ relPath, absPath, bytes });
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (entry.isDirectory()) {
|
|
239
|
-
walkLocal(absPath, relRoot, emit);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (entry.isFile()) {
|
|
244
|
-
let bytes = 0;
|
|
245
|
-
try {
|
|
246
|
-
bytes = fs.lstatSync(absPath).size;
|
|
247
|
-
} catch {
|
|
248
|
-
// pass
|
|
249
|
-
}
|
|
250
|
-
emit({ relPath, absPath, bytes });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
interface JournalVerdict {
|
|
256
|
-
clean: boolean;
|
|
257
|
-
reason: DirtyReason;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Mirror of `scope-shrink.ts::classifyOrphan`:
|
|
262
|
-
*
|
|
263
|
-
* clean = (entry exists AND hash(local) === entry.hash AND mtime <= syncedAt + 1s)
|
|
264
|
-
* OR (local file missing)
|
|
265
|
-
*
|
|
266
|
-
* Differences from the implicit path:
|
|
267
|
-
* - If there is NO journal entry at all, classify as **dirty**
|
|
268
|
-
* (`not-in-journal`). The implicit path never sees those files (it
|
|
269
|
-
* iterates the journal); here we walk the local tree so an
|
|
270
|
-
* un-journaled file is a real risk — it could be an unsynced local
|
|
271
|
-
* draft.
|
|
272
|
-
* - We don't honor `direction: "up"` differently. For an explicit
|
|
273
|
-
* narrow migration the user is asking "wipe everything outside the
|
|
274
|
-
* shared scope" — push-only authorship deserves the same dirty-file
|
|
275
|
-
* protection as pulled content (otherwise `--apply` could nuke local
|
|
276
|
-
* drafts).
|
|
277
|
-
*/
|
|
278
|
-
function classifyAgainstJournal(
|
|
279
|
-
file: NarrowFile,
|
|
280
|
-
journal: SyncJournal,
|
|
281
|
-
): JournalVerdict {
|
|
282
|
-
const entry: JournalEntry | undefined = journal.files[file.relPath];
|
|
283
|
-
if (!entry) {
|
|
284
|
-
return { clean: false, reason: "not-in-journal" };
|
|
285
|
-
}
|
|
286
|
-
// Tombstoned entries — the file shouldn't be here at all; treat as
|
|
287
|
-
// not-in-journal for safety so `--apply` doesn't silently delete it.
|
|
288
|
-
if (entry.removedAt) {
|
|
289
|
-
return { clean: false, reason: "not-in-journal" };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
let stat: fs.Stats;
|
|
293
|
-
try {
|
|
294
|
-
stat = fs.lstatSync(file.absPath);
|
|
295
|
-
} catch (err) {
|
|
296
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
297
|
-
if (code === "ENOENT") {
|
|
298
|
-
// Already gone — clean (the apply path will skip ENOENT on unlink).
|
|
299
|
-
return { clean: true, reason: "stat-error" };
|
|
300
|
-
}
|
|
301
|
-
return { clean: false, reason: "stat-error" };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// mtime guard with 1s grace for filesystem clock jitter, matching
|
|
305
|
-
// hq-cloud `scope-shrink.ts`.
|
|
306
|
-
const syncedAtMs = Date.parse(entry.syncedAt);
|
|
307
|
-
if (!Number.isNaN(syncedAtMs) && stat.mtimeMs > syncedAtMs + 1000) {
|
|
308
|
-
return { clean: false, reason: "modified-after-sync" };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Hash check — final word. Symlinks: hashFile reads contents which is
|
|
312
|
-
// wrong for symlinks (it would follow the target), so on a symlink we
|
|
313
|
-
// skip the hash check and trust mtime alone. This matches the
|
|
314
|
-
// engine's behavior in the symlink-orphan case.
|
|
315
|
-
if (stat.isSymbolicLink()) {
|
|
316
|
-
return { clean: true, reason: "stat-error" };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let actualHash: string;
|
|
320
|
-
try {
|
|
321
|
-
actualHash = hashFile(file.absPath);
|
|
322
|
-
} catch {
|
|
323
|
-
return { clean: false, reason: "stat-error" };
|
|
324
|
-
}
|
|
325
|
-
if (actualHash !== entry.hash) {
|
|
326
|
-
return { clean: false, reason: "hash-mismatch" };
|
|
327
|
-
}
|
|
328
|
-
return { clean: true, reason: "stat-error" };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ── Formatting helper (separable for tests) ────────────────────────────────
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Render the dry-run summary as plain text (no chalk — keep it pure). The
|
|
335
|
-
* CLI wrapper can colorize lines afterwards if desired.
|
|
336
|
-
*/
|
|
337
|
-
export function formatNarrowPlanSummary(plan: NarrowPlan): string {
|
|
338
|
-
const lines: string[] = [];
|
|
339
|
-
lines.push("Narrow plan summary:");
|
|
340
|
-
lines.push(` files staying: ${plan.totalStayingCount}`);
|
|
341
|
-
lines.push(
|
|
342
|
-
` clean orphans: ${plan.totalCleanCount} (${formatBytes(plan.totalCleanBytes)})`,
|
|
343
|
-
);
|
|
344
|
-
lines.push(
|
|
345
|
-
` dirty orphans: ${plan.totalDirtyCount} (${formatBytes(plan.totalDirtyBytes)})`,
|
|
346
|
-
);
|
|
347
|
-
return lines.join("\n");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** Human-readable byte counts with a fixed unit ladder. */
|
|
351
|
-
export function formatBytes(n: number): string {
|
|
352
|
-
if (!Number.isFinite(n) || n < 0) return "0 B";
|
|
353
|
-
if (n < 1024) return `${n} B`;
|
|
354
|
-
const units = ["KB", "MB", "GB", "TB"];
|
|
355
|
-
let v = n / 1024;
|
|
356
|
-
let i = 0;
|
|
357
|
-
while (v >= 1024 && i < units.length - 1) {
|
|
358
|
-
v /= 1024;
|
|
359
|
-
i++;
|
|
360
|
-
}
|
|
361
|
-
return `${v.toFixed(2)} ${units[i]}`;
|
|
362
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `narrow-hint-banner` (US-011).
|
|
3
|
-
*
|
|
4
|
-
* Coverage:
|
|
5
|
-
* 1. Emits a hint when sync mode is 'all' and not suppressed.
|
|
6
|
-
* 2. Skips emission for shared/custom memberships.
|
|
7
|
-
* 3. Dedupes per {companyUid, level} for the process lifetime.
|
|
8
|
-
* 4. Suppression via HQ_SYNC_NARROW_HINT=off (env).
|
|
9
|
-
* 5. Suppression via <hqRoot>/.hq/config.json `syncNarrowHint: 'off'`.
|
|
10
|
-
* 6. Warning + strict level messages.
|
|
11
|
-
* 7. `isStrictRefusal` only fires for strict + 'all'.
|
|
12
|
-
* 8. `resolveBannerLevel` parses env values and defaults to 'hint'.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
_resetShownForTests,
|
|
19
|
-
emitNarrowHint,
|
|
20
|
-
isStrictRefusal,
|
|
21
|
-
resolveBannerLevel,
|
|
22
|
-
shouldShowBanner,
|
|
23
|
-
} from "./narrow-hint-banner.js";
|
|
24
|
-
|
|
25
|
-
function makeSink(): { lines: string[]; write: (s: string) => void } {
|
|
26
|
-
const lines: string[] = [];
|
|
27
|
-
return {
|
|
28
|
-
lines,
|
|
29
|
-
write: (s: string) => {
|
|
30
|
-
lines.push(s);
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
_resetShownForTests();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("emitNarrowHint", () => {
|
|
40
|
-
it("emits a hint message when sync mode is 'all' and not suppressed", () => {
|
|
41
|
-
const sink = makeSink();
|
|
42
|
-
emitNarrowHint(
|
|
43
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "hint" },
|
|
44
|
-
{ write: sink.write, showOverride: true },
|
|
45
|
-
);
|
|
46
|
-
expect(sink.lines).toHaveLength(1);
|
|
47
|
-
expect(sink.lines[0]).toMatch(/switch to shared-mode sync/i);
|
|
48
|
-
expect(sink.lines[0]).toMatch(/hq sync narrow --dry-run/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("skips emission when sync mode is 'shared'", () => {
|
|
52
|
-
const sink = makeSink();
|
|
53
|
-
emitNarrowHint(
|
|
54
|
-
{ companyUid: "cmp_acme", syncMode: "shared", level: "hint" },
|
|
55
|
-
{ write: sink.write, showOverride: true },
|
|
56
|
-
);
|
|
57
|
-
expect(sink.lines).toHaveLength(0);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("skips emission when sync mode is 'custom'", () => {
|
|
61
|
-
const sink = makeSink();
|
|
62
|
-
emitNarrowHint(
|
|
63
|
-
{ companyUid: "cmp_acme", syncMode: "custom", level: "hint" },
|
|
64
|
-
{ write: sink.write, showOverride: true },
|
|
65
|
-
);
|
|
66
|
-
expect(sink.lines).toHaveLength(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("dedupes per {companyUid, level} across calls", () => {
|
|
70
|
-
const sink = makeSink();
|
|
71
|
-
const input = {
|
|
72
|
-
companyUid: "cmp_acme",
|
|
73
|
-
syncMode: "all",
|
|
74
|
-
level: "hint",
|
|
75
|
-
} as const;
|
|
76
|
-
emitNarrowHint(input, { write: sink.write, showOverride: true });
|
|
77
|
-
emitNarrowHint(input, { write: sink.write, showOverride: true });
|
|
78
|
-
emitNarrowHint(input, { write: sink.write, showOverride: true });
|
|
79
|
-
expect(sink.lines).toHaveLength(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("emits separately for different companies", () => {
|
|
83
|
-
const sink = makeSink();
|
|
84
|
-
emitNarrowHint(
|
|
85
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "hint" },
|
|
86
|
-
{ write: sink.write, showOverride: true },
|
|
87
|
-
);
|
|
88
|
-
emitNarrowHint(
|
|
89
|
-
{ companyUid: "cmp_globex", syncMode: "all", level: "hint" },
|
|
90
|
-
{ write: sink.write, showOverride: true },
|
|
91
|
-
);
|
|
92
|
-
expect(sink.lines).toHaveLength(2);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("emits separately for the same company at a different level", () => {
|
|
96
|
-
const sink = makeSink();
|
|
97
|
-
emitNarrowHint(
|
|
98
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "hint" },
|
|
99
|
-
{ write: sink.write, showOverride: true },
|
|
100
|
-
);
|
|
101
|
-
emitNarrowHint(
|
|
102
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "warning" },
|
|
103
|
-
{ write: sink.write, showOverride: true },
|
|
104
|
-
);
|
|
105
|
-
expect(sink.lines).toHaveLength(2);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("suppresses emission when showOverride=false (suppression path)", () => {
|
|
109
|
-
const sink = makeSink();
|
|
110
|
-
emitNarrowHint(
|
|
111
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "hint" },
|
|
112
|
-
{ write: sink.write, showOverride: false },
|
|
113
|
-
);
|
|
114
|
-
expect(sink.lines).toHaveLength(0);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("emits the warning-level message", () => {
|
|
118
|
-
const sink = makeSink();
|
|
119
|
-
emitNarrowHint(
|
|
120
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "warning" },
|
|
121
|
-
{ write: sink.write, showOverride: true },
|
|
122
|
-
);
|
|
123
|
-
expect(sink.lines).toHaveLength(1);
|
|
124
|
-
expect(sink.lines[0]).toMatch(/shared-mode sync is the new default/i);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("emits the strict-level message (does NOT throw or exit)", () => {
|
|
128
|
-
const sink = makeSink();
|
|
129
|
-
expect(() =>
|
|
130
|
-
emitNarrowHint(
|
|
131
|
-
{ companyUid: "cmp_acme", syncMode: "all", level: "strict" },
|
|
132
|
-
{ write: sink.write, showOverride: true },
|
|
133
|
-
),
|
|
134
|
-
).not.toThrow();
|
|
135
|
-
expect(sink.lines).toHaveLength(1);
|
|
136
|
-
expect(sink.lines[0]).toMatch(/--mode-all/);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("shouldShowBanner", () => {
|
|
141
|
-
it("returns true with no overrides", () => {
|
|
142
|
-
expect(shouldShowBanner({ envValue: undefined })).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("returns false when env value is 'off' (case-insensitive)", () => {
|
|
146
|
-
expect(shouldShowBanner({ envValue: "off" })).toBe(false);
|
|
147
|
-
expect(shouldShowBanner({ envValue: "OFF" })).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("returns true for env values other than 'off'", () => {
|
|
151
|
-
expect(shouldShowBanner({ envValue: "on" })).toBe(true);
|
|
152
|
-
expect(shouldShowBanner({ envValue: "" })).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("returns false when .hq/config.json sets syncNarrowHint='off'", () => {
|
|
156
|
-
const exists = (_p: string) => true;
|
|
157
|
-
const read = (_p: string) => JSON.stringify({ syncNarrowHint: "off" });
|
|
158
|
-
expect(
|
|
159
|
-
shouldShowBanner({
|
|
160
|
-
envValue: undefined,
|
|
161
|
-
hqRoot: "/tmp/hq",
|
|
162
|
-
existsFile: exists,
|
|
163
|
-
readFile: read,
|
|
164
|
-
}),
|
|
165
|
-
).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("returns true when .hq/config.json has no syncNarrowHint key", () => {
|
|
169
|
-
const exists = (_p: string) => true;
|
|
170
|
-
const read = (_p: string) =>
|
|
171
|
-
JSON.stringify({ activeCompany: "acme" });
|
|
172
|
-
expect(
|
|
173
|
-
shouldShowBanner({
|
|
174
|
-
envValue: undefined,
|
|
175
|
-
hqRoot: "/tmp/hq",
|
|
176
|
-
existsFile: exists,
|
|
177
|
-
readFile: read,
|
|
178
|
-
}),
|
|
179
|
-
).toBe(true);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("tolerates malformed config.json (returns true)", () => {
|
|
183
|
-
const exists = (_p: string) => true;
|
|
184
|
-
const read = (_p: string) => "{not json";
|
|
185
|
-
expect(
|
|
186
|
-
shouldShowBanner({
|
|
187
|
-
envValue: undefined,
|
|
188
|
-
hqRoot: "/tmp/hq",
|
|
189
|
-
existsFile: exists,
|
|
190
|
-
readFile: read,
|
|
191
|
-
}),
|
|
192
|
-
).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("returns true when config file does not exist", () => {
|
|
196
|
-
expect(
|
|
197
|
-
shouldShowBanner({
|
|
198
|
-
envValue: undefined,
|
|
199
|
-
hqRoot: "/tmp/hq",
|
|
200
|
-
existsFile: () => false,
|
|
201
|
-
readFile: () => {
|
|
202
|
-
throw new Error("should not be called");
|
|
203
|
-
},
|
|
204
|
-
}),
|
|
205
|
-
).toBe(true);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe("isStrictRefusal", () => {
|
|
210
|
-
it("returns true only for level=strict + syncMode=all", () => {
|
|
211
|
-
expect(isStrictRefusal("all", "strict")).toBe(true);
|
|
212
|
-
expect(isStrictRefusal("shared", "strict")).toBe(false);
|
|
213
|
-
expect(isStrictRefusal("custom", "strict")).toBe(false);
|
|
214
|
-
expect(isStrictRefusal("all", "warning")).toBe(false);
|
|
215
|
-
expect(isStrictRefusal("all", "hint")).toBe(false);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe("resolveBannerLevel", () => {
|
|
220
|
-
it("defaults to 'hint' when env is undefined", () => {
|
|
221
|
-
expect(resolveBannerLevel(undefined)).toBe("hint");
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("parses recognized values (case-insensitive)", () => {
|
|
225
|
-
expect(resolveBannerLevel("hint")).toBe("hint");
|
|
226
|
-
expect(resolveBannerLevel("warning")).toBe("warning");
|
|
227
|
-
expect(resolveBannerLevel("strict")).toBe("strict");
|
|
228
|
-
expect(resolveBannerLevel("WARNING")).toBe("warning");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("falls back to 'hint' for unknown values", () => {
|
|
232
|
-
expect(resolveBannerLevel("loud")).toBe("hint");
|
|
233
|
-
expect(resolveBannerLevel("")).toBe("hint");
|
|
234
|
-
});
|
|
235
|
-
});
|