From 8037e863148e6d4882869aa8bdb818a8fe4b8daa Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:17:02 -0800 Subject: [PATCH 1/7] Initial commit --- .github/workflows/ci.yml | 107 ++++- .gitignore | 12 +- package.json | 8 +- tests/basic/pnpm-lock.yaml | 411 ----------------- tests/build-flow/build-flow.test.ts | 311 +++++++++++++ tests/build-flow/live-build.test.ts | 200 ++++++++ tests/custom-browser-config/pnpm-lock.yaml | 435 ------------------ tests/integration/dev-server.test.ts | 179 +++++++ tests/integration/helpers.ts | 318 +++++++++++++ tests/integration/init.test.ts | 109 +++++ tests/integration/manifest-generation.test.ts | 225 +++++++++ tests/integration/publish.test.ts | 164 +++++++ tests/nested-entrypoint/pnpm-lock.yaml | 435 ------------------ tests/run-all-tests.sh | 115 ----- tests/{ => templates}/basic/bb.test.json | 0 .../{ => templates}/basic/expected/basic.json | 0 tests/{ => templates}/basic/index.ts | 0 tests/{ => templates}/basic/package.json | 3 - tests/{ => templates}/basic/tsconfig.json | 0 .../custom-browser-config/bb.test.json | 0 .../expected/custom-browser-config.json | 0 .../custom-browser-config/index.ts | 0 .../custom-browser-config/package.json | 1 - .../nested-entrypoint/bb.test.json | 0 .../expected/nested-entrypoint.json | 0 .../nested-entrypoint/package.json | 1 - .../nested-entrypoint/src/index.ts | 0 .../with-params-schema/bb.test.json | 0 .../expected/with-params-schema.json | 0 .../with-params-schema/index.ts | 0 .../with-params-schema/package.json | 3 - tests/test-all.sh | 129 ------ tests/test-dev.sh | 320 ------------- tests/test-manifest-generation.sh | 202 -------- tests/test-publish.sh | 259 ----------- tests/with-params-schema/pnpm-lock.yaml | 411 ----------------- tsconfig.integration.json | 11 + 37 files changed, 1626 insertions(+), 2743 deletions(-) delete mode 100644 tests/basic/pnpm-lock.yaml create mode 100644 tests/build-flow/build-flow.test.ts create mode 100644 tests/build-flow/live-build.test.ts delete mode 100644 tests/custom-browser-config/pnpm-lock.yaml create mode 100644 tests/integration/dev-server.test.ts create mode 100644 tests/integration/helpers.ts create mode 100644 tests/integration/init.test.ts create mode 100644 tests/integration/manifest-generation.test.ts create mode 100644 tests/integration/publish.test.ts delete mode 100644 tests/nested-entrypoint/pnpm-lock.yaml delete mode 100755 tests/run-all-tests.sh rename tests/{ => templates}/basic/bb.test.json (100%) rename tests/{ => templates}/basic/expected/basic.json (100%) rename tests/{ => templates}/basic/index.ts (100%) rename tests/{ => templates}/basic/package.json (76%) rename tests/{ => templates}/basic/tsconfig.json (100%) rename tests/{ => templates}/custom-browser-config/bb.test.json (100%) rename tests/{ => templates}/custom-browser-config/expected/custom-browser-config.json (100%) rename tests/{ => templates}/custom-browser-config/index.ts (100%) rename tests/{ => templates}/custom-browser-config/package.json (88%) rename tests/{ => templates}/nested-entrypoint/bb.test.json (100%) rename tests/{ => templates}/nested-entrypoint/expected/nested-entrypoint.json (100%) rename tests/{ => templates}/nested-entrypoint/package.json (88%) rename tests/{ => templates}/nested-entrypoint/src/index.ts (100%) rename tests/{ => templates}/with-params-schema/bb.test.json (100%) rename tests/{ => templates}/with-params-schema/expected/with-params-schema.json (100%) rename tests/{ => templates}/with-params-schema/index.ts (100%) rename tests/{ => templates}/with-params-schema/package.json (77%) delete mode 100755 tests/test-all.sh delete mode 100755 tests/test-dev.sh delete mode 100755 tests/test-manifest-generation.sh delete mode 100755 tests/test-publish.sh delete mode 100644 tests/with-params-schema/pnpm-lock.yaml create mode 100644 tsconfig.integration.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1445669..efc45e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,8 +123,101 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Install jq - run: sudo apt-get update && sudo apt-get install -y jq + - name: Enable Corepack + run: corepack enable + + - name: Setup pnpm + run: | + corepack prepare pnpm@10.12.1 --activate + pnpm config set store-dir ~/.pnpm-store + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}- + + - name: Install root dependencies + run: pnpm install --frozen-lockfile + + - name: Build SDK + run: rm -rf dist && tsup + + - name: Run integration tests + env: + BROWSERBASE_API_KEY: ${{ secrets.BB_INTEGRATION_TEST_API_KEY }} + BROWSERBASE_PROJECT_ID: ${{ secrets.BB_INTEGRATION_TEST_PROJECT_ID }} + run: pnpm run test:integration + + build-flow-tests: + name: Build Flow Tests (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["24.x"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Enable Corepack + run: corepack enable + + - name: Setup pnpm + run: | + corepack prepare pnpm@10.12.1 --activate + pnpm config set store-dir ~/.pnpm-store + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run build flow tests + run: pnpm run test:build-flow + + live-build-tests: + name: Live Build Tests (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["24.x"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} - name: Enable Corepack run: corepack enable @@ -152,7 +245,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build SDK - run: pnpm run build + run: rm -rf dist && tsup - name: Install test dependencies run: | @@ -163,12 +256,8 @@ jobs: fi done - - name: Make test scripts executable - run: chmod +x tests/*.sh - - - name: Run all integration tests - working-directory: tests + - name: Run live build tests env: BROWSERBASE_API_KEY: ${{ secrets.BB_INTEGRATION_TEST_API_KEY }} BROWSERBASE_PROJECT_ID: ${{ secrets.BB_INTEGRATION_TEST_PROJECT_ID }} - run: ./run-all-tests.sh + run: pnpm run test:live-build diff --git a/.gitignore b/.gitignore index f287e56..4e05517 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ dist-test/ +dist-integration-test/ .browserbase/ .env @@ -21,11 +22,6 @@ test-ignore.txt .gitignore.test-marker test-gitignore-dir/ -# Test artifacts -tests/*/.browserbase/ -tests/*/dev-server.log -tests/*/dev-response.json -tests/*/publish-output.log -tests/*/test.log -tests/*/.env.test -tests/*/test-ignore.txt +# Pack output +*.tgz + diff --git a/package.json b/package.json index dc9726f..dc5b6fa 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "./dist/index.js" ], "scripts": { - "build": "$npm_execpath run lint && rm -rf dist && tsup", + "build": "rm -rf dist && tsup", "build:tests": "tsc --project tsconfig.test.json", "eslint": "eslint ./src", "eslint:fix": "eslint --fix ./src", @@ -32,6 +32,12 @@ "prettier:fix": "prettier . --write --cache", "test": "$npm_execpath build:tests && node --test dist-test/**/*.test.js", "test:only": "$npm_execpath build:tests && node --test-only --test dist-test/**/*.test.js", + "build:integration": "tsc --project tsconfig.integration.json", + "pretest:integration": "$npm_execpath run build && pnpm pack", + "test:integration": "$npm_execpath run build:integration && node --test --test-timeout 60000 dist-integration-test/tests/integration/**/*.test.js", + "posttest:integration": "rm -f browserbasehq-sdk-functions-*.tgz", + "test:build-flow": "$npm_execpath run build && pnpm pack && $npm_execpath run build:integration && node --test --test-timeout 120000 dist-integration-test/tests/build-flow/build-flow.test.js", + "test:live-build": "$npm_execpath run build && $npm_execpath run build:integration && node --test --test-timeout 300000 dist-integration-test/tests/build-flow/live-build.test.js", "typecheck": "tsc --noEmit" }, "keywords": [], diff --git a/tests/basic/pnpm-lock.yaml b/tests/basic/pnpm-lock.yaml deleted file mode 100644 index e61fcc2..0000000 --- a/tests/basic/pnpm-lock.yaml +++ /dev/null @@ -1,411 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@browserbasehq/sdk-functions": - specifier: link:../.. - version: link:../.. - devDependencies: - tsx: - specifier: ^4.20.5 - version: 4.20.5 - -packages: - "@esbuild/aix-ppc64@0.25.9": - resolution: - { - integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.25.9": - resolution: - { - integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.25.9": - resolution: - { - integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.25.9": - resolution: - { - integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.25.9": - resolution: - { - integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.25.9": - resolution: - { - integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.25.9": - resolution: - { - integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.25.9": - resolution: - { - integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.25.9": - resolution: - { - integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.25.9": - resolution: - { - integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.25.9": - resolution: - { - integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.25.9": - resolution: - { - integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.25.9": - resolution: - { - integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.25.9": - resolution: - { - integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.25.9": - resolution: - { - integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.25.9": - resolution: - { - integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.25.9": - resolution: - { - integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.25.9": - resolution: - { - integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.25.9": - resolution: - { - integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.25.9": - resolution: - { - integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.25.9": - resolution: - { - integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.25.9": - resolution: - { - integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.25.9": - resolution: - { - integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.25.9": - resolution: - { - integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.25.9": - resolution: - { - integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.25.9": - resolution: - { - integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - esbuild@0.25.9: - resolution: - { - integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==, - } - engines: { node: ">=18" } - hasBin: true - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - get-tsconfig@4.10.1: - resolution: - { - integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==, - } - - resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } - - tsx@4.20.5: - resolution: - { - integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==, - } - engines: { node: ">=18.0.0" } - hasBin: true - -snapshots: - "@esbuild/aix-ppc64@0.25.9": - optional: true - - "@esbuild/android-arm64@0.25.9": - optional: true - - "@esbuild/android-arm@0.25.9": - optional: true - - "@esbuild/android-x64@0.25.9": - optional: true - - "@esbuild/darwin-arm64@0.25.9": - optional: true - - "@esbuild/darwin-x64@0.25.9": - optional: true - - "@esbuild/freebsd-arm64@0.25.9": - optional: true - - "@esbuild/freebsd-x64@0.25.9": - optional: true - - "@esbuild/linux-arm64@0.25.9": - optional: true - - "@esbuild/linux-arm@0.25.9": - optional: true - - "@esbuild/linux-ia32@0.25.9": - optional: true - - "@esbuild/linux-loong64@0.25.9": - optional: true - - "@esbuild/linux-mips64el@0.25.9": - optional: true - - "@esbuild/linux-ppc64@0.25.9": - optional: true - - "@esbuild/linux-riscv64@0.25.9": - optional: true - - "@esbuild/linux-s390x@0.25.9": - optional: true - - "@esbuild/linux-x64@0.25.9": - optional: true - - "@esbuild/netbsd-arm64@0.25.9": - optional: true - - "@esbuild/netbsd-x64@0.25.9": - optional: true - - "@esbuild/openbsd-arm64@0.25.9": - optional: true - - "@esbuild/openbsd-x64@0.25.9": - optional: true - - "@esbuild/openharmony-arm64@0.25.9": - optional: true - - "@esbuild/sunos-x64@0.25.9": - optional: true - - "@esbuild/win32-arm64@0.25.9": - optional: true - - "@esbuild/win32-ia32@0.25.9": - optional: true - - "@esbuild/win32-x64@0.25.9": - optional: true - - esbuild@0.25.9: - optionalDependencies: - "@esbuild/aix-ppc64": 0.25.9 - "@esbuild/android-arm": 0.25.9 - "@esbuild/android-arm64": 0.25.9 - "@esbuild/android-x64": 0.25.9 - "@esbuild/darwin-arm64": 0.25.9 - "@esbuild/darwin-x64": 0.25.9 - "@esbuild/freebsd-arm64": 0.25.9 - "@esbuild/freebsd-x64": 0.25.9 - "@esbuild/linux-arm": 0.25.9 - "@esbuild/linux-arm64": 0.25.9 - "@esbuild/linux-ia32": 0.25.9 - "@esbuild/linux-loong64": 0.25.9 - "@esbuild/linux-mips64el": 0.25.9 - "@esbuild/linux-ppc64": 0.25.9 - "@esbuild/linux-riscv64": 0.25.9 - "@esbuild/linux-s390x": 0.25.9 - "@esbuild/linux-x64": 0.25.9 - "@esbuild/netbsd-arm64": 0.25.9 - "@esbuild/netbsd-x64": 0.25.9 - "@esbuild/openbsd-arm64": 0.25.9 - "@esbuild/openbsd-x64": 0.25.9 - "@esbuild/openharmony-arm64": 0.25.9 - "@esbuild/sunos-x64": 0.25.9 - "@esbuild/win32-arm64": 0.25.9 - "@esbuild/win32-ia32": 0.25.9 - "@esbuild/win32-x64": 0.25.9 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.10.1: - dependencies: - resolve-pkg-maps: 1.0.0 - - resolve-pkg-maps@1.0.0: {} - - tsx@4.20.5: - dependencies: - esbuild: 0.25.9 - get-tsconfig: 4.10.1 - optionalDependencies: - fsevents: 2.3.3 diff --git a/tests/build-flow/build-flow.test.ts b/tests/build-flow/build-flow.test.ts new file mode 100644 index 0000000..afebc1a --- /dev/null +++ b/tests/build-flow/build-flow.test.ts @@ -0,0 +1,311 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { execSync } from "node:child_process"; +import { + mkdtempSync, + writeFileSync, + readFileSync, + existsSync, + rmSync, + mkdirSync, +} from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +// Resolve the project root (3 levels up from dist-integration-test/tests/build-flow/) +const PROJECT_ROOT = join(import.meta.dirname, "..", "..", ".."); + +let tarballPath: string; +const tempDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), `bb-build-test-${prefix}-`)); + tempDirs.push(dir); + return dir; +} + +function setupTempProject( + dir: string, + opts: { + type?: "module" | "commonjs"; + files: Record; + extraDeps?: string[]; + }, +): void { + const pkg: Record = { + name: "test-project", + version: "1.0.0", + private: true, + }; + if (opts.type) { + pkg["type"] = opts.type; + } + + writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2)); + + for (const [filename, content] of Object.entries(opts.files)) { + const filePath = join(dir, filename); + const fileDir = join(filePath, ".."); + if (!existsSync(fileDir)) { + mkdirSync(fileDir, { recursive: true }); + } + writeFileSync(filePath, content); + } + + // Install the tarball + any extra deps + const deps = [tarballPath, ...(opts.extraDeps ?? [])].join(" "); + execSync(`npm install ${deps}`, { + cwd: dir, + stdio: "pipe", + env: { + ...process.env, + npm_config_fund: "false", + npm_config_audit: "false", + }, + }); +} + +describe("Build Flow", () => { + before(() => { + // Pack the SDK (build is assumed to have been done by the npm script) + const packOutput = execSync("pnpm pack", { + cwd: PROJECT_ROOT, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + + // pnpm pack outputs the tarball filename + const tarballName = String(packOutput).trim().split("\n").pop()!.trim(); + tarballPath = join(PROJECT_ROOT, tarballName); + assert.ok(existsSync(tarballPath), `Tarball not found at ${tarballPath}`); + }); + + after(() => { + // Clean up temp directories + for (const dir of tempDirs) { + rmSync(dir, { recursive: true, force: true }); + } + // Clean up tarball + if (tarballPath && existsSync(tarballPath)) { + rmSync(tarballPath); + } + }); + + it("ESM import works", () => { + const dir = createTempDir("esm"); + setupTempProject(dir, { + type: "module", + files: { + "index.mjs": ` +import { defineFn } from "@browserbasehq/sdk-functions"; +if (typeof defineFn !== "function") { + process.exit(1); +} +console.log("ESM_OK"); +`, + }, + }); + + const output = execSync("node index.mjs", { + cwd: dir, + encoding: "utf-8", + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: ["pipe", "pipe", "pipe"], + }); + assert.ok(String(output).includes("ESM_OK"), "ESM import should work"); + }); + + it("CJS require works", () => { + const dir = createTempDir("cjs"); + setupTempProject(dir, { + files: { + "index.cjs": ` +const sdk = require("@browserbasehq/sdk-functions"); +if (typeof sdk.defineFn !== "function") { + process.exit(1); +} +console.log("CJS_OK"); +`, + }, + }); + + const output = execSync("node index.cjs", { + cwd: dir, + encoding: "utf-8", + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: ["pipe", "pipe", "pipe"], + }); + assert.ok(String(output).includes("CJS_OK"), "CJS require should work"); + }); + + it("TypeScript declarations are present", () => { + const dir = createTempDir("dts"); + setupTempProject(dir, { + files: { + "check.mjs": `console.log("OK");`, + }, + }); + + const dtsPath = join( + dir, + "node_modules", + "@browserbasehq", + "sdk-functions", + "dist", + "index.d.ts", + ); + const dctsPath = join( + dir, + "node_modules", + "@browserbasehq", + "sdk-functions", + "dist", + "index.d.cts", + ); + + assert.ok(existsSync(dtsPath), `.d.ts should exist at ${dtsPath}`); + assert.ok(existsSync(dctsPath), `.d.cts should exist at ${dctsPath}`); + }); + + it("bb CLI binary works", () => { + const dir = createTempDir("cli"); + setupTempProject(dir, { + type: "module", + files: { + "placeholder.mjs": `console.log("OK");`, + }, + }); + + const output = execSync("npx bb --version", { + cwd: dir, + encoding: "utf-8", + env: { ...process.env }, + stdio: ["pipe", "pipe", "pipe"], + }); + assert.ok( + String(output).trim().length > 0, + "bb --version should produce output", + ); + }); + + it("basic function: introspect works from installed package", () => { + const dir = createTempDir("introspect-basic"); + setupTempProject(dir, { + type: "module", + files: { + "index.mjs": ` +import { defineFn } from "@browserbasehq/sdk-functions"; + +defineFn("test-basic", async () => { + return { answer: "hello" }; +}); +`, + }, + }); + + execSync("node index.mjs", { + cwd: dir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + + const manifestPath = join( + dir, + ".browserbase", + "functions", + "manifests", + "test-basic.json", + ); + assert.ok(existsSync(manifestPath), "Manifest should be generated"); + + const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + assert.equal(manifest.name, "test-basic"); + assert.deepStrictEqual(manifest.config, {}); + }); + + it("function with Zod schema: introspect works from installed package", () => { + const dir = createTempDir("introspect-zod"); + setupTempProject(dir, { + type: "module", + extraDeps: ["zod"], + files: { + "index.mjs": ` +import { defineFn } from "@browserbasehq/sdk-functions"; +import z from "zod"; + +defineFn("test-zod", async (_ctx, params) => { + return { value: params.data * 2 }; +}, { + parametersSchema: z.object({ data: z.number() }), +}); +`, + }, + }); + + execSync("node index.mjs", { + cwd: dir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + + const manifestPath = join( + dir, + ".browserbase", + "functions", + "manifests", + "test-zod.json", + ); + assert.ok(existsSync(manifestPath), "Manifest should be generated"); + + const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + assert.equal(manifest.name, "test-zod"); + + const schema = manifest.config.parametersSchema; + assert.ok(schema, "parametersSchema should exist"); + assert.equal(schema.type, "object"); + assert.equal(schema.properties.data.type, "number"); + assert.deepStrictEqual(schema.required, ["data"]); + }); + + it("function with session config: introspect works from installed package", () => { + const dir = createTempDir("introspect-session"); + setupTempProject(dir, { + type: "module", + files: { + "index.mjs": ` +import { defineFn } from "@browserbasehq/sdk-functions"; + +defineFn("test-session", async (context) => { + return { sessionId: context.session.id }; +}, { + sessionConfig: { + browserSettings: { advancedStealth: true }, + }, +}); +`, + }, + }); + + execSync("node index.mjs", { + cwd: dir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + + const manifestPath = join( + dir, + ".browserbase", + "functions", + "manifests", + "test-session.json", + ); + assert.ok(existsSync(manifestPath), "Manifest should be generated"); + + const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + assert.equal(manifest.name, "test-session"); + assert.equal( + manifest.config.sessionConfig.browserSettings.advancedStealth, + true, + ); + }); +}); diff --git a/tests/build-flow/live-build.test.ts b/tests/build-flow/live-build.test.ts new file mode 100644 index 0000000..a256cf3 --- /dev/null +++ b/tests/build-flow/live-build.test.ts @@ -0,0 +1,200 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { execSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const PROJECT_ROOT = join(import.meta.dirname, "..", "..", ".."); + +const API_KEY = process.env["BROWSERBASE_API_KEY"]; +const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"]; +const API_URL = + process.env["BROWSERBASE_API_URL"] ?? "https://api.browserbase.com"; + +function requireCredentials(): void { + if (!API_KEY || !PROJECT_ID) { + throw new Error( + "BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID must be set for live build tests", + ); + } +} + +function bbApi( + method: string, + path: string, + body?: unknown, +): { status: number; data: unknown } { + const args = [ + "curl", + "-s", + "-w", + "\\n%{http_code}", + "-X", + method, + `${API_URL}${path}`, + "-H", + `x-bb-api-key: ${API_KEY}`, + "-H", + "Content-Type: application/json", + ]; + if (body) { + args.push("-d", JSON.stringify(body)); + } + + const output = execSync(args.join(" "), { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + + const lines = String(output).trim().split("\n"); + const statusCode = parseInt(lines.pop()!, 10); + const responseBody = lines.join("\n"); + + let data: unknown; + try { + data = JSON.parse(responseBody); + } catch { + data = responseBody; + } + + return { status: statusCode, data }; +} + +function pollBuildStatus( + buildId: string, + timeoutMs: number = 120_000, +): unknown { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const res = bbApi("GET", `/v1/functions/builds/${buildId}`); + const build = res.data as Record; + + if (build["status"] === "COMPLETED") { + return build; + } + if (build["status"] === "FAILED") { + throw new Error(`Build ${buildId} failed: ${JSON.stringify(build)}`); + } + + execSync("sleep 3"); + } + throw new Error(`Build ${buildId} did not complete within ${timeoutMs}ms`); +} + +let tarballPath: string; +const tempDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), `bb-live-build-${prefix}-`)); + tempDirs.push(dir); + return dir; +} + +describe("Live Build + Invoke", () => { + before(() => { + // Pack the SDK (build is assumed to have been done by the npm script) + const packOutput = execSync("pnpm pack", { + cwd: PROJECT_ROOT, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + + const tarballName = String(packOutput).trim().split("\n").pop()!.trim(); + tarballPath = join(PROJECT_ROOT, tarballName); + assert.ok(existsSync(tarballPath), `Tarball not found at ${tarballPath}`); + }); + + after(() => { + for (const dir of tempDirs) { + rmSync(dir, { recursive: true, force: true }); + } + if (tarballPath && existsSync(tarballPath)) { + rmSync(tarballPath); + } + }); + + it("publishes basic function, builds, and invokes successfully", () => { + requireCredentials(); + + // Create a temp project that installs the SDK from the tarball + const dir = createTempDir("basic"); + + const pkg = { + name: "live-build-test", + version: "1.0.0", + private: true, + type: "module", + }; + writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2)); + + writeFileSync( + join(dir, "index.mjs"), + `import { defineFn } from "@browserbasehq/sdk-functions"; + +defineFn("basic", async () => { + return { answer: "adam is cool" }; +}); +`, + ); + + // Install the SDK from the tarball + execSync(`npm install ${tarballPath}`, { + cwd: dir, + stdio: "pipe", + env: { + ...process.env, + npm_config_fund: "false", + npm_config_audit: "false", + }, + }); + + // Publish from the temp project + const publishOutput = execSync("npx bb publish index.mjs", { + cwd: dir, + encoding: "utf-8", + env: { + ...process.env, + BROWSERBASE_API_KEY: API_KEY, + BROWSERBASE_PROJECT_ID: PROJECT_ID, + }, + stdio: ["pipe", "pipe", "pipe"], + }); + + // Extract build ID from stdout + const buildIdMatch = String(publishOutput).match( + /Build ID:\s*([a-f0-9-]+)/i, + ); + assert.ok( + buildIdMatch, + `Could not find build ID in output: ${publishOutput}`, + ); + const buildId = buildIdMatch[1]!; + + // Poll until build completes + const build = pollBuildStatus(buildId) as Record; + + // Assert builtFunctions is non-empty + const builtFunctions = build["builtFunctions"] as Array< + Record + >; + assert.ok( + builtFunctions && builtFunctions.length > 0, + `Expected non-empty builtFunctions, got: ${JSON.stringify(builtFunctions)}`, + ); + + // Find the "basic" function + const basicFn = builtFunctions.find((f) => f["name"] === "basic"); + assert.ok(basicFn, `Expected a function named "basic" in builtFunctions`); + + // Invoke the built function + const functionId = basicFn["id"] as string; + assert.ok(functionId, "Function should have an id"); + + const invokeRes = bbApi("POST", `/v1/functions/${functionId}/invoke`, {}); + assert.ok( + invokeRes.status === 200 || invokeRes.status === 201, + `Invoke should succeed, got status ${invokeRes.status}: ${JSON.stringify(invokeRes.data)}`, + ); + }); +}); diff --git a/tests/custom-browser-config/pnpm-lock.yaml b/tests/custom-browser-config/pnpm-lock.yaml deleted file mode 100644 index 8a5a0f5..0000000 --- a/tests/custom-browser-config/pnpm-lock.yaml +++ /dev/null @@ -1,435 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@browserbasehq/sdk-functions": - specifier: link:../.. - version: link:../.. - playwright-core: - specifier: ^1.56.1 - version: 1.56.1 - zod: - specifier: ^4.1.12 - version: 4.1.12 - devDependencies: - tsx: - specifier: ^4.20.5 - version: 4.20.6 - -packages: - "@esbuild/aix-ppc64@0.25.12": - resolution: - { - integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.25.12": - resolution: - { - integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.25.12": - resolution: - { - integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.25.12": - resolution: - { - integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.25.12": - resolution: - { - integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.25.12": - resolution: - { - integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.25.12": - resolution: - { - integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.25.12": - resolution: - { - integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.25.12": - resolution: - { - integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.25.12": - resolution: - { - integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.25.12": - resolution: - { - integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.25.12": - resolution: - { - integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.25.12": - resolution: - { - integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.25.12": - resolution: - { - integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.25.12": - resolution: - { - integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.25.12": - resolution: - { - integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.25.12": - resolution: - { - integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.25.12": - resolution: - { - integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.25.12": - resolution: - { - integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.25.12": - resolution: - { - integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.25.12": - resolution: - { - integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.25.12": - resolution: - { - integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.25.12": - resolution: - { - integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.25.12": - resolution: - { - integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - esbuild@0.25.12: - resolution: - { - integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, - } - engines: { node: ">=18" } - hasBin: true - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - get-tsconfig@4.13.0: - resolution: - { - integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==, - } - - playwright-core@1.56.1: - resolution: - { - integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==, - } - engines: { node: ">=18" } - hasBin: true - - resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } - - tsx@4.20.6: - resolution: - { - integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==, - } - engines: { node: ">=18.0.0" } - hasBin: true - - zod@4.1.12: - resolution: - { - integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==, - } - -snapshots: - "@esbuild/aix-ppc64@0.25.12": - optional: true - - "@esbuild/android-arm64@0.25.12": - optional: true - - "@esbuild/android-arm@0.25.12": - optional: true - - "@esbuild/android-x64@0.25.12": - optional: true - - "@esbuild/darwin-arm64@0.25.12": - optional: true - - "@esbuild/darwin-x64@0.25.12": - optional: true - - "@esbuild/freebsd-arm64@0.25.12": - optional: true - - "@esbuild/freebsd-x64@0.25.12": - optional: true - - "@esbuild/linux-arm64@0.25.12": - optional: true - - "@esbuild/linux-arm@0.25.12": - optional: true - - "@esbuild/linux-ia32@0.25.12": - optional: true - - "@esbuild/linux-loong64@0.25.12": - optional: true - - "@esbuild/linux-mips64el@0.25.12": - optional: true - - "@esbuild/linux-ppc64@0.25.12": - optional: true - - "@esbuild/linux-riscv64@0.25.12": - optional: true - - "@esbuild/linux-s390x@0.25.12": - optional: true - - "@esbuild/linux-x64@0.25.12": - optional: true - - "@esbuild/netbsd-arm64@0.25.12": - optional: true - - "@esbuild/netbsd-x64@0.25.12": - optional: true - - "@esbuild/openbsd-arm64@0.25.12": - optional: true - - "@esbuild/openbsd-x64@0.25.12": - optional: true - - "@esbuild/openharmony-arm64@0.25.12": - optional: true - - "@esbuild/sunos-x64@0.25.12": - optional: true - - "@esbuild/win32-arm64@0.25.12": - optional: true - - "@esbuild/win32-ia32@0.25.12": - optional: true - - "@esbuild/win32-x64@0.25.12": - optional: true - - esbuild@0.25.12: - optionalDependencies: - "@esbuild/aix-ppc64": 0.25.12 - "@esbuild/android-arm": 0.25.12 - "@esbuild/android-arm64": 0.25.12 - "@esbuild/android-x64": 0.25.12 - "@esbuild/darwin-arm64": 0.25.12 - "@esbuild/darwin-x64": 0.25.12 - "@esbuild/freebsd-arm64": 0.25.12 - "@esbuild/freebsd-x64": 0.25.12 - "@esbuild/linux-arm": 0.25.12 - "@esbuild/linux-arm64": 0.25.12 - "@esbuild/linux-ia32": 0.25.12 - "@esbuild/linux-loong64": 0.25.12 - "@esbuild/linux-mips64el": 0.25.12 - "@esbuild/linux-ppc64": 0.25.12 - "@esbuild/linux-riscv64": 0.25.12 - "@esbuild/linux-s390x": 0.25.12 - "@esbuild/linux-x64": 0.25.12 - "@esbuild/netbsd-arm64": 0.25.12 - "@esbuild/netbsd-x64": 0.25.12 - "@esbuild/openbsd-arm64": 0.25.12 - "@esbuild/openbsd-x64": 0.25.12 - "@esbuild/openharmony-arm64": 0.25.12 - "@esbuild/sunos-x64": 0.25.12 - "@esbuild/win32-arm64": 0.25.12 - "@esbuild/win32-ia32": 0.25.12 - "@esbuild/win32-x64": 0.25.12 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - playwright-core@1.56.1: {} - - resolve-pkg-maps@1.0.0: {} - - tsx@4.20.6: - dependencies: - esbuild: 0.25.12 - get-tsconfig: 4.13.0 - optionalDependencies: - fsevents: 2.3.3 - - zod@4.1.12: {} diff --git a/tests/integration/dev-server.test.ts b/tests/integration/dev-server.test.ts new file mode 100644 index 0000000..d0552c2 --- /dev/null +++ b/tests/integration/dev-server.test.ts @@ -0,0 +1,179 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { spawn, type ChildProcess } from "node:child_process"; +import { + discoverTemplates, + getTarballPath, + setupTemplateProject, + cleanupDir, + findAvailablePort, + httpGet, + httpPost, + waitForHealthcheck, + waitForFunctionRegistration, +} from "./helpers.js"; + +const templates = discoverTemplates(); +const HAS_REAL_CREDENTIALS = !!( + process.env["BROWSERBASE_API_KEY"] && process.env["BROWSERBASE_PROJECT_ID"] +); +const API_KEY = process.env["BROWSERBASE_API_KEY"] ?? "test_key"; +const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"] ?? "test_project"; + +function getFunctionName(templateName: string): string { + return templateName; +} + +function startDevServer( + entrypoint: string, + port: number, + cwd: string, +): ChildProcess { + const child = spawn( + "npx", + ["bb", "dev", entrypoint, "--port", String(port)], + { + cwd, + stdio: ["pipe", "pipe", "pipe"], + env: { + ...process.env, + BROWSERBASE_API_KEY: API_KEY, + BROWSERBASE_PROJECT_ID: PROJECT_ID, + }, + }, + ); + return child; +} + +function killProcess(proc: ChildProcess): Promise { + return new Promise((resolve) => { + if (proc.killed || proc.exitCode !== null) { + resolve(); + return; + } + + proc.on("exit", () => resolve()); + proc.kill("SIGTERM"); + + // Force kill after 2 seconds + setTimeout(() => { + if (!proc.killed && proc.exitCode === null) { + proc.kill("SIGKILL"); + } + }, 2000); + }); +} + +const tarballPath = getTarballPath(); + +describe("Dev Server", () => { + + for (const template of templates) { + describe(template.name, () => { + let proc: ChildProcess | null = null; + let port: number; + let baseUrl: string; + let projectDir: string; + const funcName = getFunctionName(template.name); + + before(() => { + projectDir = setupTemplateProject(template, tarballPath); + }); + + after(async () => { + if (proc) { + await killProcess(proc); + proc = null; + } + cleanupDir(projectDir); + }); + + it("starts, passes healthcheck, and responds to invocation", async () => { + port = await findAvailablePort(); + baseUrl = `http://127.0.0.1:${port}`; + + // Start server + proc = startDevServer(template.entrypoint, port, projectDir); + + // Collect logs for diagnostics + let serverLogs = ""; + proc.stdout?.on("data", (chunk: Buffer) => { + serverLogs += chunk.toString(); + }); + proc.stderr?.on("data", (chunk: Buffer) => { + serverLogs += chunk.toString(); + }); + + // Wait for healthcheck + try { + await waitForHealthcheck(baseUrl, 30_000); + } catch { + assert.fail( + `Server failed to start within 30s.\nLogs:\n${serverLogs}`, + ); + } + + // Verify healthcheck response + const healthRes = await httpGet(`${baseUrl}/`); + assert.equal(healthRes.statusCode, 200); + assert.ok(healthRes.body.includes('"ok":true')); + + // Wait for function registration + try { + await waitForFunctionRegistration(baseUrl, funcName, API_KEY, 15_000); + } catch { + assert.fail( + `Function '${funcName}' not registered within 15s.\nLogs:\n${serverLogs}`, + ); + } + + // Invoke the function + const invokeRes = await httpPost( + `${baseUrl}/v1/functions/${funcName}/invoke`, + { params: {} }, + { "x-bb-api-key": API_KEY }, + ); + + if (HAS_REAL_CREDENTIALS) { + assert.ok( + invokeRes.statusCode === 200 || invokeRes.statusCode === 201, + `Invoke should return 200/201, got ${invokeRes.statusCode}.\nBody: ${invokeRes.body}\nLogs:\n${serverLogs}`, + ); + } else { + // Without real Browserbase credentials, session creation fails but the + // function should still be found (i.e. not a 404). + assert.notEqual( + invokeRes.statusCode, + 404, + `Function '${funcName}' should be registered.\nBody: ${invokeRes.body}\nLogs:\n${serverLogs}`, + ); + } + }); + + it("returns 404 for nonexistent function", async () => { + // This test relies on the server still running from the previous test. + // If the server is not running, start it fresh. + if (!proc || proc.exitCode !== null) { + port = await findAvailablePort(); + baseUrl = `http://127.0.0.1:${port}`; + proc = startDevServer(template.entrypoint, port, projectDir); + + await waitForHealthcheck(baseUrl, 30_000); + await waitForFunctionRegistration(baseUrl, funcName, API_KEY, 15_000); + } + + const res = await httpPost( + `${baseUrl}/v1/functions/nonexistent/invoke`, + { params: {} }, + { "x-bb-api-key": API_KEY }, + ); + + assert.equal( + res.statusCode, + 404, + `Expected 404 for nonexistent function, got ${res.statusCode}`, + ); + }); + }); + } +}); diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts new file mode 100644 index 0000000..9ae2bd4 --- /dev/null +++ b/tests/integration/helpers.ts @@ -0,0 +1,318 @@ +import { execSync } from "node:child_process"; +import { + readdirSync, + readFileSync, + existsSync, + cpSync, + rmSync, + mkdtempSync, +} from "node:fs"; +import { createServer } from "node:net"; +import { request } from "node:http"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +// ── Path constants ────────────────────────────────────────────────── + +// Resolve the real project root relative to the compiled JS location. +// The compiled JS lives in dist-integration-test/tests/integration/, +// so we go up 3 levels to reach the project root. +export const PROJECT_ROOT = join(import.meta.dirname, "..", "..", ".."); +const TEMPLATES_DIR = join(PROJECT_ROOT, "tests", "templates"); + +// ── Template discovery ────────────────────────────────────────────── + +export interface Template { + name: string; + dir: string; + entrypoint: string; + expectedDir: string | null; +} + +export function discoverTemplates(): Template[] { + const templates: Template[] = []; + + for (const entry of readdirSync(TEMPLATES_DIR, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + + const dir = join(TEMPLATES_DIR, entry.name); + const configPath = join(dir, "bb.test.json"); + + if (!existsSync(configPath)) continue; + + const config = JSON.parse(readFileSync(configPath, "utf-8")) as { + entrypoint: string; + }; + const expectedDir = join(dir, "expected"); + + templates.push({ + name: entry.name, + dir, + entrypoint: config.entrypoint, + expectedDir: existsSync(expectedDir) ? expectedDir : null, + }); + } + + return templates; +} + +// ── SDK tarball ───────────────────────────────────────────────────── + +/** + * Return the absolute path to the pre-built SDK tarball. + * The tarball is created once by the pretest:integration script + * (`pnpm pack`) before any test files run. + */ +export function getTarballPath(): string { + const entries = readdirSync(PROJECT_ROOT); + const tarball = entries.find( + (f) => f.startsWith("browserbasehq-sdk-functions-") && f.endsWith(".tgz"), + ); + + if (!tarball) { + throw new Error( + "SDK tarball not found in project root. " + + "Run `pnpm pack` (or use the pretest:integration script) first.", + ); + } + + return join(PROJECT_ROOT, tarball); +} + +// ── Template project setup ────────────────────────────────────────── + +const COPY_EXCLUDE = new Set([ + "node_modules", + "pnpm-lock.yaml", + ".browserbase", + "expected", +]); + +/** + * Copy a template directory to a temp dir and install the SDK tarball. + * Returns the absolute path of the temp project directory. + */ +export function setupTemplateProject( + template: Template, + tarballPath: string, +): string { + const tempDir = mkdtempSync( + join(tmpdir(), `bb-test-${template.name}-`), + ); + + // Copy template contents, excluding things we don't need in the project + cpSync(template.dir, tempDir, { + recursive: true, + filter: (src) => { + const basename = src.split("/").pop()!; + if (src === template.dir) return true; + return !COPY_EXCLUDE.has(basename); + }, + }); + + // Install SDK from tarball + all other deps from package.json. + // Use npm instead of pnpm to avoid workspace env var interference + // when running inside `pnpm test:integration`. + execSync(`npm install ${tarballPath}`, { + cwd: tempDir, + stdio: "pipe", + env: { + ...process.env, + npm_config_fund: "false", + npm_config_audit: "false", + }, + }); + + return tempDir; +} + +// ── Cleanup helpers ───────────────────────────────────────────────── + +export function cleanupDir(dir: string): void { + if (existsSync(dir)) { + rmSync(dir, { recursive: true, force: true }); + } +} + +export function cleanupFile(path: string): void { + if (existsSync(path)) { + rmSync(path); + } +} + +// ── Port allocation ────────────────────────────────────────────────── + +export function findAvailablePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + if (!addr || typeof addr === "string") { + server.close(); + reject(new Error("Could not determine port")); + return; + } + const port = addr.port; + server.close(() => resolve(port)); + }); + server.on("error", reject); + }); +} + +// ── HTTP helpers ───────────────────────────────────────────────────── + +export interface HttpResponse { + statusCode: number; + body: string; +} + +export function httpGet(url: string): Promise { + return new Promise((resolve, reject) => { + const req = request(url, { method: "GET" }, (res) => { + let body = ""; + res.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + res.on("end", () => { + resolve({ statusCode: res.statusCode ?? 0, body }); + }); + }); + req.on("error", reject); + req.end(); + }); +} + +export function httpPost( + url: string, + body: unknown, + headers?: Record, +): Promise { + return new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const parsed = new URL(url); + + const req = request( + { + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(data), + ...headers, + }, + }, + (res) => { + let responseBody = ""; + res.on("data", (chunk: Buffer) => { + responseBody += chunk.toString(); + }); + res.on("end", () => { + resolve({ statusCode: res.statusCode ?? 0, body: responseBody }); + }); + }, + ); + req.on("error", reject); + req.end(data); + }); +} + +// ── Polling helpers ────────────────────────────────────────────────── + +export async function waitForHealthcheck( + url: string, + timeoutMs: number = 30_000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const res = await httpGet(url); + if (res.statusCode === 200 && res.body.includes('"ok":true')) { + return; + } + } catch { + // server not ready yet + } + await sleep(500); + } + throw new Error(`Healthcheck at ${url} did not pass within ${timeoutMs}ms`); +} + +export async function waitForFunctionRegistration( + baseUrl: string, + funcName: string, + apiKey: string, + timeoutMs: number = 15_000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const res = await httpPost( + `${baseUrl}/v1/functions/${funcName}/invoke`, + { params: {} }, + { "x-bb-api-key": apiKey }, + ); + if (res.statusCode !== 404) { + return; + } + } catch { + // server not ready yet + } + await sleep(500); + } + throw new Error( + `Function '${funcName}' was not registered within ${timeoutMs}ms`, + ); +} + +// ── CLI runner ─────────────────────────────────────────────────────── + +export interface RunResult { + exitCode: number; + stdout: string; + stderr: string; +} + +// Path to the built CLI entry point, usable from any directory. +const CLI_PATH = join(PROJECT_ROOT, "dist", "cli.js"); + +export function runBb( + args: string, + options?: { cwd?: string; env?: Record }, +): RunResult { + try { + const stdout = execSync(`node ${CLI_PATH} ${args}`, { + cwd: options?.cwd, + env: { ...process.env, ...options?.env }, + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + return { exitCode: 0, stdout: String(stdout ?? ""), stderr: "" }; + } catch (error: unknown) { + const e = error as { + status?: number; + stdout?: string; + stderr?: string; + }; + return { + exitCode: e.status ?? 1, + stdout: e.stdout ?? "", + stderr: e.stderr ?? "", + }; + } +} + +// ── Utilities ──────────────────────────────────────────────────────── + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function normalizeJson(value: unknown): string { + return JSON.stringify( + typeof value === "string" ? JSON.parse(value) : value, + null, + 2, + ); +} diff --git a/tests/integration/init.test.ts b/tests/integration/init.test.ts new file mode 100644 index 0000000..f919b0e --- /dev/null +++ b/tests/integration/init.test.ts @@ -0,0 +1,109 @@ +import { describe, it, after } from "node:test"; +import assert from "node:assert/strict"; +import { execSync } from "node:child_process"; +import { mkdtempSync, existsSync, readFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { runBb } from "./helpers.js"; + +const tempDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), `bb-init-test-${prefix}-`)); + tempDirs.push(dir); + return dir; +} + +describe("Init Command", () => { + after(() => { + for (const dir of tempDirs) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("scaffolds correct project structure", () => { + const dir = createTempDir("scaffold"); + const projectName = "test-proj"; + + const result = runBb(`init ${projectName}`, { cwd: dir }); + + assert.equal( + result.exitCode, + 0, + `bb init should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + + const projectDir = join(dir, projectName); + assert.ok( + existsSync(join(projectDir, "package.json")), + "package.json should exist", + ); + assert.ok( + existsSync(join(projectDir, "index.ts")), + "index.ts should exist", + ); + assert.ok(existsSync(join(projectDir, ".env")), ".env should exist"); + assert.ok( + existsSync(join(projectDir, ".gitignore")), + ".gitignore should exist", + ); + assert.ok( + existsSync(join(projectDir, "tsconfig.json")), + "tsconfig.json should exist", + ); + assert.ok( + existsSync(join(projectDir, ".git")), + ".git directory should exist", + ); + }); + + it("package.json has correct contents", () => { + const dir = createTempDir("pkgjson"); + const projectName = "test-pkg"; + + runBb(`init ${projectName}`, { cwd: dir }); + + const pkgPath = join(dir, projectName, "package.json"); + assert.ok(existsSync(pkgPath), "package.json should exist"); + + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + assert.equal(pkg.type, "module", 'Should have "type": "module"'); + + // Check dependencies include expected packages + const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; + assert.ok( + allDeps["@browserbasehq/sdk-functions"], + "Should depend on @browserbasehq/sdk-functions", + ); + assert.ok(allDeps["playwright-core"], "Should depend on playwright-core"); + assert.ok(allDeps["zod"], "Should depend on zod"); + }); + + it("rejects invalid project names", () => { + const dir = createTempDir("invalid-name"); + + const result = runBb("init 123invalid", { cwd: dir }); + + assert.notEqual( + result.exitCode, + 0, + "Should reject project name starting with number", + ); + }); + + it("rejects existing directory", () => { + const dir = createTempDir("existing"); + const projectName = "existing-dir"; + + // Create the directory first + execSync(`mkdir -p ${join(dir, projectName)}`); + + const result = runBb(`init ${projectName}`, { cwd: dir }); + + assert.notEqual( + result.exitCode, + 0, + "Should reject when directory already exists", + ); + }); +}); diff --git a/tests/integration/manifest-generation.test.ts b/tests/integration/manifest-generation.test.ts new file mode 100644 index 0000000..74fb5a5 --- /dev/null +++ b/tests/integration/manifest-generation.test.ts @@ -0,0 +1,225 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { execSync } from "node:child_process"; +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { + discoverTemplates, + getTarballPath, + setupTemplateProject, + cleanupDir, +} from "./helpers.js"; + +const templates = discoverTemplates(); +const templatesWithExpected = templates.filter((t) => t.expectedDir !== null); + +const tarballPath = getTarballPath(); + +describe("Manifest Generation", () => { + + for (const template of templatesWithExpected) { + describe(template.name, () => { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(template, tarballPath); + + // Run introspection in the temp project + execSync(`pnpm tsx ${template.entrypoint}`, { + cwd: projectDir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + }); + + after(() => { + cleanupDir(projectDir); + }); + + it("creates manifest directory", () => { + const manifestsDir = join( + projectDir, + ".browserbase", + "functions", + "manifests", + ); + assert.ok( + existsSync(manifestsDir), + `Expected manifest directory at ${manifestsDir}`, + ); + }); + + it("generates the correct manifest files", () => { + const manifestsDir = join( + projectDir, + ".browserbase", + "functions", + "manifests", + ); + const expectedFiles = readdirSync(template.expectedDir!) + .filter((f) => f.endsWith(".json")) + .sort(); + const generatedFiles = readdirSync(manifestsDir) + .filter((f) => f.endsWith(".json")) + .sort(); + + assert.deepStrictEqual( + generatedFiles, + expectedFiles, + `Manifest files mismatch.\nExpected: ${expectedFiles.join(", ")}\nGenerated: ${generatedFiles.join(", ")}`, + ); + }); + + it("manifest contents match expected", () => { + const manifestsDir = join( + projectDir, + ".browserbase", + "functions", + "manifests", + ); + const expectedFiles = readdirSync(template.expectedDir!) + .filter((f) => f.endsWith(".json")) + .sort(); + + for (const file of expectedFiles) { + const expectedPath = join(template.expectedDir!, file); + const generatedPath = join(manifestsDir, file); + + const expected = JSON.parse(readFileSync(expectedPath, "utf-8")); + const generated = JSON.parse(readFileSync(generatedPath, "utf-8")); + + assert.deepStrictEqual( + generated, + expected, + `Manifest content mismatch for ${file}.\nExpected: ${JSON.stringify(expected, null, 2)}\nGenerated: ${JSON.stringify(generated, null, 2)}`, + ); + } + }); + }); + } + + // Specific assertions per test case + describe("specific assertions", () => { + const basicTemplate = templates.find((t) => t.name === "basic"); + const paramsTemplate = templates.find( + (t) => t.name === "with-params-schema", + ); + const browserConfigTemplate = templates.find( + (t) => t.name === "custom-browser-config", + ); + const nestedTemplate = templates.find( + (t) => t.name === "nested-entrypoint", + ); + + if (basicTemplate) { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(basicTemplate, tarballPath); + execSync(`pnpm tsx ${basicTemplate.entrypoint}`, { + cwd: projectDir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + }); + + after(() => { + cleanupDir(projectDir); + }); + + it('basic: manifest is { "name": "basic", "config": {} }', () => { + const manifest = readManifest(projectDir, "basic.json"); + assert.equal(manifest.name, "basic"); + assert.deepStrictEqual(manifest.config, {}); + }); + } + + if (paramsTemplate) { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(paramsTemplate, tarballPath); + execSync(`pnpm tsx ${paramsTemplate.entrypoint}`, { + cwd: projectDir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + }); + + after(() => { + cleanupDir(projectDir); + }); + + it("with-params-schema: manifest config includes parametersSchema with correct JSON Schema", () => { + const manifest = readManifest(projectDir, "with-params-schema.json"); + assert.equal(manifest.name, "with-params-schema"); + const schema = manifest.config.parametersSchema; + assert.ok(schema, "parametersSchema should exist"); + assert.equal(schema.type, "object"); + assert.equal(schema.properties.data.type, "number"); + assert.deepStrictEqual(schema.required, ["data"]); + }); + } + + if (browserConfigTemplate) { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(browserConfigTemplate, tarballPath); + execSync(`pnpm tsx ${browserConfigTemplate.entrypoint}`, { + cwd: projectDir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + }); + + after(() => { + cleanupDir(projectDir); + }); + + it("custom-browser-config: manifest config includes sessionConfig.browserSettings.advancedStealth", () => { + const manifest = readManifest(projectDir, "custom-browser-config.json"); + assert.equal(manifest.name, "custom-browser-config"); + assert.equal( + manifest.config.sessionConfig.browserSettings.advancedStealth, + true, + ); + }); + } + + if (nestedTemplate) { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(nestedTemplate, tarballPath); + execSync(`pnpm tsx ${nestedTemplate.entrypoint}`, { + cwd: projectDir, + env: { ...process.env, BB_FUNCTIONS_PHASE: "introspect" }, + stdio: "pipe", + }); + }); + + after(() => { + cleanupDir(projectDir); + }); + + it("nested-entrypoint: manifest generated correctly despite src/index.ts path", () => { + const manifest = readManifest(projectDir, "nested-entrypoint.json"); + assert.equal(manifest.name, "nested-entrypoint"); + assert.ok(manifest.config, "config should exist"); + }); + } + }); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function readManifest(projectDir: string, filename: string): any { + const manifestPath = join( + projectDir, + ".browserbase", + "functions", + "manifests", + filename, + ); + return JSON.parse(readFileSync(manifestPath, "utf-8")); +} diff --git a/tests/integration/publish.test.ts b/tests/integration/publish.test.ts new file mode 100644 index 0000000..121f12e --- /dev/null +++ b/tests/integration/publish.test.ts @@ -0,0 +1,164 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { writeFileSync, mkdirSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { + discoverTemplates, + getTarballPath, + setupTemplateProject, + cleanupDir, + runBb, +} from "./helpers.js"; + +const templates = discoverTemplates(); + +const tarballPath = getTarballPath(); + +describe("Publish CLI", () => { + + for (const template of templates) { + describe(template.name, () => { + let projectDir: string; + const artifacts: string[] = []; + + before(() => { + projectDir = setupTemplateProject(template, tarballPath); + }); + + after(() => { + for (const p of artifacts) { + if (existsSync(p)) { + rmSync(p, { recursive: true, force: true }); + } + } + cleanupDir(projectDir); + }); + + it("dry-run succeeds with valid config", () => { + const result = runBb(`publish ${template.entrypoint} --dry-run`, { + cwd: projectDir, + env: { + BROWSERBASE_API_KEY: + process.env["BROWSERBASE_API_KEY"] ?? "test_key", + BROWSERBASE_PROJECT_ID: + process.env["BROWSERBASE_PROJECT_ID"] ?? "test_project", + }, + }); + + assert.equal( + result.exitCode, + 0, + `Dry-run should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + }); + + it("missing entrypoint fails", () => { + const result = runBb("publish nonexistent.ts", { + cwd: projectDir, + }); + + assert.notEqual( + result.exitCode, + 0, + "Should fail with missing entrypoint", + ); + }); + + it("invalid file extension fails", () => { + const testFile = join(projectDir, "test.txt"); + writeFileSync(testFile, "not a valid file"); + artifacts.push(testFile); + + const result = runBb("publish test.txt", { + cwd: projectDir, + }); + + assert.notEqual( + result.exitCode, + 0, + "Should fail with invalid file extension", + ); + }); + + it("missing API key fails", () => { + const result = runBb(`publish ${template.entrypoint}`, { + cwd: projectDir, + env: { + BROWSERBASE_API_KEY: "", + BROWSERBASE_PROJECT_ID: "test_project", + }, + }); + + assert.notEqual(result.exitCode, 0, "Should fail without API key"); + }); + + it("missing project ID fails", () => { + const result = runBb(`publish ${template.entrypoint}`, { + cwd: projectDir, + env: { + BROWSERBASE_API_KEY: "test_key", + BROWSERBASE_PROJECT_ID: "", + }, + }); + + assert.notEqual(result.exitCode, 0, "Should fail without project ID"); + }); + + it("gitignore patterns are respected", () => { + // Create test artifacts that should be ignored + const gitignorePath = join(projectDir, ".gitignore"); + const testLogPath = join(projectDir, "test.log"); + const testEnvPath = join(projectDir, ".env.test"); + const testDirPath = join(projectDir, "test-gitignore-dir"); + const markerPath = join(projectDir, ".gitignore.test-marker"); + + // Skip if .gitignore already exists (don't interfere with project) + if (existsSync(gitignorePath)) { + return; + } + + writeFileSync(testLogPath, "log entry"); + writeFileSync(testEnvPath, "test-secret"); + mkdirSync(testDirPath, { recursive: true }); + writeFileSync(join(testDirPath, "ignored.txt"), "ignored content"); + + writeFileSync( + gitignorePath, + ".env.test\n*.log\ntest-gitignore-dir/\n", + ); + writeFileSync(markerPath, ""); + + artifacts.push( + testLogPath, + testEnvPath, + testDirPath, + gitignorePath, + markerPath, + ); + + const result = runBb(`publish ${template.entrypoint} --dry-run`, { + cwd: projectDir, + env: { + BROWSERBASE_API_KEY: + process.env["BROWSERBASE_API_KEY"] ?? "test_key", + BROWSERBASE_PROJECT_ID: + process.env["BROWSERBASE_PROJECT_ID"] ?? "test_project", + }, + }); + + assert.equal( + result.exitCode, + 0, + `Dry-run with gitignore should succeed.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + + // Verify ignored files are not mentioned in the output + const output = result.stdout + result.stderr; + assert.ok( + !output.includes("test.log") || output.includes(".gitignore"), + ".gitignore patterns should be respected", + ); + }); + }); + } +}); diff --git a/tests/nested-entrypoint/pnpm-lock.yaml b/tests/nested-entrypoint/pnpm-lock.yaml deleted file mode 100644 index 8a5a0f5..0000000 --- a/tests/nested-entrypoint/pnpm-lock.yaml +++ /dev/null @@ -1,435 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@browserbasehq/sdk-functions": - specifier: link:../.. - version: link:../.. - playwright-core: - specifier: ^1.56.1 - version: 1.56.1 - zod: - specifier: ^4.1.12 - version: 4.1.12 - devDependencies: - tsx: - specifier: ^4.20.5 - version: 4.20.6 - -packages: - "@esbuild/aix-ppc64@0.25.12": - resolution: - { - integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.25.12": - resolution: - { - integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.25.12": - resolution: - { - integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.25.12": - resolution: - { - integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.25.12": - resolution: - { - integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.25.12": - resolution: - { - integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.25.12": - resolution: - { - integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.25.12": - resolution: - { - integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.25.12": - resolution: - { - integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.25.12": - resolution: - { - integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.25.12": - resolution: - { - integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.25.12": - resolution: - { - integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.25.12": - resolution: - { - integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.25.12": - resolution: - { - integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.25.12": - resolution: - { - integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.25.12": - resolution: - { - integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.25.12": - resolution: - { - integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.25.12": - resolution: - { - integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.25.12": - resolution: - { - integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.25.12": - resolution: - { - integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.25.12": - resolution: - { - integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.25.12": - resolution: - { - integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.25.12": - resolution: - { - integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.25.12": - resolution: - { - integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.25.12": - resolution: - { - integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - esbuild@0.25.12: - resolution: - { - integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, - } - engines: { node: ">=18" } - hasBin: true - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - get-tsconfig@4.13.0: - resolution: - { - integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==, - } - - playwright-core@1.56.1: - resolution: - { - integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==, - } - engines: { node: ">=18" } - hasBin: true - - resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } - - tsx@4.20.6: - resolution: - { - integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==, - } - engines: { node: ">=18.0.0" } - hasBin: true - - zod@4.1.12: - resolution: - { - integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==, - } - -snapshots: - "@esbuild/aix-ppc64@0.25.12": - optional: true - - "@esbuild/android-arm64@0.25.12": - optional: true - - "@esbuild/android-arm@0.25.12": - optional: true - - "@esbuild/android-x64@0.25.12": - optional: true - - "@esbuild/darwin-arm64@0.25.12": - optional: true - - "@esbuild/darwin-x64@0.25.12": - optional: true - - "@esbuild/freebsd-arm64@0.25.12": - optional: true - - "@esbuild/freebsd-x64@0.25.12": - optional: true - - "@esbuild/linux-arm64@0.25.12": - optional: true - - "@esbuild/linux-arm@0.25.12": - optional: true - - "@esbuild/linux-ia32@0.25.12": - optional: true - - "@esbuild/linux-loong64@0.25.12": - optional: true - - "@esbuild/linux-mips64el@0.25.12": - optional: true - - "@esbuild/linux-ppc64@0.25.12": - optional: true - - "@esbuild/linux-riscv64@0.25.12": - optional: true - - "@esbuild/linux-s390x@0.25.12": - optional: true - - "@esbuild/linux-x64@0.25.12": - optional: true - - "@esbuild/netbsd-arm64@0.25.12": - optional: true - - "@esbuild/netbsd-x64@0.25.12": - optional: true - - "@esbuild/openbsd-arm64@0.25.12": - optional: true - - "@esbuild/openbsd-x64@0.25.12": - optional: true - - "@esbuild/openharmony-arm64@0.25.12": - optional: true - - "@esbuild/sunos-x64@0.25.12": - optional: true - - "@esbuild/win32-arm64@0.25.12": - optional: true - - "@esbuild/win32-ia32@0.25.12": - optional: true - - "@esbuild/win32-x64@0.25.12": - optional: true - - esbuild@0.25.12: - optionalDependencies: - "@esbuild/aix-ppc64": 0.25.12 - "@esbuild/android-arm": 0.25.12 - "@esbuild/android-arm64": 0.25.12 - "@esbuild/android-x64": 0.25.12 - "@esbuild/darwin-arm64": 0.25.12 - "@esbuild/darwin-x64": 0.25.12 - "@esbuild/freebsd-arm64": 0.25.12 - "@esbuild/freebsd-x64": 0.25.12 - "@esbuild/linux-arm": 0.25.12 - "@esbuild/linux-arm64": 0.25.12 - "@esbuild/linux-ia32": 0.25.12 - "@esbuild/linux-loong64": 0.25.12 - "@esbuild/linux-mips64el": 0.25.12 - "@esbuild/linux-ppc64": 0.25.12 - "@esbuild/linux-riscv64": 0.25.12 - "@esbuild/linux-s390x": 0.25.12 - "@esbuild/linux-x64": 0.25.12 - "@esbuild/netbsd-arm64": 0.25.12 - "@esbuild/netbsd-x64": 0.25.12 - "@esbuild/openbsd-arm64": 0.25.12 - "@esbuild/openbsd-x64": 0.25.12 - "@esbuild/openharmony-arm64": 0.25.12 - "@esbuild/sunos-x64": 0.25.12 - "@esbuild/win32-arm64": 0.25.12 - "@esbuild/win32-ia32": 0.25.12 - "@esbuild/win32-x64": 0.25.12 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - playwright-core@1.56.1: {} - - resolve-pkg-maps@1.0.0: {} - - tsx@4.20.6: - dependencies: - esbuild: 0.25.12 - get-tsconfig: 4.13.0 - optionalDependencies: - fsevents: 2.3.3 - - zod@4.1.12: {} diff --git a/tests/run-all-tests.sh b/tests/run-all-tests.sh deleted file mode 100755 index aca5ae3..0000000 --- a/tests/run-all-tests.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/bash - -# Usage: ./run-all-tests.sh -# Discovers and runs all tests in subdirectories that have bb.test.json - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# Function to print colored output -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -print_info() { - echo -e "${YELLOW}ℹ️ $1${NC}" -} - -print_header() { - echo -e "${BLUE}════════════════════════════════════════${NC}" - echo -e "${BLUE}▶ $1${NC}" - echo -e "${BLUE}════════════════════════════════════════${NC}" -} - -print_test_header() { - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ TEST: $1${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" -} - -# Get the directory of this script -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Track test results -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=() - -print_header "Browserbase SDK Tests Runner" -print_info "Discovering test directories..." - -# Find all directories with bb.test.json -TEST_DIRS=() -for dir in "$SCRIPT_DIR"/*/; do - if [ -f "$dir/bb.test.json" ]; then - TEST_NAME=$(basename "$dir") - TEST_DIRS+=("$TEST_NAME") - fi -done - -if [ ${#TEST_DIRS[@]} -eq 0 ]; then - print_error "No test directories found with bb.test.json" - exit 1 -fi - -print_info "Found ${#TEST_DIRS[@]} test(s): ${TEST_DIRS[*]}" -echo "" - -# Run tests for each directory -for test_dir in "${TEST_DIRS[@]}"; do - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - print_test_header "$test_dir" - - cd "$SCRIPT_DIR/$test_dir" - - # Read test description if available - if command -v jq >/dev/null 2>&1 && [ -f "bb.test.json" ]; then - DESCRIPTION=$(jq -r '.description // ""' bb.test.json) - if [ -n "$DESCRIPTION" ] && [ "$DESCRIPTION" != "null" ]; then - print_info "Description: $DESCRIPTION" - fi - fi - - # Run the test - if "$SCRIPT_DIR/test-all.sh"; then - print_success "Test '$test_dir' PASSED" - PASSED_TESTS=$((PASSED_TESTS + 1)) - else - print_error "Test '$test_dir' FAILED" - FAILED_TESTS+=("$test_dir") - fi - - echo "" -done - -# Final summary -print_header "Test Results Summary" -echo "Total tests run: $TOTAL_TESTS" -echo "Passed: $PASSED_TESTS" -echo "Failed: ${#FAILED_TESTS[@]}" - -if [ ${#FAILED_TESTS[@]} -gt 0 ]; then - echo "" - print_error "Failed tests:" - for failed in "${FAILED_TESTS[@]}"; do - echo " - $failed" - done - echo "" - print_error "TESTS FAILED" - exit 1 -else - echo "" - print_success "ALL TESTS PASSED! 🎉" - exit 0 -fi \ No newline at end of file diff --git a/tests/basic/bb.test.json b/tests/templates/basic/bb.test.json similarity index 100% rename from tests/basic/bb.test.json rename to tests/templates/basic/bb.test.json diff --git a/tests/basic/expected/basic.json b/tests/templates/basic/expected/basic.json similarity index 100% rename from tests/basic/expected/basic.json rename to tests/templates/basic/expected/basic.json diff --git a/tests/basic/index.ts b/tests/templates/basic/index.ts similarity index 100% rename from tests/basic/index.ts rename to tests/templates/basic/index.ts diff --git a/tests/basic/package.json b/tests/templates/basic/package.json similarity index 76% rename from tests/basic/package.json rename to tests/templates/basic/package.json index 10236c8..f4d1c2b 100644 --- a/tests/basic/package.json +++ b/tests/templates/basic/package.json @@ -8,9 +8,6 @@ "author": "", "license": "ISC", "packageManager": "pnpm@10.12.1", - "dependencies": { - "@browserbasehq/sdk-functions": "link:../.." - }, "devDependencies": { "tsx": "^4.20.5" } diff --git a/tests/basic/tsconfig.json b/tests/templates/basic/tsconfig.json similarity index 100% rename from tests/basic/tsconfig.json rename to tests/templates/basic/tsconfig.json diff --git a/tests/custom-browser-config/bb.test.json b/tests/templates/custom-browser-config/bb.test.json similarity index 100% rename from tests/custom-browser-config/bb.test.json rename to tests/templates/custom-browser-config/bb.test.json diff --git a/tests/custom-browser-config/expected/custom-browser-config.json b/tests/templates/custom-browser-config/expected/custom-browser-config.json similarity index 100% rename from tests/custom-browser-config/expected/custom-browser-config.json rename to tests/templates/custom-browser-config/expected/custom-browser-config.json diff --git a/tests/custom-browser-config/index.ts b/tests/templates/custom-browser-config/index.ts similarity index 100% rename from tests/custom-browser-config/index.ts rename to tests/templates/custom-browser-config/index.ts diff --git a/tests/custom-browser-config/package.json b/tests/templates/custom-browser-config/package.json similarity index 88% rename from tests/custom-browser-config/package.json rename to tests/templates/custom-browser-config/package.json index 39982db..70ca08b 100644 --- a/tests/custom-browser-config/package.json +++ b/tests/templates/custom-browser-config/package.json @@ -10,7 +10,6 @@ "packageManager": "pnpm@10.12.1", "dependencies": { "playwright-core": "^1.56.1", - "@browserbasehq/sdk-functions": "link:../..", "zod": "^4.1.12" }, "devDependencies": { diff --git a/tests/nested-entrypoint/bb.test.json b/tests/templates/nested-entrypoint/bb.test.json similarity index 100% rename from tests/nested-entrypoint/bb.test.json rename to tests/templates/nested-entrypoint/bb.test.json diff --git a/tests/nested-entrypoint/expected/nested-entrypoint.json b/tests/templates/nested-entrypoint/expected/nested-entrypoint.json similarity index 100% rename from tests/nested-entrypoint/expected/nested-entrypoint.json rename to tests/templates/nested-entrypoint/expected/nested-entrypoint.json diff --git a/tests/nested-entrypoint/package.json b/tests/templates/nested-entrypoint/package.json similarity index 88% rename from tests/nested-entrypoint/package.json rename to tests/templates/nested-entrypoint/package.json index d7c1443..c4c4f5c 100644 --- a/tests/nested-entrypoint/package.json +++ b/tests/templates/nested-entrypoint/package.json @@ -10,7 +10,6 @@ "packageManager": "pnpm@10.12.1", "dependencies": { "playwright-core": "^1.56.1", - "@browserbasehq/sdk-functions": "link:../..", "zod": "^4.1.12" }, "devDependencies": { diff --git a/tests/nested-entrypoint/src/index.ts b/tests/templates/nested-entrypoint/src/index.ts similarity index 100% rename from tests/nested-entrypoint/src/index.ts rename to tests/templates/nested-entrypoint/src/index.ts diff --git a/tests/with-params-schema/bb.test.json b/tests/templates/with-params-schema/bb.test.json similarity index 100% rename from tests/with-params-schema/bb.test.json rename to tests/templates/with-params-schema/bb.test.json diff --git a/tests/with-params-schema/expected/with-params-schema.json b/tests/templates/with-params-schema/expected/with-params-schema.json similarity index 100% rename from tests/with-params-schema/expected/with-params-schema.json rename to tests/templates/with-params-schema/expected/with-params-schema.json diff --git a/tests/with-params-schema/index.ts b/tests/templates/with-params-schema/index.ts similarity index 100% rename from tests/with-params-schema/index.ts rename to tests/templates/with-params-schema/index.ts diff --git a/tests/with-params-schema/package.json b/tests/templates/with-params-schema/package.json similarity index 77% rename from tests/with-params-schema/package.json rename to tests/templates/with-params-schema/package.json index 385ed14..e925100 100644 --- a/tests/with-params-schema/package.json +++ b/tests/templates/with-params-schema/package.json @@ -8,9 +8,6 @@ "author": "", "license": "ISC", "packageManager": "pnpm@10.12.1", - "dependencies": { - "@browserbasehq/sdk-functions": "link:../.." - }, "devDependencies": { "tsx": "^4.20.5" } diff --git a/tests/test-all.sh b/tests/test-all.sh deleted file mode 100755 index 84153a4..0000000 --- a/tests/test-all.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -# Usage: ./test-all.sh -# Run from within a test directory (e.g., tests/basic/) -# Runs all tests: manifest generation, dev server, and publish -# Requires bb.test.json with entrypoint configuration - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print colored output -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -print_info() { - echo -e "${YELLOW}ℹ️ $1${NC}" -} - -print_header() { - echo -e "${BLUE}════════════════════════════════════════${NC}" - echo -e "${BLUE}▶ $1${NC}" - echo -e "${BLUE}════════════════════════════════════════${NC}" -} - -# Read entrypoint from bb.test.json -if [ ! -f "bb.test.json" ]; then - print_error "Error: bb.test.json not found" - echo "Create a bb.test.json file with an 'entrypoint' field" - echo "Example: {\"entrypoint\": \"index.ts\"}" - exit 1 -fi - -# Extract entrypoint from bb.test.json -ENTRYPOINT=$(jq -r '.entrypoint' bb.test.json) - -if [ -z "$ENTRYPOINT" ] || [ "$ENTRYPOINT" = "null" ]; then - print_error "Error: No entrypoint found in bb.test.json" - exit 1 -fi - -print_info "Using entrypoint from bb.test.json: $ENTRYPOINT" - -# Check if entrypoint exists -if [ ! -f "$ENTRYPOINT" ]; then - print_error "Error: Entrypoint file '$ENTRYPOINT' not found" - exit 1 -fi - -# Detect the test directory name -TEST_DIR_NAME=$(basename "$(pwd)") -TEST_BASE_DIR=$(dirname "$(pwd)") - -print_header "Running all tests for: $TEST_DIR_NAME" -print_info "Entrypoint: $ENTRYPOINT" -print_info "Working directory: $(pwd)" - -# Track overall test results -FAILED_TESTS=() - -# Test 1: Manifest generation (if expected directory exists) -if [ -d "expected" ]; then - print_header "Test 1: Manifest Generation" - - # Use the new test-manifest-generation.sh script - if [ -f "$TEST_BASE_DIR/test-manifest-generation.sh" ]; then - if "$TEST_BASE_DIR/test-manifest-generation.sh"; then - print_success "Manifest generation test passed" - else - print_error "Manifest generation test failed" - FAILED_TESTS+=("Manifest generation") - fi - else - print_error "test-manifest-generation.sh not found" - FAILED_TESTS+=("Manifest generation") - fi -else - print_info "No expected directory found, skipping manifest test" -fi - -# Test 2: Dev server test -if [ -f "$TEST_BASE_DIR/test-dev.sh" ]; then - print_header "Test 2: Development Server" - - if "$TEST_BASE_DIR/test-dev.sh"; then - print_success "Dev server test passed" - else - print_error "Dev server test failed" - FAILED_TESTS+=("Dev server") - fi -else - print_info "No test-dev.sh found, skipping dev server test" -fi - -# Test 3: Publish test -if [ -f "$TEST_BASE_DIR/test-publish.sh" ]; then - print_header "Test 3: Publish Command" - - if "$TEST_BASE_DIR/test-publish.sh"; then - print_success "Publish test passed" - else - print_error "Publish test failed" - FAILED_TESTS+=("Publish") - fi -else - print_info "No test-publish.sh found, skipping publish test" -fi - -# Summary -print_header "Test Summary for $TEST_DIR_NAME" - -if [ ${#FAILED_TESTS[@]} -eq 0 ]; then - print_success "All tests passed! 🎉" - exit 0 -else - print_error "Failed tests: ${FAILED_TESTS[*]}" - exit 1 -fi - diff --git a/tests/test-dev.sh b/tests/test-dev.sh deleted file mode 100755 index d680593..0000000 --- a/tests/test-dev.sh +++ /dev/null @@ -1,320 +0,0 @@ -#!/bin/bash - -# Usage: ./test-dev.sh -# Run from within a test directory (e.g., tests/basic/) -# Requires bb.test.json with entrypoint configuration - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -print_info() { - echo -e "${YELLOW}ℹ️ $1${NC}" -} - -# Read bb.test.json for configuration -if [ ! -f "bb.test.json" ]; then - print_error "Error: bb.test.json not found" - echo "Create a bb.test.json file with an 'entrypoint' field" - echo "Example: {\"entrypoint\": \"index.ts\"}" - exit 1 -fi - -# Extract entrypoint from bb.test.json -ENTRYPOINT=$(jq -r '.entrypoint' bb.test.json) - -if [ -z "$ENTRYPOINT" ] || [ "$ENTRYPOINT" = "null" ]; then - print_error "Error: No entrypoint found in bb.test.json" - exit 1 -fi - -# Check if entrypoint file exists -if [ ! -f "$ENTRYPOINT" ]; then - print_error "Error: Entrypoint file '$ENTRYPOINT' not found" - exit 1 -fi - -# Detect the test directory name and expected function names -TEST_DIR_NAME=$(basename "$(pwd)") - -# Try to detect function names from the entrypoint file -FUNCTION_NAMES=() -if [ -f "$ENTRYPOINT" ]; then - # Extract function names from defineFn calls - while IFS= read -r func_name; do - FUNCTION_NAMES+=("$func_name") - done < <(grep -o 'defineFn\s*(\s*["'"'"']\([^"'"'"']*\)' "$ENTRYPOINT" | sed 's/.*["'"'"']\(.*\)/\1/') -fi - -# If no functions found, use the directory name as default -if [ ${#FUNCTION_NAMES[@]} -eq 0 ]; then - FUNCTION_NAMES=("$TEST_DIR_NAME") -fi - -PRIMARY_FUNCTION="${FUNCTION_NAMES[0]}" - -print_info "Testing dev server in: $(pwd)" -print_info "Entrypoint: $ENTRYPOINT" -print_info "Detected functions: ${FUNCTION_NAMES[*]}" -print_info "Primary function for testing: $PRIMARY_FUNCTION" - -# Cleanup function -cleanup() { - # Short circuit if no port defined - if [ -z "$PORT" ]; then - rm -f dev-server.log dev-response.json - return - fi - - # Kill any process listening on the port using lsof - print_info "Cleaning up any process on port $PORT..." - PIDS=$(lsof -t -i:$PORT 2>/dev/null) - if [ ! -z "$PIDS" ]; then - for pid in $PIDS; do - print_info "Killing process $pid on port $PORT..." - kill $pid 2>/dev/null || true - sleep 0.5 - kill -9 $pid 2>/dev/null || true - done - fi - - rm -f dev-server.log dev-response.json -} - -# Set up trap to cleanup on exit -trap cleanup EXIT - -print_info "Starting bb dev server tests..." - -# Find an available port (starting from 14113) -BASE_PORT=14113 -PORT=$BASE_PORT -MAX_PORT_ATTEMPTS=10 -for i in $(seq 0 $MAX_PORT_ATTEMPTS); do - if ! nc -z 127.0.0.1 $PORT 2>/dev/null; then - print_info "Using port $PORT for dev server" - break - else - print_info "Port $PORT is in use, trying next port..." - PORT=$((BASE_PORT + i + 1)) - fi -done - -if [ $PORT -eq $((BASE_PORT + MAX_PORT_ATTEMPTS + 1)) ]; then - print_error "Could not find an available port after $MAX_PORT_ATTEMPTS attempts" - exit 1 -fi - -# Start dev server in background -print_info "Starting dev server with 'pnpm bb dev $ENTRYPOINT --port $PORT'..." -pnpm bb dev "$ENTRYPOINT" --port "$PORT" >dev-server.log 2>&1 & - -# If the port flag doesn't work, check the log for "already in use" and retry with different port -sleep 2 -if grep -q "Port.*is already in use" dev-server.log 2>/dev/null; then - print_info "Port conflict detected, incrementing port and retrying..." - PORT=$((PORT + 1)) - print_info "Trying with port $PORT..." - - # Retry with new port - pnpm bb dev "$ENTRYPOINT" --port "$PORT" >dev-server.log 2>&1 & -fi - -# Wait for server to be ready (check for up to 30 seconds) -# Now that we have a healthcheck endpoint at /, we can properly check for server readiness -MAX_WAIT=30 -WAIT_COUNT=0 -print_info "Waiting for server to start on port $PORT..." - -while [ $WAIT_COUNT -lt $MAX_WAIT ]; do - # First check if any process is listening on the port (faster than curl) - if lsof -i:$PORT >/dev/null 2>&1; then - # Process is listening, now try to hit the healthcheck endpoint - HEALTH_CHECK=$(curl -s "http://127.0.0.1:$PORT/" 2>/dev/null) - if echo "$HEALTH_CHECK" | grep -q '"ok":true' 2>/dev/null; then - print_success "Server healthcheck responded on port $PORT" - break - fi - else - # No process listening on port - check if server crashed - # Give it a few seconds before declaring it dead (it might still be starting) - if [ $WAIT_COUNT -gt 3 ]; then - print_error "Dev server process died unexpectedly (no process listening on port $PORT)" - echo "" - echo "Full server logs:" - echo "=================" - cat dev-server.log - echo "=================" - exit 1 - fi - fi - - sleep 1 - WAIT_COUNT=$((WAIT_COUNT + 1)) - - # Provide progress updates with better error detection - if [ $WAIT_COUNT -eq 3 ]; then - # Check for common startup errors - if grep -q "Error\|error\|ERROR\|Failed\|failed\|FAILED\|Cannot\|cannot" dev-server.log 2>/dev/null; then - print_error "Startup errors detected in server log:" - echo "" - grep -i "error\|failed\|cannot" dev-server.log | head -5 - echo "" - echo "Full server logs:" - echo "=================" - cat dev-server.log - echo "=================" - exit 1 - fi - elif [ $WAIT_COUNT -eq 5 ]; then - print_info "Still waiting for server to start (attempt $WAIT_COUNT/$MAX_WAIT)..." - elif [ $WAIT_COUNT -eq 10 ]; then - print_info "Server taking longer than expected (attempt $WAIT_COUNT/$MAX_WAIT)..." - echo "Recent server logs:" - tail -10 dev-server.log - elif [ $WAIT_COUNT -eq 20 ]; then - print_info "Still trying to connect (attempt $WAIT_COUNT/$MAX_WAIT)..." - # Try to see if server is listening on our port - echo "Checking if anything is listening on port $PORT:" - lsof -i:$PORT 2>/dev/null | grep LISTEN || echo " No process found listening on port $PORT" - fi -done - -if [ $WAIT_COUNT -eq $MAX_WAIT ]; then - print_error "Dev server failed to start within $MAX_WAIT seconds" - echo "" - echo "Full server logs:" - echo "=================" - cat dev-server.log - echo "=================" - exit 1 -fi - -print_success "Dev server started successfully on port $PORT" - -# Wait for function to be registered (poll for non-404 response) -print_info "Waiting for function '$PRIMARY_FUNCTION' to be registered..." -FUNCTION_READY_WAIT=15 # Wait up to 15 seconds for function registration -FUNCTION_WAIT_COUNT=0 - -while [ $FUNCTION_WAIT_COUNT -lt $FUNCTION_READY_WAIT ]; do - # Try to invoke the function and check if it's registered (non-404) - FUNC_STATUS=$(curl -s -w "%{http_code}" -X POST "http://127.0.0.1:$PORT/v1/functions/$PRIMARY_FUNCTION/invoke" \ - -H "Content-Type: application/json" \ - -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" \ - -d '{"params": {}}' \ - -o /dev/null 2>/dev/null) - - # If we get anything other than 404, the function is registered - if [ "$FUNC_STATUS" != "404" ] && [ -n "$FUNC_STATUS" ]; then - print_success "Function '$PRIMARY_FUNCTION' is registered and ready (status: $FUNC_STATUS)" - break - fi - - sleep 0.5 - FUNCTION_WAIT_COUNT=$((FUNCTION_WAIT_COUNT + 1)) - - # Show progress every 3 seconds - if [ $((FUNCTION_WAIT_COUNT % 6)) -eq 0 ]; then - echo " Still waiting for function to register... ($FUNCTION_WAIT_COUNT seconds)" - fi -done - -if [ $FUNCTION_WAIT_COUNT -eq $FUNCTION_READY_WAIT ]; then - print_error "Function '$PRIMARY_FUNCTION' was not registered within $FUNCTION_READY_WAIT seconds" - echo "Server logs:" - tail -30 dev-server.log - exit 1 -fi - -# Test 1: Server healthcheck endpoint -print_info "Test 1: Checking server healthcheck endpoint..." -HEALTH_RESPONSE=$(curl -s "http://127.0.0.1:$PORT/" 2>/dev/null) -if echo "$HEALTH_RESPONSE" | grep -q '"ok":true' 2>/dev/null; then - print_success "Server healthcheck endpoint working correctly" -else - print_error "Server healthcheck not responding correctly" - echo "Expected: {\"ok\":true}" - echo "Got: $HEALTH_RESPONSE" - echo "Server logs:" - tail -20 dev-server.log - exit 1 -fi - -# Test 2: Invoke the primary function -print_info "Test 2: Invoking '$PRIMARY_FUNCTION' function..." -HTTP_STATUS=$(curl -s -w "%{http_code}" -X POST "http://127.0.0.1:$PORT/v1/functions/$PRIMARY_FUNCTION/invoke" \ - -H "Content-Type: application/json" \ - -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" \ - -d '{"params": {}}' \ - -o dev-response.json) - -if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "201" ]; then - print_success "Function invocation succeeded (HTTP $HTTP_STATUS)" - if [ -f dev-response.json ] && [ -s dev-response.json ]; then - echo " Response: $(cat dev-response.json | head -c 200)" - fi -else - print_error "Function invocation failed (HTTP $HTTP_STATUS)" - if [ -f dev-response.json ]; then - echo "Response: $(cat dev-response.json)" - fi - echo "Server logs:" - tail -20 dev-server.log - exit 1 -fi - -# Test 3: Test all detected functions if multiple -if [ ${#FUNCTION_NAMES[@]} -gt 1 ]; then - print_info "Testing additional functions..." - for func in "${FUNCTION_NAMES[@]:1}"; do - print_info " Invoking '$func' function..." - HTTP_STATUS=$(curl -s -w "%{http_code}" -X POST "http://127.0.0.1:$PORT/v1/functions/$func/invoke" \ - -H "Content-Type: application/json" \ - -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" \ - -d '{"params": {}}' \ - -o dev-response-$func.json) - - if [ "$HTTP_STATUS" = "200" ] || [ "$HTTP_STATUS" = "201" ]; then - print_success " Function '$func' invocation succeeded" - else - print_error " Function '$func' invocation failed (HTTP $HTTP_STATUS)" - fi - rm -f dev-response-$func.json - done -fi - -# Test 3: Invalid function returns 404 -print_info "Test 3: Testing invalid function endpoint..." -INVALID_STATUS=$(curl -s -w "%{http_code}" -X POST \ - "http://127.0.0.1:$PORT/v1/functions/nonexistent/invoke" \ - -H "Content-Type: application/json" \ - -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" \ - -d '{"params": {}}' \ - -o /dev/null) - -if [ "$INVALID_STATUS" = "404" ]; then - print_success "Invalid function correctly returns 404" -elif [ "$INVALID_STATUS" = "400" ] || [ "$INVALID_STATUS" = "422" ]; then - # Some servers return 400/422 for invalid requests - print_success "Invalid function returns error status ($INVALID_STATUS)" -else - print_info "Invalid function endpoint returned $INVALID_STATUS (expected 404)" -fi - -print_info "=========================================" -print_success "Dev server tests completed successfully!" -exit 0 diff --git a/tests/test-manifest-generation.sh b/tests/test-manifest-generation.sh deleted file mode 100755 index e92ee25..0000000 --- a/tests/test-manifest-generation.sh +++ /dev/null @@ -1,202 +0,0 @@ -#!/bin/bash - -# Usage: ./test-manifest-generation.sh -# Run from within a test directory (e.g., tests/basic/) -# Tests manifest generation against expected outputs -# Requires bb.test.json with entrypoint configuration - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print colored output -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -print_info() { - echo -e "${YELLOW}ℹ️ $1${NC}" -} - -print_header() { - echo -e "${BLUE}════════════════════════════════════════${NC}" - echo -e "${BLUE}▶ $1${NC}" - echo -e "${BLUE}════════════════════════════════════════${NC}" -} - -# Function to compare JSON files -compare_json_files() { - local file1="$1" - local file2="$2" - - # Use jq to normalize and compare JSON - if ! diff -q <(jq -S . "$file1") <(jq -S . "$file2") >/dev/null 2>&1; then - return 1 - fi - return 0 -} - -# Read bb.test.json for configuration -if [ ! -f "bb.test.json" ]; then - print_error "Error: bb.test.json not found" - echo "Create a bb.test.json file with an 'entrypoint' field" - echo "Example: {\"entrypoint\": \"index.ts\"}" - exit 1 -fi - -# Extract entrypoint from bb.test.json -ENTRYPOINT=$(jq -r '.entrypoint' bb.test.json) - -if [ -z "$ENTRYPOINT" ] || [ "$ENTRYPOINT" = "null" ]; then - print_error "Error: No entrypoint found in bb.test.json" - exit 1 -fi - -# Always use 'expected' directory -EXPECTED_DIR="expected" - -# Check if entrypoint file exists -if [ ! -f "$ENTRYPOINT" ]; then - print_error "Error: Entrypoint file '$ENTRYPOINT' not found" - exit 1 -fi - -# Check if expected directory exists -if [ ! -d "$EXPECTED_DIR" ]; then - print_error "Error: Expected directory '$EXPECTED_DIR' not found" - echo "This test requires an 'expected' directory with reference manifests" - exit 1 -fi - -# Detect the test directory name -TEST_DIR_NAME=$(basename "$(pwd)") -TEST_BASE_DIR=$(dirname "$(pwd)") - -print_header "Manifest Generation Test" -print_info "Test directory: $TEST_DIR_NAME" -print_info "Entrypoint: $ENTRYPOINT" -print_info "Expected manifests: $EXPECTED_DIR/" - -# Cleanup function -cleanup() { - # Keep .browserbase directory for debugging if test fails - if [ "$TEST_FAILED" != "1" ]; then - rm -rf .browserbase 2>/dev/null || true - fi -} - -# Set up trap to cleanup on exit -trap cleanup EXIT - -# Initialize test status -TEST_FAILED=0 - -print_info "Running manifest generation..." - -# Step 1: Clean any existing .browserbase directory -print_info "Cleaning existing .browserbase directory..." -rm -rf .browserbase 2>/dev/null || true - -# Step 2: Run introspection to generate manifests -print_info "Running introspection phase..." -if BB_FUNCTIONS_PHASE=introspect pnpm tsx "$ENTRYPOINT" 2>/dev/null; then - print_success "Introspection completed successfully" -else - print_error "Introspection failed" - exit 1 -fi - -# Step 3: Verify manifests were created -if [ ! -d ".browserbase/functions/manifests" ]; then - print_error "No manifest directory created at .browserbase/functions/manifests" - exit 1 -fi - -# Count generated manifests -MANIFEST_COUNT=$(find .browserbase/functions/manifests -name "*.json" -type f | wc -l | tr -d ' ') -if [ "$MANIFEST_COUNT" -eq 0 ]; then - print_error "No manifest files were generated" - exit 1 -fi -print_info "Generated $MANIFEST_COUNT manifest(s)" - -# Step 4: Compare manifests with expected -print_info "Comparing generated manifests with expected..." - -# Get list of expected and generated files -EXPECTED_FILES=$(cd "$EXPECTED_DIR" && find . -name "*.json" -type f | sort) -GENERATED_FILES=$(cd .browserbase/functions/manifests && find . -name "*.json" -type f | sort) - -# Check if same files exist in both directories -if [ "$EXPECTED_FILES" != "$GENERATED_FILES" ]; then - print_error "Different manifest files generated" - echo "" - echo "Expected files:" - for f in $EXPECTED_FILES; do - echo " $f" - done - echo "" - echo "Generated files:" - for f in $GENERATED_FILES; do - echo " $f" - done - TEST_FAILED=1 - echo "" - echo "Keeping .browserbase directory for debugging" - exit 1 -fi - -# Compare each file's content -COMPARISON_FAILED=0 -for file in $EXPECTED_FILES; do - EXPECTED_FILE="$EXPECTED_DIR/$file" - GENERATED_FILE=".browserbase/functions/manifests/$file" - - if ! compare_json_files "$EXPECTED_FILE" "$GENERATED_FILE"; then - print_error "Manifest content differs: $file" - - # Show the difference - echo "" - if command -v jq >/dev/null 2>&1; then - echo "Expected (formatted):" - jq -S . "$EXPECTED_FILE" 2>/dev/null || cat "$EXPECTED_FILE" - echo "" - echo "Generated (formatted):" - jq -S . "$GENERATED_FILE" 2>/dev/null || cat "$GENERATED_FILE" - echo "" - echo "Diff:" - diff <(jq -S . "$EXPECTED_FILE" 2>/dev/null) <(jq -S . "$GENERATED_FILE" 2>/dev/null) || true - else - echo "Diff:" - diff "$EXPECTED_FILE" "$GENERATED_FILE" || true - fi - echo "" - COMPARISON_FAILED=1 - else - print_success "Manifest matches: $file" - fi -done - -if [ $COMPARISON_FAILED -eq 0 ]; then - print_success "All manifests match expected!" -else - TEST_FAILED=1 - echo "Keeping .browserbase directory for debugging" - exit 1 -fi - -# Step 5: Report success -print_header "Test Summary" -print_success "Manifest generation test completed successfully!" -print_info "All generated manifests match the expected output" - -exit 0 diff --git a/tests/test-publish.sh b/tests/test-publish.sh deleted file mode 100755 index 9796d14..0000000 --- a/tests/test-publish.sh +++ /dev/null @@ -1,259 +0,0 @@ -#!/bin/bash - -# Usage: ./test-publish.sh -# Run from within a test directory (e.g., tests/basic/) -# Requires bb.test.json with entrypoint configuration - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -print_info() { - echo -e "${YELLOW}ℹ️ $1${NC}" -} - -# Read bb.test.json for configuration -if [ ! -f "bb.test.json" ]; then - print_error "Error: bb.test.json not found" - echo "Create a bb.test.json file with an 'entrypoint' field" - echo "Example: {\"entrypoint\": \"index.ts\"}" - exit 1 -fi - -# Extract entrypoint from bb.test.json -ENTRYPOINT=$(jq -r '.entrypoint' bb.test.json) - -if [ -z "$ENTRYPOINT" ] || [ "$ENTRYPOINT" = "null" ]; then - print_error "Error: No entrypoint found in bb.test.json" - exit 1 -fi - -# Check if entrypoint file exists -if [ ! -f "$ENTRYPOINT" ]; then - print_error "Error: Entrypoint file '$ENTRYPOINT' not found" - exit 1 -fi - -# Detect the test directory name -TEST_DIR_NAME=$(basename "$(pwd)") - -print_info "Testing bb publish in: $(pwd)" -print_info "Entrypoint: $ENTRYPOINT" - -# Cleanup function -cleanup() { - rm -f publish-output.log test.log .env.test test-ignore.txt - rm -rf test-gitignore-dir - # Remove .gitignore if we created it for testing - if [ -f .gitignore ] && [ -f .gitignore.test-marker ]; then - rm -f .gitignore .gitignore.test-marker - fi - # Restore .env if any test left a backup - if [ -f .env.backup_test4 ]; then - mv .env.backup_test4 .env - fi - if [ -f .env.backup_test5 ]; then - mv .env.backup_test5 .env - fi -} - -# Set up trap to cleanup on exit -trap cleanup EXIT - -print_info "Starting bb publish tests..." - -# Clean any previous state -if [ -f "package.json" ] && grep -q '"clean"' package.json; then - npm run clean 2>/dev/null || pnpm run clean 2>/dev/null || true -fi - -# Test 1: Dry run succeeds with valid configuration -print_info "Test 1: Testing dry-run mode with valid config..." -if pnpm bb publish "$ENTRYPOINT" --dry-run >publish-output.log 2>&1; then - print_success "Dry run succeeded with valid configuration" - if grep -q "Archive size:" publish-output.log || grep -q "dry-run" publish-output.log || grep -q "Dry run" publish-output.log; then - print_success "Dry run output contains expected information" - fi -else - EXIT_CODE=$? - # Some implementations might not have --dry-run flag - if grep -q "unknown option" publish-output.log || grep -q "unrecognized" publish-output.log; then - print_info "Dry-run flag not supported, continuing tests" - else - print_error "Dry run failed unexpectedly (exit code: $EXIT_CODE)" - cat publish-output.log - exit 1 - fi -fi - -# Test 2: Missing entrypoint fails with non-zero exit code -print_info "Test 2: Testing with missing entrypoint..." -if pnpm bb publish nonexistent.ts >publish-output.log 2>&1; then - print_error "Should have failed with missing entrypoint" - cat publish-output.log - exit 1 -else - EXIT_CODE=$? - print_success "Missing entrypoint returns non-zero exit code ($EXIT_CODE)" -fi - -# Test 3: Invalid file extension handling -print_info "Test 3: Testing with invalid file extension..." -touch test.txt -if pnpm bb publish test.txt >publish-output.log 2>&1; then - # Some implementations might accept any file - print_info "Accepts non-.ts files (may be intentional)" -else - EXIT_CODE=$? - print_success "Invalid file extension returns non-zero exit code ($EXIT_CODE)" -fi -rm -f test.txt - -# Test 4: Missing API key fails (test with actual publish, not dry-run) -print_info "Test 4: Testing with missing API key..." - -# Backup existing .env file if it exists -if [ -f .env ]; then - mv .env .env.backup_test4 -fi - -# Create .env with missing API key (only project ID) -echo "BROWSERBASE_PROJECT_ID=test_project" >.env - -if pnpm bb publish "$ENTRYPOINT" >publish-output.log 2>&1; then - print_error "Publish should fail without API key" - cat publish-output.log - # Restore .env before exiting - rm -f .env - if [ -f .env.backup_test4 ]; then - mv .env.backup_test4 .env - fi - exit 1 -else - EXIT_CODE=$? - print_success "Missing API key returns non-zero exit code ($EXIT_CODE)" -fi - -# Restore original .env -rm -f .env -if [ -f .env.backup_test4 ]; then - mv .env.backup_test4 .env -fi - -# Test 5: Missing project ID fails (test with actual publish, not dry-run) -print_info "Test 5: Testing with missing project ID..." - -# Backup existing .env file if it exists -if [ -f .env ]; then - mv .env .env.backup_test5 -fi - -# Create .env with missing project ID (only API key) -echo "BROWSERBASE_API_KEY=test_api_key" >.env - -if pnpm bb publish "$ENTRYPOINT" >publish-output.log 2>&1; then - print_error "Publish should fail without project ID" - cat publish-output.log - # Restore .env before exiting - rm -f .env - if [ -f .env.backup_test5 ]; then - mv .env.backup_test5 .env - fi - exit 1 -else - EXIT_CODE=$? - print_success "Missing project ID returns non-zero exit code ($EXIT_CODE)" -fi - -# Restore original .env -rm -f .env -if [ -f .env.backup_test5 ]; then - mv .env.backup_test5 .env -fi - -# Test 6: Archive respects .gitignore patterns -print_info "Test 6: Testing .gitignore respect..." - -# Ensure we don't have an existing .gitignore (tests should be isolated) -if [ -f .gitignore ]; then - print_error "Test directory should not have a .gitignore file. Please remove it." - exit 1 -fi - -# Create test files that should be ignored -echo "test-secret" >.env.test -echo "log entry" >test.log -mkdir -p test-gitignore-dir -echo "ignored content" >test-gitignore-dir/ignored.txt - -# Create a fresh .gitignore for testing -echo "# Test .gitignore - temporary for test-publish.sh" >.gitignore -echo ".env.test" >>.gitignore -echo "*.log" >>.gitignore -echo "test-gitignore-dir/" >>.gitignore - -# Create a marker file to indicate we created this .gitignore for testing -touch .gitignore.test-marker - -# Run publish with dry-run and check output -if pnpm bb publish "$ENTRYPOINT" --dry-run >publish-output.log 2>&1; then - # Check if ignored files are mentioned in the output (they shouldn't be) - if grep -q "test.log" publish-output.log; then - print_info ".gitignore might not be fully respected (test.log found in output)" - elif grep -q "test-gitignore-dir" publish-output.log; then - print_info ".gitignore might not be fully respected (test-gitignore-dir found in output)" - else - print_success ".gitignore patterns appear to be respected" - fi -fi - -# Test 7: Relative path entrypoint works -print_info "Test 7: Testing relative path entrypoint..." -if pnpm bb publish "./$ENTRYPOINT" --dry-run >publish-output.log 2>&1; then - print_success "Relative path entrypoint works" -else - print_info "Relative path entrypoint might not be supported" -fi - -# Test 8: Dry-run publish test - verify the build configuration works -print_info "Test 8: Testing dry-run publish (build configuration test)..." -print_info "Running dry-run publish (CLI will load .env if present)..." - -# The CLI will automatically load credentials from .env file -if pnpm bb publish "$ENTRYPOINT" --dry-run >publish-output.log 2>&1; then - print_success "Dry-run publish succeeded - build configuration is valid!" - - # Check for success indicators in output - if grep -q "success" publish-output.log || grep -q "Success" publish-output.log || grep -q "dry-run" publish-output.log || grep -q "Dry run" publish-output.log; then - print_success "Build configuration validated successfully" - fi - - # Show relevant output - if grep -q "Archive size:" publish-output.log; then - ARCHIVE_INFO=$(grep -i "Archive size:" publish-output.log | head -1) - print_success "Archive info: $ARCHIVE_INFO" - fi -else - EXIT_CODE=$? - print_error "Dry-run publish failed with exit code $EXIT_CODE" - echo "Output from publish command:" - cat publish-output.log - exit 1 -fi - -print_info "=========================================" -print_success "Publish tests completed successfully!" -exit 0 diff --git a/tests/with-params-schema/pnpm-lock.yaml b/tests/with-params-schema/pnpm-lock.yaml deleted file mode 100644 index e61fcc2..0000000 --- a/tests/with-params-schema/pnpm-lock.yaml +++ /dev/null @@ -1,411 +0,0 @@ -lockfileVersion: "9.0" - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - .: - dependencies: - "@browserbasehq/sdk-functions": - specifier: link:../.. - version: link:../.. - devDependencies: - tsx: - specifier: ^4.20.5 - version: 4.20.5 - -packages: - "@esbuild/aix-ppc64@0.25.9": - resolution: - { - integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [aix] - - "@esbuild/android-arm64@0.25.9": - resolution: - { - integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [android] - - "@esbuild/android-arm@0.25.9": - resolution: - { - integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [android] - - "@esbuild/android-x64@0.25.9": - resolution: - { - integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [android] - - "@esbuild/darwin-arm64@0.25.9": - resolution: - { - integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [darwin] - - "@esbuild/darwin-x64@0.25.9": - resolution: - { - integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [darwin] - - "@esbuild/freebsd-arm64@0.25.9": - resolution: - { - integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [freebsd] - - "@esbuild/freebsd-x64@0.25.9": - resolution: - { - integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [freebsd] - - "@esbuild/linux-arm64@0.25.9": - resolution: - { - integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [linux] - - "@esbuild/linux-arm@0.25.9": - resolution: - { - integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==, - } - engines: { node: ">=18" } - cpu: [arm] - os: [linux] - - "@esbuild/linux-ia32@0.25.9": - resolution: - { - integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [linux] - - "@esbuild/linux-loong64@0.25.9": - resolution: - { - integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==, - } - engines: { node: ">=18" } - cpu: [loong64] - os: [linux] - - "@esbuild/linux-mips64el@0.25.9": - resolution: - { - integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==, - } - engines: { node: ">=18" } - cpu: [mips64el] - os: [linux] - - "@esbuild/linux-ppc64@0.25.9": - resolution: - { - integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==, - } - engines: { node: ">=18" } - cpu: [ppc64] - os: [linux] - - "@esbuild/linux-riscv64@0.25.9": - resolution: - { - integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==, - } - engines: { node: ">=18" } - cpu: [riscv64] - os: [linux] - - "@esbuild/linux-s390x@0.25.9": - resolution: - { - integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==, - } - engines: { node: ">=18" } - cpu: [s390x] - os: [linux] - - "@esbuild/linux-x64@0.25.9": - resolution: - { - integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [linux] - - "@esbuild/netbsd-arm64@0.25.9": - resolution: - { - integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [netbsd] - - "@esbuild/netbsd-x64@0.25.9": - resolution: - { - integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [netbsd] - - "@esbuild/openbsd-arm64@0.25.9": - resolution: - { - integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openbsd] - - "@esbuild/openbsd-x64@0.25.9": - resolution: - { - integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [openbsd] - - "@esbuild/openharmony-arm64@0.25.9": - resolution: - { - integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [openharmony] - - "@esbuild/sunos-x64@0.25.9": - resolution: - { - integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [sunos] - - "@esbuild/win32-arm64@0.25.9": - resolution: - { - integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==, - } - engines: { node: ">=18" } - cpu: [arm64] - os: [win32] - - "@esbuild/win32-ia32@0.25.9": - resolution: - { - integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==, - } - engines: { node: ">=18" } - cpu: [ia32] - os: [win32] - - "@esbuild/win32-x64@0.25.9": - resolution: - { - integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==, - } - engines: { node: ">=18" } - cpu: [x64] - os: [win32] - - esbuild@0.25.9: - resolution: - { - integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==, - } - engines: { node: ">=18" } - hasBin: true - - fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } - os: [darwin] - - get-tsconfig@4.10.1: - resolution: - { - integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==, - } - - resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } - - tsx@4.20.5: - resolution: - { - integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==, - } - engines: { node: ">=18.0.0" } - hasBin: true - -snapshots: - "@esbuild/aix-ppc64@0.25.9": - optional: true - - "@esbuild/android-arm64@0.25.9": - optional: true - - "@esbuild/android-arm@0.25.9": - optional: true - - "@esbuild/android-x64@0.25.9": - optional: true - - "@esbuild/darwin-arm64@0.25.9": - optional: true - - "@esbuild/darwin-x64@0.25.9": - optional: true - - "@esbuild/freebsd-arm64@0.25.9": - optional: true - - "@esbuild/freebsd-x64@0.25.9": - optional: true - - "@esbuild/linux-arm64@0.25.9": - optional: true - - "@esbuild/linux-arm@0.25.9": - optional: true - - "@esbuild/linux-ia32@0.25.9": - optional: true - - "@esbuild/linux-loong64@0.25.9": - optional: true - - "@esbuild/linux-mips64el@0.25.9": - optional: true - - "@esbuild/linux-ppc64@0.25.9": - optional: true - - "@esbuild/linux-riscv64@0.25.9": - optional: true - - "@esbuild/linux-s390x@0.25.9": - optional: true - - "@esbuild/linux-x64@0.25.9": - optional: true - - "@esbuild/netbsd-arm64@0.25.9": - optional: true - - "@esbuild/netbsd-x64@0.25.9": - optional: true - - "@esbuild/openbsd-arm64@0.25.9": - optional: true - - "@esbuild/openbsd-x64@0.25.9": - optional: true - - "@esbuild/openharmony-arm64@0.25.9": - optional: true - - "@esbuild/sunos-x64@0.25.9": - optional: true - - "@esbuild/win32-arm64@0.25.9": - optional: true - - "@esbuild/win32-ia32@0.25.9": - optional: true - - "@esbuild/win32-x64@0.25.9": - optional: true - - esbuild@0.25.9: - optionalDependencies: - "@esbuild/aix-ppc64": 0.25.9 - "@esbuild/android-arm": 0.25.9 - "@esbuild/android-arm64": 0.25.9 - "@esbuild/android-x64": 0.25.9 - "@esbuild/darwin-arm64": 0.25.9 - "@esbuild/darwin-x64": 0.25.9 - "@esbuild/freebsd-arm64": 0.25.9 - "@esbuild/freebsd-x64": 0.25.9 - "@esbuild/linux-arm": 0.25.9 - "@esbuild/linux-arm64": 0.25.9 - "@esbuild/linux-ia32": 0.25.9 - "@esbuild/linux-loong64": 0.25.9 - "@esbuild/linux-mips64el": 0.25.9 - "@esbuild/linux-ppc64": 0.25.9 - "@esbuild/linux-riscv64": 0.25.9 - "@esbuild/linux-s390x": 0.25.9 - "@esbuild/linux-x64": 0.25.9 - "@esbuild/netbsd-arm64": 0.25.9 - "@esbuild/netbsd-x64": 0.25.9 - "@esbuild/openbsd-arm64": 0.25.9 - "@esbuild/openbsd-x64": 0.25.9 - "@esbuild/openharmony-arm64": 0.25.9 - "@esbuild/sunos-x64": 0.25.9 - "@esbuild/win32-arm64": 0.25.9 - "@esbuild/win32-ia32": 0.25.9 - "@esbuild/win32-x64": 0.25.9 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.10.1: - dependencies: - resolve-pkg-maps: 1.0.0 - - resolve-pkg-maps@1.0.0: {} - - tsx@4.20.5: - dependencies: - esbuild: 0.25.9 - get-tsconfig: 4.10.1 - optionalDependencies: - fsevents: 2.3.3 diff --git a/tsconfig.integration.json b/tsconfig.integration.json new file mode 100644 index 0000000..dd33961 --- /dev/null +++ b/tsconfig.integration.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist-integration-test", + "rootDir": ".", + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["tests/integration/**/*.ts", "tests/build-flow/**/*.ts"], + "exclude": ["node_modules", "dist", "dist-test", "dist-integration-test"] +} From cd4e4d5db72c610edbc2bb37b6544419db19fad5 Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:53:48 -0800 Subject: [PATCH 2/7] Local testing --- .github/workflows/ci.yml | 68 +----- package.json | 7 +- tests/build-flow/live-build.test.ts | 200 ------------------ tests/e2e/e2e.test.ts | 122 +++++++++++ tests/e2e/helpers.ts | 102 +++++++++ .../build-flow.test.ts | 27 +-- tests/integration/dev-server.test.ts | 2 +- tests/integration/helpers.ts | 3 + tests/integration/manifest-generation.test.ts | 19 +- tests/templates/basic/bb.test.json | 3 +- tests/templates/basic/expected/basic.json | 4 - .../basic/expected/sdk-e2e-basic.json | 4 + tests/templates/basic/index.ts | 2 +- .../custom-browser-config/bb.test.json | 3 +- .../sdk-e2e-custom-browser-config.json} | 2 +- .../templates/custom-browser-config/index.ts | 2 +- .../templates/nested-entrypoint/bb.test.json | 3 +- .../expected/sdk-e2e-nested-entrypoint.json} | 2 +- .../templates/nested-entrypoint/src/index.ts | 2 +- .../templates/with-params-schema/bb.test.json | 3 +- ...a.json => sdk-e2e-with-params-schema.json} | 2 +- tests/templates/with-params-schema/index.ts | 2 +- .../templates/with-params-schema/package.json | 3 + tsconfig.integration.json | 2 +- 24 files changed, 272 insertions(+), 317 deletions(-) delete mode 100644 tests/build-flow/live-build.test.ts create mode 100644 tests/e2e/e2e.test.ts create mode 100644 tests/e2e/helpers.ts rename tests/{build-flow => integration}/build-flow.test.ts (89%) delete mode 100644 tests/templates/basic/expected/basic.json create mode 100644 tests/templates/basic/expected/sdk-e2e-basic.json rename tests/templates/{nested-entrypoint/expected/nested-entrypoint.json => custom-browser-config/expected/sdk-e2e-custom-browser-config.json} (73%) rename tests/templates/{custom-browser-config/expected/custom-browser-config.json => nested-entrypoint/expected/sdk-e2e-nested-entrypoint.json} (75%) rename tests/templates/with-params-schema/expected/{with-params-schema.json => sdk-e2e-with-params-schema.json} (87%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efc45e3..c89a81f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,13 +152,10 @@ jobs: run: rm -rf dist && tsup - name: Run integration tests - env: - BROWSERBASE_API_KEY: ${{ secrets.BB_INTEGRATION_TEST_API_KEY }} - BROWSERBASE_PROJECT_ID: ${{ secrets.BB_INTEGRATION_TEST_PROJECT_ID }} run: pnpm run test:integration - build-flow-tests: - name: Build Flow Tests (Node ${{ matrix.node-version }}) + e2e-tests: + name: E2E Tests (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest strategy: fail-fast: false @@ -199,65 +196,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run build flow tests - run: pnpm run test:build-flow - - live-build-tests: - name: Live Build Tests (Node ${{ matrix.node-version }}) - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node-version: ["24.x"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Enable Corepack - run: corepack enable - - - name: Setup pnpm - run: | - corepack prepare pnpm@10.12.1 --activate - pnpm config set store-dir ~/.pnpm-store - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}- - - - name: Install root dependencies - run: pnpm install --frozen-lockfile - - - name: Build SDK - run: rm -rf dist && tsup - - - name: Install test dependencies - run: | - for dir in tests/*/; do - if [ -f "$dir/bb.test.json" ]; then - echo "Installing dependencies for $(basename $dir)..." - (cd "$dir" && pnpm install --frozen-lockfile) - fi - done - - - name: Run live build tests + - name: Run e2e tests env: BROWSERBASE_API_KEY: ${{ secrets.BB_INTEGRATION_TEST_API_KEY }} BROWSERBASE_PROJECT_ID: ${{ secrets.BB_INTEGRATION_TEST_PROJECT_ID }} - run: pnpm run test:live-build + run: pnpm run test:e2e diff --git a/package.json b/package.json index dc5b6fa..49137df 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,11 @@ "test:only": "$npm_execpath build:tests && node --test-only --test dist-test/**/*.test.js", "build:integration": "tsc --project tsconfig.integration.json", "pretest:integration": "$npm_execpath run build && pnpm pack", - "test:integration": "$npm_execpath run build:integration && node --test --test-timeout 60000 dist-integration-test/tests/integration/**/*.test.js", + "test:integration": "$npm_execpath run build:integration && node --test --test-timeout 120000 dist-integration-test/tests/integration/**/*.test.js", "posttest:integration": "rm -f browserbasehq-sdk-functions-*.tgz", - "test:build-flow": "$npm_execpath run build && pnpm pack && $npm_execpath run build:integration && node --test --test-timeout 120000 dist-integration-test/tests/build-flow/build-flow.test.js", - "test:live-build": "$npm_execpath run build && $npm_execpath run build:integration && node --test --test-timeout 300000 dist-integration-test/tests/build-flow/live-build.test.js", + "pretest:e2e": "$npm_execpath run build && pnpm pack", + "test:e2e": "$npm_execpath run build:integration && node --test --test-timeout 300000 dist-integration-test/tests/e2e/**/*.test.js", + "posttest:e2e": "rm -f browserbasehq-sdk-functions-*.tgz", "typecheck": "tsc --noEmit" }, "keywords": [], diff --git a/tests/build-flow/live-build.test.ts b/tests/build-flow/live-build.test.ts deleted file mode 100644 index a256cf3..0000000 --- a/tests/build-flow/live-build.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, it, before, after } from "node:test"; -import assert from "node:assert/strict"; -import { execSync } from "node:child_process"; -import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; - -const PROJECT_ROOT = join(import.meta.dirname, "..", "..", ".."); - -const API_KEY = process.env["BROWSERBASE_API_KEY"]; -const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"]; -const API_URL = - process.env["BROWSERBASE_API_URL"] ?? "https://api.browserbase.com"; - -function requireCredentials(): void { - if (!API_KEY || !PROJECT_ID) { - throw new Error( - "BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID must be set for live build tests", - ); - } -} - -function bbApi( - method: string, - path: string, - body?: unknown, -): { status: number; data: unknown } { - const args = [ - "curl", - "-s", - "-w", - "\\n%{http_code}", - "-X", - method, - `${API_URL}${path}`, - "-H", - `x-bb-api-key: ${API_KEY}`, - "-H", - "Content-Type: application/json", - ]; - if (body) { - args.push("-d", JSON.stringify(body)); - } - - const output = execSync(args.join(" "), { - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); - - const lines = String(output).trim().split("\n"); - const statusCode = parseInt(lines.pop()!, 10); - const responseBody = lines.join("\n"); - - let data: unknown; - try { - data = JSON.parse(responseBody); - } catch { - data = responseBody; - } - - return { status: statusCode, data }; -} - -function pollBuildStatus( - buildId: string, - timeoutMs: number = 120_000, -): unknown { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const res = bbApi("GET", `/v1/functions/builds/${buildId}`); - const build = res.data as Record; - - if (build["status"] === "COMPLETED") { - return build; - } - if (build["status"] === "FAILED") { - throw new Error(`Build ${buildId} failed: ${JSON.stringify(build)}`); - } - - execSync("sleep 3"); - } - throw new Error(`Build ${buildId} did not complete within ${timeoutMs}ms`); -} - -let tarballPath: string; -const tempDirs: string[] = []; - -function createTempDir(prefix: string): string { - const dir = mkdtempSync(join(tmpdir(), `bb-live-build-${prefix}-`)); - tempDirs.push(dir); - return dir; -} - -describe("Live Build + Invoke", () => { - before(() => { - // Pack the SDK (build is assumed to have been done by the npm script) - const packOutput = execSync("pnpm pack", { - cwd: PROJECT_ROOT, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); - - const tarballName = String(packOutput).trim().split("\n").pop()!.trim(); - tarballPath = join(PROJECT_ROOT, tarballName); - assert.ok(existsSync(tarballPath), `Tarball not found at ${tarballPath}`); - }); - - after(() => { - for (const dir of tempDirs) { - rmSync(dir, { recursive: true, force: true }); - } - if (tarballPath && existsSync(tarballPath)) { - rmSync(tarballPath); - } - }); - - it("publishes basic function, builds, and invokes successfully", () => { - requireCredentials(); - - // Create a temp project that installs the SDK from the tarball - const dir = createTempDir("basic"); - - const pkg = { - name: "live-build-test", - version: "1.0.0", - private: true, - type: "module", - }; - writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2)); - - writeFileSync( - join(dir, "index.mjs"), - `import { defineFn } from "@browserbasehq/sdk-functions"; - -defineFn("basic", async () => { - return { answer: "adam is cool" }; -}); -`, - ); - - // Install the SDK from the tarball - execSync(`npm install ${tarballPath}`, { - cwd: dir, - stdio: "pipe", - env: { - ...process.env, - npm_config_fund: "false", - npm_config_audit: "false", - }, - }); - - // Publish from the temp project - const publishOutput = execSync("npx bb publish index.mjs", { - cwd: dir, - encoding: "utf-8", - env: { - ...process.env, - BROWSERBASE_API_KEY: API_KEY, - BROWSERBASE_PROJECT_ID: PROJECT_ID, - }, - stdio: ["pipe", "pipe", "pipe"], - }); - - // Extract build ID from stdout - const buildIdMatch = String(publishOutput).match( - /Build ID:\s*([a-f0-9-]+)/i, - ); - assert.ok( - buildIdMatch, - `Could not find build ID in output: ${publishOutput}`, - ); - const buildId = buildIdMatch[1]!; - - // Poll until build completes - const build = pollBuildStatus(buildId) as Record; - - // Assert builtFunctions is non-empty - const builtFunctions = build["builtFunctions"] as Array< - Record - >; - assert.ok( - builtFunctions && builtFunctions.length > 0, - `Expected non-empty builtFunctions, got: ${JSON.stringify(builtFunctions)}`, - ); - - // Find the "basic" function - const basicFn = builtFunctions.find((f) => f["name"] === "basic"); - assert.ok(basicFn, `Expected a function named "basic" in builtFunctions`); - - // Invoke the built function - const functionId = basicFn["id"] as string; - assert.ok(functionId, "Function should have an id"); - - const invokeRes = bbApi("POST", `/v1/functions/${functionId}/invoke`, {}); - assert.ok( - invokeRes.status === 200 || invokeRes.status === 201, - `Invoke should succeed, got status ${invokeRes.status}: ${JSON.stringify(invokeRes.data)}`, - ); - }); -}); diff --git a/tests/e2e/e2e.test.ts b/tests/e2e/e2e.test.ts new file mode 100644 index 0000000..94962b2 --- /dev/null +++ b/tests/e2e/e2e.test.ts @@ -0,0 +1,122 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { execSync } from "node:child_process"; +import { copyFileSync } from "node:fs"; +import { join, basename } from "node:path"; + +import { + requireCredentials, + bbApi, + pollBuildStatus, + API_KEY, + PROJECT_ID, + discoverTemplates, + getTarballPath, + setupTemplateProject, + cleanupDir, +} from "./helpers.js"; +import type { Template } from "./helpers.js"; + +const templates: Template[] = discoverTemplates(); +const tarballPath = getTarballPath(); +const tarballName = basename(tarballPath); + +describe("E2E: Publish, Build, and Invoke", { concurrency: true }, () => { + before(() => { + requireCredentials(); + }); + + for (const template of templates) { + describe(template.name, () => { + let projectDir: string; + + before(() => { + projectDir = setupTemplateProject(template, tarballPath); + + // Copy the tarball into the project dir so it gets archived + // on publish, then re-install with pnpm using a relative path + // so the lockfile references ./ instead of an absolute path. + copyFileSync(tarballPath, join(projectDir, tarballName)); + execSync(`pnpm add ./${tarballName}`, { + cwd: projectDir, + stdio: "pipe", + env: { + ...process.env, + npm_config_fund: "false", + npm_config_audit: "false", + }, + }); + }); + + after(() => { + if (projectDir) { + cleanupDir(projectDir); + } + }); + + it("publishes, builds, and invokes successfully", () => { + // 1. Publish from the temp project + const publishOutput = execSync( + `npx bb publish ${template.entrypoint}`, + { + cwd: projectDir, + encoding: "utf-8", + env: { + ...process.env, + BROWSERBASE_API_KEY: API_KEY, + BROWSERBASE_PROJECT_ID: PROJECT_ID, + }, + stdio: ["pipe", "pipe", "pipe"], + }, + ); + + // 2. Extract build ID from publish stdout + const buildIdMatch = String(publishOutput).match( + /Build ID:\s*([a-f0-9-]+)/i, + ); + assert.ok( + buildIdMatch, + `Could not find build ID in output: ${publishOutput}`, + ); + const buildId = buildIdMatch[1]!; + + // 3. Poll until build completes + const build = pollBuildStatus(buildId) as Record; + + // 4. Assert builtFunctions contains the template's function + const builtFunctions = build["builtFunctions"] as Array< + Record + >; + assert.ok( + builtFunctions && builtFunctions.length > 0, + `Expected non-empty builtFunctions, got: ${JSON.stringify(builtFunctions)}`, + ); + + const expectedName = `sdk-e2e-${template.name}`; + const builtFn = builtFunctions.find( + (f) => f["name"] === expectedName, + ); + assert.ok( + builtFn, + `Expected a function named "${expectedName}" in builtFunctions`, + ); + + // 5. Invoke the built function + const functionId = builtFn["id"] as string; + assert.ok(functionId, "Function should have an id"); + + const invokeRes = bbApi( + "POST", + `/v1/functions/${functionId}/invoke`, + template.invokeParams, + ); + + // 6. Assert success + assert.ok( + invokeRes.status >= 200 && invokeRes.status < 300, + `Invoke should succeed, got status ${invokeRes.status}: ${JSON.stringify(invokeRes.data)}`, + ); + }); + }); + } +}); diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts new file mode 100644 index 0000000..746d292 --- /dev/null +++ b/tests/e2e/helpers.ts @@ -0,0 +1,102 @@ +import { execFileSync, execSync } from "node:child_process"; +import { join } from "node:path"; +import dotenv from "dotenv"; + +// Load .env.e2e from project root before reading env vars. +// The compiled JS lives in dist-integration-test/tests/e2e/, +// so we go up 3 levels to reach the project root. +const PROJECT_ROOT_LOCAL = join(import.meta.dirname, "..", "..", ".."); +dotenv.config({ path: join(PROJECT_ROOT_LOCAL, ".env.e2e") }); + +// ── Credentials ────────────────────────────────────────────────── + +export const API_KEY = process.env["BROWSERBASE_API_KEY"]; +export const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"]; +export const API_URL = + process.env["BROWSERBASE_API_URL"] ?? "https://api.browserbase.com"; + +export function requireCredentials(): void { + if (!API_KEY || !PROJECT_ID) { + throw new Error( + "BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID must be set for e2e tests", + ); + } +} + +// ── Browserbase API client ─────────────────────────────────────── + +export function bbApi( + method: string, + path: string, + body?: unknown, +): { status: number; data: unknown } { + const args = [ + "-s", + "-w", + "\n%{http_code}", + "-X", + method, + `${API_URL}${path}`, + "-H", + `x-bb-api-key: ${API_KEY}`, + "-H", + "Content-Type: application/json", + ]; + if (body) { + args.push("-d", JSON.stringify(body)); + } + + // Use execFileSync to avoid shell interpretation issues with + // header values that contain spaces. + const output = execFileSync("curl", args, { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }); + + const lines = String(output).trim().split("\n"); + const statusCode = parseInt(lines.pop()!, 10); + const responseBody = lines.join("\n"); + + let data: unknown; + try { + data = JSON.parse(responseBody); + } catch { + data = responseBody; + } + + return { status: statusCode, data }; +} + +// ── Build polling ──────────────────────────────────────────────── + +export function pollBuildStatus( + buildId: string, + timeoutMs: number = 120_000, +): unknown { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const res = bbApi("GET", `/v1/functions/builds/${buildId}`); + const build = res.data as Record; + + if (build["status"] === "COMPLETED") { + return build; + } + if (build["status"] === "FAILED") { + throw new Error(`Build ${buildId} failed: ${JSON.stringify(build)}`); + } + + execSync("sleep 3"); + } + throw new Error(`Build ${buildId} did not complete within ${timeoutMs}ms`); +} + +// ── Re-exports from integration helpers ────────────────────────── + +export { + discoverTemplates, + getTarballPath, + setupTemplateProject, + cleanupDir, + PROJECT_ROOT, +} from "../integration/helpers.js"; +export type { Template } from "../integration/helpers.js"; diff --git a/tests/build-flow/build-flow.test.ts b/tests/integration/build-flow.test.ts similarity index 89% rename from tests/build-flow/build-flow.test.ts rename to tests/integration/build-flow.test.ts index afebc1a..a4f4299 100644 --- a/tests/build-flow/build-flow.test.ts +++ b/tests/integration/build-flow.test.ts @@ -1,4 +1,4 @@ -import { describe, it, before, after } from "node:test"; +import { describe, it, after } from "node:test"; import assert from "node:assert/strict"; import { execSync } from "node:child_process"; import { @@ -6,16 +6,15 @@ import { writeFileSync, readFileSync, existsSync, - rmSync, mkdirSync, + rmSync, } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -// Resolve the project root (3 levels up from dist-integration-test/tests/build-flow/) -const PROJECT_ROOT = join(import.meta.dirname, "..", "..", ".."); +import { getTarballPath, PROJECT_ROOT } from "./helpers.js"; -let tarballPath: string; +const tarballPath = getTarballPath(); const tempDirs: string[] = []; function createTempDir(prefix: string): string { @@ -66,29 +65,11 @@ function setupTempProject( } describe("Build Flow", () => { - before(() => { - // Pack the SDK (build is assumed to have been done by the npm script) - const packOutput = execSync("pnpm pack", { - cwd: PROJECT_ROOT, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); - - // pnpm pack outputs the tarball filename - const tarballName = String(packOutput).trim().split("\n").pop()!.trim(); - tarballPath = join(PROJECT_ROOT, tarballName); - assert.ok(existsSync(tarballPath), `Tarball not found at ${tarballPath}`); - }); - after(() => { // Clean up temp directories for (const dir of tempDirs) { rmSync(dir, { recursive: true, force: true }); } - // Clean up tarball - if (tarballPath && existsSync(tarballPath)) { - rmSync(tarballPath); - } }); it("ESM import works", () => { diff --git a/tests/integration/dev-server.test.ts b/tests/integration/dev-server.test.ts index d0552c2..f9bea3e 100644 --- a/tests/integration/dev-server.test.ts +++ b/tests/integration/dev-server.test.ts @@ -21,7 +21,7 @@ const API_KEY = process.env["BROWSERBASE_API_KEY"] ?? "test_key"; const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"] ?? "test_project"; function getFunctionName(templateName: string): string { - return templateName; + return `sdk-e2e-${templateName}`; } function startDevServer( diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 9ae2bd4..39a0dcf 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -27,6 +27,7 @@ export interface Template { dir: string; entrypoint: string; expectedDir: string | null; + invokeParams: unknown; } export function discoverTemplates(): Template[] { @@ -42,6 +43,7 @@ export function discoverTemplates(): Template[] { const config = JSON.parse(readFileSync(configPath, "utf-8")) as { entrypoint: string; + invokeParams?: unknown; }; const expectedDir = join(dir, "expected"); @@ -50,6 +52,7 @@ export function discoverTemplates(): Template[] { dir, entrypoint: config.entrypoint, expectedDir: existsSync(expectedDir) ? expectedDir : null, + invokeParams: config.invokeParams ?? {}, }); } diff --git a/tests/integration/manifest-generation.test.ts b/tests/integration/manifest-generation.test.ts index 74fb5a5..249c4fe 100644 --- a/tests/integration/manifest-generation.test.ts +++ b/tests/integration/manifest-generation.test.ts @@ -16,7 +16,6 @@ const templatesWithExpected = templates.filter((t) => t.expectedDir !== null); const tarballPath = getTarballPath(); describe("Manifest Generation", () => { - for (const template of templatesWithExpected) { describe(template.name, () => { let projectDir: string; @@ -127,9 +126,9 @@ describe("Manifest Generation", () => { cleanupDir(projectDir); }); - it('basic: manifest is { "name": "basic", "config": {} }', () => { - const manifest = readManifest(projectDir, "basic.json"); - assert.equal(manifest.name, "basic"); + it('basic: manifest is { "name": "sdk-e2e-basic", "config": {} }', () => { + const manifest = readManifest(projectDir, "sdk-e2e-basic.json"); + assert.equal(manifest.name, "sdk-e2e-basic"); assert.deepStrictEqual(manifest.config, {}); }); } @@ -151,8 +150,8 @@ describe("Manifest Generation", () => { }); it("with-params-schema: manifest config includes parametersSchema with correct JSON Schema", () => { - const manifest = readManifest(projectDir, "with-params-schema.json"); - assert.equal(manifest.name, "with-params-schema"); + const manifest = readManifest(projectDir, "sdk-e2e-with-params-schema.json"); + assert.equal(manifest.name, "sdk-e2e-with-params-schema"); const schema = manifest.config.parametersSchema; assert.ok(schema, "parametersSchema should exist"); assert.equal(schema.type, "object"); @@ -178,8 +177,8 @@ describe("Manifest Generation", () => { }); it("custom-browser-config: manifest config includes sessionConfig.browserSettings.advancedStealth", () => { - const manifest = readManifest(projectDir, "custom-browser-config.json"); - assert.equal(manifest.name, "custom-browser-config"); + const manifest = readManifest(projectDir, "sdk-e2e-custom-browser-config.json"); + assert.equal(manifest.name, "sdk-e2e-custom-browser-config"); assert.equal( manifest.config.sessionConfig.browserSettings.advancedStealth, true, @@ -204,8 +203,8 @@ describe("Manifest Generation", () => { }); it("nested-entrypoint: manifest generated correctly despite src/index.ts path", () => { - const manifest = readManifest(projectDir, "nested-entrypoint.json"); - assert.equal(manifest.name, "nested-entrypoint"); + const manifest = readManifest(projectDir, "sdk-e2e-nested-entrypoint.json"); + assert.equal(manifest.name, "sdk-e2e-nested-entrypoint"); assert.ok(manifest.config, "config should exist"); }); } diff --git a/tests/templates/basic/bb.test.json b/tests/templates/basic/bb.test.json index d1aff5a..19e64bb 100644 --- a/tests/templates/basic/bb.test.json +++ b/tests/templates/basic/bb.test.json @@ -1,4 +1,5 @@ { "entrypoint": "index.ts", - "description": "Basic function test" + "description": "Basic function test", + "invokeParams": {} } diff --git a/tests/templates/basic/expected/basic.json b/tests/templates/basic/expected/basic.json deleted file mode 100644 index f866baf..0000000 --- a/tests/templates/basic/expected/basic.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "basic", - "config": {} -} diff --git a/tests/templates/basic/expected/sdk-e2e-basic.json b/tests/templates/basic/expected/sdk-e2e-basic.json new file mode 100644 index 0000000..9eef3a2 --- /dev/null +++ b/tests/templates/basic/expected/sdk-e2e-basic.json @@ -0,0 +1,4 @@ +{ + "name": "sdk-e2e-basic", + "config": {} +} diff --git a/tests/templates/basic/index.ts b/tests/templates/basic/index.ts index 644ce32..0b45110 100644 --- a/tests/templates/basic/index.ts +++ b/tests/templates/basic/index.ts @@ -1,5 +1,5 @@ import { defineFn } from "@browserbasehq/sdk-functions"; -defineFn("basic", async () => { +defineFn("sdk-e2e-basic", async () => { return { answer: "adam is cool" }; }); diff --git a/tests/templates/custom-browser-config/bb.test.json b/tests/templates/custom-browser-config/bb.test.json index 84b9aef..9477485 100644 --- a/tests/templates/custom-browser-config/bb.test.json +++ b/tests/templates/custom-browser-config/bb.test.json @@ -1,4 +1,5 @@ { "entrypoint": "index.ts", - "description": "Custom browser configuration test" + "description": "Custom browser configuration test", + "invokeParams": {} } diff --git a/tests/templates/nested-entrypoint/expected/nested-entrypoint.json b/tests/templates/custom-browser-config/expected/sdk-e2e-custom-browser-config.json similarity index 73% rename from tests/templates/nested-entrypoint/expected/nested-entrypoint.json rename to tests/templates/custom-browser-config/expected/sdk-e2e-custom-browser-config.json index 1565ce0..174d599 100644 --- a/tests/templates/nested-entrypoint/expected/nested-entrypoint.json +++ b/tests/templates/custom-browser-config/expected/sdk-e2e-custom-browser-config.json @@ -1,5 +1,5 @@ { - "name": "nested-entrypoint", + "name": "sdk-e2e-custom-browser-config", "config": { "sessionConfig": { "browserSettings": { diff --git a/tests/templates/custom-browser-config/index.ts b/tests/templates/custom-browser-config/index.ts index a7a7394..1148b20 100644 --- a/tests/templates/custom-browser-config/index.ts +++ b/tests/templates/custom-browser-config/index.ts @@ -10,7 +10,7 @@ const ApiResponseSchema = z.object({ }); defineFn( - "custom-browser-config", + "sdk-e2e-custom-browser-config", async (context) => { const { session } = context; diff --git a/tests/templates/nested-entrypoint/bb.test.json b/tests/templates/nested-entrypoint/bb.test.json index bde8c7f..c66228e 100644 --- a/tests/templates/nested-entrypoint/bb.test.json +++ b/tests/templates/nested-entrypoint/bb.test.json @@ -1,4 +1,5 @@ { "entrypoint": "src/index.ts", - "description": "Nested entrypoint test" + "description": "Nested entrypoint test", + "invokeParams": {} } diff --git a/tests/templates/custom-browser-config/expected/custom-browser-config.json b/tests/templates/nested-entrypoint/expected/sdk-e2e-nested-entrypoint.json similarity index 75% rename from tests/templates/custom-browser-config/expected/custom-browser-config.json rename to tests/templates/nested-entrypoint/expected/sdk-e2e-nested-entrypoint.json index 14074e9..6a11ed3 100644 --- a/tests/templates/custom-browser-config/expected/custom-browser-config.json +++ b/tests/templates/nested-entrypoint/expected/sdk-e2e-nested-entrypoint.json @@ -1,5 +1,5 @@ { - "name": "custom-browser-config", + "name": "sdk-e2e-nested-entrypoint", "config": { "sessionConfig": { "browserSettings": { diff --git a/tests/templates/nested-entrypoint/src/index.ts b/tests/templates/nested-entrypoint/src/index.ts index 96766bd..b493968 100644 --- a/tests/templates/nested-entrypoint/src/index.ts +++ b/tests/templates/nested-entrypoint/src/index.ts @@ -10,7 +10,7 @@ const ApiResponseSchema = z.object({ }); defineFn( - "nested-entrypoint", + "sdk-e2e-nested-entrypoint", async (context) => { const { session } = context; diff --git a/tests/templates/with-params-schema/bb.test.json b/tests/templates/with-params-schema/bb.test.json index 593833d..0fcf630 100644 --- a/tests/templates/with-params-schema/bb.test.json +++ b/tests/templates/with-params-schema/bb.test.json @@ -1,4 +1,5 @@ { "entrypoint": "index.ts", - "description": "Function with params schema test" + "description": "Function with params schema test", + "invokeParams": { "data": 42 } } diff --git a/tests/templates/with-params-schema/expected/with-params-schema.json b/tests/templates/with-params-schema/expected/sdk-e2e-with-params-schema.json similarity index 87% rename from tests/templates/with-params-schema/expected/with-params-schema.json rename to tests/templates/with-params-schema/expected/sdk-e2e-with-params-schema.json index 2be25ad..63a8f64 100644 --- a/tests/templates/with-params-schema/expected/with-params-schema.json +++ b/tests/templates/with-params-schema/expected/sdk-e2e-with-params-schema.json @@ -1,5 +1,5 @@ { - "name": "with-params-schema", + "name": "sdk-e2e-with-params-schema", "config": { "parametersSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", diff --git a/tests/templates/with-params-schema/index.ts b/tests/templates/with-params-schema/index.ts index 348d9fc..c919507 100644 --- a/tests/templates/with-params-schema/index.ts +++ b/tests/templates/with-params-schema/index.ts @@ -2,7 +2,7 @@ import { defineFn } from "@browserbasehq/sdk-functions"; import z from "zod"; defineFn( - "with-params-schema", + "sdk-e2e-with-params-schema", async (_ctx, params) => { const x = params.data; const returnValue = x * 2; diff --git a/tests/templates/with-params-schema/package.json b/tests/templates/with-params-schema/package.json index e925100..801e655 100644 --- a/tests/templates/with-params-schema/package.json +++ b/tests/templates/with-params-schema/package.json @@ -8,6 +8,9 @@ "author": "", "license": "ISC", "packageManager": "pnpm@10.12.1", + "dependencies": { + "zod": "^4.3.6" + }, "devDependencies": { "tsx": "^4.20.5" } diff --git a/tsconfig.integration.json b/tsconfig.integration.json index dd33961..089e336 100644 --- a/tsconfig.integration.json +++ b/tsconfig.integration.json @@ -6,6 +6,6 @@ "noUnusedLocals": false, "noUnusedParameters": false }, - "include": ["tests/integration/**/*.ts", "tests/build-flow/**/*.ts"], + "include": ["tests/integration/**/*.ts", "tests/e2e/**/*.ts"], "exclude": ["node_modules", "dist", "dist-test", "dist-integration-test"] } From 7e953eeceb3d7b2c43b6f3095ab3e0a99091dfc4 Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:07:59 -0800 Subject: [PATCH 3/7] Refactor e2e to use `bb invoke` --- tests/e2e/e2e.test.ts | 33 ++++++++++++------- .../integration/{ => cli}/dev-server.test.ts | 2 +- tests/integration/{ => cli}/init.test.ts | 2 +- tests/integration/{ => cli}/publish.test.ts | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) rename tests/integration/{ => cli}/dev-server.test.ts (99%) rename tests/integration/{ => cli}/init.test.ts (98%) rename tests/integration/{ => cli}/publish.test.ts (99%) diff --git a/tests/e2e/e2e.test.ts b/tests/e2e/e2e.test.ts index 94962b2..3c73cdf 100644 --- a/tests/e2e/e2e.test.ts +++ b/tests/e2e/e2e.test.ts @@ -1,12 +1,11 @@ import { describe, it, before, after } from "node:test"; import assert from "node:assert/strict"; -import { execSync } from "node:child_process"; +import { execSync, execFileSync } from "node:child_process"; import { copyFileSync } from "node:fs"; import { join, basename } from "node:path"; import { requireCredentials, - bbApi, pollBuildStatus, API_KEY, PROJECT_ID, @@ -101,20 +100,32 @@ describe("E2E: Publish, Build, and Invoke", { concurrency: true }, () => { `Expected a function named "${expectedName}" in builtFunctions`, ); - // 5. Invoke the built function + // 5. Invoke the built function using the CLI const functionId = builtFn["id"] as string; assert.ok(functionId, "Function should have an id"); - const invokeRes = bbApi( - "POST", - `/v1/functions/${functionId}/invoke`, - template.invokeParams, - ); + const invokeArgs = ["bb", "invoke", functionId]; + if (Object.keys(template.invokeParams as object).length > 0) { + invokeArgs.push("-p", JSON.stringify(template.invokeParams)); + } + + // execFileSync avoids shell quoting issues with JSON params + const invokeOutput = execFileSync("npx", invokeArgs, { + cwd: projectDir, + encoding: "utf-8", + env: { + ...process.env, + BROWSERBASE_API_KEY: API_KEY, + BROWSERBASE_PROJECT_ID: PROJECT_ID, + }, + stdio: ["pipe", "pipe", "pipe"], + timeout: 120_000, + }); - // 6. Assert success + // 6. bb invoke exits 0 on COMPLETED, 1 on FAILED (throws → test failure) assert.ok( - invokeRes.status >= 200 && invokeRes.status < 300, - `Invoke should succeed, got status ${invokeRes.status}: ${JSON.stringify(invokeRes.data)}`, + String(invokeOutput).includes("Invocation Details"), + `Invoke should produce details output: ${invokeOutput}`, ); }); }); diff --git a/tests/integration/dev-server.test.ts b/tests/integration/cli/dev-server.test.ts similarity index 99% rename from tests/integration/dev-server.test.ts rename to tests/integration/cli/dev-server.test.ts index f9bea3e..5bce38f 100644 --- a/tests/integration/dev-server.test.ts +++ b/tests/integration/cli/dev-server.test.ts @@ -11,7 +11,7 @@ import { httpPost, waitForHealthcheck, waitForFunctionRegistration, -} from "./helpers.js"; +} from "../helpers.js"; const templates = discoverTemplates(); const HAS_REAL_CREDENTIALS = !!( diff --git a/tests/integration/init.test.ts b/tests/integration/cli/init.test.ts similarity index 98% rename from tests/integration/init.test.ts rename to tests/integration/cli/init.test.ts index f919b0e..c0860b7 100644 --- a/tests/integration/init.test.ts +++ b/tests/integration/cli/init.test.ts @@ -4,7 +4,7 @@ import { execSync } from "node:child_process"; import { mkdtempSync, existsSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { runBb } from "./helpers.js"; +import { runBb } from "../helpers.js"; const tempDirs: string[] = []; diff --git a/tests/integration/publish.test.ts b/tests/integration/cli/publish.test.ts similarity index 99% rename from tests/integration/publish.test.ts rename to tests/integration/cli/publish.test.ts index 121f12e..ab70ccb 100644 --- a/tests/integration/publish.test.ts +++ b/tests/integration/cli/publish.test.ts @@ -8,7 +8,7 @@ import { setupTemplateProject, cleanupDir, runBb, -} from "./helpers.js"; +} from "../helpers.js"; const templates = discoverTemplates(); From bcfecb0634d1d23cb7905e7082c09e32ab3fca86 Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:08:06 -0800 Subject: [PATCH 4/7] Gate E2E to workflow_dispatch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c89a81f..7c95273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,7 @@ jobs: run: pnpm run test:integration e2e-tests: + if: github.event_name == 'workflow_dispatch' name: E2E Tests (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest strategy: From a2e34eac5eba0f99a3b1ff71fbc0a0eea7b9041e Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:22:30 -0800 Subject: [PATCH 5/7] CI failures --- .github/workflows/ci.yml | 2 +- tests/e2e/e2e.test.ts | 4 +--- tests/integration/cli/dev-server.test.ts | 1 - tests/integration/cli/publish.test.ts | 6 +----- tests/integration/helpers.ts | 4 +--- tests/integration/manifest-generation.test.ts | 15 ++++++++++++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c95273..63e6b75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build SDK - run: rm -rf dist && tsup + run: pnpm run build - name: Run integration tests run: pnpm run test:integration diff --git a/tests/e2e/e2e.test.ts b/tests/e2e/e2e.test.ts index 3c73cdf..cb97073 100644 --- a/tests/e2e/e2e.test.ts +++ b/tests/e2e/e2e.test.ts @@ -92,9 +92,7 @@ describe("E2E: Publish, Build, and Invoke", { concurrency: true }, () => { ); const expectedName = `sdk-e2e-${template.name}`; - const builtFn = builtFunctions.find( - (f) => f["name"] === expectedName, - ); + const builtFn = builtFunctions.find((f) => f["name"] === expectedName); assert.ok( builtFn, `Expected a function named "${expectedName}" in builtFunctions`, diff --git a/tests/integration/cli/dev-server.test.ts b/tests/integration/cli/dev-server.test.ts index 5bce38f..c46dac5 100644 --- a/tests/integration/cli/dev-server.test.ts +++ b/tests/integration/cli/dev-server.test.ts @@ -67,7 +67,6 @@ function killProcess(proc: ChildProcess): Promise { const tarballPath = getTarballPath(); describe("Dev Server", () => { - for (const template of templates) { describe(template.name, () => { let proc: ChildProcess | null = null; diff --git a/tests/integration/cli/publish.test.ts b/tests/integration/cli/publish.test.ts index ab70ccb..427827e 100644 --- a/tests/integration/cli/publish.test.ts +++ b/tests/integration/cli/publish.test.ts @@ -15,7 +15,6 @@ const templates = discoverTemplates(); const tarballPath = getTarballPath(); describe("Publish CLI", () => { - for (const template of templates) { describe(template.name, () => { let projectDir: string; @@ -122,10 +121,7 @@ describe("Publish CLI", () => { mkdirSync(testDirPath, { recursive: true }); writeFileSync(join(testDirPath, "ignored.txt"), "ignored content"); - writeFileSync( - gitignorePath, - ".env.test\n*.log\ntest-gitignore-dir/\n", - ); + writeFileSync(gitignorePath, ".env.test\n*.log\ntest-gitignore-dir/\n"); writeFileSync(markerPath, ""); artifacts.push( diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 39a0dcf..2592fc0 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -99,9 +99,7 @@ export function setupTemplateProject( template: Template, tarballPath: string, ): string { - const tempDir = mkdtempSync( - join(tmpdir(), `bb-test-${template.name}-`), - ); + const tempDir = mkdtempSync(join(tmpdir(), `bb-test-${template.name}-`)); // Copy template contents, excluding things we don't need in the project cpSync(template.dir, tempDir, { diff --git a/tests/integration/manifest-generation.test.ts b/tests/integration/manifest-generation.test.ts index 249c4fe..33b2e52 100644 --- a/tests/integration/manifest-generation.test.ts +++ b/tests/integration/manifest-generation.test.ts @@ -150,7 +150,10 @@ describe("Manifest Generation", () => { }); it("with-params-schema: manifest config includes parametersSchema with correct JSON Schema", () => { - const manifest = readManifest(projectDir, "sdk-e2e-with-params-schema.json"); + const manifest = readManifest( + projectDir, + "sdk-e2e-with-params-schema.json", + ); assert.equal(manifest.name, "sdk-e2e-with-params-schema"); const schema = manifest.config.parametersSchema; assert.ok(schema, "parametersSchema should exist"); @@ -177,7 +180,10 @@ describe("Manifest Generation", () => { }); it("custom-browser-config: manifest config includes sessionConfig.browserSettings.advancedStealth", () => { - const manifest = readManifest(projectDir, "sdk-e2e-custom-browser-config.json"); + const manifest = readManifest( + projectDir, + "sdk-e2e-custom-browser-config.json", + ); assert.equal(manifest.name, "sdk-e2e-custom-browser-config"); assert.equal( manifest.config.sessionConfig.browserSettings.advancedStealth, @@ -203,7 +209,10 @@ describe("Manifest Generation", () => { }); it("nested-entrypoint: manifest generated correctly despite src/index.ts path", () => { - const manifest = readManifest(projectDir, "sdk-e2e-nested-entrypoint.json"); + const manifest = readManifest( + projectDir, + "sdk-e2e-nested-entrypoint.json", + ); assert.equal(manifest.name, "sdk-e2e-nested-entrypoint"); assert.ok(manifest.config, "config should exist"); }); From 261bceac9750153b87c48a8199e61a55bb043331 Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:01:51 -0800 Subject: [PATCH 6/7] CI hang fix --- tests/integration/cli/dev-server.test.ts | 26 +++++++----------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/integration/cli/dev-server.test.ts b/tests/integration/cli/dev-server.test.ts index c46dac5..8750ad8 100644 --- a/tests/integration/cli/dev-server.test.ts +++ b/tests/integration/cli/dev-server.test.ts @@ -14,11 +14,8 @@ import { } from "../helpers.js"; const templates = discoverTemplates(); -const HAS_REAL_CREDENTIALS = !!( - process.env["BROWSERBASE_API_KEY"] && process.env["BROWSERBASE_PROJECT_ID"] -); -const API_KEY = process.env["BROWSERBASE_API_KEY"] ?? "test_key"; -const PROJECT_ID = process.env["BROWSERBASE_PROJECT_ID"] ?? "test_project"; +const API_KEY = "test_key"; +const PROJECT_ID = "test_project"; function getFunctionName(templateName: string): string { return `sdk-e2e-${templateName}`; @@ -133,20 +130,11 @@ describe("Dev Server", () => { { "x-bb-api-key": API_KEY }, ); - if (HAS_REAL_CREDENTIALS) { - assert.ok( - invokeRes.statusCode === 200 || invokeRes.statusCode === 201, - `Invoke should return 200/201, got ${invokeRes.statusCode}.\nBody: ${invokeRes.body}\nLogs:\n${serverLogs}`, - ); - } else { - // Without real Browserbase credentials, session creation fails but the - // function should still be found (i.e. not a 404). - assert.notEqual( - invokeRes.statusCode, - 404, - `Function '${funcName}' should be registered.\nBody: ${invokeRes.body}\nLogs:\n${serverLogs}`, - ); - } + assert.notEqual( + invokeRes.statusCode, + 404, + `Function '${funcName}' should be registered.\nBody: ${invokeRes.body}\nLogs:\n${serverLogs}`, + ); }); it("returns 404 for nonexistent function", async () => { From 7e82decdd026d0a692a130a890f0f6dd43a72a87 Mon Sep 17 00:00:00 2001 From: Adam McQuilkin <46639306+ajmcquilkin@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:08:50 -0800 Subject: [PATCH 7/7] CI test hang --- tests/integration/cli/dev-server.test.ts | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/integration/cli/dev-server.test.ts b/tests/integration/cli/dev-server.test.ts index 8750ad8..d31313a 100644 --- a/tests/integration/cli/dev-server.test.ts +++ b/tests/integration/cli/dev-server.test.ts @@ -32,6 +32,7 @@ function startDevServer( { cwd, stdio: ["pipe", "pipe", "pipe"], + detached: true, env: { ...process.env, BROWSERBASE_API_KEY: API_KEY, @@ -50,12 +51,32 @@ function killProcess(proc: ChildProcess): Promise { } proc.on("exit", () => resolve()); - proc.kill("SIGTERM"); + + // Kill the entire process group (npx -> node) so no orphans remain. + // The negative PID signals the whole group created by detached: true. + const pid = proc.pid; + if (pid) { + try { + process.kill(-pid, "SIGTERM"); + } catch { + proc.kill("SIGTERM"); + } + } else { + proc.kill("SIGTERM"); + } // Force kill after 2 seconds setTimeout(() => { if (!proc.killed && proc.exitCode === null) { - proc.kill("SIGKILL"); + if (pid) { + try { + process.kill(-pid, "SIGKILL"); + } catch { + // already dead + } + } else { + proc.kill("SIGKILL"); + } } }, 2000); });