@indigoai-us/hq-cli 5.77.14 → 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 +23 -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 +42 -2
- package/dist/commands/pkg-install.js +39 -2
- package/dist/commands/run.js +6 -2
- package/dist/commands/secrets.js +120 -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 -2256
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs-update-api-key.test.ts +0 -105
- 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 -103
- package/src/commands/pkg-install.ts +0 -221
- 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 -190
- 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 -2050
- package/src/commands/secrets.ts +0 -2249
- 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,633 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `hq sync narrow` (sync-narrow.ts).
|
|
3
|
-
*
|
|
4
|
-
* Stubs `SyncNarrowVaultClient`, `JournalIO`, and `FsIO` so we cover:
|
|
5
|
-
* - resolveNarrowTarget happy path + refuse-on-non-`all` (acceptance 5)
|
|
6
|
-
* - computeNarrowPlan: returns partitioned plan over a tmp tree
|
|
7
|
-
* - applyNarrow: deletes clean orphans + tombstones + flips sync-config
|
|
8
|
-
* - applyNarrow: dirty + force → deletes dirty too
|
|
9
|
-
* - formatDirtyConflictReport: includes paths + reasons + recovery hint
|
|
10
|
-
*
|
|
11
|
-
* The CLI Commander wiring is exercised by the existing
|
|
12
|
-
* `cloud.push-all.test.ts`-style action-level integration tests in other
|
|
13
|
-
* commands; we keep this file focused on the orchestrator units called by
|
|
14
|
-
* that action.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as os from "node:os";
|
|
20
|
-
import * as path from "node:path";
|
|
21
|
-
import * as crypto from "node:crypto";
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
applyNarrow,
|
|
25
|
-
computeNarrowPlan,
|
|
26
|
-
formatDirtyConflictReport,
|
|
27
|
-
resolveNarrowTarget,
|
|
28
|
-
type FsIO,
|
|
29
|
-
type JournalIO,
|
|
30
|
-
type SyncNarrowVaultClient,
|
|
31
|
-
} from "./sync-narrow.js";
|
|
32
|
-
import type {
|
|
33
|
-
ExplicitGrant,
|
|
34
|
-
Membership,
|
|
35
|
-
MembershipSyncConfig,
|
|
36
|
-
SyncJournal,
|
|
37
|
-
SyncMode,
|
|
38
|
-
} from "@indigoai-us/hq-cloud";
|
|
39
|
-
|
|
40
|
-
// ── Tmp dir + journal fixture ──────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
let tmpRoot: string;
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-sync-narrow-"));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
function writeFile(rel: string, contents: string): { abs: string; rel: string } {
|
|
53
|
-
const abs = path.join(tmpRoot, rel);
|
|
54
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
55
|
-
fs.writeFileSync(abs, contents);
|
|
56
|
-
return { abs, rel };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function sha256(s: string): string {
|
|
60
|
-
return crypto.createHash("sha256").update(s).digest("hex");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function journalFromFiles(
|
|
64
|
-
files: Array<{ rel: string; contents: string }>,
|
|
65
|
-
): SyncJournal {
|
|
66
|
-
const j: SyncJournal = {
|
|
67
|
-
version: "2",
|
|
68
|
-
lastSync: new Date().toISOString(),
|
|
69
|
-
files: {},
|
|
70
|
-
pulls: [],
|
|
71
|
-
};
|
|
72
|
-
for (const f of files) {
|
|
73
|
-
j.files[f.rel] = {
|
|
74
|
-
hash: sha256(f.contents),
|
|
75
|
-
size: Buffer.byteLength(f.contents),
|
|
76
|
-
syncedAt: new Date(Date.now() + 5000).toISOString(),
|
|
77
|
-
direction: "down",
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return j;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── Stub builders ───────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
function fakeMembership(overrides: Partial<Membership> = {}): Membership {
|
|
86
|
-
return {
|
|
87
|
-
membershipKey: "mbr_acme_123",
|
|
88
|
-
personUid: "prs_alice",
|
|
89
|
-
companyUid: "cmp_acme",
|
|
90
|
-
role: "member" as Membership["role"],
|
|
91
|
-
status: "active" as Membership["status"],
|
|
92
|
-
invitedBy: "prs_admin",
|
|
93
|
-
invitedAt: "2026-01-01T00:00:00Z",
|
|
94
|
-
...overrides,
|
|
95
|
-
} as Membership;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function fakeGrant(p: string): ExplicitGrant {
|
|
99
|
-
return {
|
|
100
|
-
companyUid: "cmp_acme",
|
|
101
|
-
path: p,
|
|
102
|
-
permission: "read",
|
|
103
|
-
source: "person",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface StubClientOpts {
|
|
108
|
-
memberships?: Membership[];
|
|
109
|
-
configs?: Record<string, MembershipSyncConfig>;
|
|
110
|
-
grants?: ExplicitGrant[];
|
|
111
|
-
setResult?: MembershipSyncConfig;
|
|
112
|
-
entities?: Record<string, { uid: string; slug: string; name?: string }>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function makeStubClient(opts: StubClientOpts): {
|
|
116
|
-
client: SyncNarrowVaultClient;
|
|
117
|
-
spies: {
|
|
118
|
-
setMembershipSyncConfig: ReturnType<typeof vi.fn>;
|
|
119
|
-
listMyExplicitGrants: ReturnType<typeof vi.fn>;
|
|
120
|
-
};
|
|
121
|
-
} {
|
|
122
|
-
const setMembershipSyncConfig = vi
|
|
123
|
-
.fn<
|
|
124
|
-
[string, { syncMode: SyncMode; customPaths?: string[] }],
|
|
125
|
-
Promise<MembershipSyncConfig>
|
|
126
|
-
>()
|
|
127
|
-
.mockImplementation(async (id, partial) => {
|
|
128
|
-
if (opts.setResult) return opts.setResult;
|
|
129
|
-
return {
|
|
130
|
-
membershipId: id,
|
|
131
|
-
syncMode: partial.syncMode,
|
|
132
|
-
isDefault: false,
|
|
133
|
-
updatedAt: "2026-05-20T12:00:00Z",
|
|
134
|
-
updatedBy: "prs_alice",
|
|
135
|
-
};
|
|
136
|
-
});
|
|
137
|
-
const listMyExplicitGrants = vi
|
|
138
|
-
.fn<[string], Promise<ExplicitGrant[]>>()
|
|
139
|
-
.mockResolvedValue(opts.grants ?? []);
|
|
140
|
-
|
|
141
|
-
const client: SyncNarrowVaultClient = {
|
|
142
|
-
listMyMemberships: vi
|
|
143
|
-
.fn<[], Promise<Membership[]>>()
|
|
144
|
-
.mockResolvedValue(opts.memberships ?? []),
|
|
145
|
-
getMembershipSyncConfig: vi
|
|
146
|
-
.fn<[string], Promise<MembershipSyncConfig>>()
|
|
147
|
-
.mockImplementation(async (id) => {
|
|
148
|
-
const cfg = opts.configs?.[id];
|
|
149
|
-
if (cfg) return cfg;
|
|
150
|
-
return { membershipId: id, syncMode: "shared", isDefault: true };
|
|
151
|
-
}),
|
|
152
|
-
setMembershipSyncConfig,
|
|
153
|
-
listMyExplicitGrants,
|
|
154
|
-
entity: {
|
|
155
|
-
get: vi
|
|
156
|
-
.fn<[string], Promise<{ uid: string; slug: string; name?: string }>>()
|
|
157
|
-
.mockImplementation(async (uid) => {
|
|
158
|
-
const e = opts.entities?.[uid];
|
|
159
|
-
if (!e) throw new Error(`unknown entity ${uid}`);
|
|
160
|
-
return e;
|
|
161
|
-
}),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
client,
|
|
167
|
-
spies: { setMembershipSyncConfig, listMyExplicitGrants },
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function makeStubJournalIO(initial?: SyncJournal): {
|
|
172
|
-
io: JournalIO;
|
|
173
|
-
reads: string[];
|
|
174
|
-
writes: Array<{ slug: string; journal: SyncJournal }>;
|
|
175
|
-
current(): SyncJournal | undefined;
|
|
176
|
-
} {
|
|
177
|
-
let state: SyncJournal | undefined = initial;
|
|
178
|
-
const reads: string[] = [];
|
|
179
|
-
const writes: Array<{ slug: string; journal: SyncJournal }> = [];
|
|
180
|
-
return {
|
|
181
|
-
io: {
|
|
182
|
-
read(slug) {
|
|
183
|
-
reads.push(slug);
|
|
184
|
-
if (!state) {
|
|
185
|
-
return { version: "2", lastSync: "", files: {}, pulls: [] };
|
|
186
|
-
}
|
|
187
|
-
// Return a deep clone so the caller mutating it doesn't change our seed.
|
|
188
|
-
return JSON.parse(JSON.stringify(state)) as SyncJournal;
|
|
189
|
-
},
|
|
190
|
-
write(slug, journal) {
|
|
191
|
-
writes.push({ slug, journal });
|
|
192
|
-
state = JSON.parse(JSON.stringify(journal)) as SyncJournal;
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
reads,
|
|
196
|
-
writes,
|
|
197
|
-
current: () => state,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function makeStubFs(): { io: FsIO; deletes: string[] } {
|
|
202
|
-
const deletes: string[] = [];
|
|
203
|
-
return {
|
|
204
|
-
io: {
|
|
205
|
-
unlinkSync(p) {
|
|
206
|
-
// Actually delete from tmpRoot so subsequent reads are honest.
|
|
207
|
-
deletes.push(p);
|
|
208
|
-
try {
|
|
209
|
-
fs.unlinkSync(p);
|
|
210
|
-
} catch (err) {
|
|
211
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
212
|
-
if (code !== "ENOENT") throw err;
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
deletes,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── resolveNarrowTarget ─────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
describe("resolveNarrowTarget", () => {
|
|
223
|
-
it("resolves membership + current sync-config for the slug", async () => {
|
|
224
|
-
const { client } = makeStubClient({
|
|
225
|
-
memberships: [fakeMembership()],
|
|
226
|
-
configs: {
|
|
227
|
-
mbr_acme_123: {
|
|
228
|
-
membershipId: "mbr_acme_123",
|
|
229
|
-
syncMode: "all",
|
|
230
|
-
isDefault: false,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
entities: { cmp_acme: { uid: "cmp_acme", slug: "acme", name: "Acme" } },
|
|
234
|
-
});
|
|
235
|
-
const result = await resolveNarrowTarget({
|
|
236
|
-
companySlug: "acme",
|
|
237
|
-
vaultClient: client,
|
|
238
|
-
});
|
|
239
|
-
expect(result.membership.membershipKey).toBe("mbr_acme_123");
|
|
240
|
-
expect(result.companyUid).toBe("cmp_acme");
|
|
241
|
-
expect(result.syncConfig.syncMode).toBe("all");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("refuses on already-shared memberships (acceptance 5)", async () => {
|
|
245
|
-
const { client } = makeStubClient({
|
|
246
|
-
memberships: [fakeMembership()],
|
|
247
|
-
configs: {
|
|
248
|
-
mbr_acme_123: {
|
|
249
|
-
membershipId: "mbr_acme_123",
|
|
250
|
-
syncMode: "shared",
|
|
251
|
-
isDefault: false,
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
entities: { cmp_acme: { uid: "cmp_acme", slug: "acme" } },
|
|
255
|
-
});
|
|
256
|
-
await expect(
|
|
257
|
-
resolveNarrowTarget({ companySlug: "acme", vaultClient: client }),
|
|
258
|
-
).rejects.toThrow(/Refusing to narrow.*syncMode='shared'/);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("refuses on custom memberships (acceptance 5)", async () => {
|
|
262
|
-
const { client } = makeStubClient({
|
|
263
|
-
memberships: [fakeMembership()],
|
|
264
|
-
configs: {
|
|
265
|
-
mbr_acme_123: {
|
|
266
|
-
membershipId: "mbr_acme_123",
|
|
267
|
-
syncMode: "custom",
|
|
268
|
-
customPaths: ["companies/acme/x/"],
|
|
269
|
-
isDefault: false,
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
entities: { cmp_acme: { uid: "cmp_acme", slug: "acme" } },
|
|
273
|
-
});
|
|
274
|
-
await expect(
|
|
275
|
-
resolveNarrowTarget({ companySlug: "acme", vaultClient: client }),
|
|
276
|
-
).rejects.toThrow(/syncMode='custom'/);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("errors out when the slug doesn't match any membership", async () => {
|
|
280
|
-
const { client } = makeStubClient({
|
|
281
|
-
memberships: [fakeMembership()],
|
|
282
|
-
entities: { cmp_acme: { uid: "cmp_acme", slug: "acme" } },
|
|
283
|
-
});
|
|
284
|
-
await expect(
|
|
285
|
-
resolveNarrowTarget({ companySlug: "ghost", vaultClient: client }),
|
|
286
|
-
).rejects.toThrow(/No membership found for company 'ghost'.*acme/);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// ── computeNarrowPlan ───────────────────────────────────────────────────────
|
|
291
|
-
|
|
292
|
-
describe("computeNarrowPlan", () => {
|
|
293
|
-
it("coalesces grants and returns a partitioned plan", async () => {
|
|
294
|
-
// Files live on disk at their hq-root-relative path…
|
|
295
|
-
writeFile("companies/acme/meetings/notes.md", "stays");
|
|
296
|
-
writeFile("companies/acme/scratch/old.md", "clean orphan");
|
|
297
|
-
|
|
298
|
-
// …but the journal keys + grant paths are COMPANY-RELATIVE — the
|
|
299
|
-
// namespace the real server + engine use. (The old fixtures used
|
|
300
|
-
// full `companies/acme/...` grant paths, which never matched the
|
|
301
|
-
// company-relative keys buildNarrowPlan emits — masking the namespace
|
|
302
|
-
// bug this test now guards against.)
|
|
303
|
-
const journal = journalFromFiles([
|
|
304
|
-
{ rel: "meetings/notes.md", contents: "stays" },
|
|
305
|
-
{ rel: "scratch/old.md", contents: "clean orphan" },
|
|
306
|
-
]);
|
|
307
|
-
|
|
308
|
-
const { client } = makeStubClient({
|
|
309
|
-
grants: [
|
|
310
|
-
fakeGrant("meetings/"),
|
|
311
|
-
fakeGrant("meetings/2026/"), // collapsed by coalesce
|
|
312
|
-
],
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
const journalIO = makeStubJournalIO(journal);
|
|
316
|
-
|
|
317
|
-
const result = await computeNarrowPlan({
|
|
318
|
-
hqRoot: tmpRoot,
|
|
319
|
-
companySlug: "acme",
|
|
320
|
-
companyUid: "cmp_acme",
|
|
321
|
-
vaultClient: client,
|
|
322
|
-
journalIO: journalIO.io,
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
expect(result.prospectivePrefixSet).toEqual(["meetings/"]);
|
|
326
|
-
expect(result.plan.totalStayingCount).toBe(1);
|
|
327
|
-
expect(result.plan.totalCleanCount).toBe(1);
|
|
328
|
-
expect(result.plan.totalDirtyCount).toBe(0);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("normalizes real-world anchored + glob grant paths (grantPathToPrefix)", async () => {
|
|
332
|
-
writeFile("companies/acme/design-pack/logo.svg", "svg");
|
|
333
|
-
writeFile("companies/acme/scratch/old.md", "clean orphan");
|
|
334
|
-
|
|
335
|
-
const journal = journalFromFiles([
|
|
336
|
-
{ rel: "design-pack/logo.svg", contents: "svg" },
|
|
337
|
-
{ rel: "scratch/old.md", contents: "clean orphan" },
|
|
338
|
-
]);
|
|
339
|
-
|
|
340
|
-
// The exact messy shapes the live vault returns: full-anchored + glob,
|
|
341
|
-
// and slug-anchored + glob — neither startsWith-matches the
|
|
342
|
-
// company-relative local keys until grantPathToPrefix de-anchors them.
|
|
343
|
-
const { client } = makeStubClient({
|
|
344
|
-
grants: [
|
|
345
|
-
fakeGrant("companies/acme/design-pack/*"),
|
|
346
|
-
fakeGrant("acme/design-pack/2026/*"), // subsumed after normalize+coalesce
|
|
347
|
-
],
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
const result = await computeNarrowPlan({
|
|
351
|
-
hqRoot: tmpRoot,
|
|
352
|
-
companySlug: "acme",
|
|
353
|
-
companyUid: "cmp_acme",
|
|
354
|
-
vaultClient: client,
|
|
355
|
-
journalIO: makeStubJournalIO(journal).io,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
expect(result.prospectivePrefixSet).toEqual(["design-pack/"]);
|
|
359
|
-
// design-pack/logo.svg stays (covered); scratch/old.md is a clean orphan.
|
|
360
|
-
expect(result.plan.totalStayingCount).toBe(1);
|
|
361
|
-
expect(result.plan.totalCleanCount).toBe(1);
|
|
362
|
-
expect(result.plan.totalDirtyCount).toBe(0);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("a wildcard '*' grant keeps everything (no orphans)", async () => {
|
|
366
|
-
writeFile("companies/acme/a.md", "a");
|
|
367
|
-
writeFile("companies/acme/sub/b.md", "b");
|
|
368
|
-
|
|
369
|
-
const { client } = makeStubClient({ grants: [fakeGrant("*")] });
|
|
370
|
-
|
|
371
|
-
const result = await computeNarrowPlan({
|
|
372
|
-
hqRoot: tmpRoot,
|
|
373
|
-
companySlug: "acme",
|
|
374
|
-
companyUid: "cmp_acme",
|
|
375
|
-
vaultClient: client,
|
|
376
|
-
journalIO: makeStubJournalIO(journalFromFiles([])).io,
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// "*" → "" → guarded to [""] (covers everything) so narrowing keeps the
|
|
380
|
-
// whole tree rather than collapsing to "nothing" and proposing deletes.
|
|
381
|
-
expect(result.prospectivePrefixSet).toEqual([""]);
|
|
382
|
-
expect(result.plan.totalCleanCount).toBe(0);
|
|
383
|
-
expect(result.plan.totalDirtyCount).toBe(0);
|
|
384
|
-
expect(result.plan.totalStayingCount).toBe(2);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// ── applyNarrow ─────────────────────────────────────────────────────────────
|
|
389
|
-
|
|
390
|
-
describe("applyNarrow", () => {
|
|
391
|
-
it("deletes clean orphans, tombstones the journal, and flips sync-config", async () => {
|
|
392
|
-
const staying = writeFile("companies/acme/meetings/keep.md", "keep");
|
|
393
|
-
const orphan = writeFile("companies/acme/scratch/drop.md", "drop me");
|
|
394
|
-
|
|
395
|
-
const journal = journalFromFiles([
|
|
396
|
-
{ rel: staying.rel, contents: "keep" },
|
|
397
|
-
{ rel: orphan.rel, contents: "drop me" },
|
|
398
|
-
]);
|
|
399
|
-
|
|
400
|
-
const { client, spies } = makeStubClient({});
|
|
401
|
-
const journalIO = makeStubJournalIO();
|
|
402
|
-
const fsIO = makeStubFs();
|
|
403
|
-
|
|
404
|
-
const result = await applyNarrow({
|
|
405
|
-
companySlug: "acme",
|
|
406
|
-
membershipId: "mbr_acme_123",
|
|
407
|
-
plan: {
|
|
408
|
-
staying: [{ relPath: staying.rel, absPath: staying.abs, bytes: 4 }],
|
|
409
|
-
clean: [{ relPath: orphan.rel, absPath: orphan.abs, bytes: 7 }],
|
|
410
|
-
dirty: [],
|
|
411
|
-
totalStayingCount: 1,
|
|
412
|
-
totalCleanCount: 1,
|
|
413
|
-
totalCleanBytes: 7,
|
|
414
|
-
totalDirtyCount: 0,
|
|
415
|
-
totalDirtyBytes: 0,
|
|
416
|
-
},
|
|
417
|
-
journal,
|
|
418
|
-
vaultClient: client,
|
|
419
|
-
force: false,
|
|
420
|
-
journalIO: journalIO.io,
|
|
421
|
-
fsIO: fsIO.io,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
expect(result.cleanDeleted).toBe(1);
|
|
425
|
-
expect(result.dirtyDeleted).toBe(0);
|
|
426
|
-
expect(result.tombstoned).toBe(1);
|
|
427
|
-
expect(result.cleanBytes).toBe(7);
|
|
428
|
-
expect(fsIO.deletes).toEqual([orphan.abs]);
|
|
429
|
-
// file is actually gone on disk
|
|
430
|
-
expect(fs.existsSync(orphan.abs)).toBe(false);
|
|
431
|
-
// staying file untouched
|
|
432
|
-
expect(fs.existsSync(staying.abs)).toBe(true);
|
|
433
|
-
|
|
434
|
-
// Journal write contains tombstone marker on the orphan path.
|
|
435
|
-
expect(journalIO.writes).toHaveLength(1);
|
|
436
|
-
const written = journalIO.writes[0]!.journal;
|
|
437
|
-
expect(written.files[orphan.rel]?.removedAt).toBeTypeOf("string");
|
|
438
|
-
expect(written.files[orphan.rel]?.removedReason).toBe("narrow_apply");
|
|
439
|
-
// staying file's journal entry is left untouched (no tombstone).
|
|
440
|
-
expect(written.files[staying.rel]?.removedAt).toBeUndefined();
|
|
441
|
-
|
|
442
|
-
// sync-config PUT was called with shared.
|
|
443
|
-
expect(spies.setMembershipSyncConfig).toHaveBeenCalledWith("mbr_acme_123", {
|
|
444
|
-
syncMode: "shared",
|
|
445
|
-
});
|
|
446
|
-
expect(result.newConfig.syncMode).toBe("shared");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("aborts when dirty files present and --force NOT set (caller-level — applyNarrow trusts plan)", async () => {
|
|
450
|
-
// applyNarrow itself doesn't gate on --force; the CLI does. This test
|
|
451
|
-
// pins the contract that applyNarrow ALSO doesn't accidentally delete
|
|
452
|
-
// dirty files when `force: false`.
|
|
453
|
-
const orphan = writeFile("companies/acme/scratch/dirty.md", "current");
|
|
454
|
-
const journal = journalFromFiles([
|
|
455
|
-
{ rel: orphan.rel, contents: "different — journal version" },
|
|
456
|
-
]);
|
|
457
|
-
|
|
458
|
-
const { client } = makeStubClient({});
|
|
459
|
-
const journalIO = makeStubJournalIO();
|
|
460
|
-
const fsIO = makeStubFs();
|
|
461
|
-
|
|
462
|
-
const result = await applyNarrow({
|
|
463
|
-
companySlug: "acme",
|
|
464
|
-
membershipId: "mbr_acme_123",
|
|
465
|
-
plan: {
|
|
466
|
-
staying: [],
|
|
467
|
-
clean: [],
|
|
468
|
-
dirty: [
|
|
469
|
-
{
|
|
470
|
-
relPath: orphan.rel,
|
|
471
|
-
absPath: orphan.abs,
|
|
472
|
-
bytes: 7,
|
|
473
|
-
reason: "hash-mismatch",
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
totalStayingCount: 0,
|
|
477
|
-
totalCleanCount: 0,
|
|
478
|
-
totalCleanBytes: 0,
|
|
479
|
-
totalDirtyCount: 1,
|
|
480
|
-
totalDirtyBytes: 7,
|
|
481
|
-
},
|
|
482
|
-
journal,
|
|
483
|
-
vaultClient: client,
|
|
484
|
-
force: false,
|
|
485
|
-
journalIO: journalIO.io,
|
|
486
|
-
fsIO: fsIO.io,
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
expect(result.dirtyDeleted).toBe(0);
|
|
490
|
-
expect(result.cleanDeleted).toBe(0);
|
|
491
|
-
expect(fs.existsSync(orphan.abs)).toBe(true);
|
|
492
|
-
// sync-config still flipped — the CLI is the one that bails out before
|
|
493
|
-
// calling applyNarrow when dirty + !force. If the CLI invariant ever
|
|
494
|
-
// weakens, this test reminds us that applyNarrow is the wrong layer
|
|
495
|
-
// to enforce the gate.
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it("with --force, deletes dirty files too and tombstones journal entries", async () => {
|
|
499
|
-
const orphan = writeFile("companies/acme/scratch/dirty.md", "current");
|
|
500
|
-
const journal = journalFromFiles([
|
|
501
|
-
{ rel: orphan.rel, contents: "journal version" },
|
|
502
|
-
]);
|
|
503
|
-
|
|
504
|
-
const { client } = makeStubClient({});
|
|
505
|
-
const journalIO = makeStubJournalIO();
|
|
506
|
-
const fsIO = makeStubFs();
|
|
507
|
-
|
|
508
|
-
const result = await applyNarrow({
|
|
509
|
-
companySlug: "acme",
|
|
510
|
-
membershipId: "mbr_acme_123",
|
|
511
|
-
plan: {
|
|
512
|
-
staying: [],
|
|
513
|
-
clean: [],
|
|
514
|
-
dirty: [
|
|
515
|
-
{
|
|
516
|
-
relPath: orphan.rel,
|
|
517
|
-
absPath: orphan.abs,
|
|
518
|
-
bytes: 7,
|
|
519
|
-
reason: "hash-mismatch",
|
|
520
|
-
},
|
|
521
|
-
],
|
|
522
|
-
totalStayingCount: 0,
|
|
523
|
-
totalCleanCount: 0,
|
|
524
|
-
totalCleanBytes: 0,
|
|
525
|
-
totalDirtyCount: 1,
|
|
526
|
-
totalDirtyBytes: 7,
|
|
527
|
-
},
|
|
528
|
-
journal,
|
|
529
|
-
vaultClient: client,
|
|
530
|
-
force: true,
|
|
531
|
-
journalIO: journalIO.io,
|
|
532
|
-
fsIO: fsIO.io,
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
expect(result.dirtyDeleted).toBe(1);
|
|
536
|
-
expect(result.dirtyBytes).toBe(7);
|
|
537
|
-
expect(result.tombstoned).toBe(1);
|
|
538
|
-
expect(fs.existsSync(orphan.abs)).toBe(false);
|
|
539
|
-
expect(
|
|
540
|
-
journalIO.writes[0]!.journal.files[orphan.rel]?.removedAt,
|
|
541
|
-
).toBeTypeOf("string");
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it("with --force on a not-in-journal dirty file: deletes the file, no tombstone (nothing to tombstone)", async () => {
|
|
545
|
-
const orphan = writeFile(
|
|
546
|
-
"companies/acme/scratch/unjournaled.md",
|
|
547
|
-
"never tracked",
|
|
548
|
-
);
|
|
549
|
-
const journal: SyncJournal = {
|
|
550
|
-
version: "2",
|
|
551
|
-
lastSync: "",
|
|
552
|
-
files: {},
|
|
553
|
-
pulls: [],
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
const { client } = makeStubClient({});
|
|
557
|
-
const journalIO = makeStubJournalIO();
|
|
558
|
-
const fsIO = makeStubFs();
|
|
559
|
-
|
|
560
|
-
const result = await applyNarrow({
|
|
561
|
-
companySlug: "acme",
|
|
562
|
-
membershipId: "mbr_acme_123",
|
|
563
|
-
plan: {
|
|
564
|
-
staying: [],
|
|
565
|
-
clean: [],
|
|
566
|
-
dirty: [
|
|
567
|
-
{
|
|
568
|
-
relPath: orphan.rel,
|
|
569
|
-
absPath: orphan.abs,
|
|
570
|
-
bytes: 13,
|
|
571
|
-
reason: "not-in-journal",
|
|
572
|
-
},
|
|
573
|
-
],
|
|
574
|
-
totalStayingCount: 0,
|
|
575
|
-
totalCleanCount: 0,
|
|
576
|
-
totalCleanBytes: 0,
|
|
577
|
-
totalDirtyCount: 1,
|
|
578
|
-
totalDirtyBytes: 13,
|
|
579
|
-
},
|
|
580
|
-
journal,
|
|
581
|
-
vaultClient: client,
|
|
582
|
-
force: true,
|
|
583
|
-
journalIO: journalIO.io,
|
|
584
|
-
fsIO: fsIO.io,
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
expect(result.dirtyDeleted).toBe(1);
|
|
588
|
-
// No journal entry existed, so tombstoned count stays at 0 for this orphan.
|
|
589
|
-
expect(result.tombstoned).toBe(0);
|
|
590
|
-
expect(fs.existsSync(orphan.abs)).toBe(false);
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// ── formatDirtyConflictReport ───────────────────────────────────────────────
|
|
595
|
-
|
|
596
|
-
describe("formatDirtyConflictReport", () => {
|
|
597
|
-
it("renders each dirty file with reason and a recovery hint", () => {
|
|
598
|
-
const out = formatDirtyConflictReport(
|
|
599
|
-
[
|
|
600
|
-
{
|
|
601
|
-
relPath: "companies/acme/scratch/x.md",
|
|
602
|
-
absPath: "/tmp/x.md",
|
|
603
|
-
bytes: 1024,
|
|
604
|
-
reason: "hash-mismatch",
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
relPath: "companies/acme/scratch/y.md",
|
|
608
|
-
absPath: "/tmp/y.md",
|
|
609
|
-
bytes: 2048,
|
|
610
|
-
reason: "modified-after-sync",
|
|
611
|
-
},
|
|
612
|
-
],
|
|
613
|
-
"acme",
|
|
614
|
-
);
|
|
615
|
-
expect(out).toContain("Aborting narrow for 'acme'");
|
|
616
|
-
expect(out).toContain("companies/acme/scratch/x.md");
|
|
617
|
-
expect(out).toContain("hash-mismatch");
|
|
618
|
-
expect(out).toContain("companies/acme/scratch/y.md");
|
|
619
|
-
expect(out).toContain("modified-after-sync");
|
|
620
|
-
expect(out).toContain("--force");
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it("caps the per-file listing at 25 with a 'more' suffix", () => {
|
|
624
|
-
const dirty = Array.from({ length: 30 }, (_, i) => ({
|
|
625
|
-
relPath: `companies/acme/scratch/${i}.md`,
|
|
626
|
-
absPath: `/tmp/${i}.md`,
|
|
627
|
-
bytes: 10,
|
|
628
|
-
reason: "hash-mismatch" as const,
|
|
629
|
-
}));
|
|
630
|
-
const out = formatDirtyConflictReport(dirty, "acme");
|
|
631
|
-
expect(out).toContain("... 5 more");
|
|
632
|
-
});
|
|
633
|
-
});
|