Introduce fakes and contract tests for AWS Lambda integration#763
Introduce fakes and contract tests for AWS Lambda integration#763
Conversation
|
Claude finished @jumboduck's task in 2m 15s —— View job PR Review: Introduce fakes and contract tests for AWS Lambda integration
Phase 1: Static AnalysisAll three checks pass cleanly:
Phase 2: Code ReviewThis is a well-structured PR that establishes a solid pattern for decoupling cloud provider tests from real AWS. All previously accepted review suggestions (error-type comment, What works well:
Minor observations (all non-blocking):
Phase 3: SummaryThis is a well-executed PR. The fakes-and-contracts pattern is a proven approach for decoupling from external services, and the implementation here is clean, thorough, and consistent with codebase conventions. The test coverage is comprehensive (11 filtering + 5 orchestration + 4 contract + 14 command tests), and the No blocking issues. Looks good to merge. |
Define a narrow LambdaAPI interface scoped to the two SDK methods we actually use (ListFunctions, GetFunctionConfiguration). Refactor getFilteredLambdaFuncs, getAndProcessOneLambdaFunc, and the new getLambdaPackageDataFromClient to accept the interface instead of *lambda.Client. The public GetLambdaPackageData creates the real client and delegates — command layer is untouched. This is Slice 1 of the fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shared runLambdaContractTests function exercises the behaviours we depend on: listing functions, marker-based pagination, getting function config, and error on missing function. Wired to real *lambda.Client in TestLambdaContract_RealAWS, env-gated behind AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. This establishes the contract grounded in real AWS behaviour — the fake (next slice) must pass the same suite. Slice 2 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FakeLambdaClient is an in-memory implementation of LambdaAPI with marker-based pagination and error responses for missing functions. It passes the same runLambdaContractTests suite that validates the real *lambda.Client, proving it is a trustworthy stand-in. Slice 3 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests getFilteredLambdaFuncs with the FakeLambdaClient covering: IncludeNames, IncludeNamesRegex, ExcludeNames, ExcludeNamesRegex, combined exclude filters, multi-page pagination with filtering, empty function lists, and invalid regex error handling. These tests run without AWS credentials and complete in milliseconds. Slice 4 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests getLambdaPackageDataFromClient with the FakeLambdaClient covering: Zip fingerprint decoding, Image raw CodeSha256, concurrent multi-function processing, empty function list, and error propagation from GetFunctionConfiguration. Also adds GetFunctionConfigurationErr field to FakeLambdaClient for error injection in tests. Slice 5 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove filtering-focused integration test cases (IncludeNamesRegex, ExcludeNames, ExcludeNamesRegex, combined filters, wrong region, invalid regex) — these are now covered by fake-backed unit tests in TestGetFilteredLambdaFuncs and TestGetLambdaPackageDataFromClient. Keep three smoke tests: invalid credentials error, one Zip function happy path, and one Image function happy path. These prove real AWS SDK wiring works without duplicating logic tests. Reduces Lambda integration tests from 8 cases to 3. Slice 6 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add NewLambdaClientFunc package-level factory to internal/aws. GetLambdaPackageData uses the factory instead of creating a client directly. Tests replace the factory to inject a FakeLambdaClient. snapshotLambda_test.go now injects the fake in SetupTest and resets in TearDownTest. All test cases run without AWS credentials — the requireAuthToBeSet/SkipIfEnvVarUnset pattern is removed entirely. Also adds make test_smoke_aws target for running contract and smoke tests against real AWS before release. Slice 7 of fakes & contract tests work (#758). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lists the next steps for ECS, S3, Azure Apps, Docker, and Kubernetes following the pattern established by the Lambda work: interface → contract tests (real first) → fake → unit tests → factory injection into command tests → trim integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f8b2aa0 to
2ff108a
Compare
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
…nction Replace hard require.NotNil on NextMarker with a t.Skip guard so the real-AWS contract test skips cleanly instead of failing with a confusing nil-pointer message if the account ever has only one function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The field always returns null in production and is being removed from the API response (kosli-dev/server#5180). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…0 updates (#769) Bumps the go-dependencies group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://github.com/Azure/azure-sdk-for-go) | `1.10.1` | `1.13.1` | | [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `1.32.13` | `1.32.14` | | [github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager](https://github.com/aws/aws-sdk-go-v2) | `0.1.13` | `0.1.15` | | [github.com/aws/aws-sdk-go-v2/service/ecs](https://github.com/aws/aws-sdk-go-v2) | `1.75.0` | `1.77.0` | | [github.com/aws/aws-sdk-go-v2/service/lambda](https://github.com/aws/aws-sdk-go-v2) | `1.88.5` | `1.89.0` | | [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `1.24.2` | `1.24.3` | | [github.com/docker/docker](https://github.com/docker/docker) | `28.3.2+incompatible` | `28.5.2+incompatible` | | [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) | `1.15.1` | `1.15.2` | Updates `github.com/Azure/azure-sdk-for-go/sdk/azidentity` from 1.10.1 to 1.13.1 - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Commits](Azure/azure-sdk-for-go@sdk/azidentity/v1.10.1...sdk/azidentity/v1.13.1) Updates `github.com/aws/aws-sdk-go-v2/config` from 1.32.13 to 1.32.14 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@config/v1.32.13...config/v1.32.14) Updates `github.com/aws/aws-sdk-go-v2/credentials` from 1.19.13 to 1.19.14 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@credentials/v1.19.13...credentials/v1.19.14) Updates `github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager` from 0.1.13 to 0.1.15 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@feature/s3/transfermanager/v0.1.13...feature/s3/transfermanager/v0.1.15) Updates `github.com/aws/aws-sdk-go-v2/service/ecs` from 1.75.0 to 1.77.0 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@service/s3/v1.75.0...service/s3/v1.77.0) Updates `github.com/aws/aws-sdk-go-v2/service/lambda` from 1.88.5 to 1.89.0 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@service/s3/v1.88.5...service/s3/v1.89.0) Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.98.0 to 1.99.0 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@service/s3/v1.98.0...service/s3/v1.99.0) Updates `github.com/aws/smithy-go` from 1.24.2 to 1.24.3 - [Release notes](https://github.com/aws/smithy-go/releases) - [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md) - [Commits](aws/smithy-go@v1.24.2...v1.24.3) Updates `github.com/docker/docker` from 28.3.2+incompatible to 28.5.2+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](moby/moby@v28.3.2...v28.5.2) Updates `github.com/open-policy-agent/opa` from 1.15.1 to 1.15.2 - [Release notes](https://github.com/open-policy-agent/opa/releases) - [Changelog](https://github.com/open-policy-agent/opa/blob/v1.15.2/CHANGELOG.md) - [Commits](open-policy-agent/opa@v1.15.1...v1.15.2) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azidentity dependency-version: 1.13.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/credentials dependency-version: 1.19.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager dependency-version: 0.1.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/ecs dependency-version: 1.77.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/lambda dependency-version: 1.89.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.99.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/aws/smithy-go dependency-version: 1.24.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/docker/docker dependency-version: 28.5.2+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/open-policy-agent/opa dependency-version: 1.15.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ci-signed-commit-bot[bot] <247774526+ci-signed-commit-bot[bot]@users.noreply.github.com>
Add a fail-fast check in the release workflow pre-build job that rejects lightweight tags with a clear error message pointing to 'make release'. Also document the correct release process in CLAUDE.md. Discovered during the v2.16.0 release when 'gh release create' was used instead of 'make release', causing never-alone-trail failures.
) Helm deep-merges values overrides with chart defaults, so simply omitting runAsUser from a values file does not remove it from the rendered spec. The default of 1000 always survives. For OpenShift environments with SCC, users must explicitly set runAsUser: null. Updated values.yaml comments with a concrete example and explanation. Regenerated README.md and docs site via helm-docs.
…hard coded value server/#5354 (#792) * Dynamically fetch PR scan test data from GitHub artifact Test 21 was hardcoded to a specific SonarCloud PR scan that would break when SonarCloud housekeeping deletes old data. Instead, download the report-task.txt from a GitHub Actions artifact uploaded by the sonar-pr-trigger workflow in cyber-dojo/differ. Resolves kosli-dev/server#5354 (test 21) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Use dynamic PR scan data for tests 18, 20, and 26 Parse the downloaded report-task.txt to extract the PR key and CE task URL, replacing the hardcoded PR 359 references in tests that use --pull-request and --sonar-ce-task-url flags. Resolves kosli-dev/server#5354 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add GH_TOKEN to test workflow for sonar PR scan tests Sonar tests download a report-task.txt artifact from cyber-dojo/differ, which requires a GitHub token with actions:read scope on that repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: check for latest version * feat: check for latest version - fix issues * feat: check for latest version - address feedback * feat: check for latest version - address feedback2 * feat: check for latest version - add version flag and don't check versions for internal commands * feat: check for latest version - minor fixes * Update internal/version/update_check_test.go Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * feat: check for latest version - minor improvements * feat: check for latest version - minor nits * feat: check for latest version - add comment * feat: check for latest version - add more comments * feat: check for latest version - add more tests --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* chore: helm-docs GHA and use more modern version * Update .github/workflows/helm-chart.yml Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * chore: helm-docs GHA and use more modern version - ping action --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: ci-signed-commit-bot[bot] <247774526+ci-signed-commit-bot[bot]@users.noreply.github.com>
* Don't allow empty strings on either side of the dot in attestation name * Add tests for attestation name check * Add tests for name check to all attest commands * Fix PR tests
Bumps [github.com/moby/spdystream](https://github.com/moby/spdystream) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/moby/spdystream/releases) - [Commits](moby/spdystream@v0.5.0...v0.5.1) --- updated-dependencies: - dependency-name: github.com/moby/spdystream dependency-version: 0.5.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the go-dependencies group with 6 updates: | Package | From | To | | --- | --- | --- | | [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `1.21.0` | `1.21.1` | | [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `1.32.14` | `1.32.15` | | [github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager](https://github.com/aws/aws-sdk-go-v2) | `0.1.15` | `0.1.16` | | [github.com/aws/smithy-go](https://github.com/aws/smithy-go) | `1.24.3` | `1.25.0` | | [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) | `5.17.2` | `5.18.0` | | [k8s.io/kubernetes](https://github.com/kubernetes/kubernetes) | `1.35.3` | `1.35.4` | Updates `github.com/Azure/azure-sdk-for-go/sdk/azcore` from 1.21.0 to 1.21.1 - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Commits](Azure/azure-sdk-for-go@sdk/azcore/v1.21.0...sdk/azcore/v1.21.1) Updates `github.com/aws/aws-sdk-go-v2/config` from 1.32.14 to 1.32.15 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@config/v1.32.14...config/v1.32.15) Updates `github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager` from 0.1.15 to 0.1.16 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Commits](aws/aws-sdk-go-v2@feature/s3/transfermanager/v0.1.15...feature/s3/transfermanager/v0.1.16) Updates `github.com/aws/smithy-go` from 1.24.3 to 1.25.0 - [Release notes](https://github.com/aws/smithy-go/releases) - [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md) - [Commits](aws/smithy-go@v1.24.3...v1.25.0) Updates `github.com/go-git/go-git/v5` from 5.17.2 to 5.18.0 - [Release notes](https://github.com/go-git/go-git/releases) - [Commits](go-git/go-git@v5.17.2...v5.18.0) Updates `k8s.io/kubernetes` from 1.35.3 to 1.35.4 - [Release notes](https://github.com/kubernetes/kubernetes/releases) - [Commits](kubernetes/kubernetes@v1.35.3...v1.35.4) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azcore dependency-version: 1.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.32.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager dependency-version: 0.1.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies - dependency-name: github.com/aws/smithy-go dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: k8s.io/kubernetes dependency-version: 1.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the github-actions-dependencies group with 1 update: [step-security/harden-runner](https://github.com/step-security/harden-runner). Updates `step-security/harden-runner` from 2.17.0 to 2.18.0 - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](step-security/harden-runner@f808768...6c3c2f2) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
A stray `pageSize = int(*params.MaxItems)` and closing brace outside the if-block caused a syntax error. Remove the dangling lines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reds - fake_lambda.go: note that real AWS returns *types.ResourceNotFoundException - aws_test.go: rename skipOrSetCreds -> skipIfCredsUnset (it only skips) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: rename smoke test targets to contract test targets Rename `test_smoke_aws` → `test_contract_aws` and `test_smoke_github` → `test_contract_github` to use consistent "contract test" terminology throughout the Makefile and TODO.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add ADR for fakes and contract tests pattern Documents the decision to use in-memory fakes and contract tests to decouple the main integration test suite from live external services, first introduced for AWS Lambda (#763) and extended to GitHub (#807). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: address PR review comments on contract testing ADR - Add 'who can run the tests' as a third problem in Context (Jon's comment) - Clarify that the shared contract function runs against both fake and real adapter — this is the mechanism that guarantees they behave identically (Tooky's comment) - Expand Consequences to reflect the contributor access improvement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Context
Addresses #758. Our cloud provider integration tests all call real external services, causing flaky CI, slow feedback, and poor edge-case coverage. This PR establishes the pattern for fixing that, starting with AWS Lambda as the template.
The approach follows Steve's comment on #758: Fakes (not mocks) kept honest by contract tests that run the same suite against both the fake and the real implementation.
What this PR introduces
1.
LambdaAPIinterface (internal/aws/aws.go)A narrow interface scoped to the two SDK methods we actually call:
The real
*lambda.Clientsatisfies this implicitly — no adapter needed. Internal helpers (getFilteredLambdaFuncs,getAndProcessOneLambdaFunc,getLambdaPackageDataFromClient) accept the interface instead of concrete types.2.
FakeLambdaClient(internal/aws/fake_lambda.go)An in-memory implementation of
LambdaAPIwith:GetFunctionConfigurationErrfield for error injection in tests3. Contract test suite (
internal/aws/lambda_contract_test.go)A shared
runLambdaContractTests(t, client, existingFunctionName)function that exercises the behaviours we depend on:ListFunctionsreturns resultsMaxItems+MarkerGetFunctionConfigurationfor existing function returns config withCodeSha256andLastModifiedGetFunctionConfigurationfor missing function returns errorThis suite runs against both the fake (always) and real AWS (env-gated behind
AWS_ACCESS_KEY_ID). If both pass, the fake is a trustworthy stand-in. If the real side drifts, the contract catches it.4. Fake-backed unit tests (
internal/aws/aws_test.go)Filtering tests (11 cases):
IncludeNames,IncludeNamesRegex,ExcludeNames,ExcludeNamesRegex, combined filters, multi-page pagination with filtering, empty results, invalid regex errors.Orchestration tests (5 cases): Zip fingerprint decoding, Image raw
CodeSha256, concurrent multi-function processing, empty function list, error propagation fromGetFunctionConfiguration.These all run without AWS credentials and complete in milliseconds.
5. Package-level factory for command-test injection (
internal/aws/aws.go)GetLambdaPackageDatausesNewLambdaClientFuncinstead of creating a client directly. This follows the existing codebase pattern (package-level globals forlogger,kosliClient,global).6. Command tests decoupled from AWS (
cmd/kosli/snapshotLambda_test.go)SetupTestinjects aFakeLambdaClientseeded with two functions (cli-testsas Zip,cli-tests-dockeras Image).TearDownTestresets the factory. All 14 test cases now run without AWS credentials — therequireAuthToBeSet/SkipIfEnvVarUnsetpattern is removed entirely.7. Trimmed integration tests
Lambda integration tests reduced from 8 cases to 3 focused smoke tests (invalid credentials, one Zip happy path, one Image happy path). Filtering and orchestration logic is now covered by fast fake-backed tests.
8.
make test_smoke_awsMakefile targetRuns contract tests and smoke tests against real AWS — intended for use before releases:
Test layers after this PR
internal/aws/)internal/aws/)internal/aws/)make test_smoke_awsinternal/aws/)make test_smoke_awscmd/kosli/)make test_integrationNext steps (tracked in
TODO.md)This PR establishes the pattern. The remaining cloud providers follow the same structure:
Each is broken down into slices in
TODO.md.How to review
The commits are structured as thin vertical slices — each is independently reviewable:
7416da85— Pure refactoring: extractLambdaAPIinterface, change signaturesc9f70331— Contract test suite wired to real AWS9b892595—FakeLambdaClientpasses the same contractd3f7fa20— 11 fake-backed filtering/pagination tests4e9789f6— 5 fake-backed orchestration tests0c87bc5a— Trim integration tests from 8 → 3ef346fe0— Factory injection into command tests, remove credential gating🤖 Generated with Claude Code