@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 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; resolves when the main frame's load completes. */
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]
@@ -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 backing server's own `initialBuckets`
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
- * [`adobe/s3mock`](https://github.com/adobe/S3Mock)), with an instrumented S3
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: "registry";
94
- reference: string;
92
+ type: "dockerfile";
93
+ content: string;
95
94
  };
96
95
  ports: number[];
97
96
  };
@@ -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
- // Backing image, pinned internally (not user-configurable). adobe/s3mock 5.1.0
4
- // is the current release; avoid exactly 5.0.0, which crashed at startup on
5
- // Java 25 (`NoClassDefFoundError: KotlinBuiltIns$2`) — fixed in 5.1.0.
6
- const S3_IMAGE = "adobe/s3mock:5.1.0";
7
- // Port the store listens on, fixed like the image and deliberately NOT an
8
- // option. The backing server's HTTP port is baked into the image, and nothing
9
- // the component returns can move it — so a `port` option could only relabel
10
- // the ready check, the declared ports and the client endpoint while the
11
- // container kept listening here. That is exactly what it did until 0.78.0: a
12
- // project that passed one got 60 s of refused probes and an environment that
13
- // never booted, with a healthy container beside it. A service reaches the
14
- // store at `http://<key>:9090`.
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
- * [`adobe/s3mock`](https://github.com/adobe/S3Mock)), with an instrumented S3
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: "registry", reference: S3_IMAGE },
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";
@@ -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 (false means it timed out). */
575
+ /** Whether `waitFor` matched; see outcome for failure reasons. */
561
576
  matched?: boolean;
562
- /** Exit code observed on `close`. */
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;
@@ -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`. Never rejects.
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: 5 s total timeout, 100 ms between polls. Throws on
95
- * timeout.
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
- timeoutMs?: number;
99
- intervalMs?: number;
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