@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,650 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* US-004 — `hq publish` packer + authenticated upload.
|
|
3
|
-
*
|
|
4
|
-
* All network is mocked (deps.upload / deps.getAccessToken) and all filesystem
|
|
5
|
-
* writes land in per-test tmp dirs — these tests never hit the real API and
|
|
6
|
-
* never mutate the working tree.
|
|
7
|
-
*
|
|
8
|
-
* E2E behaviors covered:
|
|
9
|
-
* - Logged-in verified creator + valid skill dir → pending_review listing id.
|
|
10
|
-
* - Not-logged-in user → errors with a login prompt, uploads nothing.
|
|
11
|
-
* - Invalid package.yaml → validation errors, uploads nothing.
|
|
12
|
-
* - Re-publishing the same version (409) → clear duplicate error.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
-
import * as fs from 'node:fs';
|
|
17
|
-
import * as os from 'node:os';
|
|
18
|
-
import * as path from 'node:path';
|
|
19
|
-
import * as yaml from 'js-yaml';
|
|
20
|
-
|
|
21
|
-
import * as zlib from 'node:zlib';
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
peekIdToken,
|
|
25
|
-
resolveAuthor,
|
|
26
|
-
stampAuthorYaml,
|
|
27
|
-
upsertPackProvenance,
|
|
28
|
-
buildListingNotice,
|
|
29
|
-
unwrapListing,
|
|
30
|
-
inferListingType,
|
|
31
|
-
deriveSlug,
|
|
32
|
-
summarizeContributes,
|
|
33
|
-
runPublish,
|
|
34
|
-
type PublishDeps,
|
|
35
|
-
type ListingRequest,
|
|
36
|
-
} from './publish.js';
|
|
37
|
-
import { readManifest } from '../utils/manifest.js';
|
|
38
|
-
import type { ModulesManifest, PackManifest, PackModuleDefinition } from '../types.js';
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Fixtures
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/** Build a base64url-encoded JWT-shaped string with the given payload claims. */
|
|
45
|
-
function fakeIdToken(claims: Record<string, unknown>): string {
|
|
46
|
-
const b64 = (o: unknown) =>
|
|
47
|
-
Buffer.from(JSON.stringify(o))
|
|
48
|
-
.toString('base64')
|
|
49
|
-
.replace(/\+/g, '-')
|
|
50
|
-
.replace(/\//g, '_')
|
|
51
|
-
.replace(/=+$/, '');
|
|
52
|
-
return `${b64({ alg: 'none' })}.${b64(claims)}.sig`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const VALID_MANIFEST = {
|
|
56
|
-
name: 'hq-pack-demo',
|
|
57
|
-
version: '1.0.0',
|
|
58
|
-
publisher: '@indigo',
|
|
59
|
-
access: 'public',
|
|
60
|
-
requires: { hqCore: '>=1.0.0' },
|
|
61
|
-
contributes: { skills: ['demo'] },
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** Write a valid skill pack dir under `root` and return its path. */
|
|
65
|
-
function writeValidPack(root: string, manifest: Record<string, unknown> = VALID_MANIFEST): string {
|
|
66
|
-
const dir = path.join(root, 'pack');
|
|
67
|
-
fs.mkdirSync(path.join(dir, 'skills', 'demo'), { recursive: true });
|
|
68
|
-
fs.writeFileSync(path.join(dir, 'skills', 'demo', 'SKILL.md'), '# demo skill\n');
|
|
69
|
-
fs.writeFileSync(path.join(dir, 'package.yaml'), yaml.dump(manifest));
|
|
70
|
-
return dir;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let tmpRoot: string;
|
|
74
|
-
let hqRoot: string;
|
|
75
|
-
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hq-publish-test-'));
|
|
78
|
-
// A minimal HQ root so recordProvenance has somewhere to write modules.yaml.
|
|
79
|
-
hqRoot = path.join(tmpRoot, 'hq');
|
|
80
|
-
fs.mkdirSync(hqRoot, { recursive: true });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
afterEach(() => {
|
|
84
|
-
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
85
|
-
vi.restoreAllMocks();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// Pure helpers
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
describe('peekIdToken', () => {
|
|
93
|
-
it('decodes sub + email from a JWT-shaped token', () => {
|
|
94
|
-
const tok = fakeIdToken({ sub: 'usr_123', email: 'a@b.com', name: 'Ada' });
|
|
95
|
-
expect(peekIdToken(tok)).toMatchObject({ sub: 'usr_123', email: 'a@b.com', name: 'Ada' });
|
|
96
|
-
});
|
|
97
|
-
it('returns {} on a malformed token', () => {
|
|
98
|
-
expect(peekIdToken('garbage')).toEqual({});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('resolveAuthor', () => {
|
|
103
|
-
it('uses sub as uid and derives handle/displayName', () => {
|
|
104
|
-
const a = resolveAuthor({ sub: 'usr_1', email: 'ada@b.com', name: 'Ada Lovelace' });
|
|
105
|
-
expect(a).toEqual({ uid: 'usr_1', handle: 'ada', displayName: 'Ada Lovelace' });
|
|
106
|
-
});
|
|
107
|
-
it('prefers preferred_username for the handle', () => {
|
|
108
|
-
const a = resolveAuthor({ sub: 'usr_1', preferred_username: 'countess', email: 'ada@b.com' });
|
|
109
|
-
expect(a.handle).toBe('countess');
|
|
110
|
-
});
|
|
111
|
-
it('throws when there is no sub (treated as logged out)', () => {
|
|
112
|
-
expect(() => resolveAuthor({ email: 'a@b.com' })).toThrow(/sub/);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('stampAuthorYaml', () => {
|
|
117
|
-
it('adds an author block that round-trips through YAML', () => {
|
|
118
|
-
const out = stampAuthorYaml(
|
|
119
|
-
{ ...VALID_MANIFEST },
|
|
120
|
-
{ uid: 'usr_1', handle: 'ada', displayName: 'Ada' },
|
|
121
|
-
);
|
|
122
|
-
const parsed = yaml.load(out) as Record<string, unknown>;
|
|
123
|
-
expect(parsed.author).toEqual({ uid: 'usr_1', handle: 'ada', displayName: 'Ada' });
|
|
124
|
-
expect(parsed.name).toBe('hq-pack-demo');
|
|
125
|
-
});
|
|
126
|
-
it('overwrites a pre-existing author with the publisher identity', () => {
|
|
127
|
-
const out = stampAuthorYaml(
|
|
128
|
-
{ ...VALID_MANIFEST, author: { uid: 'someone-else', handle: 'x', displayName: 'X' } },
|
|
129
|
-
{ uid: 'usr_1', handle: 'ada', displayName: 'Ada' },
|
|
130
|
-
);
|
|
131
|
-
const parsed = yaml.load(out) as { author: { uid: string } };
|
|
132
|
-
expect(parsed.author.uid).toBe('usr_1');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('upsertPackProvenance', () => {
|
|
137
|
-
const entry: PackModuleDefinition = {
|
|
138
|
-
name: 'hq-pack-demo',
|
|
139
|
-
strategy: 'package',
|
|
140
|
-
source: '/abs/pack',
|
|
141
|
-
version: '1.0.0',
|
|
142
|
-
access: 'public',
|
|
143
|
-
};
|
|
144
|
-
it('appends a new entry when none exists', () => {
|
|
145
|
-
const out = upsertPackProvenance(null, entry);
|
|
146
|
-
expect(out.modules).toHaveLength(1);
|
|
147
|
-
expect(out.modules[0]).toEqual(entry);
|
|
148
|
-
});
|
|
149
|
-
it('replaces an existing entry by name (no duplicate)', () => {
|
|
150
|
-
const initial: ModulesManifest = { version: '1', modules: [entry] };
|
|
151
|
-
const updated = upsertPackProvenance(initial, { ...entry, version: '2.0.0' });
|
|
152
|
-
expect(updated.modules).toHaveLength(1);
|
|
153
|
-
expect((updated.modules[0] as PackModuleDefinition).version).toBe('2.0.0');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('buildListingNotice', () => {
|
|
158
|
-
it('formats a pending_review success notice with the listing id', () => {
|
|
159
|
-
const notice = buildListingNotice(201, { id: 'lst_42', status: 'pending_review' }, 'hq-pack-demo', '1.0.0');
|
|
160
|
-
expect(notice).toContain('lst_42');
|
|
161
|
-
expect(notice).toContain('pending_review');
|
|
162
|
-
});
|
|
163
|
-
it('surfaces a 409 as a clear duplicate-version error', () => {
|
|
164
|
-
expect(() => buildListingNotice(409, {}, 'hq-pack-demo', '1.0.0')).toThrow(/already published|duplicate/i);
|
|
165
|
-
});
|
|
166
|
-
it('maps 401/403 to an auth error', () => {
|
|
167
|
-
expect(() => buildListingNotice(401, {}, 'hq-pack-demo', '1.0.0')).toThrow(/Not authorized|login/i);
|
|
168
|
-
});
|
|
169
|
-
it('maps a 403 NOT_VERIFIED_CREATOR to the apply guidance', () => {
|
|
170
|
-
expect(() =>
|
|
171
|
-
buildListingNotice(403, { code: 'NOT_VERIFIED_CREATOR' }, 'hq-pack-demo', '1.0.0'),
|
|
172
|
-
).toThrow(/hq creators apply/i);
|
|
173
|
-
});
|
|
174
|
-
it('maps a 400 ON_BEHALF_DISABLED to a clear "not enabled" message', () => {
|
|
175
|
-
expect(() =>
|
|
176
|
-
buildListingNotice(400, { code: 'ON_BEHALF_DISABLED' }, 'hq-pack-demo', '1.0.0'),
|
|
177
|
-
).toThrow(/on behalf|not enabled/i);
|
|
178
|
-
});
|
|
179
|
-
it('maps a 403 staff-gate (FORBIDDEN) on an --as publish to a "staff only" message', () => {
|
|
180
|
-
expect(() =>
|
|
181
|
-
buildListingNotice(403, { code: 'FORBIDDEN' }, 'hq-pack-demo', '1.0.0'),
|
|
182
|
-
).toThrow(/HQ staff|on behalf/i);
|
|
183
|
-
});
|
|
184
|
-
it('surfaces a generic server error body', () => {
|
|
185
|
-
expect(() => buildListingNotice(500, { error: 'boom' }, 'hq-pack-demo', '1.0.0')).toThrow(/boom/);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('uses the SERVER-resolved creatorHandle for attribution (Bug 2)', () => {
|
|
189
|
-
const notice = buildListingNotice(
|
|
190
|
-
201,
|
|
191
|
-
{ id: 'lst_99', status: 'pending_review', creatorHandle: 'ada-lovelace' },
|
|
192
|
-
'hq-pack-demo',
|
|
193
|
-
'1.0.0',
|
|
194
|
-
);
|
|
195
|
-
expect(notice).toContain('lst_99');
|
|
196
|
-
expect(notice).toContain('Attributed to @ada-lovelace.');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('omits the attribution clause when the server returns no creatorHandle', () => {
|
|
200
|
-
const notice = buildListingNotice(201, { id: 'lst_99' }, 'hq-pack-demo', '1.0.0');
|
|
201
|
-
expect(notice).toContain('lst_99');
|
|
202
|
-
expect(notice).not.toMatch(/Attributed to/);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('unwraps the real server { listing } envelope → real id + status + @handle (prod bug)', () => {
|
|
206
|
-
// This is the EXACT shape prod returns: a `{ listing: { listingId,
|
|
207
|
-
// creatorHandle, status } }` envelope. Reading the top level (the old
|
|
208
|
-
// behavior) produced "(unknown id)" + a local federated handle.
|
|
209
|
-
const notice = buildListingNotice(
|
|
210
|
-
201,
|
|
211
|
-
{
|
|
212
|
-
listing: {
|
|
213
|
-
listingId: 'lst_prod_123',
|
|
214
|
-
creatorHandle: 'corey',
|
|
215
|
-
status: 'pending_review',
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
'hq-pack-demo',
|
|
219
|
-
'1.0.0',
|
|
220
|
-
);
|
|
221
|
-
expect(notice).toContain('lst_prod_123');
|
|
222
|
-
expect(notice).toContain('pending_review');
|
|
223
|
-
expect(notice).toContain('Attributed to @corey.');
|
|
224
|
-
expect(notice).not.toContain('(unknown id)');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('prefers listingId over id inside a wrapped envelope', () => {
|
|
228
|
-
const notice = buildListingNotice(
|
|
229
|
-
201,
|
|
230
|
-
{ listing: { listingId: 'lst_real', id: 'lst_legacy', status: 'pending_review' } },
|
|
231
|
-
'hq-pack-demo',
|
|
232
|
-
'1.0.0',
|
|
233
|
-
);
|
|
234
|
-
expect(notice).toContain('lst_real');
|
|
235
|
-
expect(notice).not.toContain('lst_legacy');
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('unwrapListing', () => {
|
|
240
|
-
it('reads listing fields out of a { listing } envelope', () => {
|
|
241
|
-
expect(
|
|
242
|
-
unwrapListing({ listing: { listingId: 'lst_1', creatorHandle: 'corey' } }),
|
|
243
|
-
).toMatchObject({ listingId: 'lst_1', creatorHandle: 'corey' });
|
|
244
|
-
});
|
|
245
|
-
it('falls back to the top-level body when there is no envelope (compat)', () => {
|
|
246
|
-
expect(unwrapListing({ id: 'lst_2', creatorHandle: 'ada' })).toMatchObject({
|
|
247
|
-
id: 'lst_2',
|
|
248
|
-
creatorHandle: 'ada',
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
// Server-contract field derivation
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
|
|
257
|
-
function pack(overrides: Partial<PackManifest> = {}): PackManifest {
|
|
258
|
-
return {
|
|
259
|
-
name: 'hq-pack-demo',
|
|
260
|
-
version: '1.0.0',
|
|
261
|
-
publisher: '@indigo',
|
|
262
|
-
access: 'public',
|
|
263
|
-
requires: { hqCore: '>=1.0.0' },
|
|
264
|
-
contributes: { skills: ['demo'] },
|
|
265
|
-
...overrides,
|
|
266
|
-
} as PackManifest;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
describe('inferListingType', () => {
|
|
270
|
-
it('returns "worker" when the pack contributes only workers', () => {
|
|
271
|
-
expect(inferListingType(pack({ contributes: { workers: ['x'] } }))).toBe('worker');
|
|
272
|
-
});
|
|
273
|
-
it('returns "skill" when the pack contributes only skills', () => {
|
|
274
|
-
expect(inferListingType(pack({ contributes: { skills: ['demo'] } }))).toBe('skill');
|
|
275
|
-
});
|
|
276
|
-
it('prefers "skill" when the pack contributes both (ambiguous)', () => {
|
|
277
|
-
expect(
|
|
278
|
-
inferListingType(pack({ contributes: { skills: ['a'], workers: ['b'] } })),
|
|
279
|
-
).toBe('skill');
|
|
280
|
-
});
|
|
281
|
-
it('defaults to "skill" when neither skills nor workers are present', () => {
|
|
282
|
-
expect(inferListingType(pack({ contributes: { knowledge: ['k'] } }))).toBe('skill');
|
|
283
|
-
});
|
|
284
|
-
it('honors an explicit valid type hint in package.yaml over inference', () => {
|
|
285
|
-
const withHint = { ...pack({ contributes: { skills: ['demo'] } }), type: 'worker' } as PackManifest;
|
|
286
|
-
expect(inferListingType(withHint)).toBe('worker');
|
|
287
|
-
});
|
|
288
|
-
it('ignores an invalid type hint and falls back to inference', () => {
|
|
289
|
-
const withBadHint = { ...pack({ contributes: { workers: ['x'] } }), type: 'plugin' } as unknown as PackManifest;
|
|
290
|
-
expect(inferListingType(withBadHint)).toBe('worker');
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('deriveSlug', () => {
|
|
295
|
-
it('strips the hq-pack- prefix', () => {
|
|
296
|
-
expect(deriveSlug('hq-pack-demo')).toBe('demo');
|
|
297
|
-
});
|
|
298
|
-
it('falls back to the full name when there is no hq-pack- prefix', () => {
|
|
299
|
-
expect(deriveSlug('demo')).toBe('demo');
|
|
300
|
-
});
|
|
301
|
-
it('never returns an empty slug', () => {
|
|
302
|
-
expect(deriveSlug('hq-pack-').length).toBeGreaterThan(0);
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
describe('summarizeContributes', () => {
|
|
307
|
-
it('produces a string summary of non-empty categories', () => {
|
|
308
|
-
const out = summarizeContributes({ workers: ['a', 'b'], skills: ['demo'] });
|
|
309
|
-
expect(typeof out).toBe('string');
|
|
310
|
-
expect(out).toContain('workers: a, b');
|
|
311
|
-
expect(out).toContain('skills: demo');
|
|
312
|
-
});
|
|
313
|
-
it('skips empty categories and returns an empty string for none', () => {
|
|
314
|
-
expect(summarizeContributes({ skills: [] })).toBe('');
|
|
315
|
-
expect(summarizeContributes({})).toBe('');
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
// runPublish — full flow with mocked deps + tmp fs
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
function makeDeps(overrides: Partial<PublishDeps> = {}): {
|
|
324
|
-
deps: PublishDeps;
|
|
325
|
-
uploadSpy: ReturnType<typeof vi.fn>;
|
|
326
|
-
} {
|
|
327
|
-
const uploadSpy = vi.fn(async () => ({
|
|
328
|
-
status: 201,
|
|
329
|
-
body: { id: 'lst_42', status: 'pending_review' } as Record<string, unknown>,
|
|
330
|
-
}));
|
|
331
|
-
const deps: PublishDeps = {
|
|
332
|
-
loadIdToken: () => fakeIdToken({ sub: 'usr_1', email: 'ada@b.com', name: 'Ada' }),
|
|
333
|
-
getAccessToken: async () => 'access-token',
|
|
334
|
-
getIdToken: async () => 'id-token',
|
|
335
|
-
upload: uploadSpy,
|
|
336
|
-
hqRoot,
|
|
337
|
-
hqVersion: null,
|
|
338
|
-
...overrides,
|
|
339
|
-
};
|
|
340
|
-
return { deps, uploadSpy };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
describe('runPublish', () => {
|
|
344
|
-
it('logged-in + valid pack → uploads JSON listing and returns pending_review notice', async () => {
|
|
345
|
-
const packDir = writeValidPack(tmpRoot);
|
|
346
|
-
const { deps, uploadSpy } = makeDeps();
|
|
347
|
-
|
|
348
|
-
const result = await runPublish(packDir, deps);
|
|
349
|
-
|
|
350
|
-
expect(uploadSpy).toHaveBeenCalledOnce();
|
|
351
|
-
const call = uploadSpy.mock.calls[0][0];
|
|
352
|
-
expect(call.token).toBe('access-token');
|
|
353
|
-
|
|
354
|
-
// The new contract sends a JSON listing object (not raw bytes).
|
|
355
|
-
const listing = call.listing as ListingRequest;
|
|
356
|
-
expect(listing.name).toBe('hq-pack-demo');
|
|
357
|
-
expect(listing.slug).toBe('demo');
|
|
358
|
-
expect(listing.version).toBe('1.0.0');
|
|
359
|
-
expect(listing.type).toBe('skill');
|
|
360
|
-
expect(['skill', 'worker']).toContain(listing.type);
|
|
361
|
-
expect(typeof listing.contributes).toBe('string');
|
|
362
|
-
expect(listing.contributes).toContain('skills: demo');
|
|
363
|
-
|
|
364
|
-
expect(result.notice).toContain('lst_42');
|
|
365
|
-
expect(result.notice).toContain('pending_review');
|
|
366
|
-
expect(result.author.uid).toBe('usr_1');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('sends a body that is valid JSON with a base64 tarball that decodes to gzip bytes', async () => {
|
|
370
|
-
const packDir = writeValidPack(tmpRoot);
|
|
371
|
-
|
|
372
|
-
// Capture exactly what the command wiring would serialize: JSON.stringify
|
|
373
|
-
// the listing, then prove it parses and the tarball decodes to gzip.
|
|
374
|
-
let serialized = '';
|
|
375
|
-
const uploadSpy = vi.fn(async ({ listing }: { listing: ListingRequest }) => {
|
|
376
|
-
serialized = JSON.stringify(listing);
|
|
377
|
-
return {
|
|
378
|
-
status: 201,
|
|
379
|
-
body: { id: 'lst_42', status: 'pending_review' } as Record<string, unknown>,
|
|
380
|
-
};
|
|
381
|
-
});
|
|
382
|
-
const { deps } = makeDeps({ upload: uploadSpy });
|
|
383
|
-
|
|
384
|
-
await runPublish(packDir, deps);
|
|
385
|
-
|
|
386
|
-
// Body is valid JSON.
|
|
387
|
-
const parsed = JSON.parse(serialized) as ListingRequest;
|
|
388
|
-
expect(parsed.type).toBe('skill');
|
|
389
|
-
expect(typeof parsed.name).toBe('string');
|
|
390
|
-
expect(typeof parsed.slug).toBe('string');
|
|
391
|
-
expect(typeof parsed.version).toBe('string');
|
|
392
|
-
expect(typeof parsed.tarball).toBe('string');
|
|
393
|
-
expect(parsed.tarball.length).toBeGreaterThan(0);
|
|
394
|
-
|
|
395
|
-
// Required fields present + non-empty.
|
|
396
|
-
expect(parsed.name).not.toBe('');
|
|
397
|
-
expect(parsed.slug).not.toBe('');
|
|
398
|
-
expect(parsed.version).not.toBe('');
|
|
399
|
-
|
|
400
|
-
// Base64 tarball decodes to gzip bytes (gzip magic 0x1f 0x8b).
|
|
401
|
-
const bytes = Buffer.from(parsed.tarball, 'base64');
|
|
402
|
-
expect(bytes.length).toBeGreaterThan(0);
|
|
403
|
-
expect(bytes[0]).toBe(0x1f);
|
|
404
|
-
expect(bytes[1]).toBe(0x8b);
|
|
405
|
-
// And it actually gunzips without throwing.
|
|
406
|
-
expect(() => zlib.gunzipSync(bytes)).not.toThrow();
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('success notice uses the SERVER listing id + creatorHandle, not the local author handle (Bug 2)', async () => {
|
|
410
|
-
const packDir = writeValidPack(tmpRoot);
|
|
411
|
-
// The local idToken resolves to an ugly federated handle; the server stores
|
|
412
|
-
// the real id + a clean creatorHandle. The notice must show the server's.
|
|
413
|
-
const { deps } = makeDeps({
|
|
414
|
-
loadIdToken: () =>
|
|
415
|
-
fakeIdToken({
|
|
416
|
-
sub: 'Google_114624000000000000000',
|
|
417
|
-
preferred_username: 'Google_114624000000000000000',
|
|
418
|
-
name: 'Ada Lovelace',
|
|
419
|
-
}),
|
|
420
|
-
upload: vi.fn(async () => ({
|
|
421
|
-
status: 201,
|
|
422
|
-
body: {
|
|
423
|
-
id: 'lst_real_777',
|
|
424
|
-
status: 'pending_review',
|
|
425
|
-
creatorHandle: 'ada',
|
|
426
|
-
} as Record<string, unknown>,
|
|
427
|
-
})),
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const result = await runPublish(packDir, deps);
|
|
431
|
-
|
|
432
|
-
// Server-supplied id + handle appear; the ugly local federated handle does not.
|
|
433
|
-
expect(result.notice).toContain('lst_real_777');
|
|
434
|
-
expect(result.notice).toContain('Attributed to @ada.');
|
|
435
|
-
expect(result.notice).not.toContain('Google_114624');
|
|
436
|
-
expect(result.serverAttributed).toBe(true);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('unwraps the real prod { listing } envelope → real id + server @handle, not the local one (prod bug)', async () => {
|
|
440
|
-
const packDir = writeValidPack(tmpRoot);
|
|
441
|
-
// Local idToken resolves to an ugly federated handle; the server returns the
|
|
442
|
-
// real listing WRAPPED in a { listing } envelope with listingId + a clean
|
|
443
|
-
// creatorHandle. The notice must show the server's values, not the local ones.
|
|
444
|
-
const { deps } = makeDeps({
|
|
445
|
-
loadIdToken: () =>
|
|
446
|
-
fakeIdToken({
|
|
447
|
-
sub: 'Google_114624000000000000000',
|
|
448
|
-
preferred_username: 'Google_114624000000000000000',
|
|
449
|
-
name: 'Corey Epstein',
|
|
450
|
-
}),
|
|
451
|
-
upload: vi.fn(async () => ({
|
|
452
|
-
status: 201,
|
|
453
|
-
body: {
|
|
454
|
-
listing: {
|
|
455
|
-
listingId: 'lst_prod_777',
|
|
456
|
-
creatorHandle: 'corey',
|
|
457
|
-
status: 'pending_review',
|
|
458
|
-
},
|
|
459
|
-
} as Record<string, unknown>,
|
|
460
|
-
})),
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
const result = await runPublish(packDir, deps);
|
|
464
|
-
|
|
465
|
-
expect(result.notice).toContain('lst_prod_777');
|
|
466
|
-
expect(result.notice).toContain('pending_review');
|
|
467
|
-
expect(result.notice).toContain('Attributed to @corey.');
|
|
468
|
-
expect(result.notice).not.toContain('(unknown id)');
|
|
469
|
-
expect(result.notice).not.toContain('Google_114624');
|
|
470
|
-
expect(result.serverAttributed).toBe(true);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it('falls back to the local author attribution when the server omits creatorHandle', async () => {
|
|
474
|
-
const packDir = writeValidPack(tmpRoot);
|
|
475
|
-
const { deps } = makeDeps({
|
|
476
|
-
upload: vi.fn(async () => ({
|
|
477
|
-
status: 201,
|
|
478
|
-
body: { id: 'lst_42', status: 'pending_review' } as Record<string, unknown>,
|
|
479
|
-
})),
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
const result = await runPublish(packDir, deps);
|
|
483
|
-
|
|
484
|
-
expect(result.serverAttributed).toBe(false);
|
|
485
|
-
expect(result.notice).not.toMatch(/Attributed to/);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('stamps author (from sub) into the uploaded pack and records provenance', async () => {
|
|
489
|
-
const packDir = writeValidPack(tmpRoot);
|
|
490
|
-
const { deps } = makeDeps();
|
|
491
|
-
|
|
492
|
-
await runPublish(packDir, deps);
|
|
493
|
-
|
|
494
|
-
// Provenance written to modules.yaml under the hq root.
|
|
495
|
-
const manifest = readManifest(hqRoot);
|
|
496
|
-
expect(manifest?.modules).toHaveLength(1);
|
|
497
|
-
const entry = manifest!.modules[0] as PackModuleDefinition;
|
|
498
|
-
expect(entry.name).toBe('hq-pack-demo');
|
|
499
|
-
expect(entry.strategy).toBe('package');
|
|
500
|
-
expect(entry.source).toBe(packDir);
|
|
501
|
-
expect(entry.version).toBe('1.0.0');
|
|
502
|
-
|
|
503
|
-
// The user's working-tree package.yaml is NOT mutated (we stamp a copy).
|
|
504
|
-
const onDisk = yaml.load(
|
|
505
|
-
fs.readFileSync(path.join(packDir, 'package.yaml'), 'utf-8'),
|
|
506
|
-
) as Record<string, unknown>;
|
|
507
|
-
expect(onDisk.author).toBeUndefined();
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it('not logged in → errors with a login prompt and uploads nothing', async () => {
|
|
511
|
-
const packDir = writeValidPack(tmpRoot);
|
|
512
|
-
const { deps, uploadSpy } = makeDeps({ loadIdToken: () => null });
|
|
513
|
-
|
|
514
|
-
await expect(runPublish(packDir, deps)).rejects.toThrow(/Not logged in|hq login/);
|
|
515
|
-
expect(uploadSpy).not.toHaveBeenCalled();
|
|
516
|
-
// No provenance recorded.
|
|
517
|
-
expect(readManifest(hqRoot)).toBeNull();
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it('invalid package.yaml → validation error, uploads nothing', async () => {
|
|
521
|
-
// Missing required `version` → validateManifest rejects.
|
|
522
|
-
const bad = { ...VALID_MANIFEST };
|
|
523
|
-
delete (bad as Record<string, unknown>).version;
|
|
524
|
-
const packDir = writeValidPack(tmpRoot, bad);
|
|
525
|
-
const { deps, uploadSpy } = makeDeps();
|
|
526
|
-
|
|
527
|
-
await expect(runPublish(packDir, deps)).rejects.toThrow(/version/i);
|
|
528
|
-
expect(uploadSpy).not.toHaveBeenCalled();
|
|
529
|
-
expect(readManifest(hqRoot)).toBeNull();
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('missing package.yaml → errors, uploads nothing', async () => {
|
|
533
|
-
const dir = path.join(tmpRoot, 'nope');
|
|
534
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
535
|
-
const { deps, uploadSpy } = makeDeps();
|
|
536
|
-
|
|
537
|
-
await expect(runPublish(dir, deps)).rejects.toThrow(/package\.yaml missing/);
|
|
538
|
-
expect(uploadSpy).not.toHaveBeenCalled();
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('re-publishing the same version (409) → clear duplicate error', async () => {
|
|
542
|
-
const packDir = writeValidPack(tmpRoot);
|
|
543
|
-
const { deps, uploadSpy } = makeDeps({
|
|
544
|
-
upload: vi.fn(async () => ({ status: 409, body: {} as Record<string, unknown> })),
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
await expect(runPublish(packDir, deps)).rejects.toThrow(/already published|duplicate/i);
|
|
548
|
-
// The upload WAS attempted (server is the duplicate authority), but no
|
|
549
|
-
// provenance is recorded for a rejected publish.
|
|
550
|
-
expect((deps.upload as ReturnType<typeof vi.fn>)).toHaveBeenCalledOnce();
|
|
551
|
-
void uploadSpy;
|
|
552
|
-
expect(readManifest(hqRoot)).toBeNull();
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
describe('runPublish — publish on behalf (--as <handle>)', () => {
|
|
557
|
-
it('sends publishAsHandle + attributes the body to the TARGET handle (not the caller)', async () => {
|
|
558
|
-
const packDir = writeValidPack(tmpRoot);
|
|
559
|
-
const { deps, uploadSpy } = makeDeps();
|
|
560
|
-
|
|
561
|
-
const result = await runPublish(packDir, deps, { publishAsHandle: 'garrytan' });
|
|
562
|
-
|
|
563
|
-
const call = uploadSpy.mock.calls[0][0];
|
|
564
|
-
const listing = call.listing as ListingRequest;
|
|
565
|
-
// The on-behalf signal is sent to the server.
|
|
566
|
-
expect(listing.publishAsHandle).toBe('garrytan');
|
|
567
|
-
// Body attribution is the TARGET handle, never the caller's resolved handle.
|
|
568
|
-
expect(listing.creatorHandle).toBe('garrytan');
|
|
569
|
-
expect(listing.creatorHandle).not.toBe(result.author.handle);
|
|
570
|
-
// CRITICAL: on-behalf must authenticate with the ID token (staff gate reads
|
|
571
|
-
// the email claim, which the access token lacks → would 403 NO_EMAIL_CLAIM).
|
|
572
|
-
expect(call.token).toBe('id-token');
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it('on-behalf uses the ID token, normal publish uses the access token', async () => {
|
|
576
|
-
const packDir = writeValidPack(tmpRoot);
|
|
577
|
-
const onBehalf = makeDeps();
|
|
578
|
-
await runPublish(packDir, onBehalf.deps, { publishAsHandle: 'garrytan' });
|
|
579
|
-
expect(onBehalf.uploadSpy.mock.calls[0][0].token).toBe('id-token');
|
|
580
|
-
|
|
581
|
-
const normal = makeDeps();
|
|
582
|
-
await runPublish(packDir, normal.deps);
|
|
583
|
-
expect(normal.uploadSpy.mock.calls[0][0].token).toBe('access-token');
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('preserves a pre-existing manifest author (does NOT stamp the caller) on the --as path', async () => {
|
|
587
|
-
// A ported pack already names the upstream creator. With --as we must NOT
|
|
588
|
-
// overwrite that author with the publishing staff caller — validation must
|
|
589
|
-
// pass against the manifest's own author block.
|
|
590
|
-
const packDir = writeValidPack(tmpRoot, {
|
|
591
|
-
...VALID_MANIFEST,
|
|
592
|
-
author: { uid: 'cmt_garrytan', handle: 'garrytan', displayName: 'Garry Tan' },
|
|
593
|
-
});
|
|
594
|
-
const { deps, uploadSpy } = makeDeps();
|
|
595
|
-
|
|
596
|
-
const result = await runPublish(packDir, deps, { publishAsHandle: 'garrytan' });
|
|
597
|
-
|
|
598
|
-
expect(uploadSpy).toHaveBeenCalledOnce();
|
|
599
|
-
expect(result.notice).toContain('pending_review');
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('normal publish (no --as) sends NO publishAsHandle and keeps caller attribution', async () => {
|
|
603
|
-
const packDir = writeValidPack(tmpRoot);
|
|
604
|
-
const { deps, uploadSpy } = makeDeps();
|
|
605
|
-
|
|
606
|
-
const result = await runPublish(packDir, deps);
|
|
607
|
-
|
|
608
|
-
const listing = uploadSpy.mock.calls[0][0].listing as ListingRequest;
|
|
609
|
-
expect(listing.publishAsHandle).toBeUndefined();
|
|
610
|
-
expect(listing.creatorHandle).toBe(result.author.handle);
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('an empty --as handle is treated as a normal self-publish', async () => {
|
|
614
|
-
const packDir = writeValidPack(tmpRoot);
|
|
615
|
-
const { deps, uploadSpy } = makeDeps();
|
|
616
|
-
|
|
617
|
-
await runPublish(packDir, deps, { publishAsHandle: '' });
|
|
618
|
-
|
|
619
|
-
const listing = uploadSpy.mock.calls[0][0].listing as ListingRequest;
|
|
620
|
-
expect(listing.publishAsHandle).toBeUndefined();
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it('server kill-switch off (400 ON_BEHALF_DISABLED) surfaces a clear error', async () => {
|
|
624
|
-
const packDir = writeValidPack(tmpRoot);
|
|
625
|
-
const { deps } = makeDeps({
|
|
626
|
-
upload: vi.fn(async () => ({
|
|
627
|
-
status: 400,
|
|
628
|
-
body: { code: 'ON_BEHALF_DISABLED' } as Record<string, unknown>,
|
|
629
|
-
})),
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
await expect(
|
|
633
|
-
runPublish(packDir, deps, { publishAsHandle: 'garrytan' }),
|
|
634
|
-
).rejects.toThrow(/on behalf|not enabled/i);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('non-staff caller (403 FORBIDDEN) surfaces a "staff only" error', async () => {
|
|
638
|
-
const packDir = writeValidPack(tmpRoot);
|
|
639
|
-
const { deps } = makeDeps({
|
|
640
|
-
upload: vi.fn(async () => ({
|
|
641
|
-
status: 403,
|
|
642
|
-
body: { code: 'FORBIDDEN' } as Record<string, unknown>,
|
|
643
|
-
})),
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
await expect(
|
|
647
|
-
runPublish(packDir, deps, { publishAsHandle: 'garrytan' }),
|
|
648
|
-
).rejects.toThrow(/HQ staff|on behalf/i);
|
|
649
|
-
});
|
|
650
|
-
});
|