Skip to content

Commit d5ddc78

Browse files
author
bcode
committed
phase-h step 6: cross-platform smoke (Linux x64)
Two new env-gated test files exercise the in-process CDP stack against headless Chrome (Chrome 147, via google-chrome --headless=new): - packages/bcode-browser/test/cdp-smoke.test.ts — connect via profileDir, attach a target, Page.enable + navigate + waitFor("Page.loadEventFired") + Runtime.evaluate("document.title"). Verifies the vendored CDP layer (session.ts + generated.ts) drives a real browser end-to-end. - packages/bcode-browser/test/browser-execute.test.ts — exercises the full Level-1 execute path: AsyncFunction wrapping, console.log capture, return-value serialization, multi-call Session reuse via SessionStore (a follow-up snippet on the same sessionID sees the Session previously connected), and workspace dynamic-import inside a snippet (await import(absPath + "?t=" + Date.now())). Both gated on BCODE_SMOKE_CHROME=1 + BCODE_SMOKE_PROFILE_DIR. Skipped otherwise — CI does not yet drive a real browser. All 9 tests (4 workspace-import + 1 cdp-smoke + 4 browser-execute) pass on Linux x64. Compiled binary smoke verified separately: bun build --compile produces bcode-linux-x64 (148 MB) that boots cleanly, prints --help, and runs without uv on PATH (the only dependency was the deleted Python harness). Cloud-browser tool not exercised here — requires BROWSER_USE_API_KEY and outbound to api.browser-use.com. Defer to admin smoke or Phase D bring-up. Smoke matrix: Linux x64 ✓; macOS arm64 + Windows x64 pending admin-laptop runs (recorded in memory/browsercode/phase_h_smoke_log.md on the agent side).
1 parent 8875a7c commit d5ddc78

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// browser_execute end-to-end against headless Chrome. Same env-gate as
2+
// cdp-smoke.test.ts (BCODE_SMOKE_CHROME=1 + BCODE_SMOKE_PROFILE_DIR).
3+
//
4+
// Verifies: AsyncFunction snippet wrapping, console.log capture, return-
5+
// value serialization, multi-call session reuse via SessionStore, workspace
6+
// dynamic-import inside a snippet.
7+
8+
import { afterAll, beforeAll, expect, test } from "bun:test"
9+
import fs from "fs/promises"
10+
import os from "os"
11+
import path from "path"
12+
import { Effect } from "effect"
13+
import { BrowserExecute } from "../src/browser-execute"
14+
import { SessionStore } from "../src/session-store"
15+
16+
const profileDir = process.env.BCODE_SMOKE_PROFILE_DIR
17+
const enabled = process.env.BCODE_SMOKE_CHROME === "1" && profileDir
18+
19+
const sessionID = "test-" + Math.random().toString(36).slice(2, 8)
20+
let workspaceDir: string
21+
let dataDir: string
22+
23+
beforeAll(async () => {
24+
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "bcode-be-"))
25+
dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "bcode-data-"))
26+
})
27+
28+
afterAll(async () => {
29+
await SessionStore.evict(sessionID)
30+
await fs.rm(workspaceDir, { recursive: true, force: true })
31+
await fs.rm(dataDir, { recursive: true, force: true })
32+
})
33+
34+
test.skipIf(!enabled)("connect + console.log + return value", async () => {
35+
const result = await Effect.runPromise(
36+
Effect.scoped(
37+
Effect.gen(function* () {
38+
const impl = yield* BrowserExecute.make(dataDir)
39+
return yield* impl.execute(
40+
{
41+
code: `await session.connect({ profileDir: ${JSON.stringify(profileDir!)}, timeoutMs: 5000 });
42+
console.log("connected", session.isConnected());
43+
return { ok: session.isConnected() };`,
44+
},
45+
{ sessionID, workspaceDir },
46+
)
47+
}),
48+
),
49+
)
50+
expect(result.output).toContain("connected true")
51+
expect(JSON.parse(result.result)).toEqual({ ok: true })
52+
})
53+
54+
test.skipIf(!enabled)("Session is reused across calls (SessionStore)", async () => {
55+
const result = await Effect.runPromise(
56+
Effect.scoped(
57+
Effect.gen(function* () {
58+
const impl = yield* BrowserExecute.make(dataDir)
59+
return yield* impl.execute(
60+
{
61+
code: `// connect was called in the previous test on the same sessionID.
62+
console.log("still connected:", session.isConnected());
63+
return session.isConnected();`,
64+
},
65+
{ sessionID, workspaceDir },
66+
)
67+
}),
68+
),
69+
)
70+
expect(result.output).toContain("still connected: true")
71+
expect(JSON.parse(result.result)).toBe(true)
72+
})
73+
74+
test.skipIf(!enabled)("workspace import inside a snippet", async () => {
75+
const file = path.join(workspaceDir, "title.ts")
76+
await fs.writeFile(
77+
file,
78+
`export const run = async (session) => {
79+
const targets = (await session.Target.getTargets({})).targetInfos
80+
const page = targets.find((t) => t.type === "page")
81+
if (!page) {
82+
const created = await session.Target.createTarget({ url: "about:blank" })
83+
await session.use(created.targetId)
84+
} else {
85+
await session.use(page.targetId)
86+
}
87+
await session.Page.enable()
88+
await session.Page.navigate({ url: "data:text/html,<title>bcode-be</title>" })
89+
await session.waitFor("Page.loadEventFired", undefined, 5000)
90+
const r = await session.Runtime.evaluate({ expression: "document.title", returnByValue: true })
91+
return r.result.value
92+
}`,
93+
"utf8",
94+
)
95+
96+
const result = await Effect.runPromise(
97+
Effect.scoped(
98+
Effect.gen(function* () {
99+
const impl = yield* BrowserExecute.make(dataDir)
100+
return yield* impl.execute(
101+
{
102+
code: `const m = await import(${JSON.stringify(file)} + "?t=" + Date.now());
103+
const t = await m.run(session);
104+
console.log("title:", t);
105+
return t;`,
106+
},
107+
{ sessionID, workspaceDir },
108+
)
109+
}),
110+
),
111+
)
112+
expect(result.output).toContain("title: bcode-be")
113+
expect(JSON.parse(result.result)).toBe("bcode-be")
114+
})
115+
116+
test.skipIf(!enabled)("syntax error in snippet surfaces a clean failure", async () => {
117+
await expect(
118+
Effect.runPromise(
119+
Effect.scoped(
120+
Effect.gen(function* () {
121+
const impl = yield* BrowserExecute.make(dataDir)
122+
return yield* impl.execute(
123+
{
124+
code: `const x = (`,
125+
},
126+
{ sessionID, workspaceDir },
127+
)
128+
}),
129+
),
130+
),
131+
).rejects.toThrow(/syntax error/)
132+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// CDP layer smoke test against a real Chrome instance. Skipped unless
2+
// `BCODE_SMOKE_CHROME=1` and a running Chrome with `--remote-debugging-port`
3+
// is reachable via the supplied profile dir.
4+
//
5+
// The point is to verify the vendored CDP stack (session.ts + generated.ts)
6+
// drives a real browser end-to-end after the port: connect via profileDir,
7+
// navigate, query document.title, close.
8+
9+
import { expect, test } from "bun:test"
10+
import { Session } from "../src/cdp/session"
11+
12+
const profileDir = process.env.BCODE_SMOKE_PROFILE_DIR
13+
const enabled = process.env.BCODE_SMOKE_CHROME === "1" && profileDir
14+
15+
test.skipIf(!enabled)("Session connects, navigates, reads title", async () => {
16+
const session = new Session()
17+
try {
18+
await session.connect({ profileDir: profileDir!, timeoutMs: 5000 })
19+
expect(session.isConnected()).toBe(true)
20+
21+
// Pick an existing page target (chrome:// internals filtered out).
22+
const targets = (await session.domains.Target.getTargets({})).targetInfos
23+
const page = (targets as Array<{ targetId: string; type: string; url: string }>).find(
24+
(t) => t.type === "page" && !t.url.startsWith("chrome://") && !t.url.startsWith("devtools://"),
25+
) ?? (targets as Array<{ targetId: string; type: string; url: string }>).find((t) => t.type === "page")
26+
if (!page) {
27+
// Open a fresh page if nothing exists.
28+
const created = await session.domains.Target.createTarget({ url: "about:blank" })
29+
await session.use(created.targetId)
30+
} else {
31+
await session.use(page.targetId)
32+
}
33+
34+
await session.domains.Page.enable()
35+
await session.domains.Page.navigate({ url: "data:text/html,<title>bcode-smoke</title>" })
36+
await session.waitFor("Page.loadEventFired", undefined, 5000)
37+
38+
const r = (await session.domains.Runtime.evaluate({
39+
expression: "document.title",
40+
returnByValue: true,
41+
})) as { result: { value: unknown } }
42+
expect(r.result.value).toBe("bcode-smoke")
43+
} finally {
44+
session.close()
45+
}
46+
})

0 commit comments

Comments
 (0)