@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
package/src/lib/db/sql.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local SQL execution helpers (vault-databases US-004).
|
|
3
|
-
*
|
|
4
|
-
* Company scope is path-bound via resolveLocalDbPath — callers must pass the
|
|
5
|
-
* resolved company slug from HQ session/flags. Path overrides that open
|
|
6
|
-
* another company's DB are denied unless an explicit dangerous flag is set.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import Database from "better-sqlite3";
|
|
10
|
-
|
|
11
|
-
import type { LocalDb } from "./local.js";
|
|
12
|
-
import { openLocalDb } from "./local.js";
|
|
13
|
-
import {
|
|
14
|
-
normalizeCompanySlugForLocalDb,
|
|
15
|
-
resolveLocalDbDir,
|
|
16
|
-
resolveLocalDbPath,
|
|
17
|
-
type LocalDbPathEnv,
|
|
18
|
-
} from "./paths.js";
|
|
19
|
-
|
|
20
|
-
export interface SqlRunOptions extends LocalDbPathEnv {
|
|
21
|
-
company: string;
|
|
22
|
-
sql: string;
|
|
23
|
-
/** Allow INSERT/UPDATE/DELETE/DDL. Default false (read-only). */
|
|
24
|
-
write?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Dangerous: open an absolute DB path instead of the company canonical path.
|
|
27
|
-
* Denied unless allowCrossCompanyPath is true.
|
|
28
|
-
*/
|
|
29
|
-
dbPathOverride?: string;
|
|
30
|
-
/** Explicit dangerous flag to open a non-canonical local DB path. Default false. */
|
|
31
|
-
allowCrossCompanyPath?: boolean;
|
|
32
|
-
/** Output format */
|
|
33
|
-
format?: "jsonl" | "table";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SqlRunResult {
|
|
37
|
-
columns: string[];
|
|
38
|
-
rows: Record<string, unknown>[];
|
|
39
|
-
changes: number;
|
|
40
|
-
readonly: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const WRITE_PATTERN =
|
|
44
|
-
/^\s*(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|TRUNCATE|REINDEX|VACUUM|BEGIN|COMMIT|ROLLBACK)\b/i;
|
|
45
|
-
|
|
46
|
-
/** Always forbidden — opens other files/DBs and breaks company path isolation. */
|
|
47
|
-
const FORBIDDEN_SQL_PATTERN =
|
|
48
|
-
/\b(ATTACH|DETACH|LOAD_EXTENSION)\b/i;
|
|
49
|
-
|
|
50
|
-
export function stripSqlNoise(sql: string): string {
|
|
51
|
-
return sql
|
|
52
|
-
.replace(/^\s*--[^\n]*\n/gm, "")
|
|
53
|
-
.replace(/^\s*\/\*[\s\S]*?\*\//gm, "")
|
|
54
|
-
.trim();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function isWriteSql(sql: string): boolean {
|
|
58
|
-
return WRITE_PATTERN.test(stripSqlNoise(sql));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Statements that must never run via `hq db sql`, even with --write.
|
|
63
|
-
* ATTACH/DETACH would let a company-scoped session open another company's
|
|
64
|
-
* vault.db by absolute path (category-1 isolation).
|
|
65
|
-
*/
|
|
66
|
-
export function assertSqlAllowed(sql: string): void {
|
|
67
|
-
const stripped = stripSqlNoise(sql);
|
|
68
|
-
if (FORBIDDEN_SQL_PATTERN.test(stripped)) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
"ATTACH/DETACH/LOAD_EXTENSION are not allowed via hq db sql (tenant isolation); use the company-canonical local path only",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Resolve which file path to open, enforcing company isolation by default.
|
|
77
|
-
*/
|
|
78
|
-
export function resolveSqlDbPath(opts: SqlRunOptions): string {
|
|
79
|
-
const company = normalizeCompanySlugForLocalDb(opts.company);
|
|
80
|
-
const canonical = resolveLocalDbPath(company, opts);
|
|
81
|
-
|
|
82
|
-
if (!opts.dbPathOverride) {
|
|
83
|
-
return canonical;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (opts.dbPathOverride === canonical) {
|
|
87
|
-
return canonical;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!opts.allowCrossCompanyPath) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
"cross-company or path override denied: refusing to open a non-canonical local DB path without --allow-cross-company-path (dangerous)",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return opts.dbPathOverride;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function openByPath(dbPath: string): LocalDb {
|
|
100
|
-
const db = new Database(dbPath);
|
|
101
|
-
db.pragma("journal_mode = WAL");
|
|
102
|
-
db.pragma("foreign_keys = ON");
|
|
103
|
-
return db;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Execute SQL against the company local vault DB.
|
|
108
|
-
*/
|
|
109
|
-
export function runLocalSql(opts: SqlRunOptions): SqlRunResult {
|
|
110
|
-
const sql = opts.sql?.trim();
|
|
111
|
-
if (!sql) {
|
|
112
|
-
throw new Error("SQL statement is required");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
assertSqlAllowed(sql);
|
|
116
|
-
|
|
117
|
-
const writeRequested = !!opts.write;
|
|
118
|
-
if (isWriteSql(sql) && !writeRequested) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"write/DDL statement blocked in read-only mode; pass --write to allow (prefer hq db migrate for schema changes)",
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const dbPath = resolveSqlDbPath(opts);
|
|
125
|
-
const usingCanonical =
|
|
126
|
-
!opts.dbPathOverride || opts.dbPathOverride === resolveLocalDbPath(opts.company, opts);
|
|
127
|
-
|
|
128
|
-
const db: LocalDb = usingCanonical
|
|
129
|
-
? openLocalDb(opts.company, opts)
|
|
130
|
-
: openByPath(dbPath);
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
if (!writeRequested) {
|
|
134
|
-
db.pragma("query_only = ON");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const stmt = db.prepare(sql);
|
|
138
|
-
if (stmt.reader) {
|
|
139
|
-
const rows = stmt.all() as Record<string, unknown>[];
|
|
140
|
-
const columns =
|
|
141
|
-
rows.length > 0
|
|
142
|
-
? Object.keys(rows[0] as object)
|
|
143
|
-
: stmt.columns().map((c) => c.name);
|
|
144
|
-
return {
|
|
145
|
-
columns,
|
|
146
|
-
rows,
|
|
147
|
-
changes: 0,
|
|
148
|
-
readonly: !writeRequested,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const info = stmt.run();
|
|
153
|
-
return {
|
|
154
|
-
columns: ["changes", "lastInsertRowid"],
|
|
155
|
-
rows: [
|
|
156
|
-
{
|
|
157
|
-
changes: info.changes,
|
|
158
|
-
lastInsertRowid: Number(info.lastInsertRowid),
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
changes: info.changes,
|
|
162
|
-
readonly: !writeRequested,
|
|
163
|
-
};
|
|
164
|
-
} finally {
|
|
165
|
-
db.close();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function formatSqlResult(
|
|
170
|
-
result: SqlRunResult,
|
|
171
|
-
format: "jsonl" | "table" = "jsonl",
|
|
172
|
-
): string {
|
|
173
|
-
if (format === "jsonl") {
|
|
174
|
-
if (result.rows.length === 0) {
|
|
175
|
-
return JSON.stringify({ columns: result.columns, rows: 0 });
|
|
176
|
-
}
|
|
177
|
-
return result.rows.map((r) => JSON.stringify(r)).join("\n");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const cols = result.columns;
|
|
181
|
-
if (cols.length === 0) return "(no columns)";
|
|
182
|
-
const header = cols.join("\t");
|
|
183
|
-
const body = result.rows
|
|
184
|
-
.map((r) => cols.map((c) => String(r[c] ?? "")).join("\t"))
|
|
185
|
-
.join("\n");
|
|
186
|
-
return body ? `${header}\n${body}` : header;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Test helper: company dir for isolation assertions. */
|
|
190
|
-
export function companyLocalDbDir(company: string, env?: LocalDbPathEnv): string {
|
|
191
|
-
return resolveLocalDbDir(company, env);
|
|
192
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dependency-bump smoke for @indigoai-us/hq-cloud.
|
|
3
|
-
*
|
|
4
|
-
* After moving from `file:../hq-cloud` to the published `^5.23.0`, this
|
|
5
|
-
* test pins the contract: the SDK methods + types that hq-cli's
|
|
6
|
-
* sync-mode / sync-narrow / files-browse / narrow-hint-banner commands
|
|
7
|
-
* depend on must keep resolving from the npm-published package.
|
|
8
|
-
*
|
|
9
|
-
* If a future major bump on hq-cloud removes or renames any of these, the
|
|
10
|
-
* matching hq-cli command would only fail at runtime (or at the call site's
|
|
11
|
-
* own test). Locking it here catches the regression at install time in CI.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from "vitest";
|
|
15
|
-
import * as cloud from "@indigoai-us/hq-cloud";
|
|
16
|
-
|
|
17
|
-
describe("@indigoai-us/hq-cloud — pinned methods used by hq-cli", () => {
|
|
18
|
-
it("US-004 + US-008 prep — VaultClient carries the sync-browse methods", () => {
|
|
19
|
-
const proto = cloud.VaultClient.prototype as unknown as Record<
|
|
20
|
-
string,
|
|
21
|
-
unknown
|
|
22
|
-
>;
|
|
23
|
-
expect(typeof proto.listMyExplicitGrants).toBe("function");
|
|
24
|
-
expect(typeof proto.getMembershipSyncConfig).toBe("function");
|
|
25
|
-
expect(typeof proto.setMembershipSyncConfig).toBe("function");
|
|
26
|
-
expect(typeof proto.vend).toBe("function");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("US-004 type aliases used by sync-mode + sync-narrow + files-browse compile-link", () => {
|
|
30
|
-
const _grant: cloud.ExplicitGrant = {
|
|
31
|
-
companyUid: "cmp_x",
|
|
32
|
-
path: "companies/x/",
|
|
33
|
-
permission: "read",
|
|
34
|
-
source: "person",
|
|
35
|
-
};
|
|
36
|
-
const _config: cloud.MembershipSyncConfig = {
|
|
37
|
-
membershipId: "mbr_x",
|
|
38
|
-
syncMode: "shared",
|
|
39
|
-
isDefault: false,
|
|
40
|
-
updatedAt: "2026-05-20T00:00:00Z",
|
|
41
|
-
updatedBy: "prs_x",
|
|
42
|
-
};
|
|
43
|
-
const _input: cloud.SetMembershipSyncConfigInput = { syncMode: "all" };
|
|
44
|
-
expect(_grant.source).toBe("person");
|
|
45
|
-
expect(_config.syncMode).toBe("shared");
|
|
46
|
-
expect(_input.syncMode).toBe("all");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("US-008 prep — VendPurpose + VendInput keep purpose='browse' typecheckable", () => {
|
|
50
|
-
const _vendInput: cloud.VendInput = {
|
|
51
|
-
paths: ["companies/x/"],
|
|
52
|
-
operations: "read-only",
|
|
53
|
-
purpose: "browse",
|
|
54
|
-
};
|
|
55
|
-
expect(_vendInput.purpose).toBe("browse");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("dep is the published @indigoai-us/hq-cloud, not a local file: link", async () => {
|
|
59
|
-
// The package.json of the loaded module must be the npm-published one
|
|
60
|
-
// (not file:../hq-cloud which was the dev wiring during US-006 onwards).
|
|
61
|
-
// We resolve the package.json path via Node's require resolver and read
|
|
62
|
-
// its `_resolved` (pnpm) or `version` field — both confirm origin.
|
|
63
|
-
const pkgPath = require.resolve(
|
|
64
|
-
"@indigoai-us/hq-cloud/package.json",
|
|
65
|
-
);
|
|
66
|
-
const pkg = await import(pkgPath, { with: { type: "json" } });
|
|
67
|
-
expect(pkg.default.name).toBe("@indigoai-us/hq-cloud");
|
|
68
|
-
// Must be ≥ 5.23.0 — the first published version carrying the sync-
|
|
69
|
-
// browse SDK + raw vend with purpose. A lower version means the bump
|
|
70
|
-
// never made it into the lockfile / pnpm-store.
|
|
71
|
-
const [maj, min] = pkg.default.version.split(".").map(Number);
|
|
72
|
-
expect(maj).toBeGreaterThanOrEqual(5);
|
|
73
|
-
if (maj === 5) expect(min).toBeGreaterThanOrEqual(23);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `buildNarrowPlan` (local-tree-diff.ts).
|
|
3
|
-
*
|
|
4
|
-
* Uses a tmpdir fixture (same pattern as cloud-demote.test.ts) — vitest +
|
|
5
|
-
* real fs — so the symmetric-diff + dirty-classification logic is exercised
|
|
6
|
-
* end-to-end against an actual journal + filesystem.
|
|
7
|
-
*
|
|
8
|
-
* NAMESPACE CONTRACT (the bug these tests previously encoded): files are
|
|
9
|
-
* written on disk at their hq-root-relative path (`companies/<slug>/...`),
|
|
10
|
-
* but `buildNarrowPlan` emits — and the journal + grants endpoint use —
|
|
11
|
-
* COMPANY-RELATIVE keys (`meetings/a.md`). The `key()` helper derives the
|
|
12
|
-
* company-relative form from the on-disk path so journals, prefix sets, and
|
|
13
|
-
* expectations all speak the same namespace the real server does.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
17
|
-
import * as fs from "node:fs";
|
|
18
|
-
import * as os from "node:os";
|
|
19
|
-
import * as path from "node:path";
|
|
20
|
-
import * as crypto from "node:crypto";
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
buildNarrowPlan,
|
|
24
|
-
toPosixKey,
|
|
25
|
-
formatBytes,
|
|
26
|
-
formatNarrowPlanSummary,
|
|
27
|
-
} from "./local-tree-diff.js";
|
|
28
|
-
import type { SyncJournal } from "@indigoai-us/hq-cloud";
|
|
29
|
-
|
|
30
|
-
// ── Fixture helpers ─────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
let tmpRoot: string;
|
|
33
|
-
|
|
34
|
-
function writeFile(rel: string, contents: string): { abs: string; rel: string } {
|
|
35
|
-
const abs = path.join(tmpRoot, rel);
|
|
36
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
37
|
-
fs.writeFileSync(abs, contents);
|
|
38
|
-
return { abs, rel };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Strip the `companies/<slug>/` prefix from an on-disk hq-root-relative path
|
|
43
|
-
* to get the COMPANY-RELATIVE key — the namespace `buildNarrowPlan` emits and
|
|
44
|
-
* the journal + grants endpoint use.
|
|
45
|
-
*/
|
|
46
|
-
function key(rel: string): string {
|
|
47
|
-
return rel.replace(/^companies\/[^/]+\//, "");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function sha256(s: string): string {
|
|
51
|
-
return crypto.createHash("sha256").update(s).digest("hex");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Build a journal that "knows about" the given files at their current hash +
|
|
56
|
-
* mtime. Journal keys are COMPANY-RELATIVE (via `key()`), matching the engine.
|
|
57
|
-
*/
|
|
58
|
-
function journalFromFiles(
|
|
59
|
-
files: Array<{ rel: string; contents: string; syncedAt?: string }>,
|
|
60
|
-
): SyncJournal {
|
|
61
|
-
const j: SyncJournal = {
|
|
62
|
-
version: "2",
|
|
63
|
-
lastSync: new Date().toISOString(),
|
|
64
|
-
files: {},
|
|
65
|
-
pulls: [],
|
|
66
|
-
};
|
|
67
|
-
for (const f of files) {
|
|
68
|
-
j.files[key(f.rel)] = {
|
|
69
|
-
hash: sha256(f.contents),
|
|
70
|
-
size: Buffer.byteLength(f.contents),
|
|
71
|
-
// Stamp syncedAt slightly in the future so on-disk mtime <= syncedAt
|
|
72
|
-
// (the clean path needs `mtime <= syncedAt + 1s` and mkdtemp file
|
|
73
|
-
// mtimes are essentially "now").
|
|
74
|
-
syncedAt: f.syncedAt ?? new Date(Date.now() + 5000).toISOString(),
|
|
75
|
-
direction: "down",
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
return j;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-narrow-plan-"));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// ── Happy-path partitioning ─────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
describe("toPosixKey — POSIX key normalization (Windows client regression)", () => {
|
|
92
|
-
// Regression: on a non-POSIX (Windows) client, path.relative() yields
|
|
93
|
-
// "\\"-separated paths. Emitted verbatim as a vault key, `knowledge\books-eoi.md`
|
|
94
|
-
// has no "/", so the server's listing (which splits on "/") renders it flat at
|
|
95
|
-
// the root. The narrow-plan must emit POSIX keys regardless of client OS.
|
|
96
|
-
it("converts a Windows-style nested path to a POSIX key", () => {
|
|
97
|
-
expect(toPosixKey("knowledge\\books-eoi.md")).toBe("knowledge/books-eoi.md");
|
|
98
|
-
expect(toPosixKey("data\\sub\\boots-accounts.json")).toBe(
|
|
99
|
-
"data/sub/boots-accounts.json",
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("leaves an already-POSIX key unchanged (idempotent)", () => {
|
|
104
|
-
expect(toPosixKey("meetings/2026/notes.md")).toBe("meetings/2026/notes.md");
|
|
105
|
-
expect(toPosixKey("flat.md")).toBe("flat.md");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("buildNarrowPlan — partition by prefix coverage", () => {
|
|
110
|
-
it("separates files into staying / clean / dirty buckets", () => {
|
|
111
|
-
// Staying — covered by prospective prefix.
|
|
112
|
-
const staying = writeFile(
|
|
113
|
-
"companies/acme/meetings/2026/notes.md",
|
|
114
|
-
"stays here",
|
|
115
|
-
);
|
|
116
|
-
// Clean orphan — outside prospective, matches journal.
|
|
117
|
-
const cleanOrphan = writeFile(
|
|
118
|
-
"companies/acme/scratch/old.md",
|
|
119
|
-
"clean content",
|
|
120
|
-
);
|
|
121
|
-
// Dirty orphan — outside prospective, hash mismatch.
|
|
122
|
-
const dirtyOrphan = writeFile(
|
|
123
|
-
"companies/acme/scratch/dirty.md",
|
|
124
|
-
"current local content",
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const journal = journalFromFiles([
|
|
128
|
-
{ rel: staying.rel, contents: "stays here" },
|
|
129
|
-
{ rel: cleanOrphan.rel, contents: "clean content" },
|
|
130
|
-
// Journal saw an earlier version → hash mismatch with the on-disk
|
|
131
|
-
// "current local content" → dirty.
|
|
132
|
-
{ rel: dirtyOrphan.rel, contents: "JOURNAL VERSION DIFFERENT" },
|
|
133
|
-
]);
|
|
134
|
-
|
|
135
|
-
const plan = buildNarrowPlan({
|
|
136
|
-
hqRoot: tmpRoot,
|
|
137
|
-
companySlug: "acme",
|
|
138
|
-
prospectivePrefixSet: ["meetings/"],
|
|
139
|
-
journal,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(plan.staying.map((f) => f.relPath)).toEqual([key(staying.rel)]);
|
|
143
|
-
expect(plan.clean.map((f) => f.relPath)).toEqual([key(cleanOrphan.rel)]);
|
|
144
|
-
expect(plan.dirty.map((f) => f.relPath)).toEqual([key(dirtyOrphan.rel)]);
|
|
145
|
-
expect(plan.dirty[0].reason).toBe("hash-mismatch");
|
|
146
|
-
|
|
147
|
-
expect(plan.totalStayingCount).toBe(1);
|
|
148
|
-
expect(plan.totalCleanCount).toBe(1);
|
|
149
|
-
expect(plan.totalDirtyCount).toBe(1);
|
|
150
|
-
expect(plan.totalCleanBytes).toBe(Buffer.byteLength("clean content"));
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("classifies files with no journal entry as dirty (not-in-journal)", () => {
|
|
154
|
-
const orphan = writeFile("companies/acme/scratch/local-draft.md", "never synced");
|
|
155
|
-
const plan = buildNarrowPlan({
|
|
156
|
-
hqRoot: tmpRoot,
|
|
157
|
-
companySlug: "acme",
|
|
158
|
-
prospectivePrefixSet: ["meetings/"],
|
|
159
|
-
journal: journalFromFiles([]),
|
|
160
|
-
});
|
|
161
|
-
expect(plan.dirty.map((f) => f.relPath)).toEqual([key(orphan.rel)]);
|
|
162
|
-
expect(plan.dirty[0].reason).toBe("not-in-journal");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("treats tombstoned journal entries as not-in-journal (dirty)", () => {
|
|
166
|
-
const orphan = writeFile("companies/acme/scratch/tombstoned.md", "still here");
|
|
167
|
-
const journal = journalFromFiles([
|
|
168
|
-
{ rel: orphan.rel, contents: "still here" },
|
|
169
|
-
]);
|
|
170
|
-
// Mark the entry tombstoned post-hoc.
|
|
171
|
-
journal.files[key(orphan.rel)]!.removedAt = new Date().toISOString();
|
|
172
|
-
journal.files[key(orphan.rel)]!.removedReason = "scope_shrink";
|
|
173
|
-
|
|
174
|
-
const plan = buildNarrowPlan({
|
|
175
|
-
hqRoot: tmpRoot,
|
|
176
|
-
companySlug: "acme",
|
|
177
|
-
prospectivePrefixSet: ["meetings/"],
|
|
178
|
-
journal,
|
|
179
|
-
});
|
|
180
|
-
expect(plan.dirty.map((f) => f.relPath)).toEqual([key(orphan.rel)]);
|
|
181
|
-
expect(plan.dirty[0].reason).toBe("not-in-journal");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("flags modified-after-sync via mtime past syncedAt", () => {
|
|
185
|
-
const orphan = writeFile("companies/acme/scratch/touched.md", "v1");
|
|
186
|
-
const journal = journalFromFiles([
|
|
187
|
-
{ rel: orphan.rel, contents: "v1", syncedAt: new Date(0).toISOString() },
|
|
188
|
-
]);
|
|
189
|
-
const plan = buildNarrowPlan({
|
|
190
|
-
hqRoot: tmpRoot,
|
|
191
|
-
companySlug: "acme",
|
|
192
|
-
prospectivePrefixSet: ["meetings/"],
|
|
193
|
-
journal,
|
|
194
|
-
});
|
|
195
|
-
expect(plan.dirty.map((f) => f.relPath)).toEqual([key(orphan.rel)]);
|
|
196
|
-
expect(plan.dirty[0].reason).toBe("modified-after-sync");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("handles nested prefixes (broader covers narrower) by trusting the caller-coalesced set", () => {
|
|
200
|
-
const a = writeFile("companies/acme/meetings/a.md", "a");
|
|
201
|
-
const b = writeFile("companies/acme/meetings/2026/b.md", "b");
|
|
202
|
-
const c = writeFile("companies/acme/scratch/c.md", "c");
|
|
203
|
-
|
|
204
|
-
const plan = buildNarrowPlan({
|
|
205
|
-
hqRoot: tmpRoot,
|
|
206
|
-
companySlug: "acme",
|
|
207
|
-
prospectivePrefixSet: ["meetings/"],
|
|
208
|
-
journal: journalFromFiles([
|
|
209
|
-
{ rel: a.rel, contents: "a" },
|
|
210
|
-
{ rel: b.rel, contents: "b" },
|
|
211
|
-
{ rel: c.rel, contents: "c" },
|
|
212
|
-
]),
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const stayingPaths = plan.staying.map((f) => f.relPath).sort();
|
|
216
|
-
expect(stayingPaths).toEqual([key(a.rel), key(b.rel)].sort());
|
|
217
|
-
expect(plan.clean.map((f) => f.relPath)).toEqual([key(c.rel)]);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("returns an empty plan when the company folder is missing", () => {
|
|
221
|
-
const plan = buildNarrowPlan({
|
|
222
|
-
hqRoot: tmpRoot,
|
|
223
|
-
companySlug: "ghost",
|
|
224
|
-
prospectivePrefixSet: [""],
|
|
225
|
-
journal: journalFromFiles([]),
|
|
226
|
-
});
|
|
227
|
-
expect(plan.totalStayingCount).toBe(0);
|
|
228
|
-
expect(plan.totalCleanCount).toBe(0);
|
|
229
|
-
expect(plan.totalDirtyCount).toBe(0);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("everything is clean when the prefix set is empty (entire tree is orphan)", () => {
|
|
233
|
-
const a = writeFile("companies/acme/a.md", "aa");
|
|
234
|
-
const b = writeFile("companies/acme/sub/b.md", "bb");
|
|
235
|
-
const plan = buildNarrowPlan({
|
|
236
|
-
hqRoot: tmpRoot,
|
|
237
|
-
companySlug: "acme",
|
|
238
|
-
prospectivePrefixSet: [],
|
|
239
|
-
journal: journalFromFiles([
|
|
240
|
-
{ rel: a.rel, contents: "aa" },
|
|
241
|
-
{ rel: b.rel, contents: "bb" },
|
|
242
|
-
]),
|
|
243
|
-
});
|
|
244
|
-
expect(plan.totalStayingCount).toBe(0);
|
|
245
|
-
expect(plan.totalCleanCount).toBe(2);
|
|
246
|
-
expect(plan.totalDirtyCount).toBe(0);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("everything stays when the prospective prefix covers the whole company folder", () => {
|
|
250
|
-
const a = writeFile("companies/acme/a.md", "aa");
|
|
251
|
-
const b = writeFile("companies/acme/sub/b.md", "bb");
|
|
252
|
-
const plan = buildNarrowPlan({
|
|
253
|
-
hqRoot: tmpRoot,
|
|
254
|
-
companySlug: "acme",
|
|
255
|
-
// Empty-string prefix = "covers everything" per isCoveredByAny.
|
|
256
|
-
prospectivePrefixSet: [""],
|
|
257
|
-
journal: journalFromFiles([]),
|
|
258
|
-
});
|
|
259
|
-
expect(plan.totalStayingCount).toBe(2);
|
|
260
|
-
expect(plan.totalCleanCount).toBe(0);
|
|
261
|
-
expect(plan.totalDirtyCount).toBe(0);
|
|
262
|
-
// both are staying, regardless of journal state
|
|
263
|
-
expect(
|
|
264
|
-
plan.staying.map((f) => f.relPath).sort(),
|
|
265
|
-
).toEqual([key(a.rel), key(b.rel)].sort());
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// ── Formatters ──────────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
describe("formatBytes", () => {
|
|
272
|
-
it("formats bytes in a fixed unit ladder", () => {
|
|
273
|
-
expect(formatBytes(0)).toBe("0 B");
|
|
274
|
-
expect(formatBytes(512)).toBe("512 B");
|
|
275
|
-
expect(formatBytes(2048)).toBe("2.00 KB");
|
|
276
|
-
expect(formatBytes(5 * 1024 * 1024)).toBe("5.00 MB");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("clamps negative + non-finite to 0 B", () => {
|
|
280
|
-
expect(formatBytes(-1)).toBe("0 B");
|
|
281
|
-
expect(formatBytes(Number.NaN)).toBe("0 B");
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe("formatNarrowPlanSummary", () => {
|
|
286
|
-
it("renders staying / clean / dirty counts with byte totals", () => {
|
|
287
|
-
const out = formatNarrowPlanSummary({
|
|
288
|
-
staying: [],
|
|
289
|
-
clean: [],
|
|
290
|
-
dirty: [],
|
|
291
|
-
totalStayingCount: 5,
|
|
292
|
-
totalCleanCount: 3,
|
|
293
|
-
totalCleanBytes: 2048,
|
|
294
|
-
totalDirtyCount: 1,
|
|
295
|
-
totalDirtyBytes: 1024,
|
|
296
|
-
});
|
|
297
|
-
expect(out).toContain("files staying: 5");
|
|
298
|
-
expect(out).toContain("clean orphans: 3 (2.00 KB)");
|
|
299
|
-
expect(out).toContain("dirty orphans: 1 (1.00 KB)");
|
|
300
|
-
});
|
|
301
|
-
});
|