@kensio/skills 1.17.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/package.json +1 -1
- package/skills/dynamodb-single-table/SKILL.md +1 -1
- package/skills/github-issue-drafting/SKILL.md +1 -1
- package/skills/isolated-testing-style/SKILL.md +1 -1
- package/skills/pangram-check/SKILL.md +1 -1
- package/skills/part-factory-test-data/SKILL.md +1 -1
- package/skills/skill-template/SKILL.md +1 -1
- package/skills/technical-prose-style/SKILL.md +1 -1
- package/skills/yulin-aws-simulation/SKILL.md +209 -113
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ name: dynamodb-single-table
|
|
|
3
3
|
description: Model data in Amazon DynamoDB as one table by default, reading the current AWS guidance before committing to a schema, writing the access patterns down before any keys exist, holding every entity type in one table, overloading generic partition and sort keys across those types, overloading and sparsifying secondary indexes, and splitting items by write rate. Use when designing or reviewing a DynamoDB schema, when a CDK stack is about to gain a second table, when an entity needs a query it has no key for, when application code fetches from two tables to assemble one response, when a Scan or a filter expression appears, when a partition runs hot or a write throttles, and when asked how to model users, orders, events or tenants in DynamoDB.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Single-table design in DynamoDB
|
|
@@ -3,7 +3,7 @@ name: github-issue-drafting
|
|
|
3
3
|
description: Draft a GitHub issue from a short note or a rough idea, grounding every claim about the code in the repository the issue will be filed against, splitting work that is really two issues, and filing it with `gh` once the user has approved the draft. Use when turning a to-do item, a Slack message, a code TODO, a failing test or a bug report into an issue, when asked to "write up an issue for" something, to "raise", "file" or "open an issue", when asked whether something should be one issue or several, and when tidying the titles, types or labels of issues that already exist.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# GitHub issue drafting
|
|
@@ -3,7 +3,7 @@ name: isolated-testing-style
|
|
|
3
3
|
description: Write tests that start from given/when/then, use real collaborators through simulation rather than stubs and mocks, take their isolation from randomised data rather than shared setup and teardown, and assert behaviour rather than call counts. Use when writing or reviewing tests, when a test needs a collaborator faked, when reaching for a mock, spy, `toHaveBeenCalledWith`, `beforeEach`/`afterEach` fixtures or a hardcoded expected hash, when test setup has grown tangled, and when asked "how should I test this?".
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Isolated testing style
|
|
@@ -4,7 +4,7 @@ description: Send the prose of a finished document to Pangram, a commercial AI-t
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
compatibility: Needs a Pangram API key and network access. Every run is billed.
|
|
6
6
|
metadata:
|
|
7
|
-
version: "1.
|
|
7
|
+
version: "1.18.1"
|
|
8
8
|
disable-model-invocation: true
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -3,7 +3,7 @@ name: part-factory-test-data
|
|
|
3
3
|
description: Build test data with @kensio/part-factory, keeping in the factory everything a test does not care about, passing dependencies at call time so factories stay independent, and choosing between StaticFactory, DynamicFactory, VariantFactory, MappedFactory and AsyncMappedFactory. Use when writing test fixtures or builders, when a test file is full of object literals, when a shared event, message or payload shape is being hand-written, when a required field is about to be made optional to ease test setup, and when tempted to wrap a factory in a helper function that applies overrides.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Building test data with Part Factory
|
|
@@ -3,7 +3,7 @@ name: skill-template
|
|
|
3
3
|
description: Scaffold a new agent skill, writing the SKILL.md and its frontmatter to the Agent Skills specification, then wrapping it as a plugin in this repo with package.json, plugin.json and a marketplace entry. Use when the user asks to "add a new skill", "create a skill", "write a SKILL.md" or "start a new plugin", and when checking whether an existing skill is portable between agents.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Skill template
|
|
@@ -3,7 +3,7 @@ name: technical-prose-style
|
|
|
3
3
|
description: Write documentation, READMEs, code comments, commit messages, release notes and PR text as plain technical prose, by removing the constructions that make unedited LLM prose tiring to read. It targets significance tails, contrastive definition, negation framing, appositive tails, colon explainers and a vocabulary that keeps renaming the same thing. Ships a script that scores prose against Django, Go, Rust and Python documentation. Use when writing or editing docs, a README, a changelog, a blog post or any prose for human readers, when asked to improve, tighten or rewrite writing, when prose "sounds like AI" or "sounds like Claude", and when reviewing documentation in a pull request.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.18.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Technical prose style
|
|
@@ -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.
|
|
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,18 +36,19 @@ 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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
second product.
|
|
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.
|
|
40
46
|
|
|
41
|
-
## One
|
|
47
|
+
## One CDK app behind the tests, local dev and production
|
|
42
48
|
|
|
43
|
-
Describe the infrastructure once in CDK and let the synthesized output drive all three
|
|
49
|
+
Describe the infrastructure once in CDK and let the synthesized output drive all three. Deploy the
|
|
50
|
+
template files `cdk synth` wrote, one of them or all of them, and talk to the simulation the way
|
|
51
|
+
production talks to AWS.
|
|
44
52
|
|
|
45
53
|
```typescript
|
|
46
54
|
const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFile({
|
|
@@ -51,10 +59,12 @@ const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFi
|
|
|
51
59
|
await stack.waitForDeployComplete();
|
|
52
60
|
```
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
only a
|
|
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
|
+
|
|
65
|
+
A hand-written template only tests itself, and a dev environment building its own buckets is a third
|
|
66
|
+
description whose drift shows up as a bug reproducing in one place of the three. Infrastructure
|
|
67
|
+
belongs in the CDK app even when only a test needs it.
|
|
58
68
|
|
|
59
69
|
**Do not hand-roll a wrapper that reads the file and calls `deployTemplate`.** `deployTemplateFile`
|
|
60
70
|
locates the cloud assembly beside the file, and that is how staged CDK assets resolve. A wrapper
|
|
@@ -76,14 +86,32 @@ await simAws.cloudFormation().deployTemplateFile({
|
|
|
76
86
|
});
|
|
77
87
|
```
|
|
78
88
|
|
|
79
|
-
|
|
80
|
-
had something to watch. The watched file is the one CDK wrote, and the adaptation re-applies on
|
|
81
|
-
every read.
|
|
89
|
+
The watched file is the one CDK wrote, and the adaptation re-applies on every read.
|
|
82
90
|
|
|
83
91
|
`stack.output("SiteBucketName")` answers a resolved Output narrowed to a string, throwing on one the
|
|
84
|
-
template never declared. Do not hand-roll that reader.
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
template never declared. Do not hand-roll that reader. A failed `cdk synth` leaves the previous
|
|
93
|
+
template in `cdk.out` with the tests still passing against it, so check the synthesized JSON changed
|
|
94
|
+
before concluding anything from a construct change.
|
|
95
|
+
|
|
96
|
+
### Read a generated name back, never write one out
|
|
97
|
+
|
|
98
|
+
A Resource whose template leaves its name out is named `<stack name>-<logical ID>-<tail>`, as an
|
|
99
|
+
account names one. The tail is twelve lowercase hex characters derived from the other two parts,
|
|
100
|
+
standing in for the twelve random ones real CloudFormation appends. Where the two parts overrun the
|
|
101
|
+
service's limit, thirteen characters of it go to the tail and its hyphen and the rest is shared
|
|
102
|
+
between them (a 64 character limit leaves 25 each). The trimmed stack name is what an IAM policy
|
|
103
|
+
scoped by resource prefix matches, and a deploy Role allowed an action on `MyVeryLongStackName-*` is
|
|
104
|
+
refused here as an account refuses it.
|
|
105
|
+
|
|
106
|
+
The same template under the same stack name generates the same name every time. That is the one
|
|
107
|
+
property tempting a test to write the name out. Read it back instead, from a `Ref`, from an
|
|
108
|
+
`Fn::GetAtt` attribute or from the accessor of the service holding it.
|
|
109
|
+
[Names CloudFormation generates](https://yulinsim.dev/services/cloudformation/#names-cloudformation-generates)
|
|
110
|
+
covers the tail and the trimming.
|
|
111
|
+
|
|
112
|
+
An ECS container binding has no way round this. The family CloudFormation generates for an unnamed
|
|
113
|
+
task definition ends in a tail only a deployment reveals. Name the task definition Resource with the
|
|
114
|
+
`logicalId` binding form.
|
|
87
115
|
|
|
88
116
|
### Register what the app looks up, deploy what the app creates
|
|
89
117
|
|
|
@@ -93,12 +121,11 @@ simulated resource carrying it comes from. Yulin stands one up at a chosen id wi
|
|
|
93
121
|
`simAws.acm().registerCertificate({ arn, domainName })`,
|
|
94
122
|
`simAws.cognitoIdentityProvider().registerUserPool({ id, name })` and `registerUserPoolClient`.
|
|
95
123
|
|
|
96
|
-
A registration creates a resource in place of the app creating one
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
deployed resource cannot be given the id its template names.
|
|
124
|
+
A registration creates a resource in place of the app creating one. It suits `HostedZone.fromLookup`
|
|
125
|
+
or a certificate issued by hand outside the app. A resource some stack in the same app creates wants
|
|
126
|
+
deploying instead, since a registration takes its configuration from somewhere other than the
|
|
127
|
+
deployed template. Substitute in a `transform` only where a deployed resource cannot be given the id
|
|
128
|
+
its template names.
|
|
102
129
|
|
|
103
130
|
Route 53 needs least of this. An `AWS::Route53::RecordSet` naming a hosted zone id no zone holds
|
|
104
131
|
gets one registered under that id as the record is created, taking its name from the records naming
|
|
@@ -128,9 +155,9 @@ const stacks = await simAws.cloudFormation().deployCdkOut({
|
|
|
128
155
|
`stackNames` picks part of an assembly, which most apps need, since most also synthesize a
|
|
129
156
|
deployment pipeline. `stackOptions` carries the `bindings`, `parameters` and `transform` that
|
|
130
157
|
`deployTemplateFile` takes for one template, keyed the same way. Its transform is handed the Stacks
|
|
131
|
-
the same call
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
the same call already deployed, so a Stack consuming a sibling's value stays inside one call. Two
|
|
159
|
+
Stacks passing a plain string between them declare no dependency for the manifest to carry, and
|
|
160
|
+
their order in `stackNames` is what puts the value there in time.
|
|
134
161
|
|
|
135
162
|
## Intercept real SDK clients, never hand-roll stubs
|
|
136
163
|
|
|
@@ -138,7 +165,7 @@ carry, and the order they are named in is what puts the value there in time.
|
|
|
138
165
|
from the simulation, and the code under test uses the SDK exactly as it does in production.
|
|
139
166
|
|
|
140
167
|
```typescript
|
|
141
|
-
|
|
168
|
+
const simSdk = new SimSdk({ simAws });
|
|
142
169
|
simSdk.intercept(SecretsManagerClient); // Every instance, including ones made later.
|
|
143
170
|
```
|
|
144
171
|
|
|
@@ -147,33 +174,31 @@ correctly. A stub has no naming rules and verifies no signatures. A malformed Se
|
|
|
147
174
|
or a wrongly computed Cognito `SECRET_HASH` passes it and fails in production.
|
|
148
175
|
|
|
149
176
|
Intercept the class in most cases, since the code under test usually constructs its own clients.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
154
182
|
|
|
155
183
|
Whichever it is, it has to be the client the code actually calls. A `DynamoDBDocumentClient` built
|
|
156
184
|
over a `DynamoDBClient` is what the code sends through, and the document client is the one to
|
|
157
185
|
intercept. Every Command routes to the simulation by default, and an allow list of Command classes
|
|
158
186
|
narrows that where something else should handle the rest.
|
|
159
187
|
|
|
160
|
-
`SimSdk`
|
|
161
|
-
|
|
162
|
-
`simSdk
|
|
163
|
-
|
|
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.
|
|
164
193
|
|
|
165
194
|
### A fake accepts any request the simulator would refuse
|
|
166
195
|
|
|
167
196
|
A fake S3 client stubbing `send` with canned `ListObjectsV2` pages, asserted on through the
|
|
168
197
|
continuation tokens it recorded, passes for code that built its command without a `Bucket`.
|
|
169
|
-
Simulated S3
|
|
170
|
-
all apply, `IsTruncated` and `NextContinuationToken` come back as the service sends them, and
|
|
198
|
+
Simulated S3 paginates for real, down to the ETag a real multipart upload produces, and
|
|
171
199
|
`configureMaxKeysPerPage` lowers the page size so that a bucket of two objects makes a caller walk a
|
|
172
|
-
continuation.
|
|
173
|
-
|
|
174
|
-
The residue is small. A couple of answers the service never sends (a truncated page naming no
|
|
175
|
-
continuation token) can only come from a fake, and a test reaching for one should say so in a
|
|
176
|
-
comment.
|
|
200
|
+
continuation. The residue is small, and a test reaching for a fake to get an answer the service
|
|
201
|
+
never sends should say so in a comment.
|
|
177
202
|
|
|
178
203
|
## Freeze the clock and advance it deliberately
|
|
179
204
|
|
|
@@ -189,17 +214,30 @@ const simAws = new SimAws({
|
|
|
189
214
|
await simAws.clock().advanceBy({ minutes: 20 });
|
|
190
215
|
```
|
|
191
216
|
|
|
192
|
-
Time-dependent behaviour then becomes something a test asserts on in microseconds
|
|
193
|
-
the simulation keys off that clock. EventBridge rules and Scheduler schedules fire only when time
|
|
194
|
-
advanced past them, DynamoDB items pass their TTL, Secrets Manager deletions come due,
|
|
195
|
-
sessions expire, Lambda event source mappings re-poll, and inside a simulated Lambda
|
|
196
|
-
and `new Date()` report simulated time.
|
|
197
|
-
unnecessary here, since advancing this one exercises the real expiry rules of the services around
|
|
198
|
-
as well as the code's own arithmetic.
|
|
217
|
+
Time-dependent behaviour then becomes something a test asserts on in microseconds, and a good deal
|
|
218
|
+
of the simulation keys off that clock. EventBridge rules and Scheduler schedules fire only when time
|
|
219
|
+
is advanced past them, DynamoDB items pass their TTL, Secrets Manager deletions come due,
|
|
220
|
+
`AssumeRole` sessions expire, Lambda event source mappings re-poll, and inside a simulated Lambda
|
|
221
|
+
`Date.now()` and `new Date()` report simulated time. The clock stub `isolated-testing-style` allows
|
|
222
|
+
is unnecessary here, since advancing this one exercises the real expiry rules of the services around
|
|
223
|
+
it as well as the code's own arithmetic.
|
|
199
224
|
|
|
200
225
|
`simAws.clock().resume()` tracks the underlying clock and `simAws.clock().isFrozen` reports the
|
|
201
226
|
mode. Running mode suits a local dev server, and a test usually wants an advance.
|
|
202
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
|
+
|
|
203
241
|
## Assert by reading the simulation back
|
|
204
242
|
|
|
205
243
|
The simulation holds real state. After exercising the code, ask the service what happened:
|
|
@@ -211,13 +249,14 @@ const object = await simAws.s3().getObject(new GetObjectCommand({ Bucket: bucket
|
|
|
211
249
|
|
|
212
250
|
A call-count assertion holds only for today's implementation. A state assertion holds however the
|
|
213
251
|
handler is rewritten, and it fails if the call was made in a way the real service would have
|
|
214
|
-
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.
|
|
215
254
|
|
|
216
|
-
The accessors sit on more than one scope
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
The accessors sit on more than one scope. `simAws.region(name)` carries some of the services and
|
|
256
|
+
`simAws.region(name).account()` carries all of them (`logs()` among the account-only ones), so look
|
|
257
|
+
on the other scope before concluding a service is missing. Each also takes a plain
|
|
258
|
+
`{ input: { ... } }` in place of a Command object. An assertion can then read a service back without
|
|
259
|
+
adding an `@aws-sdk/client-*` package the production code has no use for.
|
|
221
260
|
|
|
222
261
|
## Drive requests into the simulation
|
|
223
262
|
|
|
@@ -238,11 +277,11 @@ A hostname simulated Route 53 answers for is requested by its own name, with no
|
|
|
238
277
|
`localUrl(...)` adapting (an `https` URL works with no certificate set up for it). That one request
|
|
239
278
|
resolves the hostname, finds the Distribution its alias records point at, and runs the deployed
|
|
240
279
|
CloudFront Function at viewer-request. The certificate, the Hosted Zone records, the Distribution's
|
|
241
|
-
aliases and the function are covered together
|
|
242
|
-
every Route 53 record missing.
|
|
280
|
+
aliases and the function are covered together, where a template assertion over the same stack passes
|
|
281
|
+
with every Route 53 record missing.
|
|
243
282
|
|
|
244
|
-
Reach for `serveSimAws` when the request comes from outside the process
|
|
245
|
-
|
|
283
|
+
Reach for `serveSimAws` when the request comes from outside the process (a browser, `curl`, an SDK
|
|
284
|
+
client pointed at a local endpoint). Both go through the same routing and service code, and
|
|
246
285
|
`SimAwsHttp` leaves parallel test files no port to collide over. See
|
|
247
286
|
[the serving docs](https://yulinsim.dev/serve/) for the API.
|
|
248
287
|
|
|
@@ -257,9 +296,9 @@ if (error instanceof Error && error.name === "ResourceNotFoundException") { ...
|
|
|
257
296
|
```
|
|
258
297
|
|
|
259
298
|
The SDK exports exception classes, which invites the `instanceof` check. It holds only while exactly
|
|
260
|
-
one copy of the SDK package is in play
|
|
261
|
-
raising its own classes
|
|
262
|
-
error names and SDK-shaped `$metadata` without being instances of the SDK classes. Fix it in
|
|
299
|
+
one copy of the SDK package is in play, and two copies in the module graph, a bundler, or a
|
|
300
|
+
simulator raising its own classes all stop it matching silently. Yulin's errors carry the service's
|
|
301
|
+
real error names and SDK-shaped `$metadata` without being instances of the SDK classes. Fix it in
|
|
263
302
|
production code, where a version skew between two `@aws-sdk/client-*` packages breaks `instanceof`
|
|
264
303
|
too. `name` is what the wire carries, and is right in both places.
|
|
265
304
|
|
|
@@ -283,47 +322,101 @@ workaround has to be maintained in every project that hits the same gap.
|
|
|
283
322
|
|
|
284
323
|
The asymmetry matters more than the volume. A simulator staying silent about something costs little,
|
|
285
324
|
leaving that behaviour uncovered where it already was. A simulator saying 200 where production says
|
|
286
|
-
403
|
|
287
|
-
false
|
|
288
|
-
|
|
289
|
-
it is forcing structural duplication.
|
|
325
|
+
403 is the opposite of what it is for. So report a false pass with what production does and what the
|
|
326
|
+
simulation did, and a false refusal with the property and the template that carries it. Raise a gap
|
|
327
|
+
costing nothing but convenience as well, once it is forcing structural duplication.
|
|
290
328
|
|
|
291
329
|
A workaround kept while the issue is open wants a comment naming that issue and a revisit when it
|
|
292
|
-
closes. Re-read
|
|
330
|
+
closes. Re-read those claims on each upgrade. They are the ones nothing tests.
|
|
331
|
+
|
|
332
|
+
## Share one simulation across the whole test suite
|
|
293
333
|
|
|
294
|
-
|
|
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.
|
|
295
338
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
randomised names.
|
|
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`:
|
|
299
341
|
|
|
300
342
|
```typescript
|
|
301
|
-
|
|
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.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
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);
|
|
302
364
|
|
|
303
|
-
beforeAll(async () => {
|
|
304
|
-
simAws = new SimAws();
|
|
305
365
|
const stack = await simAws.cloudFormation().deployTemplateFile({
|
|
306
366
|
templatePath: "cdk.out/SiteStack.template.json",
|
|
307
367
|
});
|
|
308
368
|
await stack.waitForDeployComplete();
|
|
309
|
-
});
|
|
310
369
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
370
|
+
return { simAws, simSdk, stack };
|
|
371
|
+
})();
|
|
372
|
+
|
|
373
|
+
export function yulinSuite() {
|
|
374
|
+
return ready;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// test/yulin.setup.ts
|
|
380
|
+
import { yulinSuite } from "./yulin-suite";
|
|
381
|
+
|
|
382
|
+
await yulinSuite();
|
|
315
383
|
```
|
|
316
384
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
a
|
|
320
|
-
|
|
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.
|
|
321
412
|
|
|
322
|
-
##
|
|
413
|
+
## Bind a handler and run it as a real simulated Lambda
|
|
323
414
|
|
|
324
|
-
|
|
325
|
-
function at deploy time
|
|
326
|
-
`arn`, `cdkPath` or `imageRepository
|
|
415
|
+
`bindings` is how to run your own code inside the simulation, for an `AWS::CloudFront::Function` as
|
|
416
|
+
much as for a Lambda function. Bind an in-process handler at deploy time, targeting a Lambda
|
|
417
|
+
function by `logicalId`, `functionName`, `arn`, `cdkPath` or `imageRepository`, and a CloudFront
|
|
418
|
+
Function by `logicalId`, `functionName` or `arn`. On a CloudFront Function it is what covers source
|
|
419
|
+
`cdk synth` embedded or transformed.
|
|
327
420
|
|
|
328
421
|
```typescript
|
|
329
422
|
await simAws.cloudFormation().deployTemplateFile({
|
|
@@ -358,10 +451,10 @@ accessors. A recording logger was the last reason standing, and from 1.17.1 a bo
|
|
|
358
451
|
is recorded into its log group, read back at `/aws/lambda/<function name>` through
|
|
359
452
|
`FilterLogEvents`.
|
|
360
453
|
|
|
361
|
-
The console and the process standard streams are
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
454
|
+
The console and the process standard streams are bridged for the length of an invocation, as
|
|
455
|
+
`process.env` and `Date` are. A logging library building its own `Console` over those streams at
|
|
456
|
+
module scope is recorded too, Powertools' `Logger` included, both its JSON log line and its EMF
|
|
457
|
+
metric document.
|
|
365
458
|
|
|
366
459
|
So when a test builds the application's own graph, ask what it cannot get through an invocation.
|
|
367
460
|
Expect the answer to be nothing.
|
|
@@ -370,27 +463,30 @@ Expect the answer to be nothing.
|
|
|
370
463
|
|
|
371
464
|
A bound handler gets the function's declared environment variables with nothing stubbed.
|
|
372
465
|
`SimProcessEnvironment` holds a run's variables in an `AsyncLocalStorage` store and resolves
|
|
373
|
-
`process.env` to it for the length of the run, with concurrent runs each seeing their own.
|
|
374
|
-
|
|
466
|
+
`process.env` to it for the length of the run, with concurrent runs each seeing their own. What it
|
|
467
|
+
cannot reach is a read that already happened. A handler module doing
|
|
375
468
|
`const TABLE = process.env.TABLE_NAME` at module scope is evaluated when the test file imports it,
|
|
376
469
|
long before any run, and captures the host value.
|
|
377
470
|
|
|
378
471
|
So read the environment inside the handler body, memoising there where a warm container should build
|
|
379
|
-
its clients once. The substituted `Date` works the same way
|
|
380
|
-
is the sign of a handler reading too early. `SimLambdaEnvironmentConflicts` warns about
|
|
381
|
-
only where the host value and the declared value differ, and a suite that stubs the right
|
|
382
|
-
stays quiet and never learns.
|
|
472
|
+
its clients once. The substituted `Date` works the same way, and a `vi.stubEnv` around a bound
|
|
473
|
+
handler is the sign of a handler reading too early. `SimLambdaEnvironmentConflicts` warns about
|
|
474
|
+
this, but only where the host value and the declared value differ, and a suite that stubs the right
|
|
475
|
+
values stays quiet and never learns.
|
|
383
476
|
|
|
384
|
-
###
|
|
477
|
+
### Keep the zip path for a case about the artefact
|
|
385
478
|
|
|
386
|
-
Deploying without `bindings` runs the bundle `cdk synth` produced.
|
|
387
|
-
the cloud assembly's assets into the staging bucket in simulated S3,
|
|
388
|
-
as CommonJS in a vm sandbox with its own `process.env`, `Date` and
|
|
389
|
-
module-scope problem above never arises. Both paths authorise through the
|
|
390
|
-
same policy mutation fails a zip-path test exactly as it fails a bound one.
|
|
479
|
+
Deploying a Lambda function without `bindings` runs the bundle `cdk synth` produced.
|
|
480
|
+
`deployTemplateFile` publishes the cloud assembly's assets into the staging bucket in simulated S3,
|
|
481
|
+
and the modules are evaluated as CommonJS in a vm sandbox with its own `process.env`, `Date` and
|
|
482
|
+
HTTP clients, where the module-scope problem above never arises. Both paths authorise through the
|
|
483
|
+
execution role, and the same policy mutation fails a zip-path test exactly as it fails a bound one.
|
|
391
484
|
|
|
392
|
-
|
|
393
|
-
|
|
485
|
+
What that buys is the artefact that deploys, its imports and its bundling included, and a case about
|
|
486
|
+
the bundle itself is the case to spend it on. Everywhere else it is the fallback. Every run waits on
|
|
487
|
+
the build, a stale `cdk.out` runs yesterday's handler, and the vm runtime loads CommonJS as the real
|
|
488
|
+
`nodejs` runtimes do, leaving a `NodejsFunction` synthesized with `format: OutputFormat.ESM` refused
|
|
489
|
+
at cold start.
|
|
394
490
|
|
|
395
491
|
### Outbound HTTP is answered by the simulation
|
|
396
492
|
|
|
@@ -398,7 +494,7 @@ From 1.16.2, a simulated Lambda's `fetch` and its `node:http` and `node:https` a
|
|
|
398
494
|
simulation for every hostname simulated Route 53 resolves, through the same in-process entry point a
|
|
399
495
|
request arriving on localhost uses. A Cognito user pool domain, an HTTP API and a load balancer are
|
|
400
496
|
all answered without the test knowing which of them it asked, and everything else reaches the
|
|
401
|
-
network as
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
497
|
+
network as addressed. That is what makes an OAuth authorization code exchange testable, since it
|
|
498
|
+
lives only at the pool domain's hosted `/oauth2/token` endpoint with no SDK operation behind it, and
|
|
499
|
+
it lets `CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a handler with no cache
|
|
500
|
+
primed.
|