effect-browserbase 0.3.0-beta.5 → 0.3.0-beta.7
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 +6 -4
- package/dist/testing/index.d.mts +3 -1
- package/dist/testing/index.mjs +71 -13
- package/dist/testing/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/testing/TestBrowserbase.ts +74 -10
package/README.md
CHANGED
|
@@ -266,10 +266,12 @@ under the real `BrowserbaseClient`, so the client and `Browserbase` run as they
|
|
|
266
266
|
Browserbase, for free. Sessions run until released or until their timeout on the Effect `Clock`, so
|
|
267
267
|
`TestClock` ends them. A `Script` loses a create, whose session it can list only `listedAfter` a
|
|
268
268
|
while, leaves a release pending, refuses it, or fails status reads, in turn. `connectUrl` gives
|
|
269
|
-
each session a DevTools address, such as a local Chromium's. It keeps sessions and stored contexts
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
each session a DevTools address, such as a local Chromium's. It keeps sessions and stored contexts,
|
|
270
|
+
and supplies synthetic Live View links for known session ids, with an empty page list. It does not
|
|
271
|
+
model link expiry or Browserbase's Live View behavior after a session ends. Other endpoints fail
|
|
272
|
+
the test. `sessions` tells each session's status, its original create `options` without provider
|
|
273
|
+
defaults, user metadata, releases asked, whether it was kept alive, and the stored context it
|
|
274
|
+
loaded and whether it saves to it. Its ids are UUIDs, as Browserbase's are, and it answers each id shape as Browserbase
|
|
273
275
|
does: it refuses a session id of any other shape, where it answers an unknown well-formed one as
|
|
274
276
|
not found. `BrowserbaseContract.checks` are what this package relies on Browserbase to do, each an
|
|
275
277
|
Effect over `BrowserbaseClient`; the fake passes them. Its answers also have the shapes
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as BrowserbaseError } from "../BrowserbaseError-D1EOrLNc.mjs";
|
|
2
|
-
import { n as BrowserbaseClient } from "../BrowserbaseClient-CtAQ2rg4.mjs";
|
|
2
|
+
import { n as BrowserbaseClient, p as SessionOptions } from "../BrowserbaseClient-CtAQ2rg4.mjs";
|
|
3
3
|
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
4
|
import { HttpClient } from "effect/http";
|
|
5
5
|
declare namespace BrowserbaseContract_d_exports {
|
|
@@ -66,6 +66,8 @@ type Script = typeof Script.Type;
|
|
|
66
66
|
interface Kept {
|
|
67
67
|
readonly id: string;
|
|
68
68
|
readonly status: string;
|
|
69
|
+
/** The create options as sent, without provider defaults. Only the test handle exposes them. */
|
|
70
|
+
readonly options: SessionOptions;
|
|
69
71
|
readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
|
|
70
72
|
readonly releases: number;
|
|
71
73
|
/** Whether it was created to outlive its connections, until released or timed out. */
|
package/dist/testing/index.mjs
CHANGED
|
@@ -122,9 +122,9 @@ const checks = [
|
|
|
122
122
|
* so `TestClock` drives their end. A `Script` decides how creates, releases and session reads
|
|
123
123
|
* answer, in turn, so a test can lose a create, leave a release pending or fail status reads.
|
|
124
124
|
*
|
|
125
|
-
* It keeps sessions and stored contexts and
|
|
126
|
-
* defect. It doesn't model
|
|
127
|
-
* browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
|
|
125
|
+
* It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint
|
|
126
|
+
* fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,
|
|
127
|
+
* with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
|
|
128
128
|
* of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses
|
|
129
129
|
* a session or context id of any other shape as invalid, where it answers an unknown one as not
|
|
130
130
|
* found.
|
|
@@ -184,13 +184,63 @@ var TestBrowserbase = class extends Context.Service()("effect-browserbase/testin
|
|
|
184
184
|
/** The API key the fake takes; any other is unauthorized. */
|
|
185
185
|
const apiKey = "test-browserbase-key";
|
|
186
186
|
const CreateBody = Schema.fromJsonString(Schema.Struct({
|
|
187
|
+
projectId: Schema.optional(Schema.String),
|
|
188
|
+
extensionId: Schema.optional(Schema.String),
|
|
187
189
|
timeout: Schema.optional(Schema.Finite),
|
|
188
190
|
keepAlive: Schema.optional(Schema.Boolean),
|
|
191
|
+
region: Schema.optional(Schema.Literals([
|
|
192
|
+
"us-west-2",
|
|
193
|
+
"us-east-1",
|
|
194
|
+
"eu-central-1",
|
|
195
|
+
"ap-southeast-1"
|
|
196
|
+
])),
|
|
197
|
+
proxies: Schema.optional(Schema.Union([Schema.Boolean, Schema.Array(Schema.Union([
|
|
198
|
+
Schema.Struct({
|
|
199
|
+
type: Schema.Literal("browserbase"),
|
|
200
|
+
geolocation: Schema.optional(Schema.Struct({
|
|
201
|
+
country: Schema.String,
|
|
202
|
+
state: Schema.optional(Schema.String),
|
|
203
|
+
city: Schema.optional(Schema.String)
|
|
204
|
+
})),
|
|
205
|
+
domainPattern: Schema.optional(Schema.String)
|
|
206
|
+
}),
|
|
207
|
+
Schema.Struct({
|
|
208
|
+
type: Schema.Literal("external"),
|
|
209
|
+
server: Schema.String,
|
|
210
|
+
username: Schema.optional(Schema.String),
|
|
211
|
+
password: Schema.optional(Schema.String),
|
|
212
|
+
domainPattern: Schema.optional(Schema.String)
|
|
213
|
+
}),
|
|
214
|
+
Schema.Struct({
|
|
215
|
+
type: Schema.Literal("none"),
|
|
216
|
+
domainPattern: Schema.optional(Schema.String)
|
|
217
|
+
})
|
|
218
|
+
]))])),
|
|
189
219
|
userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
190
|
-
browserSettings: Schema.optional(Schema.Struct({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
220
|
+
browserSettings: Schema.optional(Schema.Struct({
|
|
221
|
+
context: Schema.optional(Schema.Struct({
|
|
222
|
+
id: Schema.String,
|
|
223
|
+
persist: Schema.optional(Schema.Boolean)
|
|
224
|
+
})),
|
|
225
|
+
viewport: Schema.optional(Schema.Struct({
|
|
226
|
+
width: Schema.Finite,
|
|
227
|
+
height: Schema.Finite
|
|
228
|
+
})),
|
|
229
|
+
blockAds: Schema.optional(Schema.Boolean),
|
|
230
|
+
solveCaptchas: Schema.optional(Schema.Boolean),
|
|
231
|
+
recordSession: Schema.optional(Schema.Boolean),
|
|
232
|
+
logSession: Schema.optional(Schema.Boolean),
|
|
233
|
+
verified: Schema.optional(Schema.Boolean),
|
|
234
|
+
os: Schema.optional(Schema.Literals([
|
|
235
|
+
"windows",
|
|
236
|
+
"mac",
|
|
237
|
+
"linux",
|
|
238
|
+
"mobile",
|
|
239
|
+
"tablet"
|
|
240
|
+
])),
|
|
241
|
+
allowedDomains: Schema.optional(Schema.Array(Schema.String)),
|
|
242
|
+
ignoreCertificateErrors: Schema.optional(Schema.Boolean)
|
|
243
|
+
}))
|
|
194
244
|
}));
|
|
195
245
|
const ReleaseBody = Schema.fromJsonString(Schema.Struct({ status: Schema.Literal("REQUEST_RELEASE") }));
|
|
196
246
|
const iso = (millis) => DateTime.formatIso(DateTime.makeUnsafe(millis));
|
|
@@ -214,11 +264,11 @@ const describe = (fake, row, connect) => {
|
|
|
214
264
|
updatedAt: iso(end?.at ?? row.createdAt),
|
|
215
265
|
startedAt: iso(row.createdAt),
|
|
216
266
|
expiresAt: iso(row.expiresAt),
|
|
217
|
-
projectId: "test-project",
|
|
267
|
+
projectId: row.options.projectId ?? "test-project",
|
|
218
268
|
status: end?.status ?? "RUNNING",
|
|
219
269
|
proxyBytes: 0,
|
|
220
270
|
keepAlive: row.keepAlive,
|
|
221
|
-
region: "us-west-2",
|
|
271
|
+
region: row.options.region ?? "us-west-2",
|
|
222
272
|
...end === void 0 ? {} : { endedAt: iso(end.at) },
|
|
223
273
|
...row.context === void 0 ? {} : { contextId: row.context.id },
|
|
224
274
|
...row.userMetadata === void 0 ? {} : { userMetadata: row.userMetadata },
|
|
@@ -263,15 +313,17 @@ const serve = (fake, request, url) => {
|
|
|
263
313
|
const reply = fake.pending.creates.shift() ?? { _tag: "Accept" };
|
|
264
314
|
if (reply._tag === "Reject") return answer(reply.status, { message: "refused, as the script said" }, reply.retryAfterSeconds === void 0 ? {} : { "retry-after": `${reply.retryAfterSeconds}` });
|
|
265
315
|
const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));
|
|
266
|
-
|
|
316
|
+
if (body === void 0) return answer(400, { message: "Invalid session settings" });
|
|
317
|
+
const loaded = body.browserSettings?.context;
|
|
267
318
|
const createdAt = fake.now();
|
|
268
319
|
const created = {
|
|
269
320
|
id: nextId(fake),
|
|
321
|
+
options: body,
|
|
270
322
|
createdAt,
|
|
271
323
|
listedAt: createdAt + (reply._tag === "Lost" ? reply.listedAfter ?? 0 : 0),
|
|
272
|
-
expiresAt: createdAt + (body
|
|
273
|
-
keepAlive: body
|
|
274
|
-
userMetadata: body
|
|
324
|
+
expiresAt: createdAt + (body.timeout ?? 300) * 1e3,
|
|
325
|
+
keepAlive: body.keepAlive ?? false,
|
|
326
|
+
userMetadata: body.userMetadata,
|
|
275
327
|
context: loaded === void 0 ? void 0 : {
|
|
276
328
|
id: loaded.id,
|
|
277
329
|
persist: loaded.persist ?? false
|
|
@@ -296,6 +348,11 @@ const serve = (fake, request, url) => {
|
|
|
296
348
|
if (reply._tag === "Lost") return lost;
|
|
297
349
|
return row === void 0 ? answer(404, { message: "Session not found" }) : answer(200, describe(fake, row, true));
|
|
298
350
|
}
|
|
351
|
+
case "GET sessions id debug": return row === void 0 ? answer(404, { message: "Session not found" }) : answer(200, {
|
|
352
|
+
debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,
|
|
353
|
+
debuggerUrl: `https://live.browserbase.test/${row.id}`,
|
|
354
|
+
pages: []
|
|
355
|
+
});
|
|
299
356
|
case "POST sessions id": {
|
|
300
357
|
if (row === void 0) return answer(404, { message: "Session not found" });
|
|
301
358
|
if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request)))) return answer(400, { message: "status must be REQUEST_RELEASE" });
|
|
@@ -352,6 +409,7 @@ const make = Effect.fnUntraced(function* (script = {}) {
|
|
|
352
409
|
requests: Effect.sync(() => [...fake.requests]),
|
|
353
410
|
sessions: Effect.sync(() => [...fake.sessions.values()].map((row) => ({
|
|
354
411
|
id: row.id,
|
|
412
|
+
options: row.options,
|
|
355
413
|
status: endOf(fake, row)?.status ?? "RUNNING",
|
|
356
414
|
userMetadata: row.userMetadata,
|
|
357
415
|
releases: row.releases,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["clientLayer"],"sources":["../../src/testing/BrowserbaseContract.ts","../../src/testing/TestBrowserbase.ts"],"sourcesContent":["/**\n * What this package relies on the Browserbase API to do, as named checks over\n * `BrowserbaseClient`, each an Effect with no test runner around it. A package test runs them\n * against `TestBrowserbase`, which keeps the fake to them; run against Browserbase itself, they\n * show whether the fake is still faithful, at the cost of a few one-minute sessions. Checks that\n * move the test clock run only against the fake.\n *\n * @category testing\n * @since 0.3.0\n */\nimport { Clock, Effect, Schedule, Schema } from \"effect\";\nimport { TestClock } from \"effect/testing\";\n\nimport { BrowserbaseClient, type Session } from \"../BrowserbaseClient.ts\";\nimport type { BrowserbaseError } from \"../BrowserbaseError.ts\";\n\n/** A check found the API behaving otherwise. */\nexport class Broken extends Schema.TaggedError<Broken>()(\"Broken\", {\n check: Schema.String,\n detail: Schema.String,\n}) {\n override get message() {\n return `${this.check}: ${this.detail}`;\n }\n}\n\nexport interface Check {\n readonly name: string;\n /** It moves the test clock, so only the fake can run it. */\n readonly clock: boolean;\n readonly run: Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>;\n}\n\nconst check = (\n name: string,\n run: (\n expect: (holds: boolean, detail: string) => Effect.Effect<void, Broken>,\n ) => Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>,\n clock = false,\n): Check => ({\n name,\n clock,\n run: run((holds, detail) =>\n holds ? Effect.void : Effect.fail(new Broken({ check: name, detail })),\n ).pipe(Effect.withSpan(\"BrowserbaseContract.check\", { attributes: { check: name } })),\n});\n\nconst running = (session: Session) => session.status === \"PENDING\" || session.status === \"RUNNING\";\n\n/** What the call failed with, or `nothing` when it succeeded. */\nconst failure = <A, R>(call: Effect.Effect<A, BrowserbaseError, R>) =>\n Effect.match(call, { onFailure: (error) => error.reason._tag, onSuccess: () => \"nothing\" });\n\n/** Read the session until it has ended, for at most 10 seconds. */\nconst ended = (client: BrowserbaseClient[\"Service\"], id: string) =>\n client.getSession(id).pipe(\n Effect.repeat({\n until: (session) => !running(session),\n schedule: Schedule.spaced(\"1 second\").pipe(Schedule.upTo({ times: 10 })),\n }),\n );\n\nexport const checks: ReadonlyArray<Check> = [\n check(\"a new session runs, and ends once released\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* expect(running(session), `a new session is ${session.status}`);\n yield* client.releaseSession(session.id);\n const after = yield* ended(client, session.id);\n\n yield* expect(after.status === \"COMPLETED\", `a released session is ${after.status}`);\n // Releasing a session that has ended succeeds and changes nothing.\n yield* client.releaseSession(session.id);\n const again = yield* client.getSession(session.id);\n\n yield* expect(again.status === \"COMPLETED\", `released twice, it is ${again.status}`);\n }),\n ),\n check(\"a well-formed id no session has is not found\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"00000000-0000-4000-8000-000000000000\"));\n\n yield* expect(read === \"NotFound\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a session id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"not-a-session-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"sessions are found by status and user metadata\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const label = `contract-${yield* Clock.currentTimeMillis}`;\n const session = yield* client.createSession({ timeout: 60, userMetadata: { label } });\n const query = (value: string) => `user_metadata['label']:'${value}'`;\n\n // A new session is PENDING or RUNNING, and may change between the two queries, so the\n // package looks under both statuses; a check of one alone would fail a compliant API.\n const running = (value: string) =>\n Effect.map(\n Effect.forEach([\"PENDING\", \"RUNNING\"], (status) =>\n client.listSessions({ status, query: query(value) }),\n ),\n (found) => new Set(found.flat().map(({ id }) => id)),\n );\n\n const found = yield* running(label);\n const other = yield* running(`${label}-x`);\n\n yield* client.releaseSession(session.id);\n yield* ended(client, session.id);\n const gone = yield* running(label);\n // With no status, a search finds sessions in every state, as a lost create's search needs.\n const any = yield* client.listSessions({ query: query(label) });\n\n yield* expect(\n found.size === 1 && found.has(session.id),\n `${found.size} unended sessions had its label`,\n );\n yield* expect(other.size === 0, `${other.size} sessions had another label`);\n yield* expect(gone.size === 0, `${gone.size} still ran once it had ended`);\n yield* expect(\n any.some(({ id, status }) => id === session.id && status === \"COMPLETED\"),\n `with no status, a search found ${any.length} sessions, not it ended`,\n );\n }),\n ),\n check(\"a context id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getContext(\"not-a-context-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a stored context reads back, and is gone once deleted\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const created = yield* client.createContext();\n const read = yield* client.getContext(created.id);\n\n yield* client.deleteContext(created.id);\n const gone = yield* failure(client.getContext(created.id));\n\n yield* expect(read.id === created.id, `it read back as ${read.id}`);\n yield* expect(gone === \"NotFound\", `once deleted, reading it failed with ${gone}`);\n }),\n ),\n check(\n \"a session ends at its timeout\",\n (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* TestClock.adjust(\"61 seconds\");\n const after = yield* client.getSession(session.id);\n\n yield* expect(after.status === \"TIMED_OUT\", `after its timeout it is ${after.status}`);\n }),\n true,\n ),\n];\n","/**\n * An in-memory Browserbase API for tests. It is the `HttpClient` the real `BrowserbaseClient` runs\n * on, so the client and the provider above it run as they would against Browserbase, and nothing\n * leaves the process. Sessions run until released, or until their timeout on the Effect `Clock`,\n * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads\n * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.\n *\n * It keeps sessions and stored contexts and nothing else: any other endpoint fails the test with a\n * defect. It doesn't model regions, quotas or billing, and a context is only a record, with no\n * browser state. Every session hands out the script's `connectUrl`, such as the DevTools address\n * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses\n * a session or context id of any other shape as invalid, where it answers an unknown one as not\n * found.\n *\n * @category testing\n * @since 0.3.0\n */\nimport {\n Clock,\n Context,\n DateTime,\n Effect,\n Layer,\n Option,\n Predicate,\n Redacted,\n Schema,\n} from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n} from \"effect/http\";\n\nimport { type BrowserbaseClient, layer as clientLayer } from \"../BrowserbaseClient.ts\";\n\nconst Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));\n\n/** How a session create answers. */\nexport const Create = Schema.Union([\n /** The session is made, and runs. */\n Schema.TaggedStruct(\"Accept\", {}),\n /** Refused with `status`, and nothing is made. */\n Schema.TaggedStruct(\"Reject\", {\n status: Refusal,\n retryAfterSeconds: Schema.optional(Schema.Int),\n }),\n /** The session is made, but the answer holds only its id. */\n Schema.TaggedStruct(\"Malformed\", {}),\n /**\n * The session is made, but no answer arrives, and Browserbase lists it only `listedAfter`\n * milliseconds later, as a search can trail what was made.\n */\n Schema.TaggedStruct(\"Lost\", { listedAfter: Schema.optional(Schema.Finite) }),\n]);\n\n/** How a release answers. */\nexport const Release = Schema.Union([\n /** The session ends at once. */\n Schema.TaggedStruct(\"Ends\", {}),\n /** Accepted, but the session runs on for `millis`, or without them until its timeout. */\n Schema.TaggedStruct(\"Pending\", { millis: Schema.optional(Schema.Finite) }),\n /** Refused with `status`; the session runs on. */\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n /** The session ends, but no answer arrives. */\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\n/** How a session read answers. */\nexport const Read = Schema.Union([\n Schema.TaggedStruct(\"Answer\", {}),\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\nexport const Script = Schema.Struct({\n /** How creates answer, in turn; once these run out, each is accepted. */\n creates: Schema.optional(Schema.Array(Create)),\n /** How releases answer, in turn; once these run out, each ends its session. */\n releases: Schema.optional(Schema.Array(Release)),\n /** How session reads answer, in turn; once these run out, each answers. */\n reads: Schema.optional(Schema.Array(Read)),\n /** The DevTools address each session hands out. Without it, sessions have none. */\n connectUrl: Schema.optional(Schema.String),\n});\n\nexport type Script = typeof Script.Type;\n\n/** A session as the fake keeps it, with how many releases it was asked. */\nexport interface Kept {\n readonly id: string;\n readonly status: string;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly releases: number;\n /** Whether it was created to outlive its connections, until released or timed out. */\n readonly keepAlive: boolean;\n /** The stored context it loaded, and whether it saves to it as it ends. */\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n}\n\nexport interface Handle {\n /** Every request, oldest first, as `METHOD /path?query`. */\n readonly requests: Effect.Effect<ReadonlyArray<string>>;\n /** Each session as Browserbase would describe it now, oldest first. */\n readonly sessions: Effect.Effect<ReadonlyArray<Kept>>;\n /** End a session as Browserbase does on its own, as when its browser fails. */\n readonly end: (id: string) => Effect.Effect<void>;\n}\n\n/** The fake's handle, beside the `BrowserbaseClient` that `layer` runs on it. */\nexport class TestBrowserbase extends Context.Service<TestBrowserbase, Handle>()(\n \"effect-browserbase/testing/TestBrowserbase\",\n) {}\n\n/** The API key the fake takes; any other is unauthorized. */\nexport const apiKey = \"test-browserbase-key\";\n\nconst CreateBody = Schema.fromJsonString(\n Schema.Struct({\n timeout: Schema.optional(Schema.Finite),\n keepAlive: Schema.optional(Schema.Boolean),\n userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n browserSettings: Schema.optional(\n Schema.Struct({\n context: Schema.optional(\n Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),\n ),\n }),\n ),\n }),\n);\n\nconst ReleaseBody = Schema.fromJsonString(\n Schema.Struct({ status: Schema.Literal(\"REQUEST_RELEASE\") }),\n);\n\ninterface Row {\n readonly id: string;\n readonly createdAt: number;\n /** When a list first shows it. */\n readonly listedAt: number;\n readonly expiresAt: number;\n readonly keepAlive: boolean;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n /** How and when it ends, unless its timeout comes first. */\n ends: { readonly status: string; readonly at: number } | undefined;\n releases: number;\n}\n\n/** Everything one fake keeps. */\ninterface Fake {\n readonly now: () => number;\n readonly connectUrl: string | undefined;\n readonly pending: {\n readonly creates: Array<typeof Create.Type>;\n readonly releases: Array<typeof Release.Type>;\n readonly reads: Array<typeof Read.Type>;\n };\n readonly requests: Array<string>;\n readonly sessions: Map<string, Row>;\n readonly contexts: Map<string, number>;\n /** How many sessions and contexts it has made, together, for their ids. */\n readonly made: { count: number };\n}\n\nconst iso = (millis: number) => DateTime.formatIso(DateTime.makeUnsafe(millis));\n\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The next id, a UUID as Browserbase's are, counted so that every run hands out the same. */\nconst nextId = (fake: Fake) =>\n `00000000-0000-4000-8000-${(++fake.made.count).toString(16).padStart(12, \"0\")}`;\n\n/** How the session ended by now, or undefined while it runs. */\nconst endOf = (fake: Fake, row: Row) => {\n const end =\n row.ends !== undefined && row.ends.at <= row.expiresAt\n ? row.ends\n : { status: \"TIMED_OUT\", at: row.expiresAt };\n\n return end.at <= fake.now() ? end : undefined;\n};\n\n/** The session as Browserbase answers it; only creates and reads hand out its address. */\nconst describe = (fake: Fake, row: Row, connect: boolean) => {\n const end = endOf(fake, row);\n\n return {\n id: row.id,\n createdAt: iso(row.createdAt),\n updatedAt: iso(end?.at ?? row.createdAt),\n startedAt: iso(row.createdAt),\n expiresAt: iso(row.expiresAt),\n projectId: \"test-project\",\n status: end?.status ?? \"RUNNING\",\n proxyBytes: 0,\n keepAlive: row.keepAlive,\n region: \"us-west-2\",\n ...(end === undefined ? {} : { endedAt: iso(end.at) }),\n ...(row.context === undefined ? {} : { contextId: row.context.id }),\n ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),\n ...(connect && fake.connectUrl !== undefined ? { connectUrl: fake.connectUrl } : {}),\n ...(connect\n ? {\n seleniumRemoteUrl: `https://connect.browserbase.test/webdriver/${row.id}`,\n signingKey: `signing-key-${row.id}`,\n }\n : {}),\n };\n};\n\nconst bodyText = (request: HttpClientRequest.HttpClientRequest) =>\n request.body._tag === \"Uint8Array\"\n ? (request.body.text ?? new TextDecoder().decode(request.body.body))\n : \"\";\n\n/** A metadata query in Browserbase's one form, `user_metadata['a']['b']:'value'`, as a test. */\nconst metadataQuery = (query: string) => {\n const parsed = /^user_metadata((?:\\['[^']*'\\])+):'([^']*)'$/.exec(query);\n\n if (parsed === null) return undefined;\n const path = [...(parsed[1] ?? \"\").matchAll(/\\['([^']*)'\\]/g)].map((part) => part[1] ?? \"\");\n\n return (metadata: unknown) =>\n path.reduce<unknown>(\n (value, key) => (Predicate.isObject(value) ? Reflect.get(value, key) : undefined),\n metadata,\n ) === parsed[2];\n};\n\nconst unsupported = (detail: string) => Effect.die(new Error(`TestBrowserbase ${detail}`));\n\n/** Answer one request as Browserbase would, after the script's say. */\nconst serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: URL) => {\n const answer = (status: number, body?: unknown, headers: Record<string, string> = {}) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(body === undefined ? null : JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...headers },\n }),\n ),\n );\n\n const lost = Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"TestBrowserbase lost this answer, as its script said\",\n }),\n }),\n );\n\n const refused = (status: number) => answer(status, { message: \"refused, as the script said\" });\n const [, version, kind, id, rest] = url.pathname.split(\"/\");\n const row = id === undefined ? undefined : fake.sessions.get(id);\n\n fake.requests.push(`${request.method} ${url.pathname}${url.search}`);\n if (request.headers[\"x-bb-api-key\"] !== apiKey)\n return answer(401, { message: \"Invalid API key\" });\n if (version !== \"v1\") return unsupported(`serves only /v1, not ${url.pathname}`);\n // Browserbase refuses a session or context id that is not a UUID before it looks for it.\n if ((kind === \"sessions\" || kind === \"contexts\") && id !== undefined && !uuid.test(id))\n return answer(400, { message: `Invalid ${kind === \"sessions\" ? \"Session\" : \"Context\"} ID` });\n\n switch (\n `${request.method} ${kind}${id === undefined ? \"\" : \" id\"}${rest === undefined ? \"\" : ` ${rest}`}`\n ) {\n case \"POST sessions\": {\n const reply = fake.pending.creates.shift() ?? { _tag: \"Accept\" };\n\n if (reply._tag === \"Reject\")\n return answer(\n reply.status,\n { message: \"refused, as the script said\" },\n reply.retryAfterSeconds === undefined\n ? {}\n : { \"retry-after\": `${reply.retryAfterSeconds}` },\n );\n const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));\n const loaded = body?.browserSettings?.context;\n const createdAt = fake.now();\n\n const created: Row = {\n id: nextId(fake),\n createdAt,\n listedAt: createdAt + (reply._tag === \"Lost\" ? (reply.listedAfter ?? 0) : 0),\n expiresAt: createdAt + (body?.timeout ?? 300) * 1000,\n keepAlive: body?.keepAlive ?? false,\n userMetadata: body?.userMetadata,\n context:\n loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },\n ends: undefined,\n releases: 0,\n };\n\n fake.sessions.set(created.id, created);\n if (reply._tag === \"Lost\") return lost;\n\n return answer(\n 201,\n reply._tag === \"Malformed\" ? { id: created.id } : describe(fake, created, true),\n );\n }\n case \"GET sessions\": {\n const status = url.searchParams.get(\"status\");\n const query = url.searchParams.get(\"q\");\n const matches = query === null ? () => true : metadataQuery(query);\n\n if (matches === undefined) return unsupported(`cannot read the query ${query}`);\n\n return answer(\n 200,\n [...fake.sessions.values()]\n .filter(\n (kept) =>\n kept.listedAt <= fake.now() &&\n (status === null || (endOf(fake, kept)?.status ?? \"RUNNING\") === status) &&\n matches(kept.userMetadata),\n )\n .map((kept) => describe(fake, kept, false)),\n );\n }\n case \"GET sessions id\": {\n const reply = fake.pending.reads.shift() ?? { _tag: \"Answer\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n if (reply._tag === \"Lost\") return lost;\n\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, describe(fake, row, true));\n }\n case \"POST sessions id\": {\n if (row === undefined) return answer(404, { message: \"Session not found\" });\n if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))\n return answer(400, { message: \"status must be REQUEST_RELEASE\" });\n row.releases += 1;\n const reply = fake.pending.releases.shift() ?? { _tag: \"Ends\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n // Releasing a session that has ended changes nothing, and succeeds.\n if (\n endOf(fake, row) === undefined &&\n (reply._tag !== \"Pending\" || reply.millis !== undefined)\n )\n row.ends = {\n status: \"COMPLETED\",\n at: fake.now() + (reply._tag === \"Pending\" ? (reply.millis ?? 0) : 0),\n };\n\n return reply._tag === \"Lost\" ? lost : answer(200, describe(fake, row, false));\n }\n case \"POST contexts\": {\n const created = nextId(fake);\n\n fake.contexts.set(created, fake.now());\n\n return answer(201, {\n id: created,\n publicKey: \"test-public-key\",\n cipherAlgorithm: \"AES-256-CBC\",\n initializationVectorSize: 16,\n });\n }\n case \"GET contexts id\": {\n const createdAt = id === undefined ? undefined : fake.contexts.get(id);\n\n return createdAt === undefined\n ? answer(404, { message: \"Context not found\" })\n : answer(200, {\n id,\n createdAt: iso(createdAt),\n updatedAt: iso(createdAt),\n projectId: \"test-project\",\n });\n }\n case \"DELETE contexts id\":\n return id !== undefined && fake.contexts.delete(id)\n ? answer(204)\n : answer(404, { message: \"Context not found\" });\n default:\n return unsupported(`does not implement ${request.method} ${url.pathname}`);\n }\n};\n\n/** The fake as an `HttpClient`, with its handle. */\nexport const make = Effect.fnUntraced(function* (script: Script = {}) {\n const clock = yield* Clock.Clock;\n const decoded = yield* Effect.orDie(Schema.decodeEffect(Script)(script));\n\n const fake: Fake = {\n now: () => clock.currentTimeMillisUnsafe(),\n connectUrl: decoded.connectUrl,\n pending: {\n creates: [...(decoded.creates ?? [])],\n releases: [...(decoded.releases ?? [])],\n reads: [...(decoded.reads ?? [])],\n },\n requests: [],\n sessions: new Map(),\n contexts: new Map(),\n made: { count: 0 },\n };\n\n const handle: Handle = {\n requests: Effect.sync(() => [...fake.requests]),\n sessions: Effect.sync(() =>\n [...fake.sessions.values()].map((row) => ({\n id: row.id,\n status: endOf(fake, row)?.status ?? \"RUNNING\",\n userMetadata: row.userMetadata,\n releases: row.releases,\n keepAlive: row.keepAlive,\n context: row.context,\n })),\n ),\n end: (id) =>\n Effect.sync(() => {\n const row = fake.sessions.get(id);\n\n if (row !== undefined && endOf(fake, row) === undefined)\n row.ends = { status: \"ERROR\", at: fake.now() };\n }),\n };\n\n return { http: HttpClient.make((request, url) => serve(fake, request, url)), handle };\n});\n\n/** The real `BrowserbaseClient` over the fake, and the fake's handle. */\nexport const layer = (script: Script = {}): Layer.Layer<BrowserbaseClient | TestBrowserbase> =>\n Layer.unwrap(\n Effect.map(make(script), ({ http, handle }) =>\n Layer.merge(\n clientLayer({\n apiKey: Redacted.make(apiKey),\n baseUrl: \"https://api.browserbase.test\",\n }).pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, http))),\n Layer.succeed(TestBrowserbase, handle),\n ),\n ),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["clientLayer"],"sources":["../../src/testing/BrowserbaseContract.ts","../../src/testing/TestBrowserbase.ts"],"sourcesContent":["/**\n * What this package relies on the Browserbase API to do, as named checks over\n * `BrowserbaseClient`, each an Effect with no test runner around it. A package test runs them\n * against `TestBrowserbase`, which keeps the fake to them; run against Browserbase itself, they\n * show whether the fake is still faithful, at the cost of a few one-minute sessions. Checks that\n * move the test clock run only against the fake.\n *\n * @category testing\n * @since 0.3.0\n */\nimport { Clock, Effect, Schedule, Schema } from \"effect\";\nimport { TestClock } from \"effect/testing\";\n\nimport { BrowserbaseClient, type Session } from \"../BrowserbaseClient.ts\";\nimport type { BrowserbaseError } from \"../BrowserbaseError.ts\";\n\n/** A check found the API behaving otherwise. */\nexport class Broken extends Schema.TaggedError<Broken>()(\"Broken\", {\n check: Schema.String,\n detail: Schema.String,\n}) {\n override get message() {\n return `${this.check}: ${this.detail}`;\n }\n}\n\nexport interface Check {\n readonly name: string;\n /** It moves the test clock, so only the fake can run it. */\n readonly clock: boolean;\n readonly run: Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>;\n}\n\nconst check = (\n name: string,\n run: (\n expect: (holds: boolean, detail: string) => Effect.Effect<void, Broken>,\n ) => Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>,\n clock = false,\n): Check => ({\n name,\n clock,\n run: run((holds, detail) =>\n holds ? Effect.void : Effect.fail(new Broken({ check: name, detail })),\n ).pipe(Effect.withSpan(\"BrowserbaseContract.check\", { attributes: { check: name } })),\n});\n\nconst running = (session: Session) => session.status === \"PENDING\" || session.status === \"RUNNING\";\n\n/** What the call failed with, or `nothing` when it succeeded. */\nconst failure = <A, R>(call: Effect.Effect<A, BrowserbaseError, R>) =>\n Effect.match(call, { onFailure: (error) => error.reason._tag, onSuccess: () => \"nothing\" });\n\n/** Read the session until it has ended, for at most 10 seconds. */\nconst ended = (client: BrowserbaseClient[\"Service\"], id: string) =>\n client.getSession(id).pipe(\n Effect.repeat({\n until: (session) => !running(session),\n schedule: Schedule.spaced(\"1 second\").pipe(Schedule.upTo({ times: 10 })),\n }),\n );\n\nexport const checks: ReadonlyArray<Check> = [\n check(\"a new session runs, and ends once released\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* expect(running(session), `a new session is ${session.status}`);\n yield* client.releaseSession(session.id);\n const after = yield* ended(client, session.id);\n\n yield* expect(after.status === \"COMPLETED\", `a released session is ${after.status}`);\n // Releasing a session that has ended succeeds and changes nothing.\n yield* client.releaseSession(session.id);\n const again = yield* client.getSession(session.id);\n\n yield* expect(again.status === \"COMPLETED\", `released twice, it is ${again.status}`);\n }),\n ),\n check(\"a well-formed id no session has is not found\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"00000000-0000-4000-8000-000000000000\"));\n\n yield* expect(read === \"NotFound\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a session id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"not-a-session-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"sessions are found by status and user metadata\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const label = `contract-${yield* Clock.currentTimeMillis}`;\n const session = yield* client.createSession({ timeout: 60, userMetadata: { label } });\n const query = (value: string) => `user_metadata['label']:'${value}'`;\n\n // A new session is PENDING or RUNNING, and may change between the two queries, so the\n // package looks under both statuses; a check of one alone would fail a compliant API.\n const running = (value: string) =>\n Effect.map(\n Effect.forEach([\"PENDING\", \"RUNNING\"], (status) =>\n client.listSessions({ status, query: query(value) }),\n ),\n (found) => new Set(found.flat().map(({ id }) => id)),\n );\n\n const found = yield* running(label);\n const other = yield* running(`${label}-x`);\n\n yield* client.releaseSession(session.id);\n yield* ended(client, session.id);\n const gone = yield* running(label);\n // With no status, a search finds sessions in every state, as a lost create's search needs.\n const any = yield* client.listSessions({ query: query(label) });\n\n yield* expect(\n found.size === 1 && found.has(session.id),\n `${found.size} unended sessions had its label`,\n );\n yield* expect(other.size === 0, `${other.size} sessions had another label`);\n yield* expect(gone.size === 0, `${gone.size} still ran once it had ended`);\n yield* expect(\n any.some(({ id, status }) => id === session.id && status === \"COMPLETED\"),\n `with no status, a search found ${any.length} sessions, not it ended`,\n );\n }),\n ),\n check(\"a context id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getContext(\"not-a-context-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a stored context reads back, and is gone once deleted\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const created = yield* client.createContext();\n const read = yield* client.getContext(created.id);\n\n yield* client.deleteContext(created.id);\n const gone = yield* failure(client.getContext(created.id));\n\n yield* expect(read.id === created.id, `it read back as ${read.id}`);\n yield* expect(gone === \"NotFound\", `once deleted, reading it failed with ${gone}`);\n }),\n ),\n check(\n \"a session ends at its timeout\",\n (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* TestClock.adjust(\"61 seconds\");\n const after = yield* client.getSession(session.id);\n\n yield* expect(after.status === \"TIMED_OUT\", `after its timeout it is ${after.status}`);\n }),\n true,\n ),\n];\n","/**\n * An in-memory Browserbase API for tests. It is the `HttpClient` the real `BrowserbaseClient` runs\n * on, so the client and the provider above it run as they would against Browserbase, and nothing\n * leaves the process. Sessions run until released, or until their timeout on the Effect `Clock`,\n * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads\n * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.\n *\n * It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint\n * fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,\n * with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address\n * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses\n * a session or context id of any other shape as invalid, where it answers an unknown one as not\n * found.\n *\n * @category testing\n * @since 0.3.0\n */\nimport {\n Clock,\n Context,\n DateTime,\n Effect,\n Layer,\n Option,\n Predicate,\n Redacted,\n Schema,\n} from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n} from \"effect/http\";\n\nimport {\n type BrowserbaseClient,\n layer as clientLayer,\n type SessionOptions,\n} from \"../BrowserbaseClient.ts\";\n\nconst Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));\n\n/** How a session create answers. */\nexport const Create = Schema.Union([\n /** The session is made, and runs. */\n Schema.TaggedStruct(\"Accept\", {}),\n /** Refused with `status`, and nothing is made. */\n Schema.TaggedStruct(\"Reject\", {\n status: Refusal,\n retryAfterSeconds: Schema.optional(Schema.Int),\n }),\n /** The session is made, but the answer holds only its id. */\n Schema.TaggedStruct(\"Malformed\", {}),\n /**\n * The session is made, but no answer arrives, and Browserbase lists it only `listedAfter`\n * milliseconds later, as a search can trail what was made.\n */\n Schema.TaggedStruct(\"Lost\", { listedAfter: Schema.optional(Schema.Finite) }),\n]);\n\n/** How a release answers. */\nexport const Release = Schema.Union([\n /** The session ends at once. */\n Schema.TaggedStruct(\"Ends\", {}),\n /** Accepted, but the session runs on for `millis`, or without them until its timeout. */\n Schema.TaggedStruct(\"Pending\", { millis: Schema.optional(Schema.Finite) }),\n /** Refused with `status`; the session runs on. */\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n /** The session ends, but no answer arrives. */\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\n/** How a session read answers. */\nexport const Read = Schema.Union([\n Schema.TaggedStruct(\"Answer\", {}),\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\nexport const Script = Schema.Struct({\n /** How creates answer, in turn; once these run out, each is accepted. */\n creates: Schema.optional(Schema.Array(Create)),\n /** How releases answer, in turn; once these run out, each ends its session. */\n releases: Schema.optional(Schema.Array(Release)),\n /** How session reads answer, in turn; once these run out, each answers. */\n reads: Schema.optional(Schema.Array(Read)),\n /** The DevTools address each session hands out. Without it, sessions have none. */\n connectUrl: Schema.optional(Schema.String),\n});\n\nexport type Script = typeof Script.Type;\n\n/** A session as the fake keeps it, with how many releases it was asked. */\nexport interface Kept {\n readonly id: string;\n readonly status: string;\n /** The create options as sent, without provider defaults. Only the test handle exposes them. */\n readonly options: SessionOptions;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly releases: number;\n /** Whether it was created to outlive its connections, until released or timed out. */\n readonly keepAlive: boolean;\n /** The stored context it loaded, and whether it saves to it as it ends. */\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n}\n\nexport interface Handle {\n /** Every request, oldest first, as `METHOD /path?query`. */\n readonly requests: Effect.Effect<ReadonlyArray<string>>;\n /** Each session as Browserbase would describe it now, oldest first. */\n readonly sessions: Effect.Effect<ReadonlyArray<Kept>>;\n /** End a session as Browserbase does on its own, as when its browser fails. */\n readonly end: (id: string) => Effect.Effect<void>;\n}\n\n/** The fake's handle, beside the `BrowserbaseClient` that `layer` runs on it. */\nexport class TestBrowserbase extends Context.Service<TestBrowserbase, Handle>()(\n \"effect-browserbase/testing/TestBrowserbase\",\n) {}\n\n/** The API key the fake takes; any other is unauthorized. */\nexport const apiKey = \"test-browserbase-key\";\n\nconst CreateBody = Schema.fromJsonString(\n Schema.Struct({\n projectId: Schema.optional(Schema.String),\n extensionId: Schema.optional(Schema.String),\n timeout: Schema.optional(Schema.Finite),\n keepAlive: Schema.optional(Schema.Boolean),\n region: Schema.optional(\n Schema.Literals([\"us-west-2\", \"us-east-1\", \"eu-central-1\", \"ap-southeast-1\"]),\n ),\n proxies: Schema.optional(\n Schema.Union([\n Schema.Boolean,\n Schema.Array(\n Schema.Union([\n Schema.Struct({\n type: Schema.Literal(\"browserbase\"),\n geolocation: Schema.optional(\n Schema.Struct({\n country: Schema.String,\n state: Schema.optional(Schema.String),\n city: Schema.optional(Schema.String),\n }),\n ),\n domainPattern: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"external\"),\n server: Schema.String,\n username: Schema.optional(Schema.String),\n password: Schema.optional(Schema.String),\n domainPattern: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"none\"),\n domainPattern: Schema.optional(Schema.String),\n }),\n ]),\n ),\n ]),\n ),\n userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n browserSettings: Schema.optional(\n Schema.Struct({\n context: Schema.optional(\n Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),\n ),\n viewport: Schema.optional(Schema.Struct({ width: Schema.Finite, height: Schema.Finite })),\n blockAds: Schema.optional(Schema.Boolean),\n solveCaptchas: Schema.optional(Schema.Boolean),\n recordSession: Schema.optional(Schema.Boolean),\n logSession: Schema.optional(Schema.Boolean),\n verified: Schema.optional(Schema.Boolean),\n os: Schema.optional(Schema.Literals([\"windows\", \"mac\", \"linux\", \"mobile\", \"tablet\"])),\n allowedDomains: Schema.optional(Schema.Array(Schema.String)),\n ignoreCertificateErrors: Schema.optional(Schema.Boolean),\n }),\n ),\n }),\n);\n\nconst ReleaseBody = Schema.fromJsonString(\n Schema.Struct({ status: Schema.Literal(\"REQUEST_RELEASE\") }),\n);\n\ninterface Row {\n readonly id: string;\n readonly options: SessionOptions;\n readonly createdAt: number;\n /** When a list first shows it. */\n readonly listedAt: number;\n readonly expiresAt: number;\n readonly keepAlive: boolean;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n /** How and when it ends, unless its timeout comes first. */\n ends: { readonly status: string; readonly at: number } | undefined;\n releases: number;\n}\n\n/** Everything one fake keeps. */\ninterface Fake {\n readonly now: () => number;\n readonly connectUrl: string | undefined;\n readonly pending: {\n readonly creates: Array<typeof Create.Type>;\n readonly releases: Array<typeof Release.Type>;\n readonly reads: Array<typeof Read.Type>;\n };\n readonly requests: Array<string>;\n readonly sessions: Map<string, Row>;\n readonly contexts: Map<string, number>;\n /** How many sessions and contexts it has made, together, for their ids. */\n readonly made: { count: number };\n}\n\nconst iso = (millis: number) => DateTime.formatIso(DateTime.makeUnsafe(millis));\n\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The next id, a UUID as Browserbase's are, counted so that every run hands out the same. */\nconst nextId = (fake: Fake) =>\n `00000000-0000-4000-8000-${(++fake.made.count).toString(16).padStart(12, \"0\")}`;\n\n/** How the session ended by now, or undefined while it runs. */\nconst endOf = (fake: Fake, row: Row) => {\n const end =\n row.ends !== undefined && row.ends.at <= row.expiresAt\n ? row.ends\n : { status: \"TIMED_OUT\", at: row.expiresAt };\n\n return end.at <= fake.now() ? end : undefined;\n};\n\n/** The session as Browserbase answers it; only creates and reads hand out its address. */\nconst describe = (fake: Fake, row: Row, connect: boolean) => {\n const end = endOf(fake, row);\n\n return {\n id: row.id,\n createdAt: iso(row.createdAt),\n updatedAt: iso(end?.at ?? row.createdAt),\n startedAt: iso(row.createdAt),\n expiresAt: iso(row.expiresAt),\n projectId: row.options.projectId ?? \"test-project\",\n status: end?.status ?? \"RUNNING\",\n proxyBytes: 0,\n keepAlive: row.keepAlive,\n region: row.options.region ?? \"us-west-2\",\n ...(end === undefined ? {} : { endedAt: iso(end.at) }),\n ...(row.context === undefined ? {} : { contextId: row.context.id }),\n ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),\n ...(connect && fake.connectUrl !== undefined ? { connectUrl: fake.connectUrl } : {}),\n ...(connect\n ? {\n seleniumRemoteUrl: `https://connect.browserbase.test/webdriver/${row.id}`,\n signingKey: `signing-key-${row.id}`,\n }\n : {}),\n };\n};\n\nconst bodyText = (request: HttpClientRequest.HttpClientRequest) =>\n request.body._tag === \"Uint8Array\"\n ? (request.body.text ?? new TextDecoder().decode(request.body.body))\n : \"\";\n\n/** A metadata query in Browserbase's one form, `user_metadata['a']['b']:'value'`, as a test. */\nconst metadataQuery = (query: string) => {\n const parsed = /^user_metadata((?:\\['[^']*'\\])+):'([^']*)'$/.exec(query);\n\n if (parsed === null) return undefined;\n const path = [...(parsed[1] ?? \"\").matchAll(/\\['([^']*)'\\]/g)].map((part) => part[1] ?? \"\");\n\n return (metadata: unknown) =>\n path.reduce<unknown>(\n (value, key) => (Predicate.isObject(value) ? Reflect.get(value, key) : undefined),\n metadata,\n ) === parsed[2];\n};\n\nconst unsupported = (detail: string) => Effect.die(new Error(`TestBrowserbase ${detail}`));\n\n/** Answer one request as Browserbase would, after the script's say. */\nconst serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: URL) => {\n const answer = (status: number, body?: unknown, headers: Record<string, string> = {}) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(body === undefined ? null : JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...headers },\n }),\n ),\n );\n\n const lost = Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"TestBrowserbase lost this answer, as its script said\",\n }),\n }),\n );\n\n const refused = (status: number) => answer(status, { message: \"refused, as the script said\" });\n const [, version, kind, id, rest] = url.pathname.split(\"/\");\n const row = id === undefined ? undefined : fake.sessions.get(id);\n\n fake.requests.push(`${request.method} ${url.pathname}${url.search}`);\n if (request.headers[\"x-bb-api-key\"] !== apiKey)\n return answer(401, { message: \"Invalid API key\" });\n if (version !== \"v1\") return unsupported(`serves only /v1, not ${url.pathname}`);\n // Browserbase refuses a session or context id that is not a UUID before it looks for it.\n if ((kind === \"sessions\" || kind === \"contexts\") && id !== undefined && !uuid.test(id))\n return answer(400, { message: `Invalid ${kind === \"sessions\" ? \"Session\" : \"Context\"} ID` });\n\n switch (\n `${request.method} ${kind}${id === undefined ? \"\" : \" id\"}${rest === undefined ? \"\" : ` ${rest}`}`\n ) {\n case \"POST sessions\": {\n const reply = fake.pending.creates.shift() ?? { _tag: \"Accept\" };\n\n if (reply._tag === \"Reject\")\n return answer(\n reply.status,\n { message: \"refused, as the script said\" },\n reply.retryAfterSeconds === undefined\n ? {}\n : { \"retry-after\": `${reply.retryAfterSeconds}` },\n );\n const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));\n\n if (body === undefined) return answer(400, { message: \"Invalid session settings\" });\n const loaded = body.browserSettings?.context;\n const createdAt = fake.now();\n\n const created: Row = {\n id: nextId(fake),\n options: body,\n createdAt,\n listedAt: createdAt + (reply._tag === \"Lost\" ? (reply.listedAfter ?? 0) : 0),\n expiresAt: createdAt + (body.timeout ?? 300) * 1000,\n keepAlive: body.keepAlive ?? false,\n userMetadata: body.userMetadata,\n context:\n loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },\n ends: undefined,\n releases: 0,\n };\n\n fake.sessions.set(created.id, created);\n if (reply._tag === \"Lost\") return lost;\n\n return answer(\n 201,\n reply._tag === \"Malformed\" ? { id: created.id } : describe(fake, created, true),\n );\n }\n case \"GET sessions\": {\n const status = url.searchParams.get(\"status\");\n const query = url.searchParams.get(\"q\");\n const matches = query === null ? () => true : metadataQuery(query);\n\n if (matches === undefined) return unsupported(`cannot read the query ${query}`);\n\n return answer(\n 200,\n [...fake.sessions.values()]\n .filter(\n (kept) =>\n kept.listedAt <= fake.now() &&\n (status === null || (endOf(fake, kept)?.status ?? \"RUNNING\") === status) &&\n matches(kept.userMetadata),\n )\n .map((kept) => describe(fake, kept, false)),\n );\n }\n case \"GET sessions id\": {\n const reply = fake.pending.reads.shift() ?? { _tag: \"Answer\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n if (reply._tag === \"Lost\") return lost;\n\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, describe(fake, row, true));\n }\n case \"GET sessions id debug\":\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, {\n debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,\n debuggerUrl: `https://live.browserbase.test/${row.id}`,\n pages: [],\n });\n case \"POST sessions id\": {\n if (row === undefined) return answer(404, { message: \"Session not found\" });\n if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))\n return answer(400, { message: \"status must be REQUEST_RELEASE\" });\n row.releases += 1;\n const reply = fake.pending.releases.shift() ?? { _tag: \"Ends\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n // Releasing a session that has ended changes nothing, and succeeds.\n if (\n endOf(fake, row) === undefined &&\n (reply._tag !== \"Pending\" || reply.millis !== undefined)\n )\n row.ends = {\n status: \"COMPLETED\",\n at: fake.now() + (reply._tag === \"Pending\" ? (reply.millis ?? 0) : 0),\n };\n\n return reply._tag === \"Lost\" ? lost : answer(200, describe(fake, row, false));\n }\n case \"POST contexts\": {\n const created = nextId(fake);\n\n fake.contexts.set(created, fake.now());\n\n return answer(201, {\n id: created,\n publicKey: \"test-public-key\",\n cipherAlgorithm: \"AES-256-CBC\",\n initializationVectorSize: 16,\n });\n }\n case \"GET contexts id\": {\n const createdAt = id === undefined ? undefined : fake.contexts.get(id);\n\n return createdAt === undefined\n ? answer(404, { message: \"Context not found\" })\n : answer(200, {\n id,\n createdAt: iso(createdAt),\n updatedAt: iso(createdAt),\n projectId: \"test-project\",\n });\n }\n case \"DELETE contexts id\":\n return id !== undefined && fake.contexts.delete(id)\n ? answer(204)\n : answer(404, { message: \"Context not found\" });\n default:\n return unsupported(`does not implement ${request.method} ${url.pathname}`);\n }\n};\n\n/** The fake as an `HttpClient`, with its handle. */\nexport const make = Effect.fnUntraced(function* (script: Script = {}) {\n const clock = yield* Clock.Clock;\n const decoded = yield* Effect.orDie(Schema.decodeEffect(Script)(script));\n\n const fake: Fake = {\n now: () => clock.currentTimeMillisUnsafe(),\n connectUrl: decoded.connectUrl,\n pending: {\n creates: [...(decoded.creates ?? [])],\n releases: [...(decoded.releases ?? [])],\n reads: [...(decoded.reads ?? [])],\n },\n requests: [],\n sessions: new Map(),\n contexts: new Map(),\n made: { count: 0 },\n };\n\n const handle: Handle = {\n requests: Effect.sync(() => [...fake.requests]),\n sessions: Effect.sync(() =>\n [...fake.sessions.values()].map((row) => ({\n id: row.id,\n options: row.options,\n status: endOf(fake, row)?.status ?? \"RUNNING\",\n userMetadata: row.userMetadata,\n releases: row.releases,\n keepAlive: row.keepAlive,\n context: row.context,\n })),\n ),\n end: (id) =>\n Effect.sync(() => {\n const row = fake.sessions.get(id);\n\n if (row !== undefined && endOf(fake, row) === undefined)\n row.ends = { status: \"ERROR\", at: fake.now() };\n }),\n };\n\n return { http: HttpClient.make((request, url) => serve(fake, request, url)), handle };\n});\n\n/** The real `BrowserbaseClient` over the fake, and the fake's handle. */\nexport const layer = (script: Script = {}): Layer.Layer<BrowserbaseClient | TestBrowserbase> =>\n Layer.unwrap(\n Effect.map(make(script), ({ http, handle }) =>\n Layer.merge(\n clientLayer({\n apiKey: Redacted.make(apiKey),\n baseUrl: \"https://api.browserbase.test\",\n }).pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, http))),\n Layer.succeed(TestBrowserbase, handle),\n ),\n ),\n );\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-browserbase",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.7",
|
|
4
4
|
"description": "Browserbase for effect-browser: hosted browser sessions, contexts, web search and page fetch.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@effect/vitest": "4.0.0",
|
|
59
59
|
"@types/node": "26.1.2",
|
|
60
60
|
"effect": "4.0.0",
|
|
61
|
-
"effect-browser": "0.3.0-beta.
|
|
61
|
+
"effect-browser": "0.3.0-beta.7",
|
|
62
62
|
"playwright-core": "1.63.0",
|
|
63
63
|
"typescript": "7.0.2",
|
|
64
64
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"effect": "~4.0.0",
|
|
70
|
-
"effect-browser": "^0.3.0-beta.
|
|
70
|
+
"effect-browser": "^0.3.0-beta.7",
|
|
71
71
|
"playwright-core": "^1.63.0"
|
|
72
72
|
},
|
|
73
73
|
"engines": {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* so `TestClock` drives their end. A `Script` decides how creates, releases and session reads
|
|
6
6
|
* answer, in turn, so a test can lose a create, leave a release pending or fail status reads.
|
|
7
7
|
*
|
|
8
|
-
* It keeps sessions and stored contexts and
|
|
9
|
-
* defect. It doesn't model
|
|
10
|
-
* browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
|
|
8
|
+
* It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint
|
|
9
|
+
* fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,
|
|
10
|
+
* with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
|
|
11
11
|
* of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses
|
|
12
12
|
* a session or context id of any other shape as invalid, where it answers an unknown one as not
|
|
13
13
|
* found.
|
|
@@ -33,7 +33,11 @@ import {
|
|
|
33
33
|
HttpClientResponse,
|
|
34
34
|
} from "effect/http";
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
type BrowserbaseClient,
|
|
38
|
+
layer as clientLayer,
|
|
39
|
+
type SessionOptions,
|
|
40
|
+
} from "../BrowserbaseClient.ts";
|
|
37
41
|
|
|
38
42
|
const Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));
|
|
39
43
|
|
|
@@ -91,6 +95,8 @@ export type Script = typeof Script.Type;
|
|
|
91
95
|
export interface Kept {
|
|
92
96
|
readonly id: string;
|
|
93
97
|
readonly status: string;
|
|
98
|
+
/** The create options as sent, without provider defaults. Only the test handle exposes them. */
|
|
99
|
+
readonly options: SessionOptions;
|
|
94
100
|
readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
|
|
95
101
|
readonly releases: number;
|
|
96
102
|
/** Whether it was created to outlive its connections, until released or timed out. */
|
|
@@ -118,14 +124,59 @@ export const apiKey = "test-browserbase-key";
|
|
|
118
124
|
|
|
119
125
|
const CreateBody = Schema.fromJsonString(
|
|
120
126
|
Schema.Struct({
|
|
127
|
+
projectId: Schema.optional(Schema.String),
|
|
128
|
+
extensionId: Schema.optional(Schema.String),
|
|
121
129
|
timeout: Schema.optional(Schema.Finite),
|
|
122
130
|
keepAlive: Schema.optional(Schema.Boolean),
|
|
131
|
+
region: Schema.optional(
|
|
132
|
+
Schema.Literals(["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"]),
|
|
133
|
+
),
|
|
134
|
+
proxies: Schema.optional(
|
|
135
|
+
Schema.Union([
|
|
136
|
+
Schema.Boolean,
|
|
137
|
+
Schema.Array(
|
|
138
|
+
Schema.Union([
|
|
139
|
+
Schema.Struct({
|
|
140
|
+
type: Schema.Literal("browserbase"),
|
|
141
|
+
geolocation: Schema.optional(
|
|
142
|
+
Schema.Struct({
|
|
143
|
+
country: Schema.String,
|
|
144
|
+
state: Schema.optional(Schema.String),
|
|
145
|
+
city: Schema.optional(Schema.String),
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
domainPattern: Schema.optional(Schema.String),
|
|
149
|
+
}),
|
|
150
|
+
Schema.Struct({
|
|
151
|
+
type: Schema.Literal("external"),
|
|
152
|
+
server: Schema.String,
|
|
153
|
+
username: Schema.optional(Schema.String),
|
|
154
|
+
password: Schema.optional(Schema.String),
|
|
155
|
+
domainPattern: Schema.optional(Schema.String),
|
|
156
|
+
}),
|
|
157
|
+
Schema.Struct({
|
|
158
|
+
type: Schema.Literal("none"),
|
|
159
|
+
domainPattern: Schema.optional(Schema.String),
|
|
160
|
+
}),
|
|
161
|
+
]),
|
|
162
|
+
),
|
|
163
|
+
]),
|
|
164
|
+
),
|
|
123
165
|
userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
124
166
|
browserSettings: Schema.optional(
|
|
125
167
|
Schema.Struct({
|
|
126
168
|
context: Schema.optional(
|
|
127
169
|
Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),
|
|
128
170
|
),
|
|
171
|
+
viewport: Schema.optional(Schema.Struct({ width: Schema.Finite, height: Schema.Finite })),
|
|
172
|
+
blockAds: Schema.optional(Schema.Boolean),
|
|
173
|
+
solveCaptchas: Schema.optional(Schema.Boolean),
|
|
174
|
+
recordSession: Schema.optional(Schema.Boolean),
|
|
175
|
+
logSession: Schema.optional(Schema.Boolean),
|
|
176
|
+
verified: Schema.optional(Schema.Boolean),
|
|
177
|
+
os: Schema.optional(Schema.Literals(["windows", "mac", "linux", "mobile", "tablet"])),
|
|
178
|
+
allowedDomains: Schema.optional(Schema.Array(Schema.String)),
|
|
179
|
+
ignoreCertificateErrors: Schema.optional(Schema.Boolean),
|
|
129
180
|
}),
|
|
130
181
|
),
|
|
131
182
|
}),
|
|
@@ -137,6 +188,7 @@ const ReleaseBody = Schema.fromJsonString(
|
|
|
137
188
|
|
|
138
189
|
interface Row {
|
|
139
190
|
readonly id: string;
|
|
191
|
+
readonly options: SessionOptions;
|
|
140
192
|
readonly createdAt: number;
|
|
141
193
|
/** When a list first shows it. */
|
|
142
194
|
readonly listedAt: number;
|
|
@@ -193,11 +245,11 @@ const describe = (fake: Fake, row: Row, connect: boolean) => {
|
|
|
193
245
|
updatedAt: iso(end?.at ?? row.createdAt),
|
|
194
246
|
startedAt: iso(row.createdAt),
|
|
195
247
|
expiresAt: iso(row.expiresAt),
|
|
196
|
-
projectId: "test-project",
|
|
248
|
+
projectId: row.options.projectId ?? "test-project",
|
|
197
249
|
status: end?.status ?? "RUNNING",
|
|
198
250
|
proxyBytes: 0,
|
|
199
251
|
keepAlive: row.keepAlive,
|
|
200
|
-
region: "us-west-2",
|
|
252
|
+
region: row.options.region ?? "us-west-2",
|
|
201
253
|
...(end === undefined ? {} : { endedAt: iso(end.at) }),
|
|
202
254
|
...(row.context === undefined ? {} : { contextId: row.context.id }),
|
|
203
255
|
...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),
|
|
@@ -281,16 +333,19 @@ const serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: UR
|
|
|
281
333
|
: { "retry-after": `${reply.retryAfterSeconds}` },
|
|
282
334
|
);
|
|
283
335
|
const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));
|
|
284
|
-
|
|
336
|
+
|
|
337
|
+
if (body === undefined) return answer(400, { message: "Invalid session settings" });
|
|
338
|
+
const loaded = body.browserSettings?.context;
|
|
285
339
|
const createdAt = fake.now();
|
|
286
340
|
|
|
287
341
|
const created: Row = {
|
|
288
342
|
id: nextId(fake),
|
|
343
|
+
options: body,
|
|
289
344
|
createdAt,
|
|
290
345
|
listedAt: createdAt + (reply._tag === "Lost" ? (reply.listedAfter ?? 0) : 0),
|
|
291
|
-
expiresAt: createdAt + (body
|
|
292
|
-
keepAlive: body
|
|
293
|
-
userMetadata: body
|
|
346
|
+
expiresAt: createdAt + (body.timeout ?? 300) * 1000,
|
|
347
|
+
keepAlive: body.keepAlive ?? false,
|
|
348
|
+
userMetadata: body.userMetadata,
|
|
294
349
|
context:
|
|
295
350
|
loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },
|
|
296
351
|
ends: undefined,
|
|
@@ -334,6 +389,14 @@ const serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: UR
|
|
|
334
389
|
? answer(404, { message: "Session not found" })
|
|
335
390
|
: answer(200, describe(fake, row, true));
|
|
336
391
|
}
|
|
392
|
+
case "GET sessions id debug":
|
|
393
|
+
return row === undefined
|
|
394
|
+
? answer(404, { message: "Session not found" })
|
|
395
|
+
: answer(200, {
|
|
396
|
+
debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,
|
|
397
|
+
debuggerUrl: `https://live.browserbase.test/${row.id}`,
|
|
398
|
+
pages: [],
|
|
399
|
+
});
|
|
337
400
|
case "POST sessions id": {
|
|
338
401
|
if (row === undefined) return answer(404, { message: "Session not found" });
|
|
339
402
|
if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))
|
|
@@ -411,6 +474,7 @@ export const make = Effect.fnUntraced(function* (script: Script = {}) {
|
|
|
411
474
|
sessions: Effect.sync(() =>
|
|
412
475
|
[...fake.sessions.values()].map((row) => ({
|
|
413
476
|
id: row.id,
|
|
477
|
+
options: row.options,
|
|
414
478
|
status: endOf(fake, row)?.status ?? "RUNNING",
|
|
415
479
|
userMetadata: row.userMetadata,
|
|
416
480
|
releases: row.releases,
|