Skip to content

fix(ls-api): align mock with LocalStack API and add regression test#101

Draft
joe4dev wants to merge 12 commits into
localstackfrom
localstack-api-compat-test
Draft

fix(ls-api): align mock with LocalStack API and add regression test#101
joe4dev wants to merge 12 commits into
localstackfrom
localstack-api-compat-test

Conversation

@joe4dev
Copy link
Copy Markdown
Member

@joe4dev joe4dev commented Jun 2, 2026

Summary

The LocalStack ↔ RIE API contract (JSON field names, HTTP status codes, endpoint paths) is unversioned. A silent rename or status code change is a breaking change with no safe rollback, especially in Kubernetes deployments where LocalStack and the Lambda container images can be at mismatched versions.

This PR pins the contract with tests and adds a CI workflow that catches integration regressions before they reach the LocalStack main pipeline.

Changes

cmd/localstack/custom_interop_test.go — contract tests (moved to production code)

Unit tests that pin the exact field names and routing logic of the LS↔RIE API:

  • JSON field names on InvokeRequest (invoke-id, invoked-function-arn, payload, trace-id)
  • JSON field name on LogResponse (logs)
  • SendStatus routes to POST /status/{runtime_id}/ready and /error
  • SendLogs marshals with the "logs" key
  • SendResult routes to /response on success and /error on failure (both explicit flag and body inspection)

cmd/ls-api/ — mock aligned with executor_endpoint.py and automated

The mock was drifting from the real LocalStack API. Fixed:

  • All handlers return 202 Accepted (was 200 OK)
  • InvokeRequest gains the missing invoked-function-arn and trace-id fields
  • invokeErrorHandler and invokeLogsHandler now read and log request bodies
  • log.Fatal in the status goroutine downgraded to log.Error
  • /test trigger endpoint renamed to /success for clarity
  • handler.py raises on {"fail": ...} so the error path actually exercises invokeErrorHandler

New smoke-test.sh + Makefile targets (start-rie-detached, smoke-test) run a full end-to-end check locally and in CI: build RIE → start ls-api mock → verify a success invocation (auto-triggered on ready) → verify an error invocation → clean up.

.github/workflows/ls-smoke-tests.yml — new CI workflow

Runs on every push and PR to the localstack branch. Three steps: checkout, set up Go, make smoke-test.

Tests

I manually tested the ls-api using the make targets and confirm it works as expected.

The invoke-idrequest-id rename was used to validate test effectiveness:

Test Catches regression?
TestInvokeRequestContract (unit) ✅ Fails immediately — field deserializes to ""
make smoke-test (e2e) ❌ False green — RIE still returns results; empty invoke ID is silent

The unit tests in custom_interop_test.go are the effective guard for contract regressions. The smoke test complements them by catching startup failures, binary crashes, and end-to-end invocation flow breakage.

…on tests

- Return 202 Accepted from all handlers to match executor_endpoint.py
- Add missing InvokedFunctionArn and TraceId fields to InvokeRequest
- Parse {"logs":"..."} JSON in invokeLogsHandler instead of raw bytes
- Read and log request body in invokeErrorHandler
- Downgrade log.Fatal to log.Error in statusHandler goroutine
- Add main_test.go with 7 regression tests covering all endpoints,
  JSON field names, and the async invoke triggered on status/ready

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@joe4dev joe4dev changed the title fix(ls-api): align mock with LocalStack API contract and add regressi… fix(ls-api): align mock with LocalStack API and add regression test Jun 2, 2026
@joe4dev joe4dev marked this pull request as ready for review June 3, 2026 07:51
Copy link
Copy Markdown
Member

@dfangl dfangl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure what the purpose of this PR is - where is this ls-api mock used? It definitely improves the mock, but I don't quite understand where the mock itself is used and how we use it to test regressions?

@joe4dev joe4dev marked this pull request as draft June 3, 2026 10:09
@joe4dev
Copy link
Copy Markdown
Member Author

joe4dev commented Jun 3, 2026

I'm not quite sure what the purpose of this PR is - where is this ls-api mock used? It definitely improves the mock, but I don't quite understand where the mock itself is used and how we use it to test regressions?

Sorry for the noise. You're spot on. Converted back to draft.

The test is not even using the mock nor the other code 🤦

@dfangl
Copy link
Copy Markdown
Member

dfangl commented Jun 3, 2026

The test is not even using the mock nor the other code 🤦

It's using the handlers of the mock at least, but that is about it :)

joe4dev and others added 11 commits June 3, 2026 13:40
The previous tests only exercised cmd/ls-api (a manual testing tool),
so a change to the actual RIE production code in cmd/localstack would
not have been caught.

- Extract SendLogs and SendResult from the inline goroutine into
  LocalStackAdapter methods, making the API call sites testable
- Add cmd/localstack/custom_interop_test.go with 8 contract tests
  that drive the production types and methods directly:
  InvokeRequest/LogResponse JSON field names, SendStatus URL routing,
  SendLogs format, SendResult response-vs-error routing
- Remove the two misleading JSON contract tests from cmd/ls-api —
  they tested a separate struct copy and gave false confidence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t mock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…acOS

- Makefile: build-rie (Go cross-compilation), start-mock (native go run),
  start-rie (Docker; exposes port 9563 so host mock can reach RIE /invoke)
- handler.py: minimal Python Lambda function used by start-rie
- README: replace raw Linux binary instructions with make commands,
  add prerequisites section and ARCH=arm64 note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xraydaemon.go calls GetEnvOrDie("AWS_REGION") unconditionally at startup
regardless of whether X-Ray telemetry is enabled, causing an immediate panic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… startup log

- /test and /fail endpoints now log errors instead of exiting on connection failure,
  consistent with the same fix applied earlier to statusHandler
- Log the listen port on startup for easier debugging
- Update README-LOCALSTACK.md: remove "likely outdated" label, link to ls-api README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation

Adds a smoke-test.sh script and Makefile target that build the RIE and
ls-api mock, run both, verify a successful and a failing Lambda invocation
against the mock endpoint, then clean up. Used in the new ls-smoke-tests
CI workflow (.github/workflows/ls-smoke-tests.yml).

Also renames the /test trigger endpoint to /success, adds structured
logging to invokeResponseHandler/invokeErrorHandler for reliable grepping,
updates handler.py to raise on {"fail": ...} so the error path is
exercised, and builds both binaries into bin/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bug output

172.17.0.1 is unreachable from Docker containers on macOS (Docker Desktop
runs inside a VM). Switch to host.docker.internal on Darwin and keep
172.17.0.1 for Linux where it is the standard bridge gateway.

On timeout, also print RIE container status and logs alongside the ls-api
log so failures are easier to diagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the uname conditional (host.docker.internal vs 172.17.0.1) with
--add-host=host.docker.internal:host-gateway, which Docker resolves to the
correct gateway IP on both Linux and macOS. Single address, no branching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… output

Two fixes:

1. Add --platform linux/amd64 to docker run so Docker always pulls the
   x86_64 image. On Apple Silicon, Docker otherwise selects the arm64
   image, causing an exec format error when mounting the linux/amd64 RIE
   binary -- the container exits immediately without sending any callbacks.

2. Drop --rm and do explicit docker rm in cleanup instead. With --rm the
   container and its logs are deleted on exit before wait_for_log can
   retrieve them, making failures invisible. Without --rm, docker logs and
   docker inspect work correctly even after the container has stopped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r flags

Two changes:

1. Add AWS_LAMBDA_FUNCTION_VERSION (and --platform/--add-host) to a shared
   RIE_DOCKER_OPTS variable in the Makefile. smoke-test.sh was missing this
   env var, causing a startup panic in the RIE. RIE_DOCKER_OPTS uses
   deferred assignment (=) so $$LATEST survives to recipe expansion time.

2. Replace the duplicated docker_opts array in smoke-test.sh with a call
   to the new start-rie-detached Makefile target. Both start-rie and
   start-rie-detached now use the same RIE_DOCKER_OPTS, so env vars
   stay in sync automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
}

// SendLogs posts the captured invocation logs to LocalStack.
func (l *LocalStackAdapter) SendLogs(invokeId string, logs LogResponse) error {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the main refactoring changes to LS RIE production code. Extracting SendLogs and SendResult is needed for testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants