@kensio/skills 1.14.0 → 1.16.0
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 +228 -243
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.16.0"
|
|
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.16.0"
|
|
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.16.0"
|
|
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.16.0"
|
|
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.16.0"
|
|
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.16.0"
|
|
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.16.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Technical prose style
|
|
@@ -1,59 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yulin-aws-simulation
|
|
3
|
-
description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code, using it directly rather than building a harness around it, driving tests, local dev and production from one synthesized CDK template, intercepting SDK clients with SimSdk, controlling simulated time, matching service errors by name, and handling properties Yulin refuses to simulate. Use when writing or reviewing tests that touch AWS, when replacing aws-sdk-client-mock or hand-rolled AWS stubs, when a CDK stack needs testing, when test setup around Yulin is growing helper classes or wrapper functions, and when Yulin refuses a template property or an SDK command.
|
|
3
|
+
description: Use the @kensio/yulin in-process AWS simulator well when testing AWS code, using it directly rather than building a harness around it, driving tests, local dev and production from one synthesized CDK template, deploying a whole cdk.out cloud assembly, intercepting SDK clients with SimSdk, driving HTTP requests into the simulation with SimAwsHttp, controlling simulated time, matching service errors by name, and handling properties Yulin refuses to simulate. Use when writing or reviewing tests that touch AWS, when replacing aws-sdk-client-mock or hand-rolled AWS stubs, when a CDK stack needs testing, when a CloudFront Distribution, its DNS records or its certificate need testing, when test setup around Yulin is growing helper classes or wrapper functions, and when Yulin refuses a template property or an SDK command.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.16.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Testing with Yulin
|
|
10
10
|
|
|
11
11
|
[Yulin](https://yulinsim.dev/) (`@kensio/yulin`) simulates AWS in process, in memory, with no
|
|
12
|
-
network and no AWS account.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
link per guide and one per simulated service. Drop the `llms.txt` from a link for the HTML page. The
|
|
18
|
-
same pages ship in the repository, as [the README](https://github.com/KensioSoftware/yulin#readme),
|
|
19
|
-
then `docs/sdk/` for interception and `docs/services/<name>/` for each simulated service.
|
|
20
|
-
|
|
21
|
-
This skill serves the `isolated-testing-style` skill, the general argument for simulation over
|
|
22
|
-
stubs.
|
|
23
|
-
|
|
24
|
-
Yulin is deliberately flexible, and plenty of shapes work. What follows is the recommended way to
|
|
25
|
-
get the most out of it, offered as guidance. Each one says what it buys. A situation that does not
|
|
26
|
-
want that trade can go the other way knowingly.
|
|
12
|
+
network and no AWS account. This skill is how to use it well. For the API read
|
|
13
|
+
[yulinsim.dev/llms.txt](https://yulinsim.dev/llms.txt), one markdown page per guide and per
|
|
14
|
+
simulated service (drop the `llms.txt` for HTML, or read `docs/` in the repository). It serves
|
|
15
|
+
`isolated-testing-style`, the general argument for simulation over stubs. Each rule says what it
|
|
16
|
+
buys, and a case that does not want that trade can go the other way knowingly.
|
|
27
17
|
|
|
28
18
|
## Use what Yulin already gives you
|
|
29
19
|
|
|
30
|
-
The most common way to go wrong
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
that is already the framework).
|
|
34
|
-
|
|
35
|
-
It is worth resisting, because Yulin is built to be used directly:
|
|
20
|
+
The most common way to go wrong is to build something on top of it. The failure looks like a
|
|
21
|
+
`TestAwsEnvironment` class, a `setupSimulatedAws()` helper returning six things, a factory per
|
|
22
|
+
service, or a `beforeEach` that reassembles the world.
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- `using simSdk = new SimSdk()` is the teardown.
|
|
42
|
-
- `deployTemplateFile` is the environment.
|
|
24
|
+
Yulin is built to be used directly. `new SimAws()` and `new SimSdk()` are plain constructors with no
|
|
25
|
+
side effects, no network, no cleanup and no awaiting. Service accessors take the same Command
|
|
26
|
+
objects the SDK does. `using simSdk = new SimSdk()` is the teardown, `deployTemplateFile` is the
|
|
27
|
+
environment, and a simulation per test is close to free.
|
|
43
28
|
|
|
44
|
-
So
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
returning the simulation objects themselves. A bespoke return shape starts hiding them. The point at
|
|
50
|
-
which it wants a class, an options interface, or a directory, it has stopped being test setup and
|
|
51
|
-
become a second product.
|
|
29
|
+
So a test constructs, deploys if a template is involved, intercepts, exercises, then asserts by
|
|
30
|
+
reading the simulation back. A wrapper around any of those steps hides the one thing the reader
|
|
31
|
+
needs to see. A repeated sequence can be a small function in the same file returning the simulation
|
|
32
|
+
objects themselves. Once it wants a class, an options interface or a directory, it has become a
|
|
33
|
+
second product.
|
|
52
34
|
|
|
53
35
|
## One synthesized template, for tests, local dev and production
|
|
54
36
|
|
|
55
|
-
Describe the infrastructure once
|
|
56
|
-
Production deploys it. The local dev server deploys it. The tests deploy it:
|
|
37
|
+
Describe the infrastructure once in CDK and let the synthesized output drive all three:
|
|
57
38
|
|
|
58
39
|
```typescript
|
|
59
40
|
const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFile({
|
|
@@ -64,32 +45,22 @@ const stack = await simAws.region("eu-west-2").cloudFormation().deployTemplateFi
|
|
|
64
45
|
await stack.waitForDeployComplete();
|
|
65
46
|
```
|
|
66
47
|
|
|
67
|
-
A test written against a template
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
by hand is a third description of the infrastructure, drifting away from the other two at its own
|
|
72
|
-
pace, and the drift shows up as a bug that reproduces in exactly one of the three places. Pointing
|
|
73
|
-
the dev server at `cdk.out` as well removes the whole category. What runs locally is what CI tested
|
|
74
|
-
and what production deploys, and `watch` turns a `cdk synth` into a stack update in place.
|
|
75
|
-
|
|
76
|
-
The corollary is that infrastructure belongs in the CDK app even when only a test needs it. A bucket
|
|
77
|
-
conjured in test setup is infrastructure that production does not have.
|
|
48
|
+
A test written against a hand-written template only tests the hand-written template, and a dev
|
|
49
|
+
environment building its own buckets is a third description whose drift shows up as a bug
|
|
50
|
+
reproducing in exactly one of the three places. So infrastructure belongs in the CDK app even when
|
|
51
|
+
only a test needs it.
|
|
78
52
|
|
|
79
53
|
**Do not hand-roll a wrapper that reads the file and calls `deployTemplate`.** `deployTemplateFile`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
CDK
|
|
83
|
-
`No CDK assets manifest is available.`
|
|
54
|
+
locates the cloud assembly beside the file, and that is how staged CDK assets resolve. A wrapper
|
|
55
|
+
reading the JSON fails anything needing one (a `Custom::CDKBucketDeployment`, a `Code.fromAsset`
|
|
56
|
+
function) with `No CDK assets manifest is available.` Two options make a wrapper unnecessary.
|
|
84
57
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
servers. A `cdk synth` becomes a stack update without restarting the process, and resources the
|
|
92
|
-
change left alone keep what they hold.
|
|
58
|
+
- **`transform`** is given the parsed template and answers with the one to deploy, on the deployment
|
|
59
|
+
and on every re-read. Use it for what a simulation genuinely cannot resolve, such as an ARN
|
|
60
|
+
carrying a real account.
|
|
61
|
+
- **`watch`** re-applies the file when it changes, updating the stack in place and leaving untouched
|
|
62
|
+
resources holding what they held. For dev servers, where a `cdk synth` becomes a stack update
|
|
63
|
+
without restarting the process.
|
|
93
64
|
|
|
94
65
|
```typescript
|
|
95
66
|
await simAws.cloudFormation().deployTemplateFile({
|
|
@@ -99,80 +70,109 @@ await simAws.cloudFormation().deployTemplateFile({
|
|
|
99
70
|
});
|
|
100
71
|
```
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
name it. Register it yourself with `simAws.route53().registerHostedZone({ id, name })` where a test
|
|
106
|
-
depends on the name, such as one listing zones by name.
|
|
73
|
+
Together they retire the derived template file a dev server used to write into `cdk.out` so that it
|
|
74
|
+
had something to watch. The watched file is the one CDK wrote, and the adaptation re-applies on
|
|
75
|
+
every read.
|
|
107
76
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
construct change.
|
|
77
|
+
`stack.output("SiteBucketName")` answers a resolved Output narrowed to a string, throwing on one the
|
|
78
|
+
template never declared. Do not hand-roll that reader. And note that a failed `cdk synth` leaves the
|
|
79
|
+
previous template in `cdk.out` with the tests still passing against it, so check the synthesized
|
|
80
|
+
JSON changed before concluding anything from a construct change.
|
|
113
81
|
|
|
114
|
-
|
|
82
|
+
### Register what the app looks up, deploy what the app creates
|
|
115
83
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
84
|
+
A CDK app pinning an identifier as a literal string across stacks raises the question of where the
|
|
85
|
+
simulated resource carrying it comes from. Yulin stands one up at a chosen id with
|
|
86
|
+
`simAws.route53().registerHostedZone({ id, name })`,
|
|
87
|
+
`simAws.acm().registerCertificate({ arn, domainName })`,
|
|
88
|
+
`simAws.cognitoIdentityProvider().registerUserPool({ id, name })` and `registerUserPoolClient`.
|
|
119
89
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
A registration creates a resource in place of the app creating one, and that is what decides when to
|
|
91
|
+
use it. It suits `HostedZone.fromLookup` or a certificate issued by hand outside the app. A resource
|
|
92
|
+
some stack in the same app creates wants deploying, since a registration would mean configuring it
|
|
93
|
+
by hand and taking its configuration from somewhere other than the deployed template. So register
|
|
94
|
+
what the app looks up, deploy what the app creates, and substitute in a `transform` only where a
|
|
95
|
+
deployed resource cannot be given the id its template names.
|
|
123
96
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
97
|
+
Route 53 needs least of this. An `AWS::Route53::RecordSet` naming a hosted zone id no zone holds
|
|
98
|
+
gets one registered under that id as the record is created, taking its name from the records naming
|
|
99
|
+
it. Register it yourself only where a test depends on that name.
|
|
127
100
|
|
|
128
|
-
|
|
129
|
-
correctly. On a real project, swapping stubs for interception caught two bugs the same afternoon,
|
|
130
|
-
both already in production:
|
|
101
|
+
### Deploy a whole cloud assembly with `deployCdkOut`
|
|
131
102
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
never going to verify a signature.
|
|
103
|
+
`deployCdkOut` deploys the Stacks a `cdk.out` holds, each into the region its own environment names
|
|
104
|
+
in the assembly manifest, retiring the per-stack region constants. A Stack synthesized with
|
|
105
|
+
`env: { region: "us-east-1" }` lands in simulated us-east-1 wherever the call was made from, and one
|
|
106
|
+
without `env` takes the region of the scope it was asked through.
|
|
137
107
|
|
|
138
|
-
|
|
139
|
-
|
|
108
|
+
```typescript
|
|
109
|
+
const stacks = await simAws.cloudFormation().deployCdkOut({
|
|
110
|
+
directoryPath: "cdk.out",
|
|
111
|
+
stackNames: ["DnsStack", "SiteStack"], // Stack names or CDK artifact IDs.
|
|
112
|
+
stackOptions: {
|
|
113
|
+
SiteStack: {
|
|
114
|
+
bindings: [{ logicalId: "UploadFunction", handler: uploadHandler }],
|
|
115
|
+
transform: (template, deployed) =>
|
|
116
|
+
withSimulatedCertificate(template, deployed.get("DnsStack")?.output("SiteCertificateArn")),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
140
121
|
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
`stackNames` picks part of an assembly, which most apps need, since most also synthesize a
|
|
123
|
+
deployment pipeline. `stackOptions` carries the `bindings`, `parameters` and `transform` that
|
|
124
|
+
`deployTemplateFile` takes for one template, keyed the same way. Its transform is handed the Stacks
|
|
125
|
+
the same call has already deployed. A Stack consuming a sibling's value therefore stays inside one
|
|
126
|
+
call. Two Stacks passing a plain string between them declare no dependency for the manifest to
|
|
127
|
+
carry, and the order they are named in is what puts the value there in time.
|
|
143
128
|
|
|
144
|
-
|
|
129
|
+
## Intercept real SDK clients, never hand-roll stubs
|
|
145
130
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
a `DynamoDBClient` is what the code sends through. The document client is the one that needs
|
|
149
|
-
intercepting.
|
|
131
|
+
`SimSdk` replaces the `send` method of an AWS SDK client class or instance. Real clients then answer
|
|
132
|
+
from the simulation, and the code under test uses the SDK exactly as it does in production.
|
|
150
133
|
|
|
151
134
|
```typescript
|
|
152
135
|
using simSdk = new SimSdk();
|
|
153
|
-
|
|
154
|
-
const documents = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "eu-west-2" }));
|
|
155
|
-
simSdk.intercept(documents); // Not the DynamoDBClient it was built from.
|
|
136
|
+
simSdk.intercept(SecretsManagerClient); // Every instance, including ones made later.
|
|
156
137
|
```
|
|
157
138
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
### Prefer `using` over a teardown step
|
|
163
|
-
|
|
164
|
-
`SimSdk` and the interception handles it returns are disposable, so `using simSdk = new SimSdk();`
|
|
165
|
-
restores every intercepted client at the end of the scope. `simSdk.restoreAll()` and
|
|
166
|
-
`interception.restore()` do the same thing by hand.
|
|
139
|
+
A stub asserts that your code called something. The simulator asserts that it called the service
|
|
140
|
+
correctly. A stub has no naming rules and verifies no signatures. A malformed Secrets Manager name
|
|
141
|
+
or a wrongly computed Cognito `SECRET_HASH` passes it and fails in production.
|
|
167
142
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
143
|
+
Intercept the class in most cases, since the code under test usually constructs its own clients.
|
|
144
|
+
Intercept an instance when a single client should reach the simulation, and when a file's cases each
|
|
145
|
+
build their own `SimAws`. A class interception is process-wide (it shadows `send` on the class
|
|
146
|
+
prototype) and refuses a second install while the first is live, with
|
|
147
|
+
`SimSdkAlreadyInterceptedError`. An instance interception goes when the instance does.
|
|
148
|
+
|
|
149
|
+
Whichever it is, it has to be the client the code actually calls. A `DynamoDBDocumentClient` built
|
|
150
|
+
over a `DynamoDBClient` is what the code sends through, and the document client is the one to
|
|
151
|
+
intercept. Every Command routes to the simulation by default, and an allow list of Command classes
|
|
152
|
+
narrows that where something else should handle the rest.
|
|
153
|
+
|
|
154
|
+
`SimSdk` and its interception handles are disposable, so `using` restores every intercepted client
|
|
155
|
+
at the end of the scope, leaving nothing for a later test to inherit when the one before it threw.
|
|
156
|
+
`simSdk.restoreAll()` and `interception.restore()` do it by hand. Each `SimSdk` owns a `SimAws`,
|
|
157
|
+
reachable as `simSdk.simAws`, and `new SimSdk({ simAws })` shares an existing one.
|
|
158
|
+
|
|
159
|
+
### A fake accepts any request the simulator would refuse
|
|
160
|
+
|
|
161
|
+
A fake S3 client stubbing `send` with canned `ListObjectsV2` pages, asserted on through the
|
|
162
|
+
continuation tokens it recorded, passes for code that built its command without a `Bucket`.
|
|
163
|
+
Simulated S3 does the pagination for real. `Prefix`, `MaxKeys`, `ContinuationToken` and `StartAfter`
|
|
164
|
+
all apply, `IsTruncated` and `NextContinuationToken` come back as the service sends them, and
|
|
165
|
+
`configureMaxKeysPerPage` lowers the page size so that a bucket of two objects makes a caller walk a
|
|
166
|
+
continuation. Uploading real parts gives the object the real `<md5-of-the-part-md5s>-<count>` ETag.
|
|
167
|
+
|
|
168
|
+
The residue is small. A couple of answers the service never sends (a truncated page naming no
|
|
169
|
+
continuation token) can only come from a fake, and a test reaching for one should say so in a
|
|
170
|
+
comment.
|
|
171
171
|
|
|
172
172
|
## Freeze the clock and advance it deliberately
|
|
173
173
|
|
|
174
174
|
Each `SimAws` carries its own clock, independent of the host and of every other simulation in the
|
|
175
|
-
process.
|
|
175
|
+
process. Start it frozen and move it only on purpose:
|
|
176
176
|
|
|
177
177
|
```typescript
|
|
178
178
|
const simAws = new SimAws({
|
|
@@ -183,38 +183,62 @@ const simAws = new SimAws({
|
|
|
183
183
|
await simAws.clock().advanceBy({ minutes: 20 });
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
Time-dependent behaviour then becomes something a test asserts on in microseconds. A good deal of
|
|
187
|
+
the simulation keys off that clock. EventBridge rules and Scheduler schedules fire only when time is
|
|
188
|
+
advanced past them, DynamoDB items pass their TTL, Secrets Manager deletions come due, `AssumeRole`
|
|
189
|
+
sessions expire, Lambda event source mappings re-poll, and inside a simulated Lambda `Date.now()`
|
|
190
|
+
and `new Date()` report simulated time. So the clock stub `isolated-testing-style` allows is
|
|
191
|
+
unnecessary here, since advancing this one exercises the real expiry rules of the services around it
|
|
192
|
+
as well as the code's own arithmetic.
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
clock is part of the simulation, and advancing it exercises the real expiry rules of the services
|
|
196
|
-
around it rather than only the code's own arithmetic.
|
|
197
|
-
|
|
198
|
-
`simAws.clock().resume()` switches to tracking the underlying clock, and `simAws.clock().isFrozen`
|
|
199
|
-
reports which mode it is in. Running mode suits a local dev server. A test that wants it usually
|
|
200
|
-
wants an advance instead.
|
|
194
|
+
`simAws.clock().resume()` tracks the underlying clock and `simAws.clock().isFrozen` reports the
|
|
195
|
+
mode. Running mode suits a local dev server, and a test usually wants an advance.
|
|
201
196
|
|
|
202
197
|
## Assert by reading the simulation back
|
|
203
198
|
|
|
204
|
-
The simulation holds real state.
|
|
205
|
-
service what happened rather than asking the SDK what it was told:
|
|
199
|
+
The simulation holds real state. After exercising the code, ask the service what happened:
|
|
206
200
|
|
|
207
201
|
```typescript
|
|
208
202
|
// Then the upload is in the bucket, under the key the handler chose.
|
|
209
203
|
const object = await simAws.s3().getObject(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
210
204
|
```
|
|
211
205
|
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
A call-count assertion holds only for today's implementation. A state assertion holds however the
|
|
207
|
+
handler is rewritten, and it fails if the call was made in a way the real service would have
|
|
208
|
+
rejected.
|
|
209
|
+
|
|
210
|
+
The accessors sit on more than one scope, with `simAws.region(name)` carrying some of the services
|
|
211
|
+
and `simAws.region(name).account()` carrying all of them (`logs()` among the ones only the account
|
|
212
|
+
scope has), so look on the other scope before concluding a service is missing. Each also takes a
|
|
213
|
+
plain `{ input: { ... } }` in place of a Command object. An assertion can therefore read a service
|
|
214
|
+
back without adding an `@aws-sdk/client-*` package the production code has no use for.
|
|
215
|
+
|
|
216
|
+
## Drive requests into the simulation
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
Reading state back covers the resources. `SimAwsHttp` from `@kensio/yulin/serve` covers the path
|
|
219
|
+
through them, by sending a request into the environment with nothing listening. It takes what the
|
|
220
|
+
global `fetch` takes and answers with a `Response`:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const http = new SimAwsHttp({ simAws });
|
|
224
|
+
const response = await http.fetch("https://www.example.test/docs/x?a=1", { redirect: "manual" });
|
|
225
|
+
|
|
226
|
+
// Then the apex redirect the CloudFront Function performs has been applied.
|
|
227
|
+
expect(response.status).toBe(301);
|
|
228
|
+
expect(response.headers.get("location")).toBe("https://example.test/docs/x?a=1");
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A hostname simulated Route 53 answers for is requested by its own name, with no port to add and no
|
|
232
|
+
`localUrl(...)` adapting (an `https` URL works with no certificate set up for it). That one request
|
|
233
|
+
resolves the hostname, finds the Distribution its alias records point at, and runs the deployed
|
|
234
|
+
CloudFront Function at viewer-request. The certificate, the Hosted Zone records, the Distribution's
|
|
235
|
+
aliases and the function are covered together. A template assertion over the same stack passes with
|
|
236
|
+
every Route 53 record missing.
|
|
237
|
+
|
|
238
|
+
Reach for `serveSimAws` when the request comes from outside the process, such as a browser, `curl`
|
|
239
|
+
or an SDK client pointed at a local endpoint. Both go through the same routing and service code, and
|
|
240
|
+
`SimAwsHttp` leaves parallel test files no port to collide over. See
|
|
241
|
+
[the serving docs](https://yulinsim.dev/serve/) for the API.
|
|
218
242
|
|
|
219
243
|
## Match service errors by name
|
|
220
244
|
|
|
@@ -229,35 +253,18 @@ if (error instanceof Error && error.name === "ResourceNotFoundException") { ...
|
|
|
229
253
|
The SDK exports exception classes, which invites the `instanceof` check. It holds only while exactly
|
|
230
254
|
one copy of the SDK package is in play. Two copies in the module graph, a bundler, or a simulator
|
|
231
255
|
raising its own classes, and it silently stops matching. Yulin's errors carry the service's real
|
|
232
|
-
error names and SDK-shaped `$metadata
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
wire carries. The `name` check is the one that is right in both places. A version skew between two
|
|
236
|
-
`@aws-sdk/client-*` packages breaks `instanceof` in production too, just less predictably than the
|
|
237
|
-
simulator does.
|
|
256
|
+
error names and SDK-shaped `$metadata` without being instances of the SDK classes. Fix it in
|
|
257
|
+
production code, where a version skew between two `@aws-sdk/client-*` packages breaks `instanceof`
|
|
258
|
+
too. `name` is what the wire carries, and is right in both places.
|
|
238
259
|
|
|
239
260
|
## Expect refusals, and treat them as a feature
|
|
240
261
|
|
|
241
|
-
Yulin refuses a property it cannot simulate
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
properties from the synthesized template until it deploys, keeping a list, then raise them together
|
|
248
|
-
upstream.
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
// A throwaway transform used to find the floor. Delete it afterwards.
|
|
252
|
-
function stripUntilItDeploys(template: CfnTemplateBodyRecord): CfnTemplateBodyRecord {
|
|
253
|
-
// Remove one refused property, re-run, record the next refusal, repeat.
|
|
254
|
-
// Keep the list. Raise it as one issue.
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
Doing this one release at a time means one round trip per property, and you never learn how far away
|
|
259
|
-
a working simulation actually is. This is the same rule as not discovering service ceilings one
|
|
260
|
-
failed deployment at a time.
|
|
262
|
+
Yulin refuses a property it cannot simulate and never ignores one. Silently accepting something that
|
|
263
|
+
changes real behaviour would turn a deploy-time failure into a production one. The cost is that one
|
|
264
|
+
unsupported setting can make a whole stack unsimulatable, one property at a time. **Enumerate every
|
|
265
|
+
refusal in one pass.** Strip properties in a throwaway `transform` until the template deploys,
|
|
266
|
+
keeping the list, then raise them together upstream. Taking them one release at a time is one round
|
|
267
|
+
trip per property, and you never learn how far away a working simulation is.
|
|
261
268
|
|
|
262
269
|
Not every gap is a refusal. Several services record a property they cannot model and carry on,
|
|
263
270
|
reporting it as an ignored property on the stack and on the resource. Check that report before
|
|
@@ -268,37 +275,26 @@ trusting a test that depends on the setting.
|
|
|
268
275
|
Fix gaps on [the Yulin repository](https://github.com/KensioSoftware/yulin) at source. A local
|
|
269
276
|
workaround has to be maintained in every project that hits the same gap.
|
|
270
277
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
So a false pass deserves far more attention than a false refusal. A real example: Yulin authorised a
|
|
279
|
-
Lambda function URL invocation against `lambda:InvokeFunctionUrl` alone. CloudFront origin access
|
|
280
|
-
control also needs `lambda:InvokeFunction`. The tests passed, the release went out, and the endpoint
|
|
281
|
-
403'd in production. A refusal would have cost an afternoon. The false pass cost an incident.
|
|
278
|
+
The asymmetry matters more than the volume. A simulator staying silent about something costs little,
|
|
279
|
+
leaving that behaviour uncovered where it already was. A simulator saying 200 where production says
|
|
280
|
+
403 turns a deploy-time failure into a production one, the opposite of what it is for. So report a
|
|
281
|
+
false pass with what production does and what the simulation did, and a false refusal with the
|
|
282
|
+
property and the template that carries it. Raise a gap costing nothing but convenience as well, once
|
|
283
|
+
it is forcing structural duplication.
|
|
282
284
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
A workaround kept while the issue is open wants a comment naming that issue, and a revisit when it
|
|
287
|
-
closes. One test file carried a comment saying its handler could not reach simulated AWS from inside
|
|
288
|
-
a simulated Lambda, citing KensioSoftware/yulin#638. That held when it was written and was fixed in
|
|
289
|
-
1.16.0. The comment went on shaping the design of the test for months after that.
|
|
285
|
+
A workaround kept while the issue is open wants a comment naming that issue and a revisit when it
|
|
286
|
+
closes. Re-read the claims in your own comments on each upgrade. They are the ones nothing tests.
|
|
290
287
|
|
|
291
288
|
## Deploy expensive context once per test file
|
|
292
289
|
|
|
293
290
|
Vitest gives each test file its own worker, so module-level state is already isolated between files.
|
|
294
291
|
Deploy a stack once for the file and let the tests share it. Isolation inside the file comes from
|
|
295
|
-
randomised names.
|
|
292
|
+
randomised names.
|
|
296
293
|
|
|
297
294
|
```typescript
|
|
298
295
|
let simAws: SimAws;
|
|
299
296
|
|
|
300
297
|
beforeAll(async () => {
|
|
301
|
-
// Given the real synthesized stack, deployed once for this file.
|
|
302
298
|
simAws = new SimAws();
|
|
303
299
|
const stack = await simAws.cloudFormation().deployTemplateFile({
|
|
304
300
|
templatePath: "cdk.out/SiteStack.template.json",
|
|
@@ -309,25 +305,19 @@ beforeAll(async () => {
|
|
|
309
305
|
it("stores an upload", async () => {
|
|
310
306
|
// Given a key no other test in this file is using.
|
|
311
307
|
const key = `uploads/${faker.string.uuid()}.png`;
|
|
312
|
-
// ...
|
|
313
308
|
});
|
|
314
309
|
```
|
|
315
310
|
|
|
316
|
-
|
|
317
|
-
isolation you did not already have.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
seeded row, a bucket key) is cheap enough to build inside the test that needs it, and it is also
|
|
321
|
-
where it is easiest to read. A `beforeEach` that assembles state for tests that do not all want the
|
|
322
|
-
same state is the beginning of the harness this skill opens by arguing against.
|
|
311
|
+
A template deployment is the only thing usually worth hoisting, and in `beforeEach` it pays for the
|
|
312
|
+
whole stack once per test for no isolation you did not already have. The `SimSdk`, a seeded row and
|
|
313
|
+
a bucket key belong inside the test that needs them. A `beforeEach` assembling state for tests that
|
|
314
|
+
do not all want the same state is the beginning of the harness this skill opens by arguing against.
|
|
323
315
|
|
|
324
316
|
## Run the handler as a real simulated Lambda
|
|
325
317
|
|
|
326
|
-
Yulin can run an in-process handler as a function inside the simulation
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Bind a handler to a template function at deploy time with `bindings`:
|
|
318
|
+
Yulin can run an in-process handler as a function inside the simulation. Bind it to a template
|
|
319
|
+
function at deploy time with `bindings`, targeting the function by `logicalId`, `functionName`,
|
|
320
|
+
`arn`, `cdkPath` or `imageRepository`:
|
|
331
321
|
|
|
332
322
|
```typescript
|
|
333
323
|
await simAws.cloudFormation().deployTemplateFile({
|
|
@@ -336,65 +326,62 @@ await simAws.cloudFormation().deployTemplateFile({
|
|
|
336
326
|
});
|
|
337
327
|
```
|
|
338
328
|
|
|
339
|
-
The handler still runs in process
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`logicalId`, `functionName`, `arn`, `cdkPath`, or `imageRepository` for a container image function.
|
|
329
|
+
The handler still runs in process, closing over test state and stopping on a breakpoint. The
|
|
330
|
+
difference from calling it directly is that a missing `s3:PutObject` on the execution role now fails
|
|
331
|
+
the test, at the point AWS would have failed it.
|
|
343
332
|
|
|
344
333
|
### Invoke through simulated Lambda
|
|
345
334
|
|
|
346
335
|
Binding a handler proves nothing about IAM on its own. The execution role, the function's
|
|
347
|
-
environment and its outbound HTTP are all applied by the invocation
|
|
336
|
+
environment and its outbound HTTP are all applied by the invocation, and the test has to go through
|
|
348
337
|
`simAws.lambda().invoke(new InvokeCommand({ FunctionName, Payload }))` to get any of them. A test
|
|
349
338
|
holding the same handler reference and calling it directly runs it in the test's own scope, as the
|
|
350
339
|
test's own caller, with none of the three.
|
|
351
340
|
|
|
352
|
-
|
|
353
|
-
CDK stack and re-
|
|
341
|
+
To check a suite covers the policy, remove an action such as `dynamodb:GetItem` from the role in the
|
|
342
|
+
CDK stack and re-synthesize. Invoked cases fail with an `AccessDenied` naming the execution role and
|
|
343
|
+
the action. Cases calling the handler directly stay green.
|
|
354
344
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
is
|
|
358
|
-
|
|
345
|
+
### Wire the object graph once, in production
|
|
346
|
+
|
|
347
|
+
The costliest shape a Yulin suite grows is a second wiring of the application's own object graph,
|
|
348
|
+
built so that a recorder can be injected into it and asserted on. Nothing needs it. `bindings` plus
|
|
349
|
+
`invoke` run the real handler under its execution role, state reads back through the production
|
|
350
|
+
reader against the deployed table, and accounts and other fixtures come from the simulation's own
|
|
351
|
+
accessors. A recording logger was the last reason standing, and from 1.17.1 a bound handler's output
|
|
352
|
+
is recorded into its log group, read back at `/aws/lambda/<function name>` through
|
|
353
|
+
`FilterLogEvents`.
|
|
354
|
+
|
|
355
|
+
The console and the process standard streams are both bridged for the length of an invocation, in
|
|
356
|
+
the way `process.env` and `Date` are. A logging library building its own `Console` over those
|
|
357
|
+
streams at module scope is recorded too, Powertools' `Logger` included, both its JSON log line and
|
|
358
|
+
its EMF metric document.
|
|
359
359
|
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
So when a test builds the application's own graph, ask what it cannot get through an invocation.
|
|
361
|
+
Expect the answer to be nothing.
|
|
362
362
|
|
|
363
363
|
### Read the environment inside the handler
|
|
364
364
|
|
|
365
365
|
A bound handler gets the function's declared environment variables with nothing stubbed.
|
|
366
366
|
`SimProcessEnvironment` holds a run's variables in an `AsyncLocalStorage` store and resolves
|
|
367
|
-
`process.env` to
|
|
368
|
-
|
|
367
|
+
`process.env` to it for the length of the run, with concurrent runs each seeing their own. The one
|
|
368
|
+
thing it cannot reach is a read that already happened. A handler module doing
|
|
369
|
+
`const TABLE = process.env.TABLE_NAME` at module scope is evaluated when the test file imports it,
|
|
370
|
+
long before any run, and captures the host value.
|
|
369
371
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
<!-- prose-check:on -->
|
|
377
|
-
|
|
378
|
-
So read the environment inside the handler body. Memoise there where a warm container should build
|
|
379
|
-
its clients once. The substituted `Date` a bound handler reads works the same way, and a time
|
|
380
|
-
captured at module scope is captured equally early.
|
|
381
|
-
|
|
382
|
-
Yulin ships `SimLambdaEnvironmentConflicts` to warn about this, and it warns only where the host
|
|
383
|
-
value and the declared value differ. A suite that stubs the right values stays quiet and never
|
|
384
|
-
learns. The rule stays invisible for as long as the stubs agree.
|
|
385
|
-
|
|
386
|
-
A `vi.stubEnv` around a bound handler is the sign that the handler reads too early. Moving the reads
|
|
387
|
-
into the handler body removed every `vi.stubEnv` from one repository.
|
|
372
|
+
So read the environment inside the handler body, memoising there where a warm container should build
|
|
373
|
+
its clients once. The substituted `Date` works the same way. A `vi.stubEnv` around a bound handler
|
|
374
|
+
is the sign of a handler reading too early. `SimLambdaEnvironmentConflicts` warns about this, but
|
|
375
|
+
only where the host value and the declared value differ, and a suite that stubs the right values
|
|
376
|
+
stays quiet and never learns.
|
|
388
377
|
|
|
389
378
|
### What a binding buys, and what the zip path buys
|
|
390
379
|
|
|
391
380
|
Deploying without `bindings` runs the bundle `cdk synth` produced. `deployTemplateFile` publishes
|
|
392
|
-
the cloud assembly's assets into the staging bucket in simulated S3, and the
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
Both paths authorise through the execution role. The same policy mutation fails a zip-path test
|
|
397
|
-
exactly as it fails a bound one. The choice between them is about what else the test needs:
|
|
381
|
+
the cloud assembly's assets into the staging bucket in simulated S3, and the modules are evaluated
|
|
382
|
+
as CommonJS in a vm sandbox with its own `process.env`, `Date` and HTTP clients, where the
|
|
383
|
+
module-scope problem above never arises. Both paths authorise through the execution role, and the
|
|
384
|
+
same policy mutation fails a zip-path test exactly as it fails a bound one.
|
|
398
385
|
|
|
399
386
|
- **A binding** keeps a breakpoint working and lets the handler close over test state.
|
|
400
387
|
- **The zip path** exercises the artefact that deploys, its imports and its bundling included.
|
|
@@ -402,12 +389,10 @@ exactly as it fails a bound one. The choice between them is about what else the
|
|
|
402
389
|
### Outbound HTTP is answered by the simulation
|
|
403
390
|
|
|
404
391
|
From 1.16.2, a simulated Lambda's `fetch` and its `node:http` and `node:https` are answered by the
|
|
405
|
-
simulation for every hostname simulated Route 53 resolves
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
handler that cannot reach the domain leaves the whole exchange uncovered. The same routing lets
|
|
413
|
-
`CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a handler with no cache primed.
|
|
392
|
+
simulation for every hostname simulated Route 53 resolves, through the same in-process entry point a
|
|
393
|
+
request arriving on localhost uses. A Cognito user pool domain, an HTTP API and a load balancer are
|
|
394
|
+
all answered without the test knowing which of them it asked, and everything else reaches the
|
|
395
|
+
network as it was addressed. This is what makes an OAuth authorization code exchange testable, since
|
|
396
|
+
that exchange lives only at the pool domain's hosted `/oauth2/token` endpoint with no SDK operation
|
|
397
|
+
behind it. The same routing lets `CognitoJwtVerifier` fetch a simulated pool's JWKS from inside a
|
|
398
|
+
handler with no cache primed.
|