diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b360ec5809..f2f8462494 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,9 +177,6 @@ jobs: - name: Test the real-repository hot-path benchmark harness run: python3 scripts/test-bench-hot-paths.py - - name: Test the unused tool-context matching rules - run: python3 scripts/test-measure-unused-tool-context.py - # Configure the output base and restore caches before the first Bazel # invocation so this job does not start two independent Bazel servers. - uses: ./.github/actions/setup-bazel @@ -190,6 +187,15 @@ jobs: && ((github.actor_id == '25274700' && vars.TS_BAZEL_CLIENT_ID) || (github.actor_id == '158243242' && vars.TS_BAZEL_BOT_CLIENT_ID)) || '' }} + - name: Test repository policies with Bazel workers + run: >- + bazel test --config=ci --test_output=errors + //scripts:unused_tool_context_measure_test + //scripts:sqlite_plan_stability_check_test + //scripts:ambient_tokio_check_test + //scripts:rusqlite_storage_boundary_test + //scripts:packaged_lsp_bridge_test + - name: Test the ranked-tail trim replay against the Rust rule cases run: bazel run //scripts:test-measure-ranked-tail-trim.py @@ -206,7 +212,6 @@ jobs: # once here instead of once per release matrix target. - name: Test release packagers run: | - python3 scripts/test-check-packaged-lsp-bridge.py python3 scripts/test-build-mcpb.py python3 scripts/test-package-release-archive.py python3 scripts/test-plan-release-recovery.py @@ -241,22 +246,16 @@ jobs: # Every SQLite connection plans with SQLITE_DBCONFIG_ENABLE_QPSG, so a # bound partial-index predicate or LIKE pattern silently scans. - - name: Test and check SQL plans stay index-backed under QPSG - run: | - python3 scripts/test-check-sqlite-plan-stability.py - python3 scripts/check-sqlite-plan-stability.py + - name: Check SQL plans stay index-backed under QPSG + run: python3 scripts/check-sqlite-plan-stability.py # `daemon run` drives its futures on native workers, which have no # ambient Tokio runtime; production code names its Tokio handle. - - name: Test and check production code has no ambient Tokio call - run: | - python3 scripts/test-check-ambient-tokio.py - python3 scripts/check-ambient-tokio.py + - name: Check production code has no ambient Tokio call + run: python3 scripts/check-ambient-tokio.py - - name: Test and check rusqlite stays inside storage owners - run: | - python3 scripts/test-check-rusqlite-storage-boundary.py - python3 scripts/check-rusqlite-storage-boundary.py + - name: Check rusqlite stays inside storage owners + run: python3 scripts/check-rusqlite-storage-boundary.py # The macOS and Windows lanes run the same Bazel workspace the Linux # authority does, on demand: they exercise target-specific behavior the @@ -761,68 +760,59 @@ jobs: # Do not serialize them behind repository-wide Rust formatting and gates. dashboard: name: Dashboard + permissions: + contents: read + id-token: write runs-on: ubuntu-latest timeout-minutes: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ./.github/actions/setup-bazel + id: bazel + with: + cache-key: ci-dashboard + tailscale-client-id: >- + ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + && ((github.actor_id == '25274700' && vars.TS_BAZEL_CLIENT_ID) + || (github.actor_id == '158243242' && vars.TS_BAZEL_BOT_CLIENT_ID)) || '' }} + + - name: Check frontend types, tests, and artifacts + run: >- + bazel test --config=ci --test_output=errors + //dashboard:typecheck //dashboard:unit_test + //dashboard:bundle_sha256 //dashboard:bundle_determinism_test + //sdks/typescript:typecheck //sdks/typescript:unit_test + //plugin/chatgpt-extension:typecheck + //plugin/chatgpt-extension:adapter_test + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: "26" + # Committed embedded bytes are generated with the pnpm package layout. + # Keep this comparison in that same layout; Bazel's package paths change + # bundler module IDs even when the source and dependency versions match. - uses: ./.github/actions/setup-pnpm - - name: Typecheck dashboard consumers - working-directory: dashboard - run: pnpm run typecheck - - - name: Typecheck TypeScript SDK - working-directory: sdks/typescript - run: pnpm run typecheck - - - name: Run TypeScript SDK unit tests - working-directory: sdks/typescript - run: pnpm test - - - name: Build dashboard assets - working-directory: dashboard - run: pnpm run build - - - name: Run dashboard unit tests - working-directory: dashboard - run: pnpm test - - # The dashboard is one rsbuild app emitting `app-dist/` (the legacy - # shell/holographic/lcm/graph/savings bundle split no longer exists), so - # the bundle validator is the artifact authority and the determinism - # check hashes whatever the build actually produced. - - name: Verify embedded dist artifacts - working-directory: dashboard - run: | - set -euo pipefail - python3 ../scripts/check-dashboard-bundle.py app-dist - (cd app-dist && find . -type f | sort | xargs sha256sum) > /tmp/dashboard-dist.sha - pnpm run build - (cd app-dist && find . -type f | sort | xargs sha256sum) | diff /tmp/dashboard-dist.sha - - - - name: Build TypeScript SDK for the ChatGPT extension + - name: Build TypeScript SDK for the embedded artifact check working-directory: sdks/typescript run: pnpm run build - - name: Typecheck ChatGPT extension - working-directory: plugin/chatgpt-extension - run: pnpm run typecheck - - # extension.test.ts needs a tracedecay binary, which this job does not - # build; the adapter tests run against a fake `serve`. - - name: Run ChatGPT extension adapter tests - working-directory: plugin/chatgpt-extension - run: pnpm exec vitest run test/adapter.test.ts - - name: Verify ChatGPT extension embedded artifacts working-directory: plugin/chatgpt-extension run: pnpm run check:embedded + - name: Upload failed frontend test diagnostics + if: failure() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: frontend-bazel-test-logs + path: | + bazel-testlogs/dashboard/**/test.log + bazel-testlogs/sdks/typescript/**/test.log + bazel-testlogs/plugin/chatgpt-extension/**/test.log + hermes-integration: name: Hermes integration (stock) needs: [scope-gate, debug-cli] diff --git a/MODULE.bazel b/MODULE.bazel index 44e9ed0689..966a7e07df 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -176,6 +176,9 @@ npm.npm_translate_lock( "//dashboard:package.json", ], pnpm_lock = "//:pnpm-lock.yaml", + # React Router imports React types from the shared npm store, outside + # dashboard/node_modules. Make that peer declaration resolvable there. + public_hoist_packages = {"@types/react": [""]}, verify_node_modules_ignored = "//:.bazelignore", ) use_repo(npm, "npm") diff --git a/dashboard/BUILD.bazel b/dashboard/BUILD.bazel index 30b68f5741..c6183a1da7 100644 --- a/dashboard/BUILD.bazel +++ b/dashboard/BUILD.bazel @@ -1,7 +1,9 @@ +load("@aspect_rules_js//js:defs.bzl", "js_test") load("@bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin") load("@npm//:defs.bzl", "npm_link_all_packages") load("@npm//dashboard:@rsbuild/core/package_json.bzl", rsbuild_bin = "bin") load("@npm//dashboard:vitest/package_json.bzl", vitest_bin = "bin") +load("@npm//dashboard:typescript/package_json.bzl", tsc_bin = "bin") package(default_visibility = ["//visibility:public"]) @@ -37,6 +39,24 @@ vitest_bin.vitest_test( tags = ["cpu:4"], ) +tsc_bin.tsc_test( + name = "typecheck", + include_types = True, + include_transitive_types = True, + data = [":dashboard_sources", ":node_modules", "//:node_modules/@types/react"] + glob(["codegen/**", "e2e/**", "stories/**"]), + args = ["--noEmit"], + chdir = package_name(), +) + +js_test( + name = "bundle_determinism_test", + entry_point = "scripts/check-bundle-determinism.mjs", + data = [":dashboard_sources", ":node_modules"], + chdir = package_name(), + exec_properties = {"cpu_count": "4"}, + tags = ["cpu:4"], +) + rsbuild_bin.rsbuild( name = "app_dist", srcs = [ diff --git a/dashboard/scripts/check-bundle-determinism.mjs b/dashboard/scripts/check-bundle-determinism.mjs new file mode 100644 index 0000000000..44f9ac4dab --- /dev/null +++ b/dashboard/scripts/check-bundle-determinism.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { spawnSync } from "node:child_process"; + +const require = createRequire(import.meta.url); +const packageJson = require.resolve("@rsbuild/core/package.json"); +const cli = path.resolve(path.dirname(packageJson), "bin/rsbuild.js"); + +// Independently rebuild twice: a cache hit cannot prove determinism. +const temporary = await mkdtemp(path.join(tmpdir(), "dashboard-determinism-")); +try { + const outputs = []; + const destination = path.join(temporary, "app-dist"); + for (let attempt = 0; attempt < 2; attempt++) { + const result = spawnSync(process.execPath, [cli, "build"], { + env: { ...process.env, TRACEDECAY_DASHBOARD_DIST_PATH: destination }, + stdio: "inherit", + }); + if (result.error) throw result.error; + assert.equal(result.status, 0, "dashboard build failed"); + const entries = await readdir(destination, { recursive: true, withFileTypes: true }); + const files = entries.filter((entry) => entry.isFile()).map((entry) => + path.relative(destination, path.join(entry.parentPath, entry.name))).sort(); + assert.ok(files.length > 0, "dashboard build emitted no files"); + outputs.push(await Promise.all(files.map(async (file) => + [file, createHash("sha256").update(await readFile(path.join(destination, file))).digest("hex")]))); + } + assert.deepEqual(outputs[0], outputs[1], "dashboard builds must be byte-identical"); +} finally { + await rm(temporary, { recursive: true, force: true }); +} diff --git a/plugin/chatgpt-extension/BUILD.bazel b/plugin/chatgpt-extension/BUILD.bazel index 1157c99571..36219a4da7 100644 --- a/plugin/chatgpt-extension/BUILD.bazel +++ b/plugin/chatgpt-extension/BUILD.bazel @@ -1,3 +1,5 @@ +load("@npm//plugin/chatgpt-extension:typescript/package_json.bzl", tsc_bin = "bin") +load("@npm//plugin/chatgpt-extension:vitest/package_json.bzl", vitest_bin = "bin") load("@npm//:defs.bzl", "npm_link_all_packages") npm_link_all_packages(name = "node_modules") @@ -6,3 +8,19 @@ filegroup( srcs = glob(["**/*"]), visibility = ["//visibility:public"], ) + +tsc_bin.tsc_test( + name = "typecheck", + include_types = True, + include_transitive_types = True, + data = [":sources", ":node_modules", "//sdks/typescript:dist"], + args = ["--noEmit"], + chdir = package_name(), +) + +vitest_bin.vitest_test( + name = "adapter_test", + data = [":sources", ":node_modules", "//sdks/typescript:dist"], + args = ["run", "test/adapter.test.ts"], + chdir = package_name(), +) diff --git a/plugin/chatgpt-extension/test/adapter.test.ts b/plugin/chatgpt-extension/test/adapter.test.ts index 5ade6fa577..bb5095f918 100644 --- a/plugin/chatgpt-extension/test/adapter.test.ts +++ b/plugin/chatgpt-extension/test/adapter.test.ts @@ -10,6 +10,9 @@ import { createExtensionServer } from "../src/server/main.js"; import { DaemonFailure, ServeSession } from "../src/server/serve-client.js"; const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +// MCP deliberately filters child environments; use the declared Node runtime, +// not Bazel's wrapper, which depends on launcher-only environment variables. +const NODE_BINARY = process.env.JS_BINARY__NODE_BINARY ?? process.execPath; const FAKE_SERVE = path.join(PACKAGE_ROOT, "test", "fake-serve.mjs"); const ASSETS = { html: "", iconSvg: "" }; @@ -19,7 +22,7 @@ afterEach(async () => { }); function fakeSession(): ServeSession { - const session = new ServeSession({ binary: FAKE_SERVE, projectRoot: null, env: process.env, cwd: PACKAGE_ROOT }); + const session = new ServeSession({ binary: FAKE_SERVE, projectRoot: null, env: { ...process.env, PATH: `${path.dirname(NODE_BINARY)}${path.delimiter}${process.env.PATH ?? ""}` }, cwd: PACKAGE_ROOT }); cleanups.push(() => session.close()); return session; } @@ -41,7 +44,7 @@ describe("embedded server entry", () => { await symlink(path.join(PACKAGE_ROOT, "embedded", "server.mjs"), link); const client = new Client({ name: "symlink-test", version: "0.0.0" }); - await client.connect(new StdioClientTransport({ command: process.execPath, args: [link], cwd: dir })); + await client.connect(new StdioClientTransport({ command: NODE_BINARY, args: [link], cwd: dir })); cleanups.push(() => client.close()); expect(client.getServerVersion()?.name).toBe("tracedecay-chatgpt-extension"); diff --git a/sdks/typescript/BUILD.bazel b/sdks/typescript/BUILD.bazel index b295cf229c..3114450724 100644 --- a/sdks/typescript/BUILD.bazel +++ b/sdks/typescript/BUILD.bazel @@ -1,6 +1,7 @@ load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library") load("@npm//:defs.bzl", "npm_link_all_packages") load("@npm//sdks/typescript:vitest/package_json.bzl", vitest_bin = "bin") +load("@npm//sdks/typescript:typescript/package_json.bzl", tsc_bin = "bin") load("@npm//sdks/typescript:@rslib/core/package_json.bzl", rslib_bin = "bin") npm_link_all_packages(name = "node_modules") @@ -37,3 +38,12 @@ vitest_bin.vitest_test( args = ["run", "--exclude", "test/installed-package.test.ts"], chdir = package_name(), ) + +tsc_bin.tsc_test( + name = "typecheck", + include_types = True, + include_transitive_types = True, + data = [":pkg", ":node_modules"] + glob(["test/**", "*.json", "*.ts"]), + args = ["-p", "tsconfig.json", "--noEmit"], + chdir = package_name(), +)