Summary
Logger, Metrics and Batch keep per-invocation state in the Lambda InvokeStore when AWS_LAMBDA_MAX_CONCURRENCY is set. The unit tests for that behaviour, in each package's tests/unit/concurrency/ folder, depend on two properties of @aws/lambda-invoke-store that are not written down anywhere in the repo:
- The global is published lazily.
globalThis.awslambda.InvokeStore, which our stores read, only exists after something has called InvokeStore.getInstanceAsync(). In a real function the runtime does this at startup; in tests nothing does it for you.
- The instance is cached at module level and its type is fixed on first creation. If
AWS_LAMBDA_MAX_CONCURRENCY is in process.env at the time of the first getInstanceAsync() call you get the AsyncLocalStorage-backed store that isolates invocations. Otherwise you get a single-context store whose run() overwrites one field and provides no isolation. Later calls return the cached instance regardless of the environment.
This is why packages/testing/src/setupEnv.ts sets AWS_LAMBDA_BENCHMARK_MODE=1 (it makes the package expose InvokeStore._testing.reset()), why every concurrency test calls reset() in beforeEach, and why sequence() in testing-utils calls getInstanceAsync() before running the invocations. It is also why tests that touch invocation-scoped state before sequence() runs (a constructor writing a default, or a test of the fallback to shared state outside any invocation) need an explicit await InvokeStore.getInstanceAsync() placed after the env stub. #5615 adds several of these and the reason is not obvious from the diff.
The trap is that getting the order wrong does not error. Calling getInstanceAsync() before the env stub, e.g. by hoisting it into a beforeEach that runs ahead of a per-test vi.stubEnv, caches the non-isolating store and the tests fail on assertions because one invocation reads the other's state. getInstanceAsync(true) forces the isolating store if a single hoisted call is wanted.
Why is this needed?
None of this is discoverable from our code; it has to be re-derived from the @aws/lambda-invoke-store source each time, which came up again while reviewing #5615. Anyone adding a store, writing a new concurrency test, or adapting to a change in how the RIC exposes the InvokeStore needs to know it, and so does any LLM-based tooling working in the repo.
We should document this where both contributors and coding agents will find it. Since #5618 retired the docs/contributing/ pages, unit test conventions live in the "Unit tests" section of CODING_STANDARDS.md, which CONTRIBUTING.md names as the source of truth and AGENTS.md instructs agents to read before touching tests. That makes it the natural home, and its "Environment" list already covers the related vi.stubEnv() / vi.unstubAllEnvs() rule.
Which area does this relate to?
Tests, Commons, Logger, Metrics, Batch Processing
Solution
Add a short subsection or bullet group to the "Unit tests" section of CODING_STANDARDS.md covering the mechanism above, the reset() / getInstanceAsync() ordering rule with a minimal example, and the vi.stubGlobal('awslambda', undefined) / vi.unstubAllGlobals() pairing used by the "InvokeStore is not available" tests. Keep it in the same terse, rule-per-bullet style as the rest of that file, and consider a one-line pointer to it from the sequence() JSDoc in packages/testing/src/helpers.ts and a comment on the AWS_LAMBDA_BENCHMARK_MODE line in setupEnv.ts, since those are where someone hits the problem.
Acknowledgment
Future readers
Please react with 👍 and your use case to help us understand customer demand.
Summary
Logger, Metrics and Batch keep per-invocation state in the Lambda
InvokeStorewhenAWS_LAMBDA_MAX_CONCURRENCYis set. The unit tests for that behaviour, in each package'stests/unit/concurrency/folder, depend on two properties of@aws/lambda-invoke-storethat are not written down anywhere in the repo:globalThis.awslambda.InvokeStore, which our stores read, only exists after something has calledInvokeStore.getInstanceAsync(). In a real function the runtime does this at startup; in tests nothing does it for you.AWS_LAMBDA_MAX_CONCURRENCYis inprocess.envat the time of the firstgetInstanceAsync()call you get theAsyncLocalStorage-backed store that isolates invocations. Otherwise you get a single-context store whoserun()overwrites one field and provides no isolation. Later calls return the cached instance regardless of the environment.This is why
packages/testing/src/setupEnv.tssetsAWS_LAMBDA_BENCHMARK_MODE=1(it makes the package exposeInvokeStore._testing.reset()), why every concurrency test callsreset()inbeforeEach, and whysequence()in testing-utils callsgetInstanceAsync()before running the invocations. It is also why tests that touch invocation-scoped state beforesequence()runs (a constructor writing a default, or a test of the fallback to shared state outside any invocation) need an explicitawait InvokeStore.getInstanceAsync()placed after the env stub. #5615 adds several of these and the reason is not obvious from the diff.The trap is that getting the order wrong does not error. Calling
getInstanceAsync()before the env stub, e.g. by hoisting it into abeforeEachthat runs ahead of a per-testvi.stubEnv, caches the non-isolating store and the tests fail on assertions because one invocation reads the other's state.getInstanceAsync(true)forces the isolating store if a single hoisted call is wanted.Why is this needed?
None of this is discoverable from our code; it has to be re-derived from the
@aws/lambda-invoke-storesource each time, which came up again while reviewing #5615. Anyone adding a store, writing a new concurrency test, or adapting to a change in how the RIC exposes theInvokeStoreneeds to know it, and so does any LLM-based tooling working in the repo.We should document this where both contributors and coding agents will find it. Since #5618 retired the
docs/contributing/pages, unit test conventions live in the "Unit tests" section ofCODING_STANDARDS.md, whichCONTRIBUTING.mdnames as the source of truth andAGENTS.mdinstructs agents to read before touching tests. That makes it the natural home, and its "Environment" list already covers the relatedvi.stubEnv()/vi.unstubAllEnvs()rule.Which area does this relate to?
Tests, Commons, Logger, Metrics, Batch Processing
Solution
Add a short subsection or bullet group to the "Unit tests" section of
CODING_STANDARDS.mdcovering the mechanism above, thereset()/getInstanceAsync()ordering rule with a minimal example, and thevi.stubGlobal('awslambda', undefined)/vi.unstubAllGlobals()pairing used by the "InvokeStore is not available" tests. Keep it in the same terse, rule-per-bullet style as the rest of that file, and consider a one-line pointer to it from thesequence()JSDoc inpackages/testing/src/helpers.tsand a comment on theAWS_LAMBDA_BENCHMARK_MODEline insetupEnv.ts, since those are where someone hits the problem.Acknowledgment
Future readers
Please react with 👍 and your use case to help us understand customer demand.