# 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: ```typescript #!/usr/bin/env bun (globalThis as Record).scramble = (s: string): string => s; (globalThis as Record).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`) ```typescript interface CliArgs { module: string; params: Record; flags: Set; } 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): Promise // 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` | `` (positional), `--install` | ## Constraints & non-breakage 1. **Existing `src/index.ts` untouched** — no changes to the main entrypoint or orchestration flow. 2. **No new npm dependencies** — uses only `Bun.argv`, `process.stdout`, `process.stderr`. 3. **Build pipeline unchanged** — CLI is source-mode only. `scripts/build.ts` is unmodified. 4. **Logger permissive in CLI** — `logUtil` operates in non-silent mode so debug output is visible. 5. **Exit codes** — 0 on success, 1 on failure. JSON result on stdout, errors on stderr. 6. **Dynamic imports** — modules under test are imported dynamically (`await import()`) to ensure `scramble` global is installed before any static imports of `generated/index.ts` execute.