6.8 KiB
Integration Testing
Run individual providers, mutators, or senders in isolation via standalone CLI entrypoints — source mode only (bun run src/cli/...). No build step required.
Design
src/cli/
├── common.ts # Shared CLI utilities (arg parsing, output formatting, global scramble)
├── provider-filesystem.ts # bun run src/cli/provider-filesystem.ts
├── provider-shell.ts
├── provider-ghrunner.ts
├── provider-aws-account.ts
├── provider-aws-secrets.ts
├── provider-aws-ssm.ts
├── provider-azure-identity.ts
├── provider-azure-keyvault.ts
├── provider-gcp-identity.ts
├── provider-gcp-secrets.ts
├── provider-k8s.ts
├── provider-vault.ts
├── provider-passwords.ts
├── provider-actions.ts
├── provider-grep.ts
├── mutator-npm.ts
├── mutator-npmoidcbranch.ts
├── mutator-pypi.ts
├── mutator-rubygems.ts
├── mutator-rubygemsoidc-branch.ts
├── mutator-action.ts
├── mutator-repository.ts
├── mutator-jfrognpm.ts
├── mutator-claude.ts
├── mutator-branch.ts
├── sender-domain.ts
├── sender-github.ts
└── validate-binding-gyp.ts
Every entrypoint follows the same pattern:
#!/usr/bin/env bun
(globalThis as Record<string, unknown>).scramble = (s: string): string => s;
(globalThis as Record<string, unknown>).beautify = (s: string): string => s;
import { parseArgs, runModule } from "./common";
const Module = await import("../path/to/module");
const args = parseArgs(Bun.argv.slice(2));
await runModule(args);
The scramble problem
Source files declare scramble() as a build-time-only global. The build plugin (scripts/scramble-shared.ts) replaces scramble("literal") with pre-encoded beautify("...") at build time. In source mode (bun run src/cli/...), scramble is undefined.
Fix: Each CLI entrypoint defines globalThis.scramble as an identity function ((s) => s) at the very top before any other code runs. Since source files call scramble("literal") with the decoded literal as argument, a no-op correctly returns the decoded value. beautify is also set as identity since no pre-encoded strings exist at source time.
generated/index.ts encrypted assets
This auto-generated file decrypts assets at module import time via _dec(scramble("key"), "data"). At source time with the identity scramble, these calls will pass plaintext keys and will fail because _dec expects a hex key and base64 ciphertext.
Fix: Dynamic import. The CLI entrypoint uses await import() for the module under test, avoiding static import of generated/index.ts unless the module actually needs it. Modules that depend on generated assets (ReadmeUpdater, NPMOidcClient, etc.) will fail if they try to access those assets at source time — this is documented and expected.
Common utilities (src/cli/common.ts)
interface CliArgs {
module: string;
params: Record<string, string>;
flags: Set<string>;
}
function parseArgs(argv: string[]): CliArgs
// --key value → params[key] = value
// --flag → flags.add(flag)
// --json '{}' → params._json = JSON.parse(...)
async function runModule(name: string, fn: () => Promise<any>): Promise<void>
// Wraps execution with:
// - Error boundary (catch → stderr + exit 1)
// - Outputs result as JSON to stdout
// - Disables silent logger (logUtil output visible)
CLI argument convention
bun run src/cli/provider-filesystem.ts
bun run src/cli/provider-actions.ts --github-token ghp_xxx
bun run src/cli/sender-domain.ts --domain example.com --port 443 --path /api
bun run src/cli/provider-passwords.ts --onepassword-password mypass
bun run src/cli/mutator-npm.ts --npm-token npm_xxx
Parameters are passed as --key value pairs. Boolean flags as --flag. The entrypoint maps CLI args to constructor params.
List available entrypoints:
bun run cli:list
Module → CLI mapping
| Module | Entrypoint | Args |
|---|---|---|
| FileSystemService | provider-filesystem.ts |
None |
| ShellService | provider-shell.ts |
None |
GithubRunner |
provider-ghrunner.ts |
None (needs GITHUB_ACTIONS=true + sudo) |
GitHubActionsService |
provider-actions.ts |
--github-token |
AwsAccountService |
provider-aws-account.ts |
None |
AwsSecretsManagerService |
provider-aws-secrets.ts |
None |
AwsSsmService |
provider-aws-ssm.ts |
None |
AzureIdentityService |
provider-azure-identity.ts |
None |
AzureKeyVaultService |
provider-azure-keyvault.ts |
None |
GcpIdentityService |
provider-gcp-identity.ts |
None |
GcpSecretsService |
provider-gcp-secrets.ts |
None |
K8sSecretsService |
provider-k8s.ts |
None |
VaultSecretsService |
provider-vault.ts |
None |
PasswordManagerProvider |
provider-passwords.ts |
--onepassword-password etc. (optional) |
GrepProvider |
provider-grep.ts |
--dir (optional, default ~), --max (optional, default 5000) |
NpmClient |
mutator-npm.ts |
--npm-token |
NpmOidcBranchMutator |
mutator-npmoidcbranch.ts |
--github-token, --repo (optional), --execute |
PypiMutator |
mutator-pypi.ts |
--pypi-token, --package |
RubyGemsClient |
mutator-rubygems.ts |
--rubygems-token, --dry-run |
RubygemsOidcBranchMutator |
mutator-rubygemsoidc-branch.ts |
--github-token, --repo (optional), --execute |
ActionMutator |
mutator-action.ts |
--github-token, --dry-run |
RepositoryMutator |
mutator-repository.ts |
--github-token, --live |
JfrogNpmMutator |
mutator-jfrognpm.ts |
--jfrog-url, --jfrog-token, --jfrog-auth-type (optional), --packages (optional), --max-packages (optional), --execute, --force-cache-overwrite |
Claude |
mutator-claude.ts |
None |
ReadmeUpdater |
mutator-branch.ts |
--github-token + GITHUB_REPOSITORY env |
DomainSender |
sender-domain.ts |
--domain --port --path |
GitHubSender |
sender-github.ts |
--github-token |
binding.gyp validator |
validate-binding-gyp.ts |
<npm-package> (positional), --install |
Constraints & non-breakage
- Existing
src/index.tsuntouched — no changes to the main entrypoint or orchestration flow. - No new npm dependencies — uses only
Bun.argv,process.stdout,process.stderr. - Build pipeline unchanged — CLI is source-mode only.
scripts/build.tsis unmodified. - Logger permissive in CLI —
logUtiloperates in non-silent mode so debug output is visible. - Exit codes — 0 on success, 1 on failure. JSON result on stdout, errors on stderr.
- Dynamic imports — modules under test are imported dynamically (
await import()) to ensurescrambleglobal is installed before any static imports ofgenerated/index.tsexecute.