@kensio/yulin-aws-simulation 1.18.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -3
- package/package.json +1 -1
- package/skills/yulin-aws-simulation/SKILL.md +113 -34
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "yulin-aws-simulation",
|
|
4
|
-
"version": "1.18.
|
|
4
|
+
"version": "1.18.1",
|
|
5
5
|
"description": "How to test AWS code well with the @kensio/yulin in-process simulator.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kensio Software",
|
package/README.md
CHANGED
|
@@ -91,9 +91,13 @@ authorised a Lambda function URL invocation against `lambda:InvokeFunctionUrl` a
|
|
|
91
91
|
CloudFront origin access control also needs `lambda:InvokeFunction`. The tests passed, the release
|
|
92
92
|
went out, and the endpoint 403'd in production.
|
|
93
93
|
|
|
94
|
-
**
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
**Share one simulation across the whole test suite.** Create one `SimAws`, deploy the application
|
|
95
|
+
once and install one `SimSdk` interception layer in Vitest setup. Run the suite in one shared worker
|
|
96
|
+
and module context. Tests use random resource names and identifiers while the simulated AWS state
|
|
97
|
+
stays alive, in the same way they would use an AWS account or a LocalStack container. This should be
|
|
98
|
+
the majority of the suite. Put tests that advance or change the shared simulation clock in a smaller
|
|
99
|
+
isolated group, with a fresh Yulin setup for each test. Per-test and per-file simulations are
|
|
100
|
+
supported for these exceptional cases.
|
|
97
101
|
|
|
98
102
|
**Run the handler as a real simulated Lambda.** Bind an in-process handler to a template function
|
|
99
103
|
and invoke the function through simulated Lambda. Its SDK calls are then routed into the simulation
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yulin-aws-simulation
|
|
3
|
-
description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code
|
|
3
|
+
description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code. Share one simulation and SDK interception layer across tests that leave simulated time alone, and isolate the minority that change the clock. Use Yulin directly, deploy CDK-synthesized templates, bind real handlers, intercept SDK clients, drive HTTP with SimAwsHttp, control time, read generated names, match service errors by name, and handle unsupported properties. Use when writing or reviewing tests that touch AWS, replacing SDK stubs, testing CDK stacks or CloudFront resources, simplifying Yulin test setup, or diagnosing unsupported templates and commands.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.18.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Testing with Yulin
|
|
@@ -13,6 +13,13 @@ network and no AWS account. This skill is how to use it well. It serves `isolate
|
|
|
13
13
|
the general argument for simulation over stubs. Each rule says what it buys, and a case that does
|
|
14
14
|
not want that trade can go the other way knowingly.
|
|
15
15
|
|
|
16
|
+
Treat Yulin as suite infrastructure, in the same way tests treat an AWS account or a LocalStack
|
|
17
|
+
container. Create one simulation, deploy the application once and install SDK interception once in
|
|
18
|
+
the Vitest suite setup. Every test talks to that shared environment. Randomised resource names and
|
|
19
|
+
identifiers keep tests independent while the simulated AWS state remains alive for the whole suite.
|
|
20
|
+
This is the default for tests that leave the simulation clock alone. Put the smaller set of tests
|
|
21
|
+
that change simulated time in an isolated group, with their own Yulin setup.
|
|
22
|
+
|
|
16
23
|
For the API read `node_modules/@kensio/yulin/llms.txt`. It indexes the 45 markdown pages beside it
|
|
17
24
|
under `node_modules/@kensio/yulin/docs/`, one per simulated service and per feature guide,
|
|
18
25
|
documenting the version installed. Open the page it names for the service in hand and grep it for
|
|
@@ -29,13 +36,13 @@ service, or a `beforeEach` that reassembles the world.
|
|
|
29
36
|
|
|
30
37
|
Yulin is built to be used directly. `new SimAws()` and `new SimSdk()` are plain constructors with no
|
|
31
38
|
side effects, no network, no cleanup and no awaiting. Service accessors take the same Command
|
|
32
|
-
objects the SDK does.
|
|
33
|
-
environment, and
|
|
39
|
+
objects the SDK does. The suite setup creates these objects once. `deployTemplateFile` is the
|
|
40
|
+
environment, and the tests import the shared simulation objects directly.
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
simulation back. A wrapper around any of those steps hides what the reader needs to see.
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
The setup constructs, deploys and intercepts. A test exercises the application and reads the shared
|
|
43
|
+
simulation back. A wrapper around any of those steps hides what the reader needs to see. The setup
|
|
44
|
+
can be a small module returning the simulation objects themselves. Once it wants a class, an options
|
|
45
|
+
interface or a directory, it has become a second product.
|
|
39
46
|
|
|
40
47
|
## One CDK app behind the tests, local dev and production
|
|
41
48
|
|
|
@@ -52,6 +59,9 @@ const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFi
|
|
|
52
59
|
await stack.waitForDeployComplete();
|
|
53
60
|
```
|
|
54
61
|
|
|
62
|
+
The test suite deploys this template once during its shared Yulin setup. Individual tests use the
|
|
63
|
+
deployed resources and do not deploy their own copies.
|
|
64
|
+
|
|
55
65
|
A hand-written template only tests itself, and a dev environment building its own buckets is a third
|
|
56
66
|
description whose drift shows up as a bug reproducing in one place of the three. Infrastructure
|
|
57
67
|
belongs in the CDK app even when only a test needs it.
|
|
@@ -155,7 +165,7 @@ their order in `stackNames` is what puts the value there in time.
|
|
|
155
165
|
from the simulation, and the code under test uses the SDK exactly as it does in production.
|
|
156
166
|
|
|
157
167
|
```typescript
|
|
158
|
-
|
|
168
|
+
const simSdk = new SimSdk({ simAws });
|
|
159
169
|
simSdk.intercept(SecretsManagerClient); // Every instance, including ones made later.
|
|
160
170
|
```
|
|
161
171
|
|
|
@@ -164,20 +174,22 @@ correctly. A stub has no naming rules and verifies no signatures. A malformed Se
|
|
|
164
174
|
or a wrongly computed Cognito `SECRET_HASH` passes it and fails in production.
|
|
165
175
|
|
|
166
176
|
Intercept the class in most cases, since the code under test usually constructs its own clients.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
Install that class interception once in the suite setup. A class interception is process-wide (it
|
|
178
|
+
shadows `send` on the class prototype) and refuses a second install while the first is live, with
|
|
179
|
+
`SimSdkAlreadyInterceptedError`. Repeated interception in `beforeEach` or in each test file is both
|
|
180
|
+
unnecessary and an error when those files share a worker. Intercept an instance only when one
|
|
181
|
+
specific client should reach Yulin.
|
|
171
182
|
|
|
172
183
|
Whichever it is, it has to be the client the code actually calls. A `DynamoDBDocumentClient` built
|
|
173
184
|
over a `DynamoDBClient` is what the code sends through, and the document client is the one to
|
|
174
185
|
intercept. Every Command routes to the simulation by default, and an allow list of Command classes
|
|
175
186
|
narrows that where something else should handle the rest.
|
|
176
187
|
|
|
177
|
-
`SimSdk`
|
|
178
|
-
|
|
179
|
-
`simSdk
|
|
180
|
-
|
|
188
|
+
Keep the suite's `SimSdk` alive for as long as the suite. Worker exit normally removes its
|
|
189
|
+
process-wide patches. Call `simSdk.restoreAll()` if the process will continue doing other work.
|
|
190
|
+
`using simSdk = new SimSdk()` and `interception.restore()` remain useful for a deliberately
|
|
191
|
+
short-lived simulation. Each `SimSdk` owns a `SimAws`, reachable as `simSdk.simAws`, and
|
|
192
|
+
`new SimSdk({ simAws })` shares an existing one.
|
|
181
193
|
|
|
182
194
|
### A fake accepts any request the simulator would refuse
|
|
183
195
|
|
|
@@ -213,6 +225,19 @@ it as well as the code's own arithmetic.
|
|
|
213
225
|
`simAws.clock().resume()` tracks the underlying clock and `simAws.clock().isFrozen` reports the
|
|
214
226
|
mode. Running mode suits a local dev server, and a test usually wants an advance.
|
|
215
227
|
|
|
228
|
+
### Give clock-changing tests their own simulation
|
|
229
|
+
|
|
230
|
+
The clock is part of a `SimAws` instance's state. Tests sharing that instance also share its current
|
|
231
|
+
time and whether it is frozen or running. One test advancing or resuming the clock therefore changes
|
|
232
|
+
the environment underneath every other test in that group. Randomised resource names cannot isolate
|
|
233
|
+
this change.
|
|
234
|
+
|
|
235
|
+
Most tests should use the suite simulation without changing its clock. Put tests that advance the
|
|
236
|
+
clock or change its mode in a separate Vitest project or file group. Give each of those tests a
|
|
237
|
+
fresh `SimAws`, deployment and interception layer. Each test can then control time without affecting
|
|
238
|
+
another test. A small group may share one isolated simulation when its cases deliberately follow the
|
|
239
|
+
same timeline.
|
|
240
|
+
|
|
216
241
|
## Assert by reading the simulation back
|
|
217
242
|
|
|
218
243
|
The simulation holds real state. After exercising the code, ask the service what happened:
|
|
@@ -224,7 +249,8 @@ const object = await simAws.s3().getObject(new GetObjectCommand({ Bucket: bucket
|
|
|
224
249
|
|
|
225
250
|
A call-count assertion holds only for today's implementation. A state assertion holds however the
|
|
226
251
|
handler is rewritten, and it fails if the call was made in a way the real service would have
|
|
227
|
-
rejected.
|
|
252
|
+
rejected. In the shared suite environment, read the resource named by the test's random identifier.
|
|
253
|
+
Do not list the whole service and assume no other test has left state there.
|
|
228
254
|
|
|
229
255
|
The accessors sit on more than one scope. `simAws.region(name)` carries some of the services and
|
|
230
256
|
`simAws.region(name).account()` carries all of them (`logs()` among the account-only ones), so look
|
|
@@ -303,33 +329,86 @@ costing nothing but convenience as well, once it is forcing structural duplicati
|
|
|
303
329
|
A workaround kept while the issue is open wants a comment naming that issue and a revisit when it
|
|
304
330
|
closes. Re-read those claims on each upgrade. They are the ones nothing tests.
|
|
305
331
|
|
|
306
|
-
##
|
|
332
|
+
## Share one simulation across the whole test suite
|
|
333
|
+
|
|
334
|
+
The recommended lifecycle is one `SimAws`, one deployment and one `SimSdk` interception layer for
|
|
335
|
+
the tests that do not change simulated time. This should be most of the test suite. Do this in
|
|
336
|
+
Vitest setup. Tests should treat the result as a long-running AWS account or LocalStack container
|
|
337
|
+
whose state survives every test and test file.
|
|
338
|
+
|
|
339
|
+
An in-process simulation needs the test files to share a worker and module cache. Set
|
|
340
|
+
`fileParallelism: false` and `isolate: false`, then load one setup module through `setupFiles`:
|
|
307
341
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
342
|
+
```typescript
|
|
343
|
+
// vitest.config.ts
|
|
344
|
+
export default defineConfig({
|
|
345
|
+
test: {
|
|
346
|
+
fileParallelism: false,
|
|
347
|
+
isolate: false,
|
|
348
|
+
setupFiles: ["./test/yulin.setup.ts"],
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Keep the one-time work in an imported module. Vitest executes a setup file before each test file,
|
|
354
|
+
even with isolation disabled, but imported modules stay cached in the shared worker.
|
|
311
355
|
|
|
312
356
|
```typescript
|
|
313
|
-
|
|
357
|
+
// test/yulin-suite.ts
|
|
358
|
+
const ready = (async () => {
|
|
359
|
+
const simAws = new SimAws();
|
|
360
|
+
const simSdk = new SimSdk({ simAws });
|
|
361
|
+
|
|
362
|
+
simSdk.intercept(DynamoDBClient);
|
|
363
|
+
simSdk.intercept(S3Client);
|
|
314
364
|
|
|
315
|
-
beforeAll(async () => {
|
|
316
|
-
simAws = new SimAws();
|
|
317
365
|
const stack = await simAws.cloudFormation().deployTemplateFile({
|
|
318
366
|
templatePath: "cdk.out/SiteStack.template.json",
|
|
319
367
|
});
|
|
320
368
|
await stack.waitForDeployComplete();
|
|
321
|
-
});
|
|
322
369
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
370
|
+
return { simAws, simSdk, stack };
|
|
371
|
+
})();
|
|
372
|
+
|
|
373
|
+
export function yulinSuite() {
|
|
374
|
+
return ready;
|
|
375
|
+
}
|
|
327
376
|
```
|
|
328
377
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
378
|
+
```typescript
|
|
379
|
+
// test/yulin.setup.ts
|
|
380
|
+
import { yulinSuite } from "./yulin-suite";
|
|
381
|
+
|
|
382
|
+
await yulinSuite();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Tests import `yulinSuite()` and work against that same state. Give every test data a random name or
|
|
386
|
+
identifier, query by that identifier and never assume a service starts empty. A missing-resource
|
|
387
|
+
case uses a new identifier that no test created. A count assertion records the relevant count before
|
|
388
|
+
the action when it cannot query by identifier. Tests remain independent without clearing tables,
|
|
389
|
+
buckets or queues between them.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const { simAws } = await yulinSuite();
|
|
393
|
+
const key = `uploads/${faker.string.uuid()}.png`;
|
|
394
|
+
|
|
395
|
+
// Exercise the application, then read this test's object from shared S3 state.
|
|
396
|
+
const object = await simAws.s3().getObject({ input: { Bucket: bucket, Key: key } });
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Do not put the simulation in Vitest `globalSetup`. That hook runs outside the test workers and can
|
|
400
|
+
only pass serializable values into them. `setupFiles` runs in the test worker and can share the live
|
|
401
|
+
`SimAws` object through the cached module above.
|
|
402
|
+
|
|
403
|
+
This shared setup also shares the simulation clock. Keep tests that call `advanceBy`, resume the
|
|
404
|
+
clock or otherwise change simulated time out of this group. Put them in a separate Vitest project or
|
|
405
|
+
file pattern, such as `*.clock.iso.test.ts`, and give each test an isolated Yulin setup. The split
|
|
406
|
+
keeps the common suite setup cheap while allowing the smaller clock-testing group to control time.
|
|
407
|
+
|
|
408
|
+
Creating Yulin once per test or once per file is supported. Reserve it for a case that deliberately
|
|
409
|
+
tests an entire fresh account, an independent clock or incompatible interception. Use the shared
|
|
410
|
+
setup by default. A `beforeEach` or file-level `beforeAll` that deploys the application again
|
|
411
|
+
usually turns random test data into repeated infrastructure work.
|
|
333
412
|
|
|
334
413
|
## Bind a handler and run it as a real simulated Lambda
|
|
335
414
|
|