@specific.dev/spectest 0.128.0 → 0.130.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/README.md +54 -0
- package/dist/browser.d.ts +12 -5
- package/dist/browser.js +13 -13
- package/dist/components/s3-runtime/README.md +33 -0
- package/dist/components/s3-runtime/go.mod +11 -0
- package/dist/components/s3-runtime/go.sum +54 -0
- package/dist/components/s3-runtime/main.go +77 -0
- package/dist/components/s3-runtime/metadata.patch +12 -0
- package/dist/components/s3.d.ts +4 -5
- package/dist/components/s3.js +24 -14
- package/dist/index.d.ts +2 -2
- package/dist/recorder.d.ts +29 -2
- package/dist/terminal.d.ts +31 -17
- package/dist/terminal.js +261 -105
- package/package.json +1 -1
- package/src/browser-navigation.test.ts +36 -0
- package/src/browser.ts +26 -18
- package/src/components/s3-docker.test.ts +159 -0
- package/src/components/s3-runtime/README.md +33 -0
- package/src/components/s3-runtime/go.mod +11 -0
- package/src/components/s3-runtime/go.sum +54 -0
- package/src/components/s3-runtime/main.go +77 -0
- package/src/components/s3-runtime/metadata.patch +12 -0
- package/src/components/s3.test.ts +36 -0
- package/src/components/s3.ts +25 -16
- package/src/index.ts +2 -2
- package/src/recorder.ts +22 -2
- package/src/terminal.test.ts +324 -0
- package/src/terminal.ts +269 -119
package/README.md
CHANGED
|
@@ -24,6 +24,60 @@ work; do not replace readiness with sleeps or arbitrary `.first()` selections.
|
|
|
24
24
|
SDK 0.123.0 uses native Playwright assertions while retaining Spectest's API and
|
|
25
25
|
recording. Ambiguous single-element selectors fail immediately; make them unique.
|
|
26
26
|
|
|
27
|
+
For terminal readiness, use `terminal.waitFor` directly. It retries the condition
|
|
28
|
+
and throws on timeout, so neither an outer `ctx.wait` nor an extra assertion is
|
|
29
|
+
needed to check that text appears:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
await terminal.waitFor("Rack is initialized", /Current rack status:\s*Initialized/, {
|
|
33
|
+
timeoutMs: 35 * 60_000,
|
|
34
|
+
intervalMs: 5_000,
|
|
35
|
+
failOn: [
|
|
36
|
+
{ description: "Setup was rejected", matcher: "Start Rack Setup Failed" },
|
|
37
|
+
{ description: "Initialization failed", matcher: "Initialization Failed" },
|
|
38
|
+
{ description: "Initialization panicked", matcher: "Initialization Panicked" },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
await terminal.press("Ctrl+C"); // Separate cleanup after the condition succeeds.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
SDK 0.129.0 records the string/regex matcher (or identifies a custom predicate),
|
|
45
|
+
timeout, and start/end positions for this wait. The dashboard shows its condition
|
|
46
|
+
and outcome and replays up to the result. Later inputs belong to their own steps;
|
|
47
|
+
the full terminal session can be opened separately. `ctx.wait` remains useful
|
|
48
|
+
when a retry must repeat actions, rather than just observe terminal state.
|
|
49
|
+
|
|
50
|
+
Terminal waits record matched, timed-out, failure-condition, predicate-error,
|
|
51
|
+
process-exit and cancelled outcomes. `failOn` checks the rendered screen before
|
|
52
|
+
checking success; string and regex conditions are recorded explicitly. For a
|
|
53
|
+
custom predicate, provide `condition: "Expected sled has a valid bootstrap IPv6 address"`
|
|
54
|
+
to explain the actual check separately from its step title. Async predicates are
|
|
55
|
+
awaited, and their rejection is recorded. A final matching screen succeeds even
|
|
56
|
+
if the process has exited; otherwise exit fails the wait after final output is
|
|
57
|
+
drained. No `Promise.race` with `terminal.exited` is needed.
|
|
58
|
+
|
|
59
|
+
Pass `signal: controller.signal` to cancel a wait without closing its terminal.
|
|
60
|
+
Cancellation stops SDK polling; it cannot forcibly stop work started inside a
|
|
61
|
+
custom predicate. `terminal.close({ timeoutMs: 10_000 })` cancels pending waits,
|
|
62
|
+
sends EOF, escalates to a kill, and bounds cleanup including output drain. The
|
|
63
|
+
default close deadline is 10 seconds; repeated calls share the same result or
|
|
64
|
+
failure. `terminal.exited` records the exit when observed after completion, not
|
|
65
|
+
when its promise is first read. A stalled output drain fails after 5 seconds
|
|
66
|
+
instead of hanging indefinitely. Test code should preserve its original failure
|
|
67
|
+
if diagnostic or cleanup operations also fail.
|
|
68
|
+
|
|
69
|
+
Browser navigation accepts public Playwright-style options, without a backend
|
|
70
|
+
cast or `pageOp` call:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await browser.goto(consoleUrl, { timeout: 120_000, waitUntil: "domcontentloaded" });
|
|
74
|
+
await browser.reload({ timeout: 120_000 });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
These options also apply to `goBack` and `goForward`. Navigation deadlines do not
|
|
78
|
+
cancel a surrounding `ctx.poll`, and retrying a navigation after a visible error
|
|
79
|
+
is a recovery action that should remain visible in the test results.
|
|
80
|
+
|
|
27
81
|
Pub/Sub integrations are project-owned as of SDK 0.98.0. The former
|
|
28
82
|
`pubsub`/`pubsubClient` exports and `/components/gcp` entry point are removed.
|
|
29
83
|
Use a local service definition or `serviceGroup()` wrapper; the repository’s
|
package/dist/browser.d.ts
CHANGED
|
@@ -124,6 +124,11 @@ export interface Mouse {
|
|
|
124
124
|
/** Wheel-scroll by a pixel delta. */
|
|
125
125
|
wheel(dx: number, dy: number): Promise<void>;
|
|
126
126
|
}
|
|
127
|
+
export interface NavigationOptions {
|
|
128
|
+
/** Playwright navigation timeout in milliseconds; 0 disables the deadline. */
|
|
129
|
+
timeout?: number;
|
|
130
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
131
|
+
}
|
|
127
132
|
/**
|
|
128
133
|
* Headless browser session — Playwright `Page`-shaped. Select elements with
|
|
129
134
|
* the `getBy*`/`locator` roots (returning a {@link Locator}) and act on them;
|
|
@@ -136,14 +141,14 @@ export interface Browser {
|
|
|
136
141
|
url(): string;
|
|
137
142
|
/** Current page `<title>` (async, like Playwright's `page.title()`). */
|
|
138
143
|
title(): Promise<string>;
|
|
139
|
-
/** Navigate to a URL;
|
|
140
|
-
goto(url: string): Promise<void>;
|
|
144
|
+
/** Navigate to a URL; waits for the main frame load unless waitUntil is supplied. */
|
|
145
|
+
goto(url: string, options?: NavigationOptions): Promise<void>;
|
|
141
146
|
/** Navigate back in session history. */
|
|
142
|
-
goBack(): Promise<void>;
|
|
147
|
+
goBack(options?: NavigationOptions): Promise<void>;
|
|
143
148
|
/** Navigate forward in session history. */
|
|
144
|
-
goForward(): Promise<void>;
|
|
149
|
+
goForward(options?: NavigationOptions): Promise<void>;
|
|
145
150
|
/** Reload the current page. */
|
|
146
|
-
reload(): Promise<void>;
|
|
151
|
+
reload(options?: NavigationOptions): Promise<void>;
|
|
147
152
|
readonly keyboard: Keyboard;
|
|
148
153
|
readonly mouse: Mouse;
|
|
149
154
|
/** Root CSS query. */
|
|
@@ -363,6 +368,8 @@ export declare function acquirePersistentDesktop(endpoint: string, windowIndex:
|
|
|
363
368
|
desktopChrome?: DesktopChrome;
|
|
364
369
|
}>;
|
|
365
370
|
export interface RecordableFields {
|
|
371
|
+
timeout: number;
|
|
372
|
+
waitUntil: NonNullable<NavigationOptions["waitUntil"]>;
|
|
366
373
|
target: RecordedBrowserLocatorPart[];
|
|
367
374
|
destinationTarget: RecordedBrowserLocatorPart[];
|
|
368
375
|
url: string;
|
package/dist/browser.js
CHANGED
|
@@ -1491,11 +1491,11 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1491
1491
|
keyboard,
|
|
1492
1492
|
mouse,
|
|
1493
1493
|
touchscreen,
|
|
1494
|
-
goto(url) {
|
|
1494
|
+
goto(url, options) {
|
|
1495
1495
|
recorder?.noteNavigation?.(url);
|
|
1496
|
-
return instrumented("goto", { url }, async () => {
|
|
1496
|
+
return instrumented("goto", { url, ...options }, async () => {
|
|
1497
1497
|
try {
|
|
1498
|
-
await holder.page.goto(url, { waitUntil: "load" });
|
|
1498
|
+
await holder.page.goto(url, { waitUntil: "load", ...options });
|
|
1499
1499
|
}
|
|
1500
1500
|
catch (err) {
|
|
1501
1501
|
// Restored-renderer DNS bug (see `rebuildView`): only when the
|
|
@@ -1508,23 +1508,23 @@ function buildBackend(holder, recorder, buildOpts) {
|
|
|
1508
1508
|
if (!(await daemonResolves(url)))
|
|
1509
1509
|
throw err;
|
|
1510
1510
|
await rebuildView(holder);
|
|
1511
|
-
await holder.page.goto(url, { waitUntil: "load" });
|
|
1511
|
+
await holder.page.goto(url, { waitUntil: "load", ...options });
|
|
1512
1512
|
}
|
|
1513
1513
|
});
|
|
1514
1514
|
},
|
|
1515
|
-
goBack() {
|
|
1516
|
-
return instrumented("goBack", {}, async () => {
|
|
1517
|
-
await holder.page.goBack();
|
|
1515
|
+
goBack(options) {
|
|
1516
|
+
return instrumented("goBack", { ...options }, async () => {
|
|
1517
|
+
await holder.page.goBack(options);
|
|
1518
1518
|
});
|
|
1519
1519
|
},
|
|
1520
|
-
goForward() {
|
|
1521
|
-
return instrumented("goForward", {}, async () => {
|
|
1522
|
-
await holder.page.goForward();
|
|
1520
|
+
goForward(options) {
|
|
1521
|
+
return instrumented("goForward", { ...options }, async () => {
|
|
1522
|
+
await holder.page.goForward(options);
|
|
1523
1523
|
});
|
|
1524
1524
|
},
|
|
1525
|
-
reload() {
|
|
1526
|
-
return instrumented("reload", {}, async () => {
|
|
1527
|
-
await holder.page.reload();
|
|
1525
|
+
reload(options) {
|
|
1526
|
+
return instrumented("reload", { ...options }, async () => {
|
|
1527
|
+
await holder.page.reload(options);
|
|
1528
1528
|
});
|
|
1529
1529
|
},
|
|
1530
1530
|
locator: (css) => makeLocator(backend, strategy, { steps: [{ m: "locator", args: [css] }] }),
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Native S3 component runtime
|
|
2
|
+
|
|
3
|
+
GoFakeS3 is pinned by `go.mod`/`go.sum`. `s3.ts` embeds these assets into a constant
|
|
4
|
+
multi-stage Dockerfile, builds for the worker's native architecture, and ships
|
|
5
|
+
only the static binary and dependency licenses. The Go builder is pinned to a
|
|
6
|
+
multi-architecture digest. No project files or options enter the image build.
|
|
7
|
+
|
|
8
|
+
The entrypoint retains the component's port 9090, path-style addressing, no
|
|
9
|
+
automatic buckets, no signature/date validation, and permissive browser CORS.
|
|
10
|
+
The memory backend supports pagination and versioning. Full VM snapshots clone
|
|
11
|
+
its buckets, versions and pending multipart uploads; restarting the process
|
|
12
|
+
starts empty, matching S3Mock's default temporary store.
|
|
13
|
+
|
|
14
|
+
`metadata.patch` preserves Cache-Control, Content-Language and Expires on object
|
|
15
|
+
uploads, multipart initiation and copies. Upstream currently drops these headers
|
|
16
|
+
in `metadataHeaders`. The patch applies to the vendored, checksum-verified module;
|
|
17
|
+
a failed patch stops the build. Recheck it when changing the upstream pin.
|
|
18
|
+
|
|
19
|
+
From `sdk`, run the real-image contract on a Linux Docker host:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
SPECTEST_TEST_S3_DOCKER=1 bun test src/components/s3-docker.test.ts
|
|
23
|
+
SPECTEST_TEST_S3_DOCKER=1 SPECTEST_TEST_S3_BASELINE=1 bun test src/components/s3-docker.test.ts
|
|
24
|
+
bun test src/components/s3.test.ts src/components/internal-fetch.test.ts
|
|
25
|
+
bun run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The baseline uses S3Mock 5.1.0. The contract covers helpers and recording, setup,
|
|
29
|
+
credentials, HTTPS with preserved Host, browser CORS, unsigned/stale signatures,
|
|
30
|
+
metadata, copy/range requests, multipart, paginated listing, versioning, and
|
|
31
|
+
fresh-process lifecycle. It does not prove every S3 API is interchangeable:
|
|
32
|
+
GoFakeS3 lacks some S3Mock features, including tagging, lifecycle and retention.
|
|
33
|
+
Java/Spring/S3Mock-specific environment settings are no longer interpreted.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module spectest-s3
|
|
2
|
+
|
|
3
|
+
go 1.24
|
|
4
|
+
|
|
5
|
+
require github.com/johannesboyne/gofakes3 v1.2.1-0.20261011201219-0016ef2bf153
|
|
6
|
+
|
|
7
|
+
require (
|
|
8
|
+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
|
9
|
+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
|
|
10
|
+
golang.org/x/tools v0.8.0 // indirect
|
|
11
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
|
2
|
+
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
|
3
|
+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
|
4
|
+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
|
5
|
+
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
|
6
|
+
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
|
7
|
+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75 h1:S61/E3N01oral6B3y9hZ2E1iFDqCZPPOBoBQretCnBI=
|
|
8
|
+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75/go.mod h1:bDMQbkI1vJbNjnvJYpPTSNYBkI/VIv18ngWb/K84tkk=
|
|
9
|
+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
|
10
|
+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
|
11
|
+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
|
12
|
+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
|
13
|
+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
|
14
|
+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
|
15
|
+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
|
16
|
+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
|
17
|
+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
|
18
|
+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
|
19
|
+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
|
20
|
+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
|
21
|
+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
|
22
|
+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
|
23
|
+
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
|
24
|
+
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
|
25
|
+
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
|
26
|
+
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
|
27
|
+
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
|
28
|
+
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
|
29
|
+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
30
|
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
31
|
+
github.com/johannesboyne/gofakes3 v1.2.1-0.20261011201219-0016ef2bf153 h1:ZaLBZJfUnKBlrPu0AX0T/i1N9BNU54j8sl9mCLzmpI0=
|
|
32
|
+
github.com/johannesboyne/gofakes3 v1.2.1-0.20261011201219-0016ef2bf153/go.mod h1:UHhRZRod9rENGFrUWTYnQHZqlNgSmjOq8DaD/ATQYRM=
|
|
33
|
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
34
|
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
35
|
+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
|
36
|
+
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
|
37
|
+
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
|
|
38
|
+
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
|
39
|
+
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
|
40
|
+
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
41
|
+
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
|
42
|
+
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
|
43
|
+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
|
|
44
|
+
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
|
|
45
|
+
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
|
46
|
+
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
47
|
+
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
|
48
|
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
49
|
+
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
|
50
|
+
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
|
51
|
+
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
|
|
52
|
+
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
|
53
|
+
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|
54
|
+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The component's native entrypoint. Keep its defaults compatible with S3Mock;
|
|
2
|
+
// the upstream CLI enforces clock skew and has different browser CORS defaults.
|
|
3
|
+
package main
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"log"
|
|
7
|
+
"net/http"
|
|
8
|
+
|
|
9
|
+
"github.com/johannesboyne/gofakes3"
|
|
10
|
+
"github.com/johannesboyne/gofakes3/backend/s3mem"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func main() {
|
|
14
|
+
// VM snapshots include RAM: buckets, versions and in-flight multipart state
|
|
15
|
+
// fork together. A fresh process starts empty, as S3Mock did by default.
|
|
16
|
+
// Unlike the Bolt backend, s3mem also supports bucket versioning.
|
|
17
|
+
backend := s3mem.New()
|
|
18
|
+
server := gofakes3.New(backend,
|
|
19
|
+
gofakes3.WithTimeSkewLimit(0), // S3Mock accepts arbitrary/expired signatures.
|
|
20
|
+
gofakes3.WithHostBucket(false),
|
|
21
|
+
gofakes3.WithAutoBucket(false),
|
|
22
|
+
gofakes3.WithLogger(gofakes3.GlobalLog()),
|
|
23
|
+
).Server()
|
|
24
|
+
log.Fatal(http.ListenAndServe(":9090", cors(server)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Preserve S3Mock's permissive browser uploads, including arbitrary metadata
|
|
28
|
+
// headers. GoFakeS3's default allow-list rejects custom x-amz-meta-* headers and
|
|
29
|
+
// only exposes ETag on preflights, hiding it from actual browser responses.
|
|
30
|
+
func cors(next http.Handler) http.Handler {
|
|
31
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
32
|
+
origin := r.Header.Get("Origin")
|
|
33
|
+
w.Header().Add("Vary", "Origin")
|
|
34
|
+
w.Header().Add("Vary", "Access-Control-Request-Method")
|
|
35
|
+
w.Header().Add("Vary", "Access-Control-Request-Headers")
|
|
36
|
+
if origin != "" && r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
|
|
37
|
+
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
38
|
+
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
39
|
+
w.Header().Set("Access-Control-Allow-Methods", r.Header.Get("Access-Control-Request-Method"))
|
|
40
|
+
w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers"))
|
|
41
|
+
w.WriteHeader(http.StatusOK)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
writer := &corsWriter{ResponseWriter: w, hasOrigin: origin != ""}
|
|
45
|
+
next.ServeHTTP(writer, r)
|
|
46
|
+
if !writer.written {
|
|
47
|
+
writer.WriteHeader(http.StatusOK)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type corsWriter struct {
|
|
53
|
+
http.ResponseWriter
|
|
54
|
+
hasOrigin bool
|
|
55
|
+
written bool
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func (w *corsWriter) WriteHeader(status int) {
|
|
59
|
+
if w.written {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
w.written = true
|
|
63
|
+
if w.hasOrigin {
|
|
64
|
+
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
65
|
+
w.Header().Set("Access-Control-Expose-Headers", "*")
|
|
66
|
+
} else {
|
|
67
|
+
w.Header().Del("Access-Control-Allow-Origin")
|
|
68
|
+
}
|
|
69
|
+
w.ResponseWriter.WriteHeader(status)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (w *corsWriter) Write(body []byte) (int, error) {
|
|
73
|
+
if !w.written {
|
|
74
|
+
w.WriteHeader(http.StatusOK)
|
|
75
|
+
}
|
|
76
|
+
return w.ResponseWriter.Write(body)
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
2
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
3
|
+
@@ -1191,6 +1191,9 @@
|
|
4
|
+
for hk, hv := range headers {
|
|
5
|
+
if strings.HasPrefix(hk, "X-Amz-") ||
|
|
6
|
+
hk == "Content-Type" ||
|
|
7
|
+
+ hk == "Cache-Control" ||
|
|
8
|
+
+ hk == "Content-Language" ||
|
|
9
|
+
+ hk == "Expires" ||
|
|
10
|
+
hk == "Content-Disposition" ||
|
|
11
|
+
hk == "Content-Encoding" {
|
|
12
|
+
meta[hk] = hv[0]
|
package/dist/components/s3.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type S3ClientLike } from "../s3.js";
|
|
2
2
|
export interface S3Options {
|
|
3
3
|
/**
|
|
4
|
-
* Buckets to create on first boot. The
|
|
5
|
-
* env is unreliable across versions, so the component creates them in `setup`
|
|
4
|
+
* Buckets to create on first boot. The component creates them in `setup`
|
|
6
5
|
* (a plain CreateBucket `PUT`) — captured into the warm snapshot, never
|
|
7
6
|
* re-run on warm starts/forks.
|
|
8
7
|
*/
|
|
@@ -42,7 +41,7 @@ export interface S3Helpers {
|
|
|
42
41
|
}
|
|
43
42
|
/**
|
|
44
43
|
* A ready-to-use, S3-compatible object store (backed by
|
|
45
|
-
* [
|
|
44
|
+
* [GoFakeS3](https://github.com/johannesboyne/gofakes3)), with an instrumented S3
|
|
46
45
|
* client on `ctx.svc.<key>.client`. Drop into `environment.services`:
|
|
47
46
|
*
|
|
48
47
|
* ```ts
|
|
@@ -90,8 +89,8 @@ export declare function s3(opts?: S3Options): {
|
|
|
90
89
|
}[] | undefined;
|
|
91
90
|
env?: Record<string, string> | undefined;
|
|
92
91
|
image: {
|
|
93
|
-
type: "
|
|
94
|
-
|
|
92
|
+
type: "dockerfile";
|
|
93
|
+
content: string;
|
|
95
94
|
};
|
|
96
95
|
ports: number[];
|
|
97
96
|
};
|
package/dist/components/s3.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { rawFetch } from "../harness/raw-fetch.js";
|
|
2
3
|
import { S3Client } from "../s3.js";
|
|
3
|
-
//
|
|
4
|
-
// is
|
|
5
|
-
//
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
// Build once per architecture/cache from pinned source: upstream's Docker Hub
|
|
5
|
+
// image is amd64-only. Only the static native server ships in the runtime image.
|
|
6
|
+
// Keep the Dockerfile independent of options so stores share the build cache.
|
|
7
|
+
const S3_DOCKERFILE = `
|
|
8
|
+
FROM golang:1.27.2-alpine@sha256:f92b6ef800e499660581efdabdf25d9d817a9d124eaf900924f0504e7e27e12d AS build
|
|
9
|
+
RUN apk add --no-cache patch
|
|
10
|
+
WORKDIR /src
|
|
11
|
+
${["go.mod", "go.sum", "main.go", "metadata.patch"].map((file) => {
|
|
12
|
+
const data = readFileSync(new URL(`./s3-runtime/${file}`, import.meta.url)).toString("base64");
|
|
13
|
+
return `RUN printf '%s' '${data}' | base64 -d > ${file}`;
|
|
14
|
+
}).join("\n")}
|
|
15
|
+
RUN go mod vendor && patch -p1 < metadata.patch
|
|
16
|
+
RUN CGO_ENABLED=0 go build -mod=vendor -trimpath -ldflags='-s -w' -o /s3-server .
|
|
17
|
+
FROM scratch
|
|
18
|
+
COPY --from=build /s3-server /s3-server
|
|
19
|
+
COPY --from=build /src/vendor/github.com/johannesboyne/gofakes3/LICENSE /licenses/gofakes3
|
|
20
|
+
COPY --from=build /src/vendor/github.com/ryszard/goskiplist/LICENSE /licenses/goskiplist
|
|
21
|
+
EXPOSE 9090
|
|
22
|
+
ENTRYPOINT ["/s3-server"]
|
|
23
|
+
`;
|
|
24
|
+
// Public endpoint stays fixed across backing-server changes.
|
|
15
25
|
const S3_PORT = 9090;
|
|
16
26
|
/**
|
|
17
27
|
* A ready-to-use, S3-compatible object store (backed by
|
|
18
|
-
* [
|
|
28
|
+
* [GoFakeS3](https://github.com/johannesboyne/gofakes3)), with an instrumented S3
|
|
19
29
|
* client on `ctx.svc.<key>.client`. Drop into `environment.services`:
|
|
20
30
|
*
|
|
21
31
|
* ```ts
|
|
@@ -51,7 +61,7 @@ export function s3(opts = {}) {
|
|
|
51
61
|
const secretAccessKey = opts.secretAccessKey ?? "s3";
|
|
52
62
|
const region = opts.region ?? "us-east-1";
|
|
53
63
|
return {
|
|
54
|
-
image: { type: "
|
|
64
|
+
image: { type: "dockerfile", content: S3_DOCKERFILE },
|
|
55
65
|
ports: [S3_PORT],
|
|
56
66
|
...(opts.env ? { env: opts.env } : {}),
|
|
57
67
|
...(opts.hosts
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { Annotated } from "./annotate.js";
|
|
|
18
18
|
export { SQL, type SqlClient, type SqlOptions } from "./sql.js";
|
|
19
19
|
export { RedisClient, type RedisClientLike, type RedisOptions } from "./redis.js";
|
|
20
20
|
export { S3Client, type S3ClientLike, type S3File, type S3ClientOptions } from "./s3.js";
|
|
21
|
-
export type { Browser, BrowserOptions, Keyboard, Mouse, Touchscreen } from "./browser.js";
|
|
21
|
+
export type { Browser, BrowserOptions, NavigationOptions, Keyboard, Mouse, Touchscreen } from "./browser.js";
|
|
22
22
|
export type { MicrophoneFixture } from "./harness/microphone-fixture.js";
|
|
23
23
|
export type { FakeWebSocket, FakeWebSocketHandlers } from "./fake-websocket.js";
|
|
24
24
|
import type { FakeWebSocketHandlers } from "./fake-websocket.js";
|
|
@@ -34,7 +34,7 @@ export type { Mobile, MobileApp } from "./mobile.js";
|
|
|
34
34
|
import type { Mobile, MobileApp } from "./mobile.js";
|
|
35
35
|
export type { Desktop, DesktopApp, DesktopOptions } from "./desktop.js";
|
|
36
36
|
import type { Desktop, DesktopApp, DesktopOptions } from "./desktop.js";
|
|
37
|
-
export type { Terminal, TerminalOpts, TerminalResult } from "./terminal.js";
|
|
37
|
+
export type { Terminal, TerminalOpts, TerminalResult, TerminalWaitOptions, TerminalFailureCondition, TerminalCloseOptions } from "./terminal.js";
|
|
38
38
|
import type { Terminal, TerminalOpts, TerminalResult } from "./terminal.js";
|
|
39
39
|
export { type CoverageAdapter, type CoverageCaptureContext, type CoverageConfigureInfo, type CoverageSourceOptions, type CoverageSourceContext, type CoverageLoadContext, type ServiceCoverage, } from "./coverage.js";
|
|
40
40
|
import { type ServiceCoverage } from "./coverage.js";
|
package/dist/recorder.d.ts
CHANGED
|
@@ -407,6 +407,9 @@ export interface RecordedBrowserLocatorPart {
|
|
|
407
407
|
}
|
|
408
408
|
export interface BrowserEvent extends BaseEvent {
|
|
409
409
|
kind: "browser";
|
|
410
|
+
/** Explicit navigation options, when supplied by the test. */
|
|
411
|
+
timeout?: number;
|
|
412
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
410
413
|
action: BrowserAction;
|
|
411
414
|
target?: RecordedBrowserLocatorPart[];
|
|
412
415
|
destinationTarget?: RecordedBrowserLocatorPart[];
|
|
@@ -534,6 +537,18 @@ export interface TerminalEvent extends BaseEvent {
|
|
|
534
537
|
}
|
|
535
538
|
/** Action taken on an open interactive terminal session. */
|
|
536
539
|
export type TerminalStepAction = "send" | "sendLine" | "press" | "waitFor" | "exit" | "close";
|
|
540
|
+
export type TerminalWaitMatcher = {
|
|
541
|
+
kind: "text";
|
|
542
|
+
value: string;
|
|
543
|
+
} | {
|
|
544
|
+
kind: "regex";
|
|
545
|
+
value: string;
|
|
546
|
+
flags: string;
|
|
547
|
+
} | {
|
|
548
|
+
kind: "custom";
|
|
549
|
+
description?: string;
|
|
550
|
+
};
|
|
551
|
+
export type TerminalWaitOutcome = "matched" | "timed_out" | "failure_matched" | "predicate_error" | "process_exited" | "cancelled";
|
|
537
552
|
/**
|
|
538
553
|
* One operation on an open interactive terminal — analogous to
|
|
539
554
|
* `BrowserEvent`. The session's heavy asciicast frames live on the
|
|
@@ -557,9 +572,21 @@ export interface TerminalStepEvent extends BaseEvent {
|
|
|
557
572
|
description?: string;
|
|
558
573
|
/** How many polls `waitFor` made. */
|
|
559
574
|
attempts?: number;
|
|
560
|
-
/** Whether `waitFor` matched
|
|
575
|
+
/** Whether `waitFor` matched; see outcome for failure reasons. */
|
|
561
576
|
matched?: boolean;
|
|
562
|
-
/**
|
|
577
|
+
/** The condition being observed, including an optional custom-predicate description. */
|
|
578
|
+
matcher?: TerminalWaitMatcher;
|
|
579
|
+
outcome?: TerminalWaitOutcome;
|
|
580
|
+
failureMatchers?: {
|
|
581
|
+
description: string;
|
|
582
|
+
matcher: TerminalWaitMatcher;
|
|
583
|
+
}[];
|
|
584
|
+
intervalMs?: number;
|
|
585
|
+
timeoutMs?: number;
|
|
586
|
+
/** Wait boundaries on the session clock, including time with no output. */
|
|
587
|
+
castStartTimeSec?: number;
|
|
588
|
+
castEndTimeSec?: number;
|
|
589
|
+
/** Exit code observed on exit, close, or a failed wait. */
|
|
563
590
|
exitCode?: number;
|
|
564
591
|
/** Rendered screen at the moment of the op (post-action), truncated. */
|
|
565
592
|
screenPreview: string;
|
package/dist/terminal.d.ts
CHANGED
|
@@ -25,6 +25,25 @@ export interface TerminalOpts {
|
|
|
25
25
|
*/
|
|
26
26
|
timeoutMs?: number;
|
|
27
27
|
}
|
|
28
|
+
/** A visible terminal failure that should stop a wait immediately. */
|
|
29
|
+
export interface TerminalFailureCondition {
|
|
30
|
+
description: string;
|
|
31
|
+
matcher: string | RegExp;
|
|
32
|
+
}
|
|
33
|
+
export interface TerminalWaitOptions {
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
intervalMs?: number;
|
|
36
|
+
/** Cancel this observation without closing the terminal. */
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/** Describe a custom predicate's condition separately from the step title. */
|
|
39
|
+
condition?: string;
|
|
40
|
+
/** Checked against the rendered screen before the success matcher. */
|
|
41
|
+
failOn?: readonly TerminalFailureCondition[];
|
|
42
|
+
}
|
|
43
|
+
export interface TerminalCloseOptions {
|
|
44
|
+
/** Total deadline including graceful exit, kill and pipe drain. Default 10 s. */
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}
|
|
28
47
|
/** Result of the one-shot `ctx.terminal(...)` convenience. */
|
|
29
48
|
export interface TerminalResult {
|
|
30
49
|
/** Full decoded PTY output (raw bytes, ANSI escapes included). Capped. */
|
|
@@ -50,7 +69,8 @@ export interface Terminal {
|
|
|
50
69
|
/**
|
|
51
70
|
* Resolves when the underlying process ends — by `close()`, by the
|
|
52
71
|
* inner command exiting, or by timeout — to a `TerminalResult` with
|
|
53
|
-
* the full captured `output` and `exitCode`.
|
|
72
|
+
* the full captured `output` and `exitCode`. Rejects if output cannot
|
|
73
|
+
* be drained after exit.
|
|
54
74
|
*
|
|
55
75
|
* The result is provenance-tagged against the `exit` step, so an
|
|
56
76
|
* `expect((await term.exited).output).toContain(...)` /
|
|
@@ -91,29 +111,23 @@ export interface Terminal {
|
|
|
91
111
|
* `description` is a short human label that shows up in the event
|
|
92
112
|
* log so timelines don't fill with anonymous waits.
|
|
93
113
|
*
|
|
94
|
-
* Defaults:
|
|
95
|
-
*
|
|
114
|
+
* Defaults: 10 s total timeout, 100 ms between polls. Throws on timeout,
|
|
115
|
+
* a failure condition, predicate error, cancellation, or process exit before
|
|
116
|
+
* a match. On exit the final drained screen is checked first. Async custom
|
|
117
|
+
* predicates are supported; cancellation stops polling but cannot forcibly
|
|
118
|
+
* cancel work started by the predicate itself.
|
|
96
119
|
*/
|
|
97
|
-
waitFor(description: string, matcher: string, opts?:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}): Promise<Wrapped<string>>;
|
|
101
|
-
waitFor(description: string, matcher: RegExp, opts?: {
|
|
102
|
-
timeoutMs?: number;
|
|
103
|
-
intervalMs?: number;
|
|
104
|
-
}): Promise<Wrapped<RegExpMatchArray>>;
|
|
105
|
-
waitFor<T = unknown>(description: string, matcher: (screen: string, output: string) => T, opts?: {
|
|
106
|
-
timeoutMs?: number;
|
|
107
|
-
intervalMs?: number;
|
|
108
|
-
}): Promise<Wrapped<NonNullable<Awaited<T>>>>;
|
|
120
|
+
waitFor(description: string, matcher: string, opts?: TerminalWaitOptions): Promise<Wrapped<string>>;
|
|
121
|
+
waitFor(description: string, matcher: RegExp, opts?: TerminalWaitOptions): Promise<Wrapped<RegExpMatchArray>>;
|
|
122
|
+
waitFor<T = unknown>(description: string, matcher: (screen: string, output: string) => T, opts?: TerminalWaitOptions): Promise<Wrapped<NonNullable<Awaited<T>>>>;
|
|
109
123
|
/**
|
|
110
124
|
* Tear down. Writes Ctrl-D to stdin to nudge the shell out, then
|
|
111
125
|
* SIGKILLs after a grace period. Idempotent. Resolves to a
|
|
112
126
|
* `TerminalResult` (tagged against the `close` step) so you can assert
|
|
113
127
|
* on the transcript of a session that doesn't self-terminate. Optional
|
|
114
|
-
* — terminals are not auto-closed when a test ends.
|
|
128
|
+
* — terminals are not auto-closed when a test ends. Rejects on cleanup timeout.
|
|
115
129
|
*/
|
|
116
|
-
close(): Promise<Wrapped<TerminalResult>>;
|
|
130
|
+
close(opts?: TerminalCloseOptions): Promise<Wrapped<TerminalResult>>;
|
|
117
131
|
}
|
|
118
132
|
/**
|
|
119
133
|
* The concrete handle `openTerminal` returns. Extends the public
|