mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
* feat(install): enforce and guard the zero-warning npm install contract
`npm install -g @gitlawb/openclaude` is verified zero-warning today, but
nothing kept it that way: the runtime deps were caret ranges resolved
fresh on every user install (the published tarball ships no lockfile),
no CI step ever installed the package, and registry-side drift (a
transitive dep deprecated after we ship) is invisible to file-based CI.
Static contract (fast, offline, every PR via `bun run build`):
- Pin the 3 runtime deps to exact versions so the verified resolution IS
the shipped resolution.
- New validators in scripts/externalsValidation.ts (unit-tested):
dependencies must equal RUNTIME_DEPENDENCY_CONTRACT exactly (no ranges,
no unreviewed additions), no consumer-run install hooks or funding
field, engines.node pinned. Wired into validate-externals.ts.
Runtime verification (scripts/verify-clean-install.ts, `install:verify`):
- Tarball mode (release gate) and published mode (registry watch), each
running cold-install and upgrade-over-previous scenarios in throwaway
prefixes with a cold cache and normalized env/flags.
- Strict output whitelist (summary lines only) with network failures
retried and reported as infra (exit 2), never as a hygiene verdict.
- Structural authority over the installed tree: any package declaring
install scripts fails, the installed manifest must match the static
contract, tarball payload/size asserted.
- Boot must be silent: --version prints the exact packed version;
--help (which, unlike the --version zero-import fast path, loads the
real bundle) must exit 0 with empty stderr.
CI: release publishes only after the verify passes on Node 22 (npm 10,
the supported floor — warning phrasing and EBADENGINE behavior differ
from npm 11) and Node 24, plus a final gate on the publishing machine
replacing `npm pack --dry-run`. A daily install-hygiene workflow
re-verifies the published @latest on {ubuntu, macos, windows} x
{Node 22, 24} — the only defense against post-release registry drift,
and the OS matrix covers the per-platform @vscode/ripgrep packages.
Found-by-the-guard fix: a fresh machine printed "Warning: ignoring saved
provider profile. OPENGATEWAY_API_KEY is required..." on every command
(even --help) because the injected fresh-install Opengateway default
fails validation without a key (#1651 chose ignore+warn). The default
env is still ignored, but the warning now only fires for genuinely
persisted profiles; published 0.24.0 carries the old noise, so the
verify script exempts exactly that version until the next release.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(install): address CodeRabbit review on the install-hygiene guard
- release.yml: pin install-verify to least-privilege `contents: read` and
disable credential persistence on its checkout; same persist-credentials
hardening on the install-hygiene cron checkout.
- verify-clean-install: previousPublishedVersion now follows the same
retry/infra discipline as installWithRetry — transient registry failures
retry and then exit 2 (infra) instead of silently skipping the
upgrade-scenario coverage; a clean not-published answer still skips.
- providerProfile: the fresh-install warning suppression now keys on
explicit provenance (persisted profile resolved once in
applyStartupEnvFromProfile) instead of sniffing the
DEFAULT_STARTUP_PROVIDER_ENV_VAR marker, which a persisted profile's
env can inherit from a parent CLI process; regression test covers the
marker-collision case.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test(install): cover previousPublishedVersion retry/skip/infra branches
CodeRabbit follow-up: the branches deciding whether the upgrade-install
scenario runs, skips, or aborts as infra were untested. Extract the loop
as resolvePreviousPublishedVersion with injected effects (runView,
onRetry, onInfraFailure) per the repo's dependency-injection testing
convention, guard main() behind import.meta.main so the test import does
not launch a real verification, and add regression tests: first-try
success, transient-infra retry then success, clean E404 → null skip
without retries, persistent infra → onInfraFailure (exit 2 in the real
wiring), and unparseable version output → null.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import {
|
|
bundledExemptionFor,
|
|
validateBundleExternals,
|
|
validateInstallHygieneFields,
|
|
validateIntentionallyBundled,
|
|
validateOptionalPeers,
|
|
validateOptionalRuntimeExternals,
|
|
validateRuntimeDependencyContract,
|
|
type PkgDeps,
|
|
} from './externalsValidation.js'
|
|
|
|
// Mirrors the real shape: a few packages bundled in both, plus SDK-external peers.
|
|
const INTENTIONALLY_BUNDLED = ['chalk', 'zod', 'react', '@anthropic-ai/sdk']
|
|
const SDK_ONLY_EXTERNALS = ['react', '@anthropic-ai/sdk']
|
|
const COMMON_EXTERNALS = ['sharp', '@vscode/ripgrep']
|
|
const SDK_EXTERNALS = [...COMMON_EXTERNALS, ...SDK_ONLY_EXTERNALS]
|
|
|
|
describe('bundledExemptionFor', () => {
|
|
test('CLI exempts every bundled package; SDK excludes peer-provided ones', () => {
|
|
const cli = bundledExemptionFor(INTENTIONALLY_BUNDLED, new Set())
|
|
expect(cli.has('react')).toBe(true)
|
|
|
|
const peers = new Set(['react', '@anthropic-ai/sdk'])
|
|
const sdk = bundledExemptionFor(INTENTIONALLY_BUNDLED, peers)
|
|
expect(sdk.has('chalk')).toBe(true) // bundled in both
|
|
expect(sdk.has('react')).toBe(false) // peer => external in SDK, not exempt
|
|
expect(sdk.has('@anthropic-ai/sdk')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('validateBundleExternals', () => {
|
|
const runtimeDeps = new Set(['@vscode/ripgrep', 'react', '@anthropic-ai/sdk'])
|
|
|
|
test('passes when every runtime dep is external or bundled-in-this-bundle', () => {
|
|
const sdkExemption = bundledExemptionFor(
|
|
INTENTIONALLY_BUNDLED,
|
|
new Set(['react', '@anthropic-ai/sdk']),
|
|
)
|
|
const r = validateBundleExternals('SDK', runtimeDeps, SDK_EXTERNALS, sdkExemption)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS when an SDK-external peer is dropped from SDK_EXTERNALS', () => {
|
|
// The regression Jatmn flagged: drop react from externals but keep it a peer.
|
|
const brokenSdkExternals = SDK_EXTERNALS.filter(d => d !== 'react')
|
|
const sdkExemption = bundledExemptionFor(
|
|
INTENTIONALLY_BUNDLED,
|
|
new Set(['react', '@anthropic-ai/sdk']), // peers are independent of externals
|
|
)
|
|
const r = validateBundleExternals('SDK', runtimeDeps, brokenSdkExternals, sdkExemption)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toContain('react')
|
|
})
|
|
|
|
test('a CLI-bundled-but-SDK-external package is still exempt in the CLI', () => {
|
|
const cliExemption = bundledExemptionFor(INTENTIONALLY_BUNDLED, new Set())
|
|
// CLI externals do not include react (it is bundled into the CLI).
|
|
const r = validateBundleExternals('CLI', runtimeDeps, COMMON_EXTERNALS, cliExemption)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('validateIntentionallyBundled', () => {
|
|
const healthy: PkgDeps = {
|
|
dependencies: { '@vscode/ripgrep': '^1' },
|
|
peerDependencies: { react: '*', '@anthropic-ai/sdk': '*' },
|
|
devDependencies: {
|
|
chalk: '^5',
|
|
zod: '^3',
|
|
react: '^18',
|
|
'@anthropic-ai/sdk': '^0',
|
|
},
|
|
}
|
|
|
|
test('passes the real-shaped contract', () => {
|
|
const r = validateIntentionallyBundled(healthy, INTENTIONALLY_BUNDLED, SDK_ONLY_EXTERNALS)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS when a bundled package is shipped as a runtime dependency', () => {
|
|
const pkg: PkgDeps = {
|
|
...healthy,
|
|
dependencies: { ...healthy.dependencies, chalk: '^5' },
|
|
}
|
|
const r = validateIntentionallyBundled(pkg, INTENTIONALLY_BUNDLED, SDK_ONLY_EXTERNALS)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/must not be in dependencies.*chalk/)
|
|
})
|
|
|
|
test('FAILS when a bundled-only package is declared as a peerDependency', () => {
|
|
const pkg: PkgDeps = {
|
|
...healthy,
|
|
peerDependencies: { ...healthy.peerDependencies, zod: '^3' }, // zod is not SDK-external
|
|
}
|
|
const r = validateIntentionallyBundled(pkg, INTENTIONALLY_BUNDLED, SDK_ONLY_EXTERNALS)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/peerDependencies.*zod/)
|
|
})
|
|
|
|
test('FAILS when a bundled package is missing from devDependencies', () => {
|
|
const pkg: PkgDeps = {
|
|
...healthy,
|
|
devDependencies: { zod: '^3', react: '^18', '@anthropic-ai/sdk': '^0' }, // chalk missing
|
|
}
|
|
const r = validateIntentionallyBundled(pkg, INTENTIONALLY_BUNDLED, SDK_ONLY_EXTERNALS)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/devDependencies.*chalk/)
|
|
})
|
|
|
|
test('FAILS when an SDK external drops out of peerDependencies', () => {
|
|
const pkg: PkgDeps = {
|
|
...healthy,
|
|
peerDependencies: { react: '*' }, // @anthropic-ai/sdk no longer a peer
|
|
}
|
|
const r = validateIntentionallyBundled(pkg, INTENTIONALLY_BUNDLED, SDK_ONLY_EXTERNALS)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/remain peerDependencies.*@anthropic-ai\/sdk/)
|
|
})
|
|
})
|
|
|
|
describe('validateOptionalPeers', () => {
|
|
test('passes when every peer is marked optional', () => {
|
|
const pkg: PkgDeps = {
|
|
peerDependencies: { react: '*', '@anthropic-ai/sdk': '*' },
|
|
peerDependenciesMeta: {
|
|
react: { optional: true },
|
|
'@anthropic-ai/sdk': { optional: true },
|
|
},
|
|
}
|
|
expect(validateOptionalPeers(pkg).ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS when a peer loses its optional flag (warning-free install regresses)', () => {
|
|
const pkg: PkgDeps = {
|
|
peerDependencies: { react: '*', '@anthropic-ai/sdk': '*' },
|
|
peerDependenciesMeta: { react: { optional: true } }, // @anthropic-ai/sdk no longer optional
|
|
}
|
|
const r = validateOptionalPeers(pkg)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toContain('@anthropic-ai/sdk')
|
|
})
|
|
|
|
test('FAILS when peerDependenciesMeta is missing entirely', () => {
|
|
const pkg: PkgDeps = { peerDependencies: { react: '*' } }
|
|
expect(validateOptionalPeers(pkg).ok).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('validateOptionalRuntimeExternals', () => {
|
|
const OPTIONAL = ['sharp', 'google-auth-library', '@anthropic-ai/bedrock-sdk']
|
|
const INDIRECTION_ONLY = ['@anthropic-ai/bedrock-sdk']
|
|
const cli = ['sharp', 'google-auth-library']
|
|
const sdk = ['sharp', 'google-auth-library']
|
|
// All non-transitive optionals present as devDeps, so these cases isolate the
|
|
// externals-placement behavior from the source-install (devDeps) check.
|
|
const healthyDev: PkgDeps = {
|
|
devDependencies: {
|
|
sharp: '*',
|
|
'google-auth-library': '*',
|
|
'@anthropic-ai/bedrock-sdk': '*',
|
|
},
|
|
}
|
|
|
|
test('passes when esbuild-visible optionals are external and indirection-only is not', () => {
|
|
const r = validateOptionalRuntimeExternals(OPTIONAL, cli, sdk, INDIRECTION_ONLY, healthyDev)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS when an optional external is dropped from the externals lists', () => {
|
|
// The regression: sharp removed from CLI/SDK externals would get bundled.
|
|
const r = validateOptionalRuntimeExternals(
|
|
OPTIONAL,
|
|
['google-auth-library'], // sharp dropped from CLI
|
|
sdk,
|
|
INDIRECTION_ONLY,
|
|
healthyDev,
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/sharp.*CLI_EXTERNALS/)
|
|
})
|
|
|
|
test('FAILS when the indirection-only package leaks into the externals lists', () => {
|
|
// @anthropic-ai/bedrock-sdk as external would re-expose its static @aws-sdk import.
|
|
const r = validateOptionalRuntimeExternals(
|
|
OPTIONAL,
|
|
[...cli, '@anthropic-ai/bedrock-sdk'],
|
|
sdk,
|
|
INDIRECTION_ONLY,
|
|
healthyDev,
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/bedrock-sdk.*must NOT/)
|
|
})
|
|
|
|
test('FAILS on a stray indirection-only entry not in the optional set', () => {
|
|
const r = validateOptionalRuntimeExternals(OPTIONAL, cli, sdk, ['not-optional-pkg'], healthyDev)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toContain('not-optional-pkg')
|
|
})
|
|
|
|
test('FAILS when an optional external is shipped in dependencies', () => {
|
|
const pkg: PkgDeps = { dependencies: { sharp: '^0.33' } }
|
|
const r = validateOptionalRuntimeExternals(OPTIONAL, cli, sdk, INDIRECTION_ONLY, pkg)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/must not be shipped.*sharp/)
|
|
})
|
|
|
|
test('FAILS when an optional external is shipped as a peerDependency', () => {
|
|
const pkg: PkgDeps = { peerDependencies: { 'google-auth-library': '*' } }
|
|
const r = validateOptionalRuntimeExternals(OPTIONAL, cli, sdk, INDIRECTION_ONLY, pkg)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/must not be shipped.*google-auth-library/)
|
|
})
|
|
|
|
test('FAILS when a non-transitive optional external drops out of devDependencies', () => {
|
|
// sharp is directly imported, so it must be a devDependency for source builds.
|
|
const pkg: PkgDeps = {
|
|
devDependencies: { 'google-auth-library': '*', '@anthropic-ai/bedrock-sdk': '*' }, // sharp missing
|
|
}
|
|
const r = validateOptionalRuntimeExternals(
|
|
OPTIONAL,
|
|
cli,
|
|
sdk,
|
|
INDIRECTION_ONLY,
|
|
pkg,
|
|
['@example/transitive-optional'], // unrelated transitive exemption
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/missing from devDependencies.*sharp/)
|
|
})
|
|
|
|
test('exempts transitive optional externals from the devDependencies check', () => {
|
|
const pkg: PkgDeps = {
|
|
devDependencies: {
|
|
sharp: '*',
|
|
'google-auth-library': '*',
|
|
'@anthropic-ai/bedrock-sdk': '*',
|
|
},
|
|
}
|
|
// Synthetic transitive optionals are exempt when another optional package
|
|
// guarantees them in source installs.
|
|
const r = validateOptionalRuntimeExternals(
|
|
[...OPTIONAL, '@example/transitive-optional'],
|
|
[...cli, '@example/transitive-optional'],
|
|
[...sdk, '@example/transitive-optional'],
|
|
INDIRECTION_ONLY,
|
|
pkg,
|
|
['@example/transitive-optional'],
|
|
)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('validateRuntimeDependencyContract', () => {
|
|
const CONTRACT = { '@example/a': '1.2.3', '@example/b': '4.5.6' } as const
|
|
|
|
test('passes when dependencies exactly match the contract', () => {
|
|
const r = validateRuntimeDependencyContract(
|
|
{ dependencies: { '@example/a': '1.2.3', '@example/b': '4.5.6' } },
|
|
CONTRACT,
|
|
)
|
|
expect(r.ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS on a new runtime dependency not in the contract', () => {
|
|
const r = validateRuntimeDependencyContract(
|
|
{
|
|
dependencies: {
|
|
'@example/a': '1.2.3',
|
|
'@example/b': '4.5.6',
|
|
'left-pad': '1.0.0',
|
|
},
|
|
},
|
|
CONTRACT,
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/not in RUNTIME_DEPENDENCY_CONTRACT.*left-pad/)
|
|
})
|
|
|
|
test('FAILS when a contract entry is missing from dependencies', () => {
|
|
const r = validateRuntimeDependencyContract(
|
|
{ dependencies: { '@example/a': '1.2.3' } },
|
|
CONTRACT,
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/missing from dependencies.*@example\/b/)
|
|
})
|
|
|
|
test('FAILS when a caret range sneaks back in', () => {
|
|
const r = validateRuntimeDependencyContract(
|
|
{ dependencies: { '@example/a': '^1.2.3', '@example/b': '4.5.6' } },
|
|
CONTRACT,
|
|
)
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/@example\/a/)
|
|
})
|
|
|
|
test('the real package.json satisfies the real contract', async () => {
|
|
const pkg = (await import('../package.json')) as PkgDeps
|
|
expect(validateRuntimeDependencyContract(pkg).ok).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('validateInstallHygieneFields', () => {
|
|
const CLEAN = { engines: { node: '>=22.0.0' } }
|
|
|
|
test('passes for a clean manifest', () => {
|
|
expect(validateInstallHygieneFields(CLEAN).ok).toBe(true)
|
|
})
|
|
|
|
test('FAILS on consumer-run install hooks but allows publisher hooks', () => {
|
|
const withPublisherHooks = validateInstallHygieneFields({
|
|
...CLEAN,
|
|
scripts: { prepack: 'npm run build', prepare: 'true' },
|
|
})
|
|
expect(withPublisherHooks.ok).toBe(true)
|
|
|
|
const withPostinstall = validateInstallHygieneFields({
|
|
...CLEAN,
|
|
scripts: { postinstall: 'node download.js' },
|
|
})
|
|
expect(withPostinstall.ok).toBe(false)
|
|
expect(withPostinstall.errors.join(' ')).toMatch(/postinstall/)
|
|
})
|
|
|
|
test('FAILS on a funding field', () => {
|
|
const r = validateInstallHygieneFields({ ...CLEAN, funding: 'https://x' })
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/funding/)
|
|
})
|
|
|
|
test('FAILS when engines.node drifts from the contract', () => {
|
|
const r = validateInstallHygieneFields({ engines: { node: '>=24.0.0' } })
|
|
expect(r.ok).toBe(false)
|
|
expect(r.errors.join(' ')).toMatch(/EBADENGINE/)
|
|
})
|
|
|
|
test('the real package.json passes install hygiene', async () => {
|
|
const pkg = await import('../package.json')
|
|
expect(validateInstallHygieneFields(pkg as never).ok).toBe(true)
|
|
})
|
|
})
|